changeset 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents 6c6239aec462
children 05b8fd21089c
files COPYING Core/Cache/ICachePageProvider.h Core/Cache/ICacheable.h Core/Cache/LeastRecentlyUsedIndex.h Core/Cache/MemoryCache.cpp Core/Cache/MemoryCache.h Core/Cache/MemoryObjectCache.cpp Core/Cache/MemoryObjectCache.h Core/Cache/MemoryStringCache.cpp Core/Cache/MemoryStringCache.h Core/Cache/SharedArchive.cpp Core/Cache/SharedArchive.h Core/ChunkedBuffer.cpp Core/ChunkedBuffer.h Core/Compatibility.h Core/Compression/DeflateBaseCompressor.cpp Core/Compression/DeflateBaseCompressor.h Core/Compression/GzipCompressor.cpp Core/Compression/GzipCompressor.h Core/Compression/HierarchicalZipWriter.cpp Core/Compression/HierarchicalZipWriter.h Core/Compression/IBufferCompressor.h Core/Compression/ZipWriter.cpp Core/Compression/ZipWriter.h Core/Compression/ZlibCompressor.cpp Core/Compression/ZlibCompressor.h Core/DicomFormat/DicomArray.cpp Core/DicomFormat/DicomArray.h Core/DicomFormat/DicomElement.h Core/DicomFormat/DicomImageInformation.cpp Core/DicomFormat/DicomImageInformation.h Core/DicomFormat/DicomInstanceHasher.cpp Core/DicomFormat/DicomInstanceHasher.h Core/DicomFormat/DicomIntegerPixelAccessor.cpp Core/DicomFormat/DicomIntegerPixelAccessor.h Core/DicomFormat/DicomMap.cpp Core/DicomFormat/DicomMap.h Core/DicomFormat/DicomTag.cpp Core/DicomFormat/DicomTag.h Core/DicomFormat/DicomValue.cpp Core/DicomFormat/DicomValue.h Core/DicomNetworking/DicomAssociation.cpp Core/DicomNetworking/DicomAssociation.h Core/DicomNetworking/DicomAssociationParameters.cpp Core/DicomNetworking/DicomAssociationParameters.h Core/DicomNetworking/DicomControlUserConnection.cpp Core/DicomNetworking/DicomControlUserConnection.h Core/DicomNetworking/DicomFindAnswers.cpp Core/DicomNetworking/DicomFindAnswers.h Core/DicomNetworking/DicomServer.cpp Core/DicomNetworking/DicomServer.h Core/DicomNetworking/DicomStoreUserConnection.cpp Core/DicomNetworking/DicomStoreUserConnection.h Core/DicomNetworking/IApplicationEntityFilter.h Core/DicomNetworking/IFindRequestHandler.h Core/DicomNetworking/IFindRequestHandlerFactory.h Core/DicomNetworking/IGetRequestHandler.h Core/DicomNetworking/IGetRequestHandlerFactory.h Core/DicomNetworking/IMoveRequestHandler.h Core/DicomNetworking/IMoveRequestHandlerFactory.h Core/DicomNetworking/IStorageCommitmentRequestHandler.h Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h Core/DicomNetworking/IStoreRequestHandler.h Core/DicomNetworking/IStoreRequestHandlerFactory.h Core/DicomNetworking/IWorklistRequestHandler.h Core/DicomNetworking/IWorklistRequestHandlerFactory.h Core/DicomNetworking/Internals/CommandDispatcher.cpp Core/DicomNetworking/Internals/CommandDispatcher.h Core/DicomNetworking/Internals/FindScp.cpp Core/DicomNetworking/Internals/FindScp.h Core/DicomNetworking/Internals/GetScp.cpp Core/DicomNetworking/Internals/GetScp.h Core/DicomNetworking/Internals/MoveScp.cpp Core/DicomNetworking/Internals/MoveScp.h Core/DicomNetworking/Internals/StoreScp.cpp Core/DicomNetworking/Internals/StoreScp.h Core/DicomNetworking/NetworkingCompatibility.h Core/DicomNetworking/RemoteModalityParameters.cpp Core/DicomNetworking/RemoteModalityParameters.h Core/DicomNetworking/TimeoutDicomConnectionManager.cpp Core/DicomNetworking/TimeoutDicomConnectionManager.h Core/DicomParsing/DcmtkTranscoder.cpp Core/DicomParsing/DcmtkTranscoder.h Core/DicomParsing/DicomDirWriter.cpp Core/DicomParsing/DicomDirWriter.h Core/DicomParsing/DicomModification.cpp Core/DicomParsing/DicomModification.h Core/DicomParsing/DicomWebJsonVisitor.cpp Core/DicomParsing/DicomWebJsonVisitor.h Core/DicomParsing/FromDcmtkBridge.cpp Core/DicomParsing/FromDcmtkBridge.h Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h Core/DicomParsing/IDicomTranscoder.cpp Core/DicomParsing/IDicomTranscoder.h Core/DicomParsing/ITagVisitor.h Core/DicomParsing/Internals/DicomFrameIndex.cpp Core/DicomParsing/Internals/DicomFrameIndex.h Core/DicomParsing/Internals/DicomImageDecoder.cpp Core/DicomParsing/Internals/DicomImageDecoder.h Core/DicomParsing/MemoryBufferTranscoder.cpp Core/DicomParsing/MemoryBufferTranscoder.h Core/DicomParsing/ParsedDicomDir.cpp Core/DicomParsing/ParsedDicomDir.h Core/DicomParsing/ParsedDicomFile.cpp Core/DicomParsing/ParsedDicomFile.h Core/DicomParsing/ToDcmtkBridge.cpp Core/DicomParsing/ToDcmtkBridge.h Core/Endianness.h Core/EnumerationDictionary.h Core/Enumerations.cpp Core/Enumerations.h Core/Enumerations_TransferSyntaxes.impl.h Core/FileBuffer.cpp Core/FileBuffer.h Core/FileStorage/FileInfo.h Core/FileStorage/FilesystemStorage.cpp Core/FileStorage/FilesystemStorage.h Core/FileStorage/IStorageArea.h Core/FileStorage/MemoryStorageArea.cpp Core/FileStorage/MemoryStorageArea.h Core/FileStorage/StorageAccessor.cpp Core/FileStorage/StorageAccessor.h Core/HttpClient.cpp Core/HttpClient.h Core/HttpServer/BufferHttpSender.cpp Core/HttpServer/BufferHttpSender.h Core/HttpServer/FilesystemHttpHandler.cpp Core/HttpServer/FilesystemHttpHandler.h Core/HttpServer/FilesystemHttpSender.cpp Core/HttpServer/FilesystemHttpSender.h Core/HttpServer/HttpContentNegociation.cpp Core/HttpServer/HttpContentNegociation.h Core/HttpServer/HttpFileSender.cpp Core/HttpServer/HttpFileSender.h Core/HttpServer/HttpOutput.cpp Core/HttpServer/HttpOutput.h Core/HttpServer/HttpServer.cpp Core/HttpServer/HttpServer.h Core/HttpServer/HttpStreamTranscoder.cpp Core/HttpServer/HttpStreamTranscoder.h Core/HttpServer/HttpToolbox.cpp Core/HttpServer/HttpToolbox.h Core/HttpServer/IHttpHandler.h Core/HttpServer/IHttpOutputStream.h Core/HttpServer/IHttpStreamAnswer.h Core/HttpServer/IIncomingHttpRequestFilter.h Core/HttpServer/MultipartStreamReader.cpp Core/HttpServer/MultipartStreamReader.h Core/HttpServer/StringHttpOutput.cpp Core/HttpServer/StringHttpOutput.h Core/HttpServer/StringMatcher.cpp Core/HttpServer/StringMatcher.h Core/IDynamicObject.h Core/Images/Font.cpp Core/Images/Font.h Core/Images/FontRegistry.cpp Core/Images/FontRegistry.h Core/Images/IImageWriter.cpp Core/Images/IImageWriter.h Core/Images/Image.cpp Core/Images/Image.h Core/Images/ImageAccessor.cpp Core/Images/ImageAccessor.h Core/Images/ImageBuffer.cpp Core/Images/ImageBuffer.h Core/Images/ImageProcessing.cpp Core/Images/ImageProcessing.h Core/Images/ImageTraits.h Core/Images/JpegErrorManager.cpp Core/Images/JpegErrorManager.h Core/Images/JpegReader.cpp Core/Images/JpegReader.h Core/Images/JpegWriter.cpp Core/Images/JpegWriter.h Core/Images/PamReader.cpp Core/Images/PamReader.h Core/Images/PamWriter.cpp Core/Images/PamWriter.h Core/Images/PixelTraits.h Core/Images/PngReader.cpp Core/Images/PngReader.h Core/Images/PngWriter.cpp Core/Images/PngWriter.h Core/JobsEngine/GenericJobUnserializer.cpp Core/JobsEngine/GenericJobUnserializer.h Core/JobsEngine/IJob.h Core/JobsEngine/IJobUnserializer.h Core/JobsEngine/JobInfo.cpp Core/JobsEngine/JobInfo.h Core/JobsEngine/JobStatus.cpp Core/JobsEngine/JobStatus.h Core/JobsEngine/JobStepResult.cpp Core/JobsEngine/JobStepResult.h Core/JobsEngine/JobsEngine.cpp Core/JobsEngine/JobsEngine.h Core/JobsEngine/JobsRegistry.cpp Core/JobsEngine/JobsRegistry.h Core/JobsEngine/Operations/IJobOperation.h Core/JobsEngine/Operations/JobOperationValue.h Core/JobsEngine/Operations/JobOperationValues.cpp Core/JobsEngine/Operations/JobOperationValues.h Core/JobsEngine/Operations/LogJobOperation.cpp Core/JobsEngine/Operations/LogJobOperation.h Core/JobsEngine/Operations/NullOperationValue.h Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp Core/JobsEngine/Operations/SequenceOfOperationsJob.h Core/JobsEngine/Operations/StringOperationValue.h Core/JobsEngine/SetOfCommandsJob.cpp Core/JobsEngine/SetOfCommandsJob.h Core/JobsEngine/SetOfInstancesJob.cpp Core/JobsEngine/SetOfInstancesJob.h Core/Logging.cpp Core/Logging.h Core/Lua/LuaContext.cpp Core/Lua/LuaContext.h Core/Lua/LuaFunctionCall.cpp Core/Lua/LuaFunctionCall.h Core/MetricsRegistry.cpp Core/MetricsRegistry.h Core/MultiThreading/IRunnableBySteps.h Core/MultiThreading/RunnableWorkersPool.cpp Core/MultiThreading/RunnableWorkersPool.h Core/MultiThreading/Semaphore.cpp Core/MultiThreading/Semaphore.h Core/MultiThreading/SharedMessageQueue.cpp Core/MultiThreading/SharedMessageQueue.h Core/OrthancException.h Core/OrthancFramework.cpp Core/OrthancFramework.h Core/Pkcs11.cpp Core/Pkcs11.h Core/PrecompiledHeaders.cpp Core/PrecompiledHeaders.h Core/RestApi/RestApi.cpp Core/RestApi/RestApi.h Core/RestApi/RestApiCall.cpp Core/RestApi/RestApiCall.h Core/RestApi/RestApiDeleteCall.h Core/RestApi/RestApiGetCall.cpp Core/RestApi/RestApiGetCall.h Core/RestApi/RestApiHierarchy.cpp Core/RestApi/RestApiHierarchy.h Core/RestApi/RestApiOutput.cpp Core/RestApi/RestApiOutput.h Core/RestApi/RestApiPath.cpp Core/RestApi/RestApiPath.h Core/RestApi/RestApiPostCall.h Core/RestApi/RestApiPutCall.h Core/SQLite/Connection.cpp Core/SQLite/Connection.h Core/SQLite/FunctionContext.cpp Core/SQLite/FunctionContext.h Core/SQLite/IScalarFunction.h Core/SQLite/ITransaction.h Core/SQLite/NonCopyable.h Core/SQLite/OrthancSQLiteException.h Core/SQLite/README.txt Core/SQLite/SQLiteTypes.h Core/SQLite/Statement.cpp Core/SQLite/Statement.h Core/SQLite/StatementId.cpp Core/SQLite/StatementId.h Core/SQLite/StatementReference.cpp Core/SQLite/StatementReference.h Core/SQLite/Transaction.cpp Core/SQLite/Transaction.h Core/SerializationToolbox.cpp Core/SerializationToolbox.h Core/SharedLibrary.cpp Core/SharedLibrary.h Core/SystemToolbox.cpp Core/SystemToolbox.h Core/TemporaryFile.cpp Core/TemporaryFile.h Core/Toolbox.cpp Core/Toolbox.h Core/WebServiceParameters.cpp Core/WebServiceParameters.h OrthancExplorer/explorer.css OrthancExplorer/explorer.html OrthancExplorer/explorer.js OrthancExplorer/file-upload.js OrthancExplorer/images/unsupported.png OrthancExplorer/libs/date.js OrthancExplorer/libs/images/ajax-loader.gif OrthancExplorer/libs/images/icons-18-black.png OrthancExplorer/libs/images/icons-18-white.png OrthancExplorer/libs/images/icons-36-black.png OrthancExplorer/libs/images/icons-36-white.png OrthancExplorer/libs/images/notes.txt OrthancExplorer/libs/jqm.page.params.js OrthancExplorer/libs/jqtree-icons.png OrthancExplorer/libs/jqtree.css OrthancExplorer/libs/jquery-file-upload/css/jquery.fileupload-ui.css OrthancExplorer/libs/jquery-file-upload/css/style.css OrthancExplorer/libs/jquery-file-upload/img/loading.gif OrthancExplorer/libs/jquery-file-upload/img/progressbar.gif OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.postmessage-transport.js OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.xdr-transport.js OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-fp.js OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-ui.js OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload.js OrthancExplorer/libs/jquery-file-upload/js/jquery.iframe-transport.js OrthancExplorer/libs/jquery-file-upload/js/locale.js OrthancExplorer/libs/jquery-file-upload/js/vendor/jquery.ui.widget.js OrthancExplorer/libs/jquery.blockui.js OrthancExplorer/libs/jquery.min.js OrthancExplorer/libs/jquery.mobile.min.css OrthancExplorer/libs/jquery.mobile.min.js OrthancExplorer/libs/jquery.mobile.simpledialog.min.css OrthancExplorer/libs/jquery.mobile.simpledialog2.js OrthancExplorer/libs/slimbox2.js OrthancExplorer/libs/slimbox2/closelabel.gif OrthancExplorer/libs/slimbox2/loading.gif OrthancExplorer/libs/slimbox2/nextlabel.gif OrthancExplorer/libs/slimbox2/prevlabel.gif OrthancExplorer/libs/slimbox2/slimbox2-rtl.css OrthancExplorer/libs/slimbox2/slimbox2.css OrthancExplorer/libs/tree.jquery.js OrthancExplorer/orthanc-logo.png OrthancExplorer/query-retrieve.js OrthancFramework/COPYING OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake OrthancFramework/Resources/CMake/BoostConfiguration.cmake OrthancFramework/Resources/CMake/BoostConfiguration.sh OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake OrthancFramework/Resources/CMake/Compiler.cmake OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake OrthancFramework/Resources/CMake/DownloadPackage.cmake OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake OrthancFramework/Resources/CMake/LibP11Configuration.cmake OrthancFramework/Resources/CMake/LibPngConfiguration.cmake OrthancFramework/Resources/CMake/LuaConfiguration.cmake OrthancFramework/Resources/CMake/MongooseConfiguration.cmake OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.0.2.cmake OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake OrthancFramework/Resources/CMake/Uninstall.cmake.in OrthancFramework/Resources/CMake/UuidConfiguration.cmake OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/Run2.cpp OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/app.js OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/arith.patch OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/index.html OrthancFramework/Resources/CMake/WebAssembly/arith.h OrthancFramework/Resources/CMake/ZlibConfiguration.cmake OrthancFramework/Resources/CMakeToolchains/CrossToolchain.cmake OrthancFramework/Resources/CMakeToolchains/LinuxStandardBaseToolchain.cmake OrthancFramework/Resources/CMakeToolchains/MinGW-W64-Toolchain32.cmake OrthancFramework/Resources/CMakeToolchains/MinGW-W64-Toolchain64.cmake OrthancFramework/Resources/CMakeToolchains/MinGWToolchain.cmake OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json OrthancFramework/Resources/CodeGeneration/EncodingTests.h OrthancFramework/Resources/CodeGeneration/EncodingTests.py OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache OrthancFramework/Resources/DcmtkTools/CMakeLists.txt OrthancFramework/Resources/DcmtkTools/dummy.cpp OrthancFramework/Resources/EmbedResources.py OrthancFramework/Resources/Fonts/GenerateFont.py OrthancFramework/Resources/Fonts/README.txt OrthancFramework/Resources/Fonts/UbuntuMonoBold-16.json OrthancFramework/Resources/Graveyard/EclipseCodingStyle.xml OrthancFramework/Resources/Graveyard/FromDcmtkBridge.cpp OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasks.h OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h OrthancFramework/Resources/Graveyard/Multithreading/ICommand.h OrthancFramework/Resources/Graveyard/Multithreading/ILockable.h OrthancFramework/Resources/Graveyard/Multithreading/Locker.h OrthancFramework/Resources/Graveyard/Multithreading/Mutex.cpp OrthancFramework/Resources/Graveyard/Multithreading/Mutex.h OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.h OrthancFramework/Resources/Graveyard/TestTranscoding.cpp OrthancFramework/Resources/Patches/boost-1.65.1-linux-standard-base.patch OrthancFramework/Resources/Patches/boost-1.66.0-linux-standard-base.patch OrthancFramework/Resources/Patches/boost-1.67.0-linux-standard-base.patch OrthancFramework/Resources/Patches/boost-1.68.0-linux-standard-base.patch OrthancFramework/Resources/Patches/boost-1.69.0-linux-standard-base.patch OrthancFramework/Resources/Patches/civetweb-1.11.patch OrthancFramework/Resources/Patches/civetweb-1.12.patch OrthancFramework/Resources/Patches/curl-7.57.0-cmake.patch OrthancFramework/Resources/Patches/curl-7.64.0-cmake.patch OrthancFramework/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch OrthancFramework/Resources/Patches/dcmtk-3.6.0-mingw64.patch OrthancFramework/Resources/Patches/dcmtk-3.6.0-speed.patch OrthancFramework/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch OrthancFramework/Resources/Patches/dcmtk-3.6.2-private.dic OrthancFramework/Resources/Patches/dcmtk-3.6.2.patch OrthancFramework/Resources/Patches/dcmtk-3.6.4.patch OrthancFramework/Resources/Patches/dcmtk-3.6.5.patch OrthancFramework/Resources/Patches/dcmtk-dcdict_orthanc.cc OrthancFramework/Resources/Patches/dcmtk.txt OrthancFramework/Resources/Patches/e2fsprogs-1.43.8-apple.patch OrthancFramework/Resources/Patches/e2fsprogs-1.44.5-apple.patch OrthancFramework/Resources/Patches/libp11-0.4.0.patch OrthancFramework/Resources/Patches/mongoose-3.1-patch.diff OrthancFramework/Resources/Patches/mongoose-3.8-patch.diff OrthancFramework/Resources/Patches/openssl-1.1.1-conf.h.in OrthancFramework/Resources/Patches/openssl-1.1.1g.patch OrthancFramework/Resources/RetrieveCACertificates.py OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt OrthancFramework/Resources/Samples/MicroService/README.txt OrthancFramework/Resources/Samples/MicroService/Sample.cpp OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h OrthancFramework/Resources/ThirdParty/base64/base64.cpp OrthancFramework/Resources/ThirdParty/base64/base64.h OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt OrthancFramework/Resources/ThirdParty/icu/README.txt OrthancFramework/Resources/ThirdParty/icu/Version.cmake OrthancFramework/Resources/ThirdParty/md5/md5.c OrthancFramework/Resources/ThirdParty/md5/md5.h OrthancFramework/Resources/ThirdParty/minizip/NOTES OrthancFramework/Resources/ThirdParty/minizip/crypt.h OrthancFramework/Resources/ThirdParty/minizip/ioapi.c OrthancFramework/Resources/ThirdParty/minizip/ioapi.h OrthancFramework/Resources/ThirdParty/minizip/zip.c OrthancFramework/Resources/ThirdParty/minizip/zip.h OrthancFramework/Resources/ThirdParty/patch/NOTES.txt OrthancFramework/Resources/ThirdParty/patch/msys-1.0.dll OrthancFramework/Resources/ThirdParty/patch/patch.exe OrthancFramework/Resources/ThirdParty/patch/patch.exe.manifest OrthancFramework/Resources/WindowsResources.py OrthancFramework/Resources/WindowsResources.rc OrthancFramework/Sources/Cache/ICachePageProvider.h OrthancFramework/Sources/Cache/ICacheable.h OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h OrthancFramework/Sources/Cache/MemoryCache.cpp OrthancFramework/Sources/Cache/MemoryCache.h OrthancFramework/Sources/Cache/MemoryObjectCache.cpp OrthancFramework/Sources/Cache/MemoryObjectCache.h OrthancFramework/Sources/Cache/MemoryStringCache.cpp OrthancFramework/Sources/Cache/MemoryStringCache.h OrthancFramework/Sources/Cache/SharedArchive.cpp OrthancFramework/Sources/Cache/SharedArchive.h OrthancFramework/Sources/ChunkedBuffer.cpp OrthancFramework/Sources/ChunkedBuffer.h OrthancFramework/Sources/Compatibility.h OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp OrthancFramework/Sources/Compression/DeflateBaseCompressor.h OrthancFramework/Sources/Compression/GzipCompressor.cpp OrthancFramework/Sources/Compression/GzipCompressor.h OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp OrthancFramework/Sources/Compression/HierarchicalZipWriter.h OrthancFramework/Sources/Compression/IBufferCompressor.h OrthancFramework/Sources/Compression/ZipWriter.cpp OrthancFramework/Sources/Compression/ZipWriter.h OrthancFramework/Sources/Compression/ZlibCompressor.cpp OrthancFramework/Sources/Compression/ZlibCompressor.h OrthancFramework/Sources/DicomFormat/DicomArray.cpp OrthancFramework/Sources/DicomFormat/DicomArray.h OrthancFramework/Sources/DicomFormat/DicomElement.h OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp OrthancFramework/Sources/DicomFormat/DicomImageInformation.h OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h OrthancFramework/Sources/DicomFormat/DicomMap.cpp OrthancFramework/Sources/DicomFormat/DicomMap.h OrthancFramework/Sources/DicomFormat/DicomTag.cpp OrthancFramework/Sources/DicomFormat/DicomTag.h OrthancFramework/Sources/DicomFormat/DicomValue.cpp OrthancFramework/Sources/DicomFormat/DicomValue.h OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp OrthancFramework/Sources/DicomNetworking/DicomAssociation.h OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h OrthancFramework/Sources/DicomNetworking/DicomServer.cpp OrthancFramework/Sources/DicomNetworking/DicomServer.h OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp OrthancFramework/Sources/DicomParsing/DicomDirWriter.h OrthancFramework/Sources/DicomParsing/DicomModification.cpp OrthancFramework/Sources/DicomParsing/DicomModification.h OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h OrthancFramework/Sources/DicomParsing/ITagVisitor.h OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h OrthancFramework/Sources/Endianness.h OrthancFramework/Sources/EnumerationDictionary.h OrthancFramework/Sources/Enumerations.cpp OrthancFramework/Sources/Enumerations.h OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h OrthancFramework/Sources/FileBuffer.cpp OrthancFramework/Sources/FileBuffer.h OrthancFramework/Sources/FileStorage/FileInfo.h OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp OrthancFramework/Sources/FileStorage/FilesystemStorage.h OrthancFramework/Sources/FileStorage/IStorageArea.h OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp OrthancFramework/Sources/FileStorage/MemoryStorageArea.h OrthancFramework/Sources/FileStorage/StorageAccessor.cpp OrthancFramework/Sources/FileStorage/StorageAccessor.h OrthancFramework/Sources/HttpClient.cpp OrthancFramework/Sources/HttpClient.h OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp OrthancFramework/Sources/HttpServer/BufferHttpSender.h OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp OrthancFramework/Sources/HttpServer/HttpContentNegociation.h OrthancFramework/Sources/HttpServer/HttpFileSender.cpp OrthancFramework/Sources/HttpServer/HttpFileSender.h OrthancFramework/Sources/HttpServer/HttpOutput.cpp OrthancFramework/Sources/HttpServer/HttpOutput.h OrthancFramework/Sources/HttpServer/HttpServer.cpp OrthancFramework/Sources/HttpServer/HttpServer.h OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h OrthancFramework/Sources/HttpServer/HttpToolbox.cpp OrthancFramework/Sources/HttpServer/HttpToolbox.h OrthancFramework/Sources/HttpServer/IHttpHandler.h OrthancFramework/Sources/HttpServer/IHttpOutputStream.h OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp OrthancFramework/Sources/HttpServer/MultipartStreamReader.h OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp OrthancFramework/Sources/HttpServer/StringHttpOutput.h OrthancFramework/Sources/HttpServer/StringMatcher.cpp OrthancFramework/Sources/HttpServer/StringMatcher.h OrthancFramework/Sources/IDynamicObject.h OrthancFramework/Sources/Images/Font.cpp OrthancFramework/Sources/Images/Font.h OrthancFramework/Sources/Images/FontRegistry.cpp OrthancFramework/Sources/Images/FontRegistry.h OrthancFramework/Sources/Images/IImageWriter.cpp OrthancFramework/Sources/Images/IImageWriter.h OrthancFramework/Sources/Images/Image.cpp OrthancFramework/Sources/Images/Image.h OrthancFramework/Sources/Images/ImageAccessor.cpp OrthancFramework/Sources/Images/ImageAccessor.h OrthancFramework/Sources/Images/ImageBuffer.cpp OrthancFramework/Sources/Images/ImageBuffer.h OrthancFramework/Sources/Images/ImageProcessing.cpp OrthancFramework/Sources/Images/ImageProcessing.h OrthancFramework/Sources/Images/ImageTraits.h OrthancFramework/Sources/Images/JpegErrorManager.cpp OrthancFramework/Sources/Images/JpegErrorManager.h OrthancFramework/Sources/Images/JpegReader.cpp OrthancFramework/Sources/Images/JpegReader.h OrthancFramework/Sources/Images/JpegWriter.cpp OrthancFramework/Sources/Images/JpegWriter.h OrthancFramework/Sources/Images/PamReader.cpp OrthancFramework/Sources/Images/PamReader.h OrthancFramework/Sources/Images/PamWriter.cpp OrthancFramework/Sources/Images/PamWriter.h OrthancFramework/Sources/Images/PixelTraits.h OrthancFramework/Sources/Images/PngReader.cpp OrthancFramework/Sources/Images/PngReader.h OrthancFramework/Sources/Images/PngWriter.cpp OrthancFramework/Sources/Images/PngWriter.h OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h OrthancFramework/Sources/JobsEngine/IJob.h OrthancFramework/Sources/JobsEngine/IJobUnserializer.h OrthancFramework/Sources/JobsEngine/JobInfo.cpp OrthancFramework/Sources/JobsEngine/JobInfo.h OrthancFramework/Sources/JobsEngine/JobStatus.cpp OrthancFramework/Sources/JobsEngine/JobStatus.h OrthancFramework/Sources/JobsEngine/JobStepResult.cpp OrthancFramework/Sources/JobsEngine/JobStepResult.h OrthancFramework/Sources/JobsEngine/JobsEngine.cpp OrthancFramework/Sources/JobsEngine/JobsEngine.h OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp OrthancFramework/Sources/JobsEngine/JobsRegistry.h OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h OrthancFramework/Sources/JobsEngine/Operations/JobOperationValue.h OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h OrthancFramework/Sources/Logging.cpp OrthancFramework/Sources/Logging.h OrthancFramework/Sources/Lua/LuaContext.cpp OrthancFramework/Sources/Lua/LuaContext.h OrthancFramework/Sources/Lua/LuaFunctionCall.cpp OrthancFramework/Sources/Lua/LuaFunctionCall.h OrthancFramework/Sources/MetricsRegistry.cpp OrthancFramework/Sources/MetricsRegistry.h OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h OrthancFramework/Sources/MultiThreading/Semaphore.cpp OrthancFramework/Sources/MultiThreading/Semaphore.h OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h OrthancFramework/Sources/OrthancException.h OrthancFramework/Sources/OrthancFramework.cpp OrthancFramework/Sources/OrthancFramework.h OrthancFramework/Sources/Pkcs11.cpp OrthancFramework/Sources/Pkcs11.h OrthancFramework/Sources/PrecompiledHeaders.cpp OrthancFramework/Sources/PrecompiledHeaders.h OrthancFramework/Sources/RestApi/RestApi.cpp OrthancFramework/Sources/RestApi/RestApi.h OrthancFramework/Sources/RestApi/RestApiCall.cpp OrthancFramework/Sources/RestApi/RestApiCall.h OrthancFramework/Sources/RestApi/RestApiDeleteCall.h OrthancFramework/Sources/RestApi/RestApiGetCall.cpp OrthancFramework/Sources/RestApi/RestApiGetCall.h OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp OrthancFramework/Sources/RestApi/RestApiHierarchy.h OrthancFramework/Sources/RestApi/RestApiOutput.cpp OrthancFramework/Sources/RestApi/RestApiOutput.h OrthancFramework/Sources/RestApi/RestApiPath.cpp OrthancFramework/Sources/RestApi/RestApiPath.h OrthancFramework/Sources/RestApi/RestApiPostCall.h OrthancFramework/Sources/RestApi/RestApiPutCall.h OrthancFramework/Sources/SQLite/Connection.cpp OrthancFramework/Sources/SQLite/Connection.h OrthancFramework/Sources/SQLite/FunctionContext.cpp OrthancFramework/Sources/SQLite/FunctionContext.h OrthancFramework/Sources/SQLite/IScalarFunction.h OrthancFramework/Sources/SQLite/ITransaction.h OrthancFramework/Sources/SQLite/NonCopyable.h OrthancFramework/Sources/SQLite/OrthancSQLiteException.h OrthancFramework/Sources/SQLite/README.txt OrthancFramework/Sources/SQLite/SQLiteTypes.h OrthancFramework/Sources/SQLite/Statement.cpp OrthancFramework/Sources/SQLite/Statement.h OrthancFramework/Sources/SQLite/StatementId.cpp OrthancFramework/Sources/SQLite/StatementId.h OrthancFramework/Sources/SQLite/StatementReference.cpp OrthancFramework/Sources/SQLite/StatementReference.h OrthancFramework/Sources/SQLite/Transaction.cpp OrthancFramework/Sources/SQLite/Transaction.h OrthancFramework/Sources/SerializationToolbox.cpp OrthancFramework/Sources/SerializationToolbox.h OrthancFramework/Sources/SharedLibrary.cpp OrthancFramework/Sources/SharedLibrary.h OrthancFramework/Sources/SystemToolbox.cpp OrthancFramework/Sources/SystemToolbox.h OrthancFramework/Sources/TemporaryFile.cpp OrthancFramework/Sources/TemporaryFile.h OrthancFramework/Sources/Toolbox.cpp OrthancFramework/Sources/Toolbox.h OrthancFramework/Sources/WebServiceParameters.cpp OrthancFramework/Sources/WebServiceParameters.h OrthancFramework/UnitTestsSources/FrameworkTests.cpp OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp OrthancFramework/UnitTestsSources/LoggingTests.cpp OrthancFramework/UnitTestsSources/PluginsTests.cpp OrthancFramework/UnitTestsSources/RestApiTests.cpp OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp OrthancFramework/UnitTestsSources/SQLiteTests.cpp OrthancFramework/UnitTestsSources/StreamTests.cpp OrthancFramework/UnitTestsSources/ToolboxTests.cpp OrthancFramework/UnitTestsSources/ZipTests.cpp OrthancServer/COPYING OrthancServer/Database/Compatibility/DatabaseLookup.cpp OrthancServer/Database/Compatibility/DatabaseLookup.h OrthancServer/Database/Compatibility/ICreateInstance.cpp OrthancServer/Database/Compatibility/ICreateInstance.h OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp OrthancServer/Database/Compatibility/IGetChildrenMetadata.h OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp OrthancServer/Database/Compatibility/ILookupResourceAndParent.h OrthancServer/Database/Compatibility/ILookupResources.cpp OrthancServer/Database/Compatibility/ILookupResources.h OrthancServer/Database/Compatibility/ISetResourcesContent.h OrthancServer/Database/Compatibility/SetOfResources.cpp OrthancServer/Database/Compatibility/SetOfResources.h OrthancServer/Database/IDatabaseListener.h OrthancServer/Database/IDatabaseWrapper.h OrthancServer/Database/InstallTrackAttachmentsSize.sql OrthancServer/Database/PrepareDatabase.sql OrthancServer/Database/ResourcesContent.cpp OrthancServer/Database/ResourcesContent.h OrthancServer/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Database/SQLiteDatabaseWrapper.h OrthancServer/Database/Upgrade3To4.sql OrthancServer/Database/Upgrade4To5.sql OrthancServer/DicomInstanceOrigin.cpp OrthancServer/DicomInstanceOrigin.h OrthancServer/DicomInstanceToStore.cpp OrthancServer/DicomInstanceToStore.h OrthancServer/EmbeddedResourceHttpHandler.cpp OrthancServer/EmbeddedResourceHttpHandler.h OrthancServer/ExportedResource.cpp OrthancServer/ExportedResource.h OrthancServer/IDicomImageDecoder.h OrthancServer/IServerListener.h OrthancServer/LuaScripting.cpp OrthancServer/LuaScripting.h OrthancServer/OrthancConfiguration.cpp OrthancServer/OrthancConfiguration.h OrthancServer/OrthancExplorer/explorer.css OrthancServer/OrthancExplorer/explorer.html OrthancServer/OrthancExplorer/explorer.js OrthancServer/OrthancExplorer/file-upload.js OrthancServer/OrthancExplorer/images/unsupported.png OrthancServer/OrthancExplorer/libs/date.js OrthancServer/OrthancExplorer/libs/images/ajax-loader.gif OrthancServer/OrthancExplorer/libs/images/icons-18-black.png OrthancServer/OrthancExplorer/libs/images/icons-18-white.png OrthancServer/OrthancExplorer/libs/images/icons-36-black.png OrthancServer/OrthancExplorer/libs/images/icons-36-white.png OrthancServer/OrthancExplorer/libs/images/notes.txt OrthancServer/OrthancExplorer/libs/jqm.page.params.js OrthancServer/OrthancExplorer/libs/jqtree-icons.png OrthancServer/OrthancExplorer/libs/jqtree.css OrthancServer/OrthancExplorer/libs/jquery-file-upload/css/jquery.fileupload-ui.css OrthancServer/OrthancExplorer/libs/jquery-file-upload/css/style.css OrthancServer/OrthancExplorer/libs/jquery-file-upload/img/loading.gif OrthancServer/OrthancExplorer/libs/jquery-file-upload/img/progressbar.gif OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.postmessage-transport.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.xdr-transport.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-fp.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-ui.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.iframe-transport.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/locale.js OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/vendor/jquery.ui.widget.js OrthancServer/OrthancExplorer/libs/jquery.blockui.js OrthancServer/OrthancExplorer/libs/jquery.min.js OrthancServer/OrthancExplorer/libs/jquery.mobile.min.css OrthancServer/OrthancExplorer/libs/jquery.mobile.min.js OrthancServer/OrthancExplorer/libs/jquery.mobile.simpledialog.min.css OrthancServer/OrthancExplorer/libs/jquery.mobile.simpledialog2.js OrthancServer/OrthancExplorer/libs/slimbox2.js OrthancServer/OrthancExplorer/libs/slimbox2/closelabel.gif OrthancServer/OrthancExplorer/libs/slimbox2/loading.gif OrthancServer/OrthancExplorer/libs/slimbox2/nextlabel.gif OrthancServer/OrthancExplorer/libs/slimbox2/prevlabel.gif OrthancServer/OrthancExplorer/libs/slimbox2/slimbox2-rtl.css OrthancServer/OrthancExplorer/libs/slimbox2/slimbox2.css OrthancServer/OrthancExplorer/libs/tree.jquery.js OrthancServer/OrthancExplorer/orthanc-logo.png OrthancServer/OrthancExplorer/query-retrieve.js OrthancServer/OrthancFindRequestHandler.cpp OrthancServer/OrthancFindRequestHandler.h OrthancServer/OrthancGetRequestHandler.cpp OrthancServer/OrthancGetRequestHandler.h OrthancServer/OrthancHttpHandler.cpp OrthancServer/OrthancHttpHandler.h OrthancServer/OrthancInitialization.cpp OrthancServer/OrthancInitialization.h OrthancServer/OrthancMoveRequestHandler.cpp OrthancServer/OrthancMoveRequestHandler.h OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/OrthancRestApi/OrthancRestApi.cpp OrthancServer/OrthancRestApi/OrthancRestApi.h OrthancServer/OrthancRestApi/OrthancRestArchive.cpp OrthancServer/OrthancRestApi/OrthancRestChanges.cpp OrthancServer/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/OrthancRestApi/OrthancRestResources.cpp OrthancServer/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Plugins/Engine/IPluginServiceProvider.h OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp OrthancServer/Plugins/Engine/OrthancPluginDatabase.h OrthancServer/Plugins/Engine/OrthancPlugins.cpp OrthancServer/Plugins/Engine/OrthancPlugins.h OrthancServer/Plugins/Engine/PluginsEnumerations.cpp OrthancServer/Plugins/Engine/PluginsEnumerations.h OrthancServer/Plugins/Engine/PluginsErrorDictionary.cpp OrthancServer/Plugins/Engine/PluginsErrorDictionary.h OrthancServer/Plugins/Engine/PluginsJob.cpp OrthancServer/Plugins/Engine/PluginsJob.h OrthancServer/Plugins/Engine/PluginsManager.cpp OrthancServer/Plugins/Engine/PluginsManager.h OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp OrthancServer/Plugins/Samples/Basic/CMakeLists.txt OrthancServer/Plugins/Samples/Basic/Plugin.c OrthancServer/Plugins/Samples/Common/DicomDatasetReader.cpp OrthancServer/Plugins/Samples/Common/DicomDatasetReader.h OrthancServer/Plugins/Samples/Common/DicomPath.cpp OrthancServer/Plugins/Samples/Common/DicomPath.h OrthancServer/Plugins/Samples/Common/DicomTag.cpp OrthancServer/Plugins/Samples/Common/DicomTag.h OrthancServer/Plugins/Samples/Common/ExportedSymbols.list OrthancServer/Plugins/Samples/Common/FullOrthancDataset.cpp OrthancServer/Plugins/Samples/Common/FullOrthancDataset.h OrthancServer/Plugins/Samples/Common/IDicomDataset.h OrthancServer/Plugins/Samples/Common/IOrthancConnection.cpp OrthancServer/Plugins/Samples/Common/IOrthancConnection.h OrthancServer/Plugins/Samples/Common/OrthancHttpConnection.cpp OrthancServer/Plugins/Samples/Common/OrthancHttpConnection.h OrthancServer/Plugins/Samples/Common/OrthancPluginConnection.cpp OrthancServer/Plugins/Samples/Common/OrthancPluginConnection.h OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h OrthancServer/Plugins/Samples/Common/OrthancPluginException.h OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake OrthancServer/Plugins/Samples/Common/SimplifiedOrthancDataset.cpp OrthancServer/Plugins/Samples/Common/SimplifiedOrthancDataset.h OrthancServer/Plugins/Samples/Common/VersionScript.map OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt OrthancServer/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/app.js OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/index.html OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/style.css OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt OrthancServer/Plugins/Samples/CustomImageDecoder/Plugin.cpp OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp OrthancServer/Plugins/Samples/ModalityWorklists/README OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl OrthancServer/Plugins/Samples/README.txt OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp OrthancServer/Plugins/Samples/ServeFolders/README OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt OrthancServer/Plugins/Samples/StorageCommitmentScp/Plugin.cpp OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt OrthancServer/Plugins/Samples/WebSkeleton/Configuration.h OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake OrthancServer/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp OrthancServer/Plugins/Samples/WebSkeleton/NOTES.txt OrthancServer/Plugins/Samples/WebSkeleton/StaticResources/index.html OrthancServer/PrecompiledHeadersServer.cpp OrthancServer/PrecompiledHeadersServer.h OrthancServer/QueryRetrieveHandler.cpp OrthancServer/QueryRetrieveHandler.h OrthancServer/Resources/Configuration.json OrthancServer/Resources/DicomConformanceStatement.py OrthancServer/Resources/DicomConformanceStatement.txt OrthancServer/Resources/ErrorCodes.json OrthancServer/Resources/GenerateAnonymizationProfile.py OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.h OrthancServer/Resources/Graveyard/DatabasePluginSample/CMakeLists.txt OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.cpp OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.h OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h OrthancServer/Resources/Graveyard/DatabasePluginSample/Plugin.cpp OrthancServer/Resources/Graveyard/SetupAnonymization2011.cpp OrthancServer/Resources/ImplementationNotes/JobsEngineStates.dot OrthancServer/Resources/ImplementationNotes/JobsEngineStates.pdf OrthancServer/Resources/OldBuildInstructions.txt OrthancServer/Resources/Orthanc.doxygen OrthancServer/Resources/OrthancLogo.png OrthancServer/Resources/OrthancLogoDocumentation.png OrthancServer/Resources/OrthancPlugin.doxygen OrthancServer/Resources/Samples/CppHelpers/Logging/ILogger.h OrthancServer/Resources/Samples/CppHelpers/Logging/NullLogger.h OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.h OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h OrthancServer/Resources/Samples/CppHelpers/README.md OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py OrthancServer/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua OrthancServer/Resources/Samples/Lua/Autorouting.lua OrthancServer/Resources/Samples/Lua/AutoroutingConditional.lua OrthancServer/Resources/Samples/Lua/AutoroutingModification.lua OrthancServer/Resources/Samples/Lua/CallDcm2Xml.lua OrthancServer/Resources/Samples/Lua/CallImageJ.lua OrthancServer/Resources/Samples/Lua/CallWebService.js OrthancServer/Resources/Samples/Lua/CallWebService.lua OrthancServer/Resources/Samples/Lua/IncomingFindRequestFilter.lua OrthancServer/Resources/Samples/Lua/ModifyInstanceWithSequence.lua OrthancServer/Resources/Samples/Lua/OnStableStudy.lua OrthancServer/Resources/Samples/Lua/ParseDoseReport.lua OrthancServer/Resources/Samples/Lua/TransferSyntaxDisable.lua OrthancServer/Resources/Samples/Lua/TransferSyntaxEnable.lua OrthancServer/Resources/Samples/Lua/WriteToDisk.lua OrthancServer/Resources/Samples/Python/AnonymizeAllPatients.py OrthancServer/Resources/Samples/Python/ArchiveAllPatients.py OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py OrthancServer/Resources/Samples/Python/AutoClassify.py OrthancServer/Resources/Samples/Python/ChangesLoop.py OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py OrthancServer/Resources/Samples/Python/DeleteAllStudies.py OrthancServer/Resources/Samples/Python/DownloadAnonymized.py OrthancServer/Resources/Samples/Python/HighPerformanceAutoRouting.py OrthancServer/Resources/Samples/Python/ManualModification.py OrthancServer/Resources/Samples/Python/Replicate.py OrthancServer/Resources/Samples/Python/RestToolbox.py OrthancServer/Resources/Samples/README.txt OrthancServer/Resources/Samples/Tools/CMakeLists.txt OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer.js OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/drawing.js OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/index.html OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js OrthancServer/Resources/Samples/WebApplications/NodeToolbox.js OrthancServer/Resources/Samples/WebApplications/README.txt OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt OrthancServer/Resources/Testing/Issue32/Cpp/main.cpp OrthancServer/Resources/Testing/Issue32/Java/README.txt OrthancServer/Resources/Testing/Issue32/Java/pom.xml OrthancServer/Resources/Testing/Issue32/Java/src/test/java/io/osimis/AppTest.java OrthancServer/Resources/Toolbox.lua OrthancServer/Search/DatabaseConstraint.cpp OrthancServer/Search/DatabaseConstraint.h OrthancServer/Search/DatabaseLookup.cpp OrthancServer/Search/DatabaseLookup.h OrthancServer/Search/DicomTagConstraint.cpp OrthancServer/Search/DicomTagConstraint.h OrthancServer/Search/HierarchicalMatcher.cpp OrthancServer/Search/HierarchicalMatcher.h OrthancServer/Search/ISqlLookupFormatter.cpp OrthancServer/Search/ISqlLookupFormatter.h OrthancServer/ServerContext.cpp OrthancServer/ServerContext.h OrthancServer/ServerEnumerations.cpp OrthancServer/ServerEnumerations.h OrthancServer/ServerIndex.cpp OrthancServer/ServerIndex.h OrthancServer/ServerIndexChange.h OrthancServer/ServerJobs/ArchiveJob.cpp OrthancServer/ServerJobs/ArchiveJob.h OrthancServer/ServerJobs/CleaningInstancesJob.cpp OrthancServer/ServerJobs/CleaningInstancesJob.h OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.h OrthancServer/ServerJobs/DicomMoveScuJob.cpp OrthancServer/ServerJobs/DicomMoveScuJob.h OrthancServer/ServerJobs/IStorageCommitmentFactory.h OrthancServer/ServerJobs/LuaJobManager.cpp OrthancServer/ServerJobs/LuaJobManager.h OrthancServer/ServerJobs/MergeStudyJob.cpp OrthancServer/ServerJobs/MergeStudyJob.h OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp OrthancServer/ServerJobs/Operations/StorePeerOperation.h OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp OrthancServer/ServerJobs/Operations/StoreScuOperation.h OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp OrthancServer/ServerJobs/Operations/SystemCallOperation.h OrthancServer/ServerJobs/OrthancJobUnserializer.cpp OrthancServer/ServerJobs/OrthancJobUnserializer.h OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/ServerJobs/OrthancPeerStoreJob.h OrthancServer/ServerJobs/ResourceModificationJob.cpp OrthancServer/ServerJobs/ResourceModificationJob.h OrthancServer/ServerJobs/SplitStudyJob.cpp OrthancServer/ServerJobs/SplitStudyJob.h OrthancServer/ServerJobs/StorageCommitmentScpJob.cpp OrthancServer/ServerJobs/StorageCommitmentScpJob.h OrthancServer/ServerToolbox.cpp OrthancServer/ServerToolbox.h OrthancServer/SliceOrdering.cpp OrthancServer/SliceOrdering.h OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h OrthancServer/Sources/Database/Compatibility/ICreateInstance.cpp OrthancServer/Sources/Database/Compatibility/ICreateInstance.h OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.cpp OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.h OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp OrthancServer/Sources/Database/Compatibility/ILookupResources.h OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp OrthancServer/Sources/Database/Compatibility/SetOfResources.h OrthancServer/Sources/Database/IDatabaseListener.h OrthancServer/Sources/Database/IDatabaseWrapper.h OrthancServer/Sources/Database/InstallTrackAttachmentsSize.sql OrthancServer/Sources/Database/PrepareDatabase.sql OrthancServer/Sources/Database/ResourcesContent.cpp OrthancServer/Sources/Database/ResourcesContent.h OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h OrthancServer/Sources/Database/Upgrade3To4.sql OrthancServer/Sources/Database/Upgrade4To5.sql OrthancServer/Sources/DicomInstanceOrigin.cpp OrthancServer/Sources/DicomInstanceOrigin.h OrthancServer/Sources/DicomInstanceToStore.cpp OrthancServer/Sources/DicomInstanceToStore.h OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp OrthancServer/Sources/EmbeddedResourceHttpHandler.h OrthancServer/Sources/ExportedResource.cpp OrthancServer/Sources/ExportedResource.h OrthancServer/Sources/IDicomImageDecoder.h OrthancServer/Sources/IServerListener.h OrthancServer/Sources/LuaScripting.cpp OrthancServer/Sources/LuaScripting.h OrthancServer/Sources/OrthancConfiguration.cpp OrthancServer/Sources/OrthancConfiguration.h OrthancServer/Sources/OrthancFindRequestHandler.cpp OrthancServer/Sources/OrthancFindRequestHandler.h OrthancServer/Sources/OrthancGetRequestHandler.cpp OrthancServer/Sources/OrthancGetRequestHandler.h OrthancServer/Sources/OrthancHttpHandler.cpp OrthancServer/Sources/OrthancHttpHandler.h OrthancServer/Sources/OrthancInitialization.cpp OrthancServer/Sources/OrthancInitialization.h OrthancServer/Sources/OrthancMoveRequestHandler.cpp OrthancServer/Sources/OrthancMoveRequestHandler.h OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp OrthancServer/Sources/PrecompiledHeadersServer.cpp OrthancServer/Sources/PrecompiledHeadersServer.h OrthancServer/Sources/QueryRetrieveHandler.cpp OrthancServer/Sources/QueryRetrieveHandler.h OrthancServer/Sources/Search/DatabaseConstraint.cpp OrthancServer/Sources/Search/DatabaseConstraint.h OrthancServer/Sources/Search/DatabaseLookup.cpp OrthancServer/Sources/Search/DatabaseLookup.h OrthancServer/Sources/Search/DicomTagConstraint.cpp OrthancServer/Sources/Search/DicomTagConstraint.h OrthancServer/Sources/Search/HierarchicalMatcher.cpp OrthancServer/Sources/Search/HierarchicalMatcher.h OrthancServer/Sources/Search/ISqlLookupFormatter.cpp OrthancServer/Sources/Search/ISqlLookupFormatter.h OrthancServer/Sources/ServerContext.cpp OrthancServer/Sources/ServerContext.h OrthancServer/Sources/ServerEnumerations.cpp OrthancServer/Sources/ServerEnumerations.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h OrthancServer/Sources/ServerIndexChange.h OrthancServer/Sources/ServerJobs/ArchiveJob.cpp OrthancServer/Sources/ServerJobs/ArchiveJob.h OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h OrthancServer/Sources/ServerJobs/IStorageCommitmentFactory.h OrthancServer/Sources/ServerJobs/LuaJobManager.cpp OrthancServer/Sources/ServerJobs/LuaJobManager.h OrthancServer/Sources/ServerJobs/MergeStudyJob.cpp OrthancServer/Sources/ServerJobs/MergeStudyJob.h OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp OrthancServer/Sources/ServerJobs/Operations/DeleteResourceOperation.h OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.h OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp OrthancServer/Sources/ServerJobs/Operations/ModifyInstanceOperation.h OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.cpp OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.h OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.h OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.h OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.h OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp OrthancServer/Sources/ServerJobs/ResourceModificationJob.h OrthancServer/Sources/ServerJobs/SplitStudyJob.cpp OrthancServer/Sources/ServerJobs/SplitStudyJob.h OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h OrthancServer/Sources/ServerToolbox.cpp OrthancServer/Sources/ServerToolbox.h OrthancServer/Sources/SliceOrdering.cpp OrthancServer/Sources/SliceOrdering.h OrthancServer/Sources/StorageCommitmentReports.cpp OrthancServer/Sources/StorageCommitmentReports.h OrthancServer/Sources/main.cpp OrthancServer/StorageCommitmentReports.cpp OrthancServer/StorageCommitmentReports.h OrthancServer/UnitTestsSources/BitbucketCACertificates.h OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp OrthancServer/UnitTestsSources/DicomMapTests.cpp OrthancServer/UnitTestsSources/FileStorageTests.cpp OrthancServer/UnitTestsSources/FromDcmtkTests.cpp OrthancServer/UnitTestsSources/ImageTests.cpp OrthancServer/UnitTestsSources/LuaTests.cpp OrthancServer/UnitTestsSources/MemoryCacheTests.cpp OrthancServer/UnitTestsSources/MultiThreadingTests.cpp OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.cpp OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.h OrthancServer/UnitTestsSources/ServerIndexTests.cpp OrthancServer/UnitTestsSources/UnitTestsMain.cpp OrthancServer/UnitTestsSources/VersionsTests.cpp OrthancServer/main.cpp Plugins/Engine/IPluginServiceProvider.h Plugins/Engine/OrthancPluginDatabase.cpp Plugins/Engine/OrthancPluginDatabase.h Plugins/Engine/OrthancPlugins.cpp Plugins/Engine/OrthancPlugins.h Plugins/Engine/PluginsEnumerations.cpp Plugins/Engine/PluginsEnumerations.h Plugins/Engine/PluginsErrorDictionary.cpp Plugins/Engine/PluginsErrorDictionary.h Plugins/Engine/PluginsJob.cpp Plugins/Engine/PluginsJob.h Plugins/Engine/PluginsManager.cpp Plugins/Engine/PluginsManager.h Plugins/Include/orthanc/OrthancCDatabasePlugin.h Plugins/Include/orthanc/OrthancCPlugin.h Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp Plugins/Samples/Basic/CMakeLists.txt Plugins/Samples/Basic/Plugin.c Plugins/Samples/Common/DicomDatasetReader.cpp Plugins/Samples/Common/DicomDatasetReader.h Plugins/Samples/Common/DicomPath.cpp Plugins/Samples/Common/DicomPath.h Plugins/Samples/Common/DicomTag.cpp Plugins/Samples/Common/DicomTag.h Plugins/Samples/Common/ExportedSymbols.list Plugins/Samples/Common/FullOrthancDataset.cpp Plugins/Samples/Common/FullOrthancDataset.h Plugins/Samples/Common/IDicomDataset.h Plugins/Samples/Common/IOrthancConnection.cpp Plugins/Samples/Common/IOrthancConnection.h Plugins/Samples/Common/OrthancHttpConnection.cpp Plugins/Samples/Common/OrthancHttpConnection.h Plugins/Samples/Common/OrthancPluginConnection.cpp Plugins/Samples/Common/OrthancPluginConnection.h Plugins/Samples/Common/OrthancPluginCppWrapper.cpp Plugins/Samples/Common/OrthancPluginCppWrapper.h Plugins/Samples/Common/OrthancPluginException.h Plugins/Samples/Common/OrthancPlugins.cmake Plugins/Samples/Common/SimplifiedOrthancDataset.cpp Plugins/Samples/Common/SimplifiedOrthancDataset.h Plugins/Samples/Common/VersionScript.map Plugins/Samples/ConnectivityChecks/CMakeLists.txt Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake Plugins/Samples/ConnectivityChecks/Plugin.cpp Plugins/Samples/ConnectivityChecks/WebResources/app.js Plugins/Samples/ConnectivityChecks/WebResources/index.html Plugins/Samples/ConnectivityChecks/WebResources/style.css Plugins/Samples/CustomImageDecoder/CMakeLists.txt Plugins/Samples/CustomImageDecoder/Plugin.cpp Plugins/Samples/ModalityWorklists/CMakeLists.txt Plugins/Samples/ModalityWorklists/Plugin.cpp Plugins/Samples/ModalityWorklists/README Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl Plugins/Samples/README.txt Plugins/Samples/ServeFolders/CMakeLists.txt Plugins/Samples/ServeFolders/Plugin.cpp Plugins/Samples/ServeFolders/README Plugins/Samples/StorageArea/CMakeLists.txt Plugins/Samples/StorageArea/Plugin.cpp Plugins/Samples/StorageCommitmentScp/CMakeLists.txt Plugins/Samples/StorageCommitmentScp/Plugin.cpp Plugins/Samples/WebSkeleton/CMakeLists.txt Plugins/Samples/WebSkeleton/Configuration.h Plugins/Samples/WebSkeleton/Framework/EmbedResources.py Plugins/Samples/WebSkeleton/Framework/Framework.cmake Plugins/Samples/WebSkeleton/Framework/Plugin.cpp Plugins/Samples/WebSkeleton/NOTES.txt Plugins/Samples/WebSkeleton/StaticResources/index.html Resources/CMake/AutoGeneratedCode.cmake Resources/CMake/BoostConfiguration.cmake Resources/CMake/BoostConfiguration.sh Resources/CMake/CivetwebConfiguration.cmake Resources/CMake/Compiler.cmake Resources/CMake/DcmtkConfiguration.cmake Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake Resources/CMake/DownloadOrthancFramework.cmake Resources/CMake/DownloadPackage.cmake Resources/CMake/GoogleTestConfiguration.cmake Resources/CMake/JsonCppConfiguration.cmake Resources/CMake/LibCurlConfiguration.cmake Resources/CMake/LibIconvConfiguration.cmake Resources/CMake/LibIcuConfiguration.cmake Resources/CMake/LibJpegConfiguration.cmake Resources/CMake/LibP11Configuration.cmake Resources/CMake/LibPngConfiguration.cmake Resources/CMake/LuaConfiguration.cmake Resources/CMake/MongooseConfiguration.cmake Resources/CMake/OpenSslConfiguration.cmake Resources/CMake/OpenSslConfigurationStatic-1.0.2.cmake Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake Resources/CMake/OrthancFrameworkConfiguration.cmake Resources/CMake/OrthancFrameworkParameters.cmake Resources/CMake/PugixmlConfiguration.cmake Resources/CMake/SQLiteConfiguration.cmake Resources/CMake/Uninstall.cmake.in Resources/CMake/UuidConfiguration.cmake Resources/CMake/VisualStudioPrecompiledHeaders.cmake Resources/CMake/ZlibConfiguration.cmake Resources/Configuration.json Resources/CrossToolchain.cmake Resources/DcmtkTools/CMakeLists.txt Resources/DcmtkTools/dummy.cpp Resources/DicomConformanceStatement.py Resources/DicomConformanceStatement.txt Resources/DicomTransferSyntaxes.json Resources/EclipseCodingStyle.xml Resources/EmbedResources.py Resources/EncodingTests.h Resources/EncodingTests.py Resources/ErrorCodes.json Resources/Fonts/GenerateFont.py Resources/Fonts/README.txt Resources/Fonts/UbuntuMonoBold-16.json Resources/GenerateAnonymizationProfile.py Resources/GenerateErrorCodes.py Resources/GenerateTransferSyntaxes.py Resources/GenerateTransferSyntaxesDcmtk.mustache Resources/GenerateTransferSyntaxesEnumerations.mustache Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp Resources/Graveyard/DatabaseOptimizations/LookupResource.h Resources/Graveyard/DatabasePluginSample/CMakeLists.txt Resources/Graveyard/DatabasePluginSample/Database.cpp Resources/Graveyard/DatabasePluginSample/Database.h Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h Resources/Graveyard/DatabasePluginSample/Plugin.cpp Resources/Graveyard/FromDcmtkBridge.cpp Resources/Graveyard/Multithreading/BagOfTasks.h Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp Resources/Graveyard/Multithreading/BagOfTasksProcessor.h Resources/Graveyard/Multithreading/ICommand.h Resources/Graveyard/Multithreading/ILockable.h Resources/Graveyard/Multithreading/Locker.h Resources/Graveyard/Multithreading/Mutex.cpp Resources/Graveyard/Multithreading/Mutex.h Resources/Graveyard/Multithreading/ReaderWriterLock.cpp Resources/Graveyard/Multithreading/ReaderWriterLock.h Resources/Graveyard/SetupAnonymization2011.cpp Resources/Graveyard/TestTranscoding.cpp Resources/ImplementationNotes/JobsEngineStates.dot Resources/ImplementationNotes/JobsEngineStates.pdf Resources/LinuxStandardBaseToolchain.cmake Resources/MinGW-W64-Toolchain32.cmake Resources/MinGW-W64-Toolchain64.cmake Resources/MinGWToolchain.cmake Resources/OldBuildInstructions.txt Resources/Orthanc.doxygen Resources/OrthancLogo.png Resources/OrthancLogoDocumentation.png Resources/OrthancPlugin.doxygen Resources/Patches/boost-1.65.1-linux-standard-base.patch Resources/Patches/boost-1.66.0-linux-standard-base.patch Resources/Patches/boost-1.67.0-linux-standard-base.patch Resources/Patches/boost-1.68.0-linux-standard-base.patch Resources/Patches/boost-1.69.0-linux-standard-base.patch Resources/Patches/civetweb-1.11.patch Resources/Patches/civetweb-1.12.patch Resources/Patches/curl-7.57.0-cmake.patch Resources/Patches/curl-7.64.0-cmake.patch Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch Resources/Patches/dcmtk-3.6.0-mingw64.patch Resources/Patches/dcmtk-3.6.0-speed.patch Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch Resources/Patches/dcmtk-3.6.2-private.dic Resources/Patches/dcmtk-3.6.2.patch Resources/Patches/dcmtk-3.6.4.patch Resources/Patches/dcmtk-3.6.5.patch Resources/Patches/dcmtk-dcdict_orthanc.cc Resources/Patches/dcmtk.txt Resources/Patches/e2fsprogs-1.43.8-apple.patch Resources/Patches/e2fsprogs-1.44.5-apple.patch Resources/Patches/libp11-0.4.0.patch Resources/Patches/mongoose-3.1-patch.diff Resources/Patches/mongoose-3.8-patch.diff Resources/Patches/openssl-1.1.1-conf.h.in Resources/Patches/openssl-1.1.1g.patch Resources/RetrieveCACertificates.py Resources/Samples/CppHelpers/Logging/ILogger.h Resources/Samples/CppHelpers/Logging/NullLogger.h Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp Resources/Samples/CppHelpers/Logging/OrthancLogger.h Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h Resources/Samples/CppHelpers/README.md Resources/Samples/ImportDicomFiles/ImportDicomFiles.py Resources/Samples/Lua/AutomatedJpeg2kCompression.lua Resources/Samples/Lua/Autorouting.lua Resources/Samples/Lua/AutoroutingConditional.lua Resources/Samples/Lua/AutoroutingModification.lua Resources/Samples/Lua/CallDcm2Xml.lua Resources/Samples/Lua/CallImageJ.lua Resources/Samples/Lua/CallWebService.js Resources/Samples/Lua/CallWebService.lua Resources/Samples/Lua/IncomingFindRequestFilter.lua Resources/Samples/Lua/ModifyInstanceWithSequence.lua Resources/Samples/Lua/OnStableStudy.lua Resources/Samples/Lua/ParseDoseReport.lua Resources/Samples/Lua/TransferSyntaxDisable.lua Resources/Samples/Lua/TransferSyntaxEnable.lua Resources/Samples/Lua/WriteToDisk.lua Resources/Samples/OrthancFramework/MicroService/CMakeLists.txt Resources/Samples/OrthancFramework/MicroService/README.txt Resources/Samples/OrthancFramework/MicroService/Sample.cpp Resources/Samples/Python/AnonymizeAllPatients.py Resources/Samples/Python/ArchiveAllPatients.py Resources/Samples/Python/ArchiveStudiesInTimeRange.py Resources/Samples/Python/AutoClassify.py Resources/Samples/Python/ChangesLoop.py Resources/Samples/Python/ContinuousPatientAnonymization.py Resources/Samples/Python/DeleteAllStudies.py Resources/Samples/Python/DownloadAnonymized.py Resources/Samples/Python/HighPerformanceAutoRouting.py Resources/Samples/Python/ManualModification.py Resources/Samples/Python/Replicate.py Resources/Samples/Python/RestToolbox.py Resources/Samples/README.txt Resources/Samples/Tools/CMakeLists.txt Resources/Samples/Tools/RecoverCompressedFile.cpp Resources/Samples/WebApplications/DrawingDicomizer.js Resources/Samples/WebApplications/DrawingDicomizer/drawing.js Resources/Samples/WebApplications/DrawingDicomizer/index.html Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js Resources/Samples/WebApplications/NodeToolbox.js Resources/Samples/WebApplications/README.txt Resources/Testing/Issue32/Cpp/CMakeLists.txt Resources/Testing/Issue32/Cpp/main.cpp Resources/Testing/Issue32/Java/README.txt Resources/Testing/Issue32/Java/pom.xml Resources/Testing/Issue32/Java/src/test/java/io/osimis/AppTest.java Resources/ThirdParty/VisualStudio/stdint.h Resources/ThirdParty/base64/base64.cpp Resources/ThirdParty/base64/base64.h Resources/ThirdParty/icu/CMakeLists.txt Resources/ThirdParty/icu/README.txt Resources/ThirdParty/icu/Version.cmake Resources/ThirdParty/md5/md5.c Resources/ThirdParty/md5/md5.h Resources/ThirdParty/minizip/NOTES Resources/ThirdParty/minizip/crypt.h Resources/ThirdParty/minizip/ioapi.c Resources/ThirdParty/minizip/ioapi.h Resources/ThirdParty/minizip/zip.c Resources/ThirdParty/minizip/zip.h Resources/ThirdParty/patch/NOTES.txt Resources/ThirdParty/patch/msys-1.0.dll Resources/ThirdParty/patch/patch.exe Resources/ThirdParty/patch/patch.exe.manifest Resources/Toolbox.lua Resources/WebAssembly/ArithmeticTests/CMakeLists.txt Resources/WebAssembly/ArithmeticTests/Run2.cpp Resources/WebAssembly/ArithmeticTests/app.js Resources/WebAssembly/ArithmeticTests/arith.patch Resources/WebAssembly/ArithmeticTests/index.html Resources/WebAssembly/arith.h Resources/WindowsResources.py Resources/WindowsResources.rc UnitTestsSources/BitbucketCACertificates.h UnitTestsSources/DatabaseLookupTests.cpp UnitTestsSources/DicomMapTests.cpp UnitTestsSources/FileStorageTests.cpp UnitTestsSources/FrameworkTests.cpp UnitTestsSources/FromDcmtkTests.cpp UnitTestsSources/ImageProcessingTests.cpp UnitTestsSources/ImageTests.cpp UnitTestsSources/JpegLosslessTests.cpp UnitTestsSources/LoggingTests.cpp UnitTestsSources/LuaTests.cpp UnitTestsSources/MemoryCacheTests.cpp UnitTestsSources/MultiThreadingTests.cpp UnitTestsSources/PluginsTests.cpp UnitTestsSources/PrecompiledHeadersUnitTests.cpp UnitTestsSources/PrecompiledHeadersUnitTests.h UnitTestsSources/RestApiTests.cpp UnitTestsSources/SQLiteChromiumTests.cpp UnitTestsSources/SQLiteTests.cpp UnitTestsSources/ServerIndexTests.cpp UnitTestsSources/StreamTests.cpp UnitTestsSources/ToolboxTests.cpp UnitTestsSources/UnitTestsMain.cpp UnitTestsSources/VersionsTests.cpp UnitTestsSources/ZipTests.cpp
diffstat 1473 files changed, 184047 insertions(+), 183373 deletions(-) [+]
line wrap: on
line diff
--- a/COPYING	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,674 +0,0 @@
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
-  The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works.  By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users.  We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors.  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
-  To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights.  Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received.  You must make sure that they, too, receive
-or can get the source code.  And you must show them these terms so they
-know their rights.
-
-  Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
-  For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software.  For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
-  Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so.  This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software.  The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable.  Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products.  If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
-  Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary.  To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                       TERMS AND CONDITIONS
-
-  0. Definitions.
-
-  "This License" refers to version 3 of the GNU General Public License.
-
-  "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
-  "The Program" refers to any copyrightable work licensed under this
-License.  Each licensee is addressed as "you".  "Licensees" and
-"recipients" may be individuals or organizations.
-
-  To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy.  The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
-  A "covered work" means either the unmodified Program or a work based
-on the Program.
-
-  To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy.  Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
-  To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies.  Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
-  An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License.  If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
-  1. Source Code.
-
-  The "source code" for a work means the preferred form of the work
-for making modifications to it.  "Object code" means any non-source
-form of a work.
-
-  A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
-  The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form.  A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
-  The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities.  However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work.  For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
-  The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
-  The Corresponding Source for a work in source code form is that
-same work.
-
-  2. Basic Permissions.
-
-  All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met.  This License explicitly affirms your unlimited
-permission to run the unmodified Program.  The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work.  This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
-  You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force.  You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright.  Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
-  Conveying under any other circumstances is permitted solely under
-the conditions stated below.  Sublicensing is not allowed; section 10
-makes it unnecessary.
-
-  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
-  No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
-  When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
-  4. Conveying Verbatim Copies.
-
-  You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
-  You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
-  5. Conveying Modified Source Versions.
-
-  You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
-    a) The work must carry prominent notices stating that you modified
-    it, and giving a relevant date.
-
-    b) The work must carry prominent notices stating that it is
-    released under this License and any conditions added under section
-    7.  This requirement modifies the requirement in section 4 to
-    "keep intact all notices".
-
-    c) You must license the entire work, as a whole, under this
-    License to anyone who comes into possession of a copy.  This
-    License will therefore apply, along with any applicable section 7
-    additional terms, to the whole of the work, and all its parts,
-    regardless of how they are packaged.  This License gives no
-    permission to license the work in any other way, but it does not
-    invalidate such permission if you have separately received it.
-
-    d) If the work has interactive user interfaces, each must display
-    Appropriate Legal Notices; however, if the Program has interactive
-    interfaces that do not display Appropriate Legal Notices, your
-    work need not make them do so.
-
-  A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit.  Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
-  6. Conveying Non-Source Forms.
-
-  You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
-    a) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by the
-    Corresponding Source fixed on a durable physical medium
-    customarily used for software interchange.
-
-    b) Convey the object code in, or embodied in, a physical product
-    (including a physical distribution medium), accompanied by a
-    written offer, valid for at least three years and valid for as
-    long as you offer spare parts or customer support for that product
-    model, to give anyone who possesses the object code either (1) a
-    copy of the Corresponding Source for all the software in the
-    product that is covered by this License, on a durable physical
-    medium customarily used for software interchange, for a price no
-    more than your reasonable cost of physically performing this
-    conveying of source, or (2) access to copy the
-    Corresponding Source from a network server at no charge.
-
-    c) Convey individual copies of the object code with a copy of the
-    written offer to provide the Corresponding Source.  This
-    alternative is allowed only occasionally and noncommercially, and
-    only if you received the object code with such an offer, in accord
-    with subsection 6b.
-
-    d) Convey the object code by offering access from a designated
-    place (gratis or for a charge), and offer equivalent access to the
-    Corresponding Source in the same way through the same place at no
-    further charge.  You need not require recipients to copy the
-    Corresponding Source along with the object code.  If the place to
-    copy the object code is a network server, the Corresponding Source
-    may be on a different server (operated by you or a third party)
-    that supports equivalent copying facilities, provided you maintain
-    clear directions next to the object code saying where to find the
-    Corresponding Source.  Regardless of what server hosts the
-    Corresponding Source, you remain obligated to ensure that it is
-    available for as long as needed to satisfy these requirements.
-
-    e) Convey the object code using peer-to-peer transmission, provided
-    you inform other peers where the object code and Corresponding
-    Source of the work are being offered to the general public at no
-    charge under subsection 6d.
-
-  A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
-  A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling.  In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage.  For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product.  A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
-  "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source.  The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
-  If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information.  But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
-  The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed.  Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
-  Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
-  7. Additional Terms.
-
-  "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law.  If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
-  When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it.  (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.)  You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
-  Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
-    a) Disclaiming warranty or limiting liability differently from the
-    terms of sections 15 and 16 of this License; or
-
-    b) Requiring preservation of specified reasonable legal notices or
-    author attributions in that material or in the Appropriate Legal
-    Notices displayed by works containing it; or
-
-    c) Prohibiting misrepresentation of the origin of that material, or
-    requiring that modified versions of such material be marked in
-    reasonable ways as different from the original version; or
-
-    d) Limiting the use for publicity purposes of names of licensors or
-    authors of the material; or
-
-    e) Declining to grant rights under trademark law for use of some
-    trade names, trademarks, or service marks; or
-
-    f) Requiring indemnification of licensors and authors of that
-    material by anyone who conveys the material (or modified versions of
-    it) with contractual assumptions of liability to the recipient, for
-    any liability that these contractual assumptions directly impose on
-    those licensors and authors.
-
-  All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10.  If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term.  If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
-  If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
-  Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
-  8. Termination.
-
-  You may not propagate or modify a covered work except as expressly
-provided under this License.  Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
-  However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
-  Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
-  Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License.  If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
-  9. Acceptance Not Required for Having Copies.
-
-  You are not required to accept this License in order to receive or
-run a copy of the Program.  Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance.  However,
-nothing other than this License grants you permission to propagate or
-modify any covered work.  These actions infringe copyright if you do
-not accept this License.  Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
-  10. Automatic Licensing of Downstream Recipients.
-
-  Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License.  You are not responsible
-for enforcing compliance by third parties with this License.
-
-  An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations.  If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
-  You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License.  For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
-  11. Patents.
-
-  A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based.  The
-work thus licensed is called the contributor's "contributor version".
-
-  A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version.  For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
-  Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
-  In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement).  To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
-  If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients.  "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
-  If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
-  A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License.  You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
-  Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
-  12. No Surrender of Others' Freedom.
-
-  If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all.  For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
-  13. Use with the GNU Affero General Public License.
-
-  Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work.  The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
-  14. Revised Versions of this License.
-
-  The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-  Each version is given a distinguishing version number.  If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation.  If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
-  If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
-  Later license versions may give you additional or different
-permissions.  However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
-  15. Disclaimer of Warranty.
-
-  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. Limitation of Liability.
-
-  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
-  17. Interpretation of Sections 15 and 16.
-
-  If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU 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/>.
-
-Also add information on how to contact you by electronic and paper mail.
-
-  If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
-    <program>  Copyright (C) <year>  <name of author>
-    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
-  You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-<http://www.gnu.org/licenses/>.
-
-  The GNU General Public License does not permit incorporating your program
-into proprietary programs.  If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.  But first, please read
-<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- a/Core/Cache/ICachePageProvider.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../IDynamicObject.h"
-
-namespace Orthanc
-{
-  namespace Deprecated
-  {
-    class ICachePageProvider
-    {
-    public:
-      virtual ~ICachePageProvider()
-      {
-      }
-
-      virtual IDynamicObject* Provide(const std::string& id) = 0;
-    };
-  }
-}
--- a/Core/Cache/ICacheable.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 ICacheable : public boost::noncopyable
-  {
-  public:
-    virtual ~ICacheable()
-    {
-    }
-
-    virtual size_t GetMemoryUsage() const = 0;
-  };
-}
--- a/Core/Cache/LeastRecentlyUsedIndex.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,359 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <map>
-#include <vector>
-#include <boost/noncopyable.hpp>
-#include <cassert>
-
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-namespace Orthanc
-{
-  /**
-   * This class implements the index of a cache with least recently
-   * used (LRU) recycling policy. All the items of the cache index
-   * can be associated with a payload.
-   * Reference: http://stackoverflow.com/a/2504317
-   **/
-  template <typename T, typename Payload = NullType>
-  class LeastRecentlyUsedIndex : public boost::noncopyable
-  {
-  private:
-    typedef std::list< std::pair<T, Payload> >  Queue;
-    typedef std::map<T, typename Queue::iterator>  Index;
-
-    Index  index_;
-    Queue  queue_;
-
-    /**
-     * Internal method for debug builds to check whether the internal
-     * data structures are not corrupted.
-     **/
-    void CheckInvariants() const;
-
-  public:
-    /**
-     * Add a new element to the cache index, and make it the most
-     * recent element.
-     * \param id The ID of the element.
-     * \param payload The payload of the element.
-     **/
-    void Add(T id, Payload payload = Payload());
-
-    void AddOrMakeMostRecent(T id, Payload payload = Payload());
-
-    /**
-     * When accessing an element of the cache, this method tags the
-     * element as the most recently used.
-     * \param id The most recently accessed item.
-     **/
-    void MakeMostRecent(T id);
-
-    void MakeMostRecent(T id, Payload updatedPayload);
-
-    /**
-     * Remove an element from the cache index.
-     * \param id The item to remove.
-     **/
-    Payload Invalidate(T id);
-
-    /**
-     * Get the oldest element in the cache and remove it.
-     * \return The oldest item.
-     **/
-    T RemoveOldest();
-
-    /**
-     * Get the oldest element in the cache, remove it and return the
-     * associated payload.
-     * \param payload Where to store the associated payload.
-     * \return The oldest item.
-     **/
-    T RemoveOldest(Payload& payload);
-
-    /**
-     * Check whether an element is contained in the cache.
-     * \param id The item.
-     * \return \c true iff the item is indexed by the cache.
-     **/
-    bool Contains(T id) const
-    {
-      return index_.find(id) != index_.end();
-    }
-
-    bool Contains(T id, Payload& payload) const
-    {
-      typename Index::const_iterator it = index_.find(id);
-      if (it == index_.end())
-      {
-        return false;
-      }
-      else
-      {
-        payload = it->second->second;
-        return true;
-      }
-    }
-
-    /**
-     * Return the number of elements in the cache.
-     * \return The number of elements.
-     **/
-    size_t GetSize() const
-    {
-      assert(index_.size() == queue_.size());
-      return queue_.size();
-    }
-
-    /**
-     * Check whether the cache index is empty.
-     * \return \c true iff the cache is empty.
-     **/
-    bool IsEmpty() const
-    {
-      return index_.empty();
-    }
-
-    const T& GetOldest() const;
-    
-    const Payload& GetOldestPayload() const;
-
-    void GetAllKeys(std::vector<T>& keys) const
-    {
-      keys.clear();
-      keys.reserve(GetSize());
-      for (typename Index::const_iterator it = index_.begin(); it != index_.end(); it++)
-      {
-        keys.push_back(it->first);
-      }
-    }
-
-  };
-
-
-
-
-  /******************************************************************
-   ** Implementation of the template
-   ******************************************************************/
-
-  template <typename T, typename Payload>
-  void LeastRecentlyUsedIndex<T, Payload>::CheckInvariants() const
-  {
-#ifndef NDEBUG
-    assert(index_.size() == queue_.size());
-
-    for (typename Index::const_iterator 
-           it = index_.begin(); it != index_.end(); it++)
-    {
-      assert(it->second != queue_.end());
-      assert(it->second->first == it->first);
-    }
-#endif
-  }
-
-
-  template <typename T, typename Payload>
-  void LeastRecentlyUsedIndex<T, Payload>::Add(T id, Payload payload)
-  {
-    if (Contains(id))
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    queue_.push_front(std::make_pair(id, payload));
-    index_[id] = queue_.begin();
-
-    CheckInvariants();
-  }
-
-
-  template <typename T, typename Payload>
-  void LeastRecentlyUsedIndex<T, Payload>::MakeMostRecent(T id)
-  {
-    if (!Contains(id))
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    typename Index::iterator it = index_.find(id);
-    assert(it != index_.end());
-
-    std::pair<T, Payload> item = *(it->second);
-    
-    queue_.erase(it->second);
-    queue_.push_front(item);
-    index_[id] = queue_.begin();
-
-    CheckInvariants();
-  }
-
-
-  template <typename T, typename Payload>
-  void LeastRecentlyUsedIndex<T, Payload>::AddOrMakeMostRecent(T id, Payload payload)
-  {
-    typename Index::iterator it = index_.find(id);
-
-    if (it != index_.end())
-    {
-      // Already existing. Make it most recent.
-      std::pair<T, Payload> item = *(it->second);
-      item.second = payload;
-      queue_.erase(it->second);
-      queue_.push_front(item);
-    }
-    else
-    {
-      // New item
-      queue_.push_front(std::make_pair(id, payload));
-    }
-
-    index_[id] = queue_.begin();
-
-    CheckInvariants();
-  }
-
-
-  template <typename T, typename Payload>
-  void LeastRecentlyUsedIndex<T, Payload>::MakeMostRecent(T id, Payload updatedPayload)
-  {
-    if (!Contains(id))
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    typename Index::iterator it = index_.find(id);
-    assert(it != index_.end());
-
-    std::pair<T, Payload> item = *(it->second);
-    item.second = updatedPayload;
-    
-    queue_.erase(it->second);
-    queue_.push_front(item);
-    index_[id] = queue_.begin();
-
-    CheckInvariants();
-  }
-
-
-  template <typename T, typename Payload>
-  Payload LeastRecentlyUsedIndex<T, Payload>::Invalidate(T id)
-  {
-    if (!Contains(id))
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    typename Index::iterator it = index_.find(id);
-    assert(it != index_.end());
-
-    Payload payload = it->second->second;
-    queue_.erase(it->second);
-    index_.erase(it);
-
-    CheckInvariants();
-    return payload;
-  }
-
-
-  template <typename T, typename Payload>
-  T LeastRecentlyUsedIndex<T, Payload>::RemoveOldest(Payload& payload)
-  {
-    if (IsEmpty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    std::pair<T, Payload> item = queue_.back();
-    T oldest = item.first;
-    payload = item.second;
-
-    queue_.pop_back();
-    assert(index_.find(oldest) != index_.end());
-    index_.erase(oldest);
-
-    CheckInvariants();
-
-    return oldest;
-  }
-
-
-  template <typename T, typename Payload>
-  T LeastRecentlyUsedIndex<T, Payload>::RemoveOldest()
-  {
-    if (IsEmpty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    std::pair<T, Payload> item = queue_.back();
-    T oldest = item.first;
-
-    queue_.pop_back();
-    assert(index_.find(oldest) != index_.end());
-    index_.erase(oldest);
-
-    CheckInvariants();
-
-    return oldest;
-  }
-
-
-  template <typename T, typename Payload>
-  const T& LeastRecentlyUsedIndex<T, Payload>::GetOldest() const
-  {
-    if (IsEmpty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    return queue_.back().first;
-  }
-
-
-  template <typename T, typename Payload>
-  const Payload& LeastRecentlyUsedIndex<T, Payload>::GetOldestPayload() const
-  {
-    if (IsEmpty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    return queue_.back().second;
-  }
-}
--- a/Core/Cache/MemoryCache.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MemoryCache.h"
-
-#include "../Logging.h"
-
-namespace Orthanc
-{
-  namespace Deprecated
-  {
-    MemoryCache::Page& MemoryCache::Load(const std::string& id)
-    {
-      // Reuse the cache entry if it already exists
-      Page* p = NULL;
-      if (index_.Contains(id, p))
-      {
-        VLOG(1) << "Reusing a cache page";
-        assert(p != NULL);
-        index_.MakeMostRecent(id);
-        return *p;
-      }
-
-      // The id is not in the cache yet. Make some room if the cache
-      // is full.
-      if (index_.GetSize() == cacheSize_)
-      {
-        VLOG(1) << "Dropping the oldest cache page";
-        index_.RemoveOldest(p);
-        delete p;
-      }
-
-      // Create a new cache page
-      std::unique_ptr<Page> result(new Page);
-      result->id_ = id;
-      result->content_.reset(provider_.Provide(id));
-
-      // Add the newly create page to the cache
-      VLOG(1) << "Registering new data in a cache page";
-      p = result.release();
-      index_.Add(id, p);
-      return *p;
-    }
-
-    MemoryCache::MemoryCache(ICachePageProvider& provider,
-                             size_t cacheSize) : 
-      provider_(provider),
-      cacheSize_(cacheSize)
-    {
-    }
-
-    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())
-      {
-        Page* element = NULL;
-        index_.RemoveOldest(element);
-        assert(element != NULL);
-        delete element;
-      }
-    }
-
-    IDynamicObject& MemoryCache::Access(const std::string& id)
-    {
-      return *Load(id).content_;
-    }
-  }
-}
--- a/Core/Cache/MemoryCache.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.h"
-#include "ICachePageProvider.h"
-#include "LeastRecentlyUsedIndex.h"
-
-#include <memory>
-
-namespace Orthanc
-{
-  namespace Deprecated
-  {
-    /**
-     * WARNING: This class is NOT thread-safe.
-     **/
-    class MemoryCache
-    {
-    private:
-      struct Page
-      {
-        std::string id_;
-        std::unique_ptr<IDynamicObject> content_;
-      };
-
-      ICachePageProvider& provider_;
-      size_t cacheSize_;
-      LeastRecentlyUsedIndex<std::string, Page*>  index_;
-
-      Page& Load(const std::string& id);
-
-    public:
-      MemoryCache(ICachePageProvider& provider,
-                  size_t cacheSize);
-
-      ~MemoryCache();
-
-      IDynamicObject& Access(const std::string& id);
-
-      void Invalidate(const std::string& id);
-    };
-  }
-}
--- a/Core/Cache/MemoryObjectCache.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,282 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MemoryObjectCache.h"
-
-#include "../Compatibility.h"
-
-namespace Orthanc
-{
-  class MemoryObjectCache::Item : public boost::noncopyable
-  {
-  private:
-    ICacheable*               value_;
-    boost::posix_time::ptime  time_;
-
-  public:
-    explicit Item(ICacheable* value) :   // Takes ownership
-    value_(value),
-    time_(boost::posix_time::second_clock::local_time())
-    {
-      if (value == NULL)
-      {
-        throw OrthancException(ErrorCode_NullPointer);
-      }
-    }
-
-    ~Item()
-    {
-      assert(value_ != NULL);
-      delete value_;
-    }
-
-    ICacheable& GetValue() const
-    {
-      assert(value_ != NULL);
-      return *value_;
-    }
-
-    const boost::posix_time::ptime& GetTime() const
-    {
-      return time_;
-    }
-  };
-
-
-  void MemoryObjectCache::Recycle(size_t targetSize)
-  {
-    // WARNING: "cacheMutex_" must be locked
-    while (currentSize_ > targetSize)
-    {
-      assert(!content_.IsEmpty());
-        
-      Item* item = NULL;
-      content_.RemoveOldest(item);
-
-      assert(item != NULL);
-      const size_t size = item->GetValue().GetMemoryUsage();
-      delete item;
-
-      assert(currentSize_ >= size);
-      currentSize_ -= size;
-    }
-
-    // Post-condition: "currentSize_ <= targetSize"
-  }
-    
-
-  MemoryObjectCache::MemoryObjectCache() :
-    currentSize_(0),
-    maxSize_(100 * 1024 * 1024)  // 100 MB
-  {
-  }
-
-
-  MemoryObjectCache::~MemoryObjectCache()
-  {
-    Recycle(0);
-    assert(content_.IsEmpty());
-  }
-
-
-  size_t MemoryObjectCache::GetMaximumSize()
-  {
-#if !defined(__EMSCRIPTEN__)
-    boost::mutex::scoped_lock lock(cacheMutex_);
-#endif
-
-    return maxSize_;
-  }
-
-
-  void MemoryObjectCache::SetMaximumSize(size_t size)
-  {
-    if (size == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-      
-#if !defined(__EMSCRIPTEN__)
-    // Make sure no accessor is currently open (as its data may be
-    // removed if recycling is needed)
-    WriterLock contentLock(contentMutex_);
-
-    // Lock the global structure of the cache
-    boost::mutex::scoped_lock cacheLock(cacheMutex_);
-#endif
-
-    Recycle(size);
-    maxSize_ = size;
-  }
-
-
-  void MemoryObjectCache::Acquire(const std::string& key,
-                                  ICacheable* value)
-  {
-    std::unique_ptr<Item> item(new Item(value));
-
-    if (value == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else
-    {
-#if !defined(__EMSCRIPTEN__)
-      // Make sure no accessor is currently open (as its data may be
-      // removed if recycling is needed)
-      WriterLock contentLock(contentMutex_);
-
-      // Lock the global structure of the cache
-      boost::mutex::scoped_lock cacheLock(cacheMutex_);
-#endif
-
-      const size_t size = item->GetValue().GetMemoryUsage();
-
-      if (size > maxSize_)
-      {
-        // This object is too large to be stored in the cache, discard it
-      }
-      else if (content_.Contains(key))
-      {
-        // Value already stored, don't overwrite the old value
-        content_.MakeMostRecent(key);
-      }
-      else
-      {
-        Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
-        assert(currentSize_ + size <= maxSize_);
-
-        content_.Add(key, item.release());
-        currentSize_ += size;
-      }
-    }
-  }
-
-
-  void MemoryObjectCache::Invalidate(const std::string& key)
-  {
-#if !defined(__EMSCRIPTEN__)
-    // Make sure no accessor is currently open (as it may correspond
-    // to the key to remove)
-    WriterLock contentLock(contentMutex_);
-
-    // Lock the global structure of the cache
-    boost::mutex::scoped_lock cacheLock(cacheMutex_);
-#endif
-
-    Item* item = NULL;
-    if (content_.Contains(key, item))
-    {
-      assert(item != NULL);
-      const size_t size = item->GetValue().GetMemoryUsage();
-      delete item;
-
-      content_.Invalidate(key);
-          
-      assert(currentSize_ >= size);
-      currentSize_ -= size;
-    }
-  }
-
-
-  MemoryObjectCache::Accessor::Accessor(MemoryObjectCache& cache,
-                                        const std::string& key,
-                                        bool unique) :
-    item_(NULL)
-  {
-#if !defined(__EMSCRIPTEN__)
-    if (unique)
-    {
-      writerLock_ = WriterLock(cache.contentMutex_);
-    }
-    else
-    {
-      readerLock_ = ReaderLock(cache.contentMutex_);
-    }
-
-    // Lock the global structure of the cache, must be *after* the
-    // reader/writer lock
-    cacheLock_ = boost::mutex::scoped_lock(cache.cacheMutex_);
-#endif
-
-    if (cache.content_.Contains(key, item_))
-    {
-      cache.content_.MakeMostRecent(key);
-    }
-    
-#if !defined(__EMSCRIPTEN__)
-    cacheLock_.unlock();
-
-    if (item_ == NULL)
-    {
-      // This item does not exist in the cache, we can release the
-      // reader/writer lock
-      if (unique)
-      {
-        writerLock_.unlock();
-      }
-      else
-      {
-        readerLock_.unlock();
-      }
-    }
-#endif
-  }
-
-
-  ICacheable& MemoryObjectCache::Accessor::GetValue() const
-  {
-    if (IsValid())
-    {
-      return item_->GetValue();
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-
-  const boost::posix_time::ptime& MemoryObjectCache::Accessor::GetTime() const
-  {
-    if (IsValid())
-    {
-      return item_->GetTime();
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }        
-  }
-}
--- a/Core/Cache/MemoryObjectCache.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ICacheable.h"
-#include "LeastRecentlyUsedIndex.h"
-
-#if !defined(__EMSCRIPTEN__)
-// Multithreading is not supported in WebAssembly
-#  include <boost/thread/mutex.hpp>
-#  include <boost/thread/shared_mutex.hpp>
-#endif
-
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-
-namespace Orthanc
-{
-  class MemoryObjectCache : public boost::noncopyable
-  {
-  private:
-    class Item;
-
-#if !defined(__EMSCRIPTEN__)
-    typedef boost::unique_lock<boost::shared_mutex> WriterLock;
-    typedef boost::shared_lock<boost::shared_mutex> ReaderLock;
-
-    // This mutex protects modifications to the structure of the cache (monitor)
-    boost::mutex   cacheMutex_;
-
-    // This mutex protects modifications to the items that are stored in the cache
-    boost::shared_mutex contentMutex_;
-#endif
-
-    size_t currentSize_;
-    size_t maxSize_;
-    LeastRecentlyUsedIndex<std::string, Item*>  content_;
-
-    void Recycle(size_t targetSize);
-    
-  public:
-    MemoryObjectCache();
-
-    ~MemoryObjectCache();
-
-    size_t GetMaximumSize();
-
-    void SetMaximumSize(size_t size);
-
-    void Acquire(const std::string& key,
-                 ICacheable* value);
-
-    void Invalidate(const std::string& key);
-
-    class Accessor : public boost::noncopyable
-    {
-    private:
-#if !defined(__EMSCRIPTEN__)
-      ReaderLock                 readerLock_;
-      WriterLock                 writerLock_;
-      boost::mutex::scoped_lock  cacheLock_;
-#endif
-      
-      Item*  item_;
-
-    public:
-      Accessor(MemoryObjectCache& cache,
-               const std::string& key,
-               bool unique);
-
-      bool IsValid() const
-      {
-        return item_ != NULL;
-      }
-
-      ICacheable& GetValue() const;
-
-      const boost::posix_time::ptime& GetTime() const;
-    };
-  };
-}
--- a/Core/Cache/MemoryStringCache.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MemoryStringCache.h"
-
-namespace Orthanc
-{
-  class MemoryStringCache::StringValue : public ICacheable
-  {
-  private:
-    std::string  content_;
-
-  public:
-    StringValue(const std::string& content) :
-      content_(content)
-    {
-    }
-      
-    const std::string& GetContent() const
-    {
-      return content_;
-    }
-
-    virtual size_t GetMemoryUsage() const
-    {
-      return content_.size();
-    }      
-  };
-
-
-  void MemoryStringCache::Add(const std::string& key,
-                              const std::string& value)
-  {
-    cache_.Acquire(key, new StringValue(value));
-  }
-
-  
-  bool MemoryStringCache::Fetch(std::string& value,
-                                const std::string& key)
-  {
-    MemoryObjectCache::Accessor reader(cache_, key, false /* multiple readers are allowed */);
-
-    if (reader.IsValid())
-    {
-      value = dynamic_cast<StringValue&>(reader.GetValue()).GetContent();
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-}
--- a/Core/Cache/MemoryStringCache.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MemoryObjectCache.h"
-
-namespace Orthanc
-{
-  /**
-   * Facade object around "MemoryObjectCache" that caches a dictionary
-   * of strings, using the "fetch/add" paradigm of memcached.
-   **/
-  class MemoryStringCache : public boost::noncopyable
-  {
-  private:
-    class StringValue;
-
-    MemoryObjectCache  cache_;
-
-  public:
-    size_t GetMaximumSize()
-    {
-      return cache_.GetMaximumSize();
-    }
-    
-    void SetMaximumSize(size_t size)
-    {
-      cache_.SetMaximumSize(size);
-    }
-
-    void Add(const std::string& key,
-             const std::string& value);
-    
-    void Invalidate(const std::string& key)
-    {
-      cache_.Invalidate(key);
-    }
-
-    bool Fetch(std::string& value,
-               const std::string& key);
-  };
-}
--- a/Core/Cache/SharedArchive.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "SharedArchive.h"
-
-#include "../Toolbox.h"
-
-
-namespace Orthanc
-{
-  void SharedArchive::RemoveInternal(const std::string& id)
-  {
-    Archive::iterator it = archive_.find(id);
-
-    if (it != archive_.end())
-    {
-      delete it->second;
-      archive_.erase(it);
-
-      lru_.Invalidate(id);
-    }
-  }
-
-
-  SharedArchive::Accessor::Accessor(SharedArchive& that,
-                                    const std::string& id) :
-    lock_(that.mutex_)
-  {
-    Archive::iterator it = that.archive_.find(id);
-
-    if (it == that.archive_.end())
-    {
-      item_ = NULL;
-    }
-    else
-    {
-      that.lru_.MakeMostRecent(id);
-      item_ = it->second;
-    }
-  }
-
-
-  IDynamicObject& SharedArchive::Accessor::GetItem() const
-  {
-    if (item_ == NULL)
-    {
-      // "IsValid()" should have been called
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return *item_;
-    }
-  }  
-
-
-  SharedArchive::SharedArchive(size_t maxSize) : 
-    maxSize_(maxSize)
-  {
-    if (maxSize == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  SharedArchive::~SharedArchive()
-  {
-    for (Archive::iterator it = archive_.begin();
-         it != archive_.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-
-  std::string SharedArchive::Add(IDynamicObject* obj)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (archive_.size() == maxSize_)
-    {
-      // The quota has been reached, remove the oldest element
-      RemoveInternal(lru_.GetOldest());
-    }
-
-    std::string id = Toolbox::GenerateUuid();
-    RemoveInternal(id);  // Should never be useful because of UUID
-
-    archive_[id] = obj;
-    lru_.Add(id);
-
-    return id;
-  }
-
-
-  void SharedArchive::Remove(const std::string& id)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    RemoveInternal(id);      
-  }
-
-
-  void SharedArchive::List(std::list<std::string>& items)
-  {
-    items.clear();
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      for (Archive::const_iterator it = archive_.begin();
-           it != archive_.end(); ++it)
-      {
-        items.push_back(it->first);
-      }
-    }
-  }
-}
--- a/Core/Cache/SharedArchive.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 SharedArchive cannot be used in sandboxed environments
-#endif
-
-#include "LeastRecentlyUsedIndex.h"
-#include "../IDynamicObject.h"
-
-#include <map>
-#include <boost/thread.hpp>
-
-namespace Orthanc
-{
-  class SharedArchive : public boost::noncopyable
-  {
-  private:
-    typedef std::map<std::string, IDynamicObject*>  Archive;
-
-    size_t         maxSize_;
-    boost::mutex   mutex_;
-    Archive        archive_;
-    LeastRecentlyUsedIndex<std::string> lru_;
-
-    void RemoveInternal(const std::string& id);
-
-  public:
-    class Accessor : public boost::noncopyable
-    {
-    private:
-      boost::mutex::scoped_lock  lock_;
-      IDynamicObject*            item_;
-
-    public:
-      Accessor(SharedArchive& that,
-               const std::string& id);
-
-      bool IsValid() const
-      {
-        return item_ != NULL;
-      }
-      
-      IDynamicObject& GetItem() const;
-    };
-
-
-    SharedArchive(size_t maxSize);
-
-    ~SharedArchive();
-
-    std::string Add(IDynamicObject* obj);  // Takes the ownership
-
-    void Remove(const std::string& id);
-
-    void List(std::list<std::string>& items);
-  };
-}
-
-
--- a/Core/ChunkedBuffer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ChunkedBuffer.h"
-
-#include <cassert>
-#include <string.h>
-
-
-namespace Orthanc
-{
-  void ChunkedBuffer::Clear()
-  {
-    numBytes_ = 0;
-
-    for (Chunks::iterator it = chunks_.begin(); 
-         it != chunks_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  void ChunkedBuffer::AddChunk(const void* chunkData,
-                               size_t chunkSize)
-  {
-    if (chunkSize == 0)
-    {
-      return;
-    }
-    else
-    {
-      assert(chunkData != NULL);
-      chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
-      numBytes_ += chunkSize;
-    }
-  }
-
-
-  void ChunkedBuffer::AddChunk(const std::string& chunk)
-  {
-    if (chunk.size() > 0)
-    {
-      AddChunk(&chunk[0], chunk.size());
-    }
-  }
-
-
-  void ChunkedBuffer::AddChunkDestructive(std::string& chunk)
-  {
-    size_t chunkSize = chunk.size();
-    
-    if (chunkSize > 0)
-    {
-      chunks_.push_back(new std::string);
-      chunks_.back()->swap(chunk);
-      numBytes_ += chunkSize;
-    }
-  }
-
-
-  void ChunkedBuffer::Flatten(std::string& result)
-  {
-    result.resize(numBytes_);
-
-    size_t pos = 0;
-    for (Chunks::iterator it = chunks_.begin(); 
-         it != chunks_.end(); ++it)
-    {
-      assert(*it != NULL);
-
-      size_t s = (*it)->size();
-      if (s != 0)
-      {
-        memcpy(&result[pos], (*it)->c_str(), s);
-        pos += s;
-      }
-
-      delete *it;
-    }
-
-    chunks_.clear();
-    numBytes_ = 0;
-  }
-}
--- a/Core/ChunkedBuffer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#include <boost/noncopyable.hpp>
-#include <list>
-#include <string>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC ChunkedBuffer : public boost::noncopyable
-  {
-  private:
-    typedef std::list<std::string*>  Chunks;
-    size_t numBytes_;
-    Chunks chunks_;
-  
-    void Clear();
-
-  public:
-    ChunkedBuffer() : numBytes_(0)
-    {
-    }
-
-    ~ChunkedBuffer()
-    {
-      Clear();
-    }
-
-    size_t GetNumBytes() const
-    {
-      return numBytes_;
-    }
-
-    void AddChunk(const void* chunkData,
-                  size_t chunkSize);
-
-    void AddChunk(const std::string& chunk);
-
-    // The source content will be emptied
-    void AddChunkDestructive(std::string& chunk);
-
-    void Flatten(std::string& result);
-  };
-}
--- a/Core/Compatibility.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-//#define Orthanc_Compatibility_h_STR2(x) #x
-//#define Orthanc_Compatibility_h_STR1(x) Orthanc_Compatibility_h_STR2(x)
-
-//#pragma message("__cplusplus = " Orthanc_Compatibility_h_STR1(__cplusplus))
-
-#if (defined _MSC_VER)
-//#  pragma message("_MSC_VER = " Orthanc_Compatibility_h_STR1(_MSC_VER))
-//#  pragma message("_MSVC_LANG = " Orthanc_Compatibility_h_STR1(_MSVC_LANG))
-// The __cplusplus macro cannot be used in Visual C++ < 1914 (VC++ 15.7)
-// However, even in recent versions, __cplusplus will only be correct (that is,
-// correctly defines the supported C++ version) if a special flag is passed to
-// the compiler ("/Zc:__cplusplus")
-// To make this header more robust, we use the _MSVC_LANG equivalent macro.
-
-// please note that not all C++11 features are supported when _MSC_VER == 1600
-// (or higher). This header file can be made for fine-grained, if required, 
-// based on specific _MSC_VER values
-
-#  if _MSC_VER >= 1600
-#    define ORTHANC_Cxx03_DETECTED 0
-#  else
-#    define ORTHANC_Cxx03_DETECTED 1
-#  endif
-
-#else
-// of _MSC_VER is not defined, we assume __cplusplus is correctly defined
-// if __cplusplus is not defined (very old compilers??), then the following
-// test will compare 0 < 201103L and will be true --> safe.
-#  if __cplusplus < 201103L
-#    define ORTHANC_Cxx03_DETECTED 1
-#  else
-#    define ORTHANC_Cxx03_DETECTED 0
-#  endif
-#endif
-
-#if ORTHANC_Cxx03_DETECTED == 1
-//#pragma message("C++ 11 support is not present.")
-
-/**
- * "std::unique_ptr" was introduced in C++11, and "std::auto_ptr" was
- * removed in C++17. We emulate "std::auto_ptr" using boost: "The
- * smart pointer unique_ptr [is] a drop-in replacement for
- * std::unique_ptr, usable also from C++03 compilers." This is only
- * available if Boost >= 1.57.0 (from November 2014).
- * https://www.boost.org/doc/libs/1_57_0/doc/html/move/reference.html#header.boost.move.unique_ptr_hpp
- **/
-
-#include <boost/move/unique_ptr.hpp>
-
-namespace std
-{
-  template <typename T>
-  class unique_ptr : public boost::movelib::unique_ptr<T>
-  {
-  public:
-    explicit unique_ptr() :
-      boost::movelib::unique_ptr<T>()
-    {
-    }      
-
-    explicit unique_ptr(T* p) :
-      boost::movelib::unique_ptr<T>(p)
-    {
-    }      
-  };
-}
-#else
-//# pragma message("C++ 11 support is present.")
-# include <memory>
-#endif
--- a/Core/Compression/DeflateBaseCompressor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DeflateBaseCompressor.h"
-
-#include "../OrthancException.h"
-#include "../Logging.h"
-
-#include <string.h>
-
-namespace Orthanc
-{
-  void DeflateBaseCompressor::SetCompressionLevel(uint8_t level)
-  {
-    if (level >= 10)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Zlib compression level must be between 0 (no compression) and 9 (highest compression)");
-    }
-
-    compressionLevel_ = level;
-  }
-
-
-  uint64_t DeflateBaseCompressor::ReadUncompressedSizePrefix(const void* compressed,
-                                                             size_t compressedSize)
-  {
-    if (compressedSize == 0)
-    {
-      return 0;
-    }
-
-    if (compressedSize < sizeof(uint64_t))
-    {
-      throw OrthancException(ErrorCode_CorruptedFile, "The compressed buffer is ill-formed");
-    }
-
-    uint64_t size;
-    memcpy(&size, compressed, sizeof(uint64_t));
-
-    return size;
-  }
-
-}
--- a/Core/Compression/DeflateBaseCompressor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "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
-{
-  class ORTHANC_PUBLIC DeflateBaseCompressor : public IBufferCompressor
-  {
-  private:
-    uint8_t compressionLevel_;
-    bool    prefixWithUncompressedSize_;
-
-  protected:
-    uint64_t ReadUncompressedSizePrefix(const void* compressed,
-                                        size_t compressedSize);
-
-  public:
-    DeflateBaseCompressor() : 
-      compressionLevel_(6),
-      prefixWithUncompressedSize_(false)
-    {
-    }
-
-    void SetCompressionLevel(uint8_t level);
-    
-    void SetPrefixWithUncompressedSize(bool prefix)
-    {
-      prefixWithUncompressedSize_ = prefix;
-    }
-
-    bool HasPrefixWithUncompressedSize() const
-    {
-      return prefixWithUncompressedSize_;
-    }
-
-    uint8_t GetCompressionLevel() const
-    {
-      return compressionLevel_;
-    }
-  };
-}
--- a/Core/Compression/GzipCompressor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,280 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "GzipCompressor.h"
-
-#include <stdio.h>
-#include <string.h>
-#include <zlib.h>
-
-#include "../OrthancException.h"
-#include "../Logging.h"
-
-namespace Orthanc
-{
-  uint64_t GzipCompressor::GuessUncompressedSize(const void* compressed,
-                                                 size_t compressedSize)
-  {
-    /**
-     * "Is there a way to find out the size of the original file which
-     * is inside a GZIP file? [...] There is no truly reliable way,
-     * other than gunzipping the stream. You do not need to save the
-     * result of the decompression, so you can determine the size by
-     * simply reading and decoding the entire file without taking up
-     * space with the decompressed result.
-     *
-     * There is an unreliable way to determine the uncompressed size,
-     * which is to look at the last four bytes of the gzip file, which
-     * is the uncompressed length of that entry modulo 232 in little
-     * endian order.
-     * 
-     * It is unreliable because a) the uncompressed data may be longer
-     * than 2^32 bytes, and b) the gzip file may consist of multiple
-     * gzip streams, in which case you would find the length of only
-     * the last of those streams.
-     * 
-     * If you are in control of the source of the gzip files, you know
-     * that they consist of single gzip streams, and you know that
-     * they are less than 2^32 bytes uncompressed, then and only then
-     * can you use those last four bytes with confidence."
-     *
-     * http://stackoverflow.com/a/9727599/881731
-     **/
-
-    if (compressedSize < 4)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    const uint8_t* p = reinterpret_cast<const uint8_t*>(compressed) + compressedSize - 4;
-
-    return ((static_cast<uint32_t>(p[0]) << 0) +
-            (static_cast<uint32_t>(p[1]) << 8) +
-            (static_cast<uint32_t>(p[2]) << 16) +
-            (static_cast<uint32_t>(p[3]) << 24));            
-  }
-
-
-
-  void GzipCompressor::Compress(std::string& compressed,
-                                const void* uncompressed,
-                                size_t uncompressedSize)
-  {
-    uLongf compressedSize = compressBound(static_cast<uLong>(uncompressedSize))
-      + 1024 /* security margin */;
-    
-    if (compressedSize == 0)
-    {
-      compressedSize = 1;
-    }
-
-    uint8_t* target;
-    if (HasPrefixWithUncompressedSize())
-    {
-      compressed.resize(compressedSize + sizeof(uint64_t));
-      target = reinterpret_cast<uint8_t*>(&compressed[0]) + sizeof(uint64_t);
-    }
-    else
-    {
-      compressed.resize(compressedSize);
-      target = reinterpret_cast<uint8_t*>(&compressed[0]);
-    }
-
-    z_stream stream;
-    memset(&stream, 0, sizeof(stream));
-
-    stream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(uncompressed));
-    stream.next_out = reinterpret_cast<Bytef*>(target);
-
-    stream.avail_in = static_cast<uInt>(uncompressedSize);
-    stream.avail_out = static_cast<uInt>(compressedSize);
-
-    // Ensure no overflow (if the buffer is too large for the current archicture)
-    if (static_cast<size_t>(stream.avail_in) != uncompressedSize ||
-        static_cast<size_t>(stream.avail_out) != compressedSize)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-    
-    // Initialize the compression engine
-    int error = deflateInit2(&stream, 
-                             GetCompressionLevel(), 
-                             Z_DEFLATED,
-                             MAX_WBITS + 16,      // ask for gzip output
-                             8,                   // default memory level
-                             Z_DEFAULT_STRATEGY);
-
-    if (error != Z_OK)
-    {
-      // Cannot initialize zlib
-      compressed.clear();
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Compress the input buffer
-    error = deflate(&stream, Z_FINISH);
-
-    if (error != Z_STREAM_END)
-    {
-      deflateEnd(&stream);
-      compressed.clear();
-
-      switch (error)
-      {
-      case Z_MEM_ERROR:
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-      }  
-    }
-
-    size_t size = stream.total_out;
-
-    if (deflateEnd(&stream) != Z_OK)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // The compression was successful
-    if (HasPrefixWithUncompressedSize())
-    {
-      uint64_t s = static_cast<uint64_t>(uncompressedSize);
-      memcpy(&compressed[0], &s, sizeof(uint64_t));
-      compressed.resize(size + sizeof(uint64_t));
-    }
-    else
-    {
-      compressed.resize(size);
-    }
-  }
-
-
-  void GzipCompressor::Uncompress(std::string& uncompressed,
-                                  const void* compressed,
-                                  size_t compressedSize)
-  {
-    uint64_t uncompressedSize;
-    const uint8_t* source = reinterpret_cast<const uint8_t*>(compressed);
-
-    if (HasPrefixWithUncompressedSize())
-    {
-      uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
-      source += sizeof(uint64_t);
-      compressedSize -= sizeof(uint64_t);
-    }
-    else
-    {
-      uncompressedSize = GuessUncompressedSize(compressed, compressedSize);
-    }
-
-    try
-    {
-      uncompressed.resize(static_cast<size_t>(uncompressedSize));
-    }
-    catch (...)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    z_stream stream;
-    memset(&stream, 0, sizeof(stream));
-
-    char dummy = '\0';  // zlib does not like NULL output buffers (even if the uncompressed data is empty)
-    stream.next_in = const_cast<Bytef*>(source);
-    stream.next_out = reinterpret_cast<Bytef*>(uncompressedSize == 0 ? &dummy : &uncompressed[0]);
-
-    stream.avail_in = static_cast<uInt>(compressedSize);
-    stream.avail_out = static_cast<uInt>(uncompressedSize);
-
-    // Ensure no overflow (if the buffer is too large for the current archicture)
-    if (static_cast<size_t>(stream.avail_in) != compressedSize ||
-        static_cast<size_t>(stream.avail_out) != uncompressedSize)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    // Initialize the compression engine
-    int error = inflateInit2(&stream, 
-                             MAX_WBITS + 16);  // this is a gzip input
-
-    if (error != Z_OK)
-    {
-      // Cannot initialize zlib
-      uncompressed.clear();
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Uncompress the input buffer
-    error = inflate(&stream, Z_FINISH);
-
-    if (error != Z_STREAM_END)
-    {
-      inflateEnd(&stream);
-      uncompressed.clear();
-
-      switch (error)
-      {
-        case Z_MEM_ERROR:
-          throw OrthancException(ErrorCode_NotEnoughMemory);
-          
-        case Z_BUF_ERROR:
-        case Z_NEED_DICT:
-          throw OrthancException(ErrorCode_BadFileFormat);
-          
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    size_t size = stream.total_out;
-
-    if (inflateEnd(&stream) != Z_OK)
-    {
-      uncompressed.clear();
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (size != uncompressedSize)
-    {
-      uncompressed.clear();
-
-      // The uncompressed size was not that properly guess, presumably
-      // because of a file size over 4GB. Should fallback to
-      // stream-based decompression.
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "The uncompressed size of a gzip-encoded buffer was not properly guessed");
-    }
-  }
-}
--- a/Core/Compression/GzipCompressor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DeflateBaseCompressor.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC GzipCompressor : public DeflateBaseCompressor
-  {
-  private:
-    uint64_t GuessUncompressedSize(const void* compressed,
-                                   size_t compressedSize);
-
-  public:
-    GzipCompressor()
-    {
-      SetPrefixWithUncompressedSize(false);
-    }
-
-    virtual void Compress(std::string& compressed,
-                          const void* uncompressed,
-                          size_t uncompressedSize);
-
-    virtual void Uncompress(std::string& uncompressed,
-                            const void* compressed,
-                            size_t compressedSize);
-  };
-}
--- a/Core/Compression/HierarchicalZipWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,182 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HierarchicalZipWriter.h"
-
-#include "../Toolbox.h"
-#include "../OrthancException.h"
-
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  std::string HierarchicalZipWriter::Index::KeepAlphanumeric(const std::string& source)
-  {
-    std::string result;
-
-    bool lastSpace = false;
-
-    result.reserve(source.size());
-    for (size_t i = 0; i < source.size(); i++)
-    {
-      char c = source[i];
-      if (c == '^')
-        c = ' ';
-
-      if (c <= 127 && 
-          c >= 0)
-      {
-        if (isspace(c)) 
-        {
-          if (!lastSpace)
-          {
-            lastSpace = true;
-            result.push_back(' ');
-          }
-        }
-        else if (isalnum(c) || 
-                 c == '.' || 
-                 c == '_')
-        {
-          result.push_back(c);
-          lastSpace = false;
-        }
-      }
-    }
-
-    return Toolbox::StripSpaces(result);
-  }
-
-  std::string HierarchicalZipWriter::Index::GetCurrentDirectoryPath() const
-  {
-    std::string result;
-
-    Stack::const_iterator it = stack_.begin();
-    ++it;  // Skip the root node (to avoid absolute paths)
-
-    while (it != stack_.end())
-    {
-      result += (*it)->name_ + "/";
-      ++it;
-    }
-
-    return result;
-  }
-
-  std::string HierarchicalZipWriter::Index::EnsureUniqueFilename(const char* filename)
-  {
-    std::string standardized = KeepAlphanumeric(filename);
-
-    Directory& d = *stack_.back();
-    Directory::Content::iterator it = d.content_.find(standardized);
-
-    if (it == d.content_.end())
-    {
-      d.content_[standardized] = 1;
-      return standardized;
-    }
-    else
-    {
-      it->second++;
-      return standardized + "-" + boost::lexical_cast<std::string>(it->second);
-    }    
-  }
-
-  HierarchicalZipWriter::Index::Index()
-  {
-    stack_.push_back(new Directory);
-  }
-
-  HierarchicalZipWriter::Index::~Index()
-  {
-    for (Stack::iterator it = stack_.begin(); it != stack_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-  std::string HierarchicalZipWriter::Index::OpenFile(const char* name)
-  {
-    return GetCurrentDirectoryPath() + EnsureUniqueFilename(name);
-  }
-
-  void HierarchicalZipWriter::Index::OpenDirectory(const char* name)
-  {
-    std::string d = EnsureUniqueFilename(name);
-
-    // Push the new directory onto the stack
-    stack_.push_back(new Directory);
-    stack_.back()->name_ = d;
-  }
-
-  void HierarchicalZipWriter::Index::CloseDirectory()
-  {
-    if (IsRoot())
-    {
-      // Cannot close the root node
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    delete stack_.back();
-    stack_.pop_back();
-  }
-
-
-  HierarchicalZipWriter::HierarchicalZipWriter(const char* path)
-  {
-    writer_.SetOutputPath(path);
-    writer_.Open();
-  }
-
-  HierarchicalZipWriter::~HierarchicalZipWriter()
-  {
-    writer_.Close();
-  }
-
-  void HierarchicalZipWriter::OpenFile(const char* name)
-  {
-    std::string p = indexer_.OpenFile(name);
-    writer_.OpenFile(p.c_str());
-  }
-
-  void HierarchicalZipWriter::OpenDirectory(const char* name)
-  {
-    indexer_.OpenDirectory(name);
-  }
-
-  void HierarchicalZipWriter::CloseDirectory()
-  {
-    indexer_.CloseDirectory();
-  }
-}
--- a/Core/Compression/HierarchicalZipWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ZipWriter.h"
-
-#include <map>
-#include <list>
-#include <boost/lexical_cast.hpp>
-
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-#  include <gtest/gtest_prod.h>
-#endif
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC HierarchicalZipWriter : public boost::noncopyable
-  {
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-    FRIEND_TEST(HierarchicalZipWriter, Index);
-    FRIEND_TEST(HierarchicalZipWriter, Filenames);
-#endif
-
-  private:
-    class ORTHANC_PUBLIC Index
-    {
-    private:
-      struct Directory
-      {
-        typedef std::map<std::string, unsigned int>  Content;
-
-        std::string name_;
-        Content  content_;
-      };
-
-      typedef std::list<Directory*> Stack;
-  
-      Stack stack_;
-
-      std::string EnsureUniqueFilename(const char* filename);
-
-    public:
-      Index();
-
-      ~Index();
-
-      bool IsRoot() const
-      {
-        return stack_.size() == 1;
-      }
-
-      std::string OpenFile(const char* name);
-
-      void OpenDirectory(const char* name);
-
-      void CloseDirectory();
-
-      std::string GetCurrentDirectoryPath() const;
-
-      static std::string KeepAlphanumeric(const std::string& source);
-    };
-
-    Index indexer_;
-    ZipWriter writer_;
-
-  public:
-    HierarchicalZipWriter(const char* path);
-
-    ~HierarchicalZipWriter();
-
-    void SetZip64(bool isZip64)
-    {
-      writer_.SetZip64(isZip64);
-    }
-
-    bool IsZip64() const
-    {
-      return writer_.IsZip64();
-    }
-
-    void SetCompressionLevel(uint8_t level)
-    {
-      writer_.SetCompressionLevel(level);
-    }
-
-    uint8_t GetCompressionLevel() const
-    {
-      return writer_.GetCompressionLevel();
-    }
-
-    void SetAppendToExisting(bool append)
-    {
-      writer_.SetAppendToExisting(append);
-    }
-    
-    bool IsAppendToExisting() const
-    {
-      return writer_.IsAppendToExisting();
-    }
-    
-    void OpenFile(const char* name);
-
-    void OpenDirectory(const char* name);
-
-    void CloseDirectory();
-
-    std::string GetCurrentDirectoryPath() const
-    {
-      return indexer_.GetCurrentDirectoryPath();
-    }
-
-    void Write(const void* data, size_t length)
-    {
-      writer_.Write(data, length);
-    }
-
-    void Write(const std::string& data)
-    {
-      writer_.Write(data);
-    }
-  };
-}
--- a/Core/Compression/IBufferCompressor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-
-#include <string>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC IBufferCompressor : public boost::noncopyable
-  {
-  public:
-    virtual ~IBufferCompressor()
-    {
-    }
-
-    virtual void Compress(std::string& compressed,
-                          const void* uncompressed,
-                          size_t uncompressedSize) = 0;
-
-    virtual void Uncompress(std::string& uncompressed,
-                            const void* compressed,
-                            size_t compressedSize) = 0;
-
-    static void Compress(std::string& compressed,
-                         IBufferCompressor& compressor,
-                         const std::string& uncompressed)
-    {
-      compressor.Compress(compressed, 
-                          uncompressed.size() == 0 ? NULL : uncompressed.c_str(), 
-                          uncompressed.size());
-    }
-
-    static void Uncompress(std::string& uncompressed,
-                           IBufferCompressor& compressor,
-                           const std::string& compressed)
-    {
-      compressor.Uncompress(uncompressed, 
-                            compressed.size() == 0 ? NULL : compressed.c_str(), 
-                            compressed.size());
-    }
-  };
-}
--- a/Core/Compression/ZipWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,262 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-#include "ZipWriter.h"
-
-#include <limits>
-#include <boost/filesystem.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-#include "../../Resources/ThirdParty/minizip/zip.h"
-#include "../OrthancException.h"
-#include "../Logging.h"
-
-
-static void PrepareFileInfo(zip_fileinfo& zfi)
-{
-  memset(&zfi, 0, sizeof(zfi));
-
-  using namespace boost::posix_time;
-  ptime now = second_clock::local_time();
-
-  boost::gregorian::date today = now.date();
-  ptime midnight(today);
-
-  time_duration sinceMidnight = now - midnight;
-  zfi.tmz_date.tm_sec = static_cast<unsigned int>(sinceMidnight.seconds());  // seconds after the minute - [0,59]
-  zfi.tmz_date.tm_min = static_cast<unsigned int>(sinceMidnight.minutes());  // minutes after the hour - [0,59]
-  zfi.tmz_date.tm_hour = static_cast<unsigned int>(sinceMidnight.hours());  // hours since midnight - [0,23]
-
-  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_day.html
-  zfi.tmz_date.tm_mday = today.day();  // day of the month - [1,31]
-
-  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_month.html
-  zfi.tmz_date.tm_mon = today.month() - 1;  // months since January - [0,11]
-
-  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_year.html
-  zfi.tmz_date.tm_year = today.year();  // years - [1980..2044]
-}
-
-
-
-namespace Orthanc
-{
-  struct ZipWriter::PImpl
-  {
-    zipFile file_;
-
-    PImpl() : file_(NULL)
-    {
-    }
-  };
-
-  ZipWriter::ZipWriter() :
-    pimpl_(new PImpl),
-    isZip64_(false),
-    hasFileInZip_(false),
-    append_(false),
-    compressionLevel_(6)
-  {
-  }
-
-  ZipWriter::~ZipWriter()
-  {
-    Close();
-  }
-
-  void ZipWriter::Close()
-  {
-    if (IsOpen())
-    {
-      zipClose(pimpl_->file_, "Created by Orthanc");
-      pimpl_->file_ = NULL;
-      hasFileInZip_ = false;
-    }
-  }
-
-  bool ZipWriter::IsOpen() const
-  {
-    return pimpl_->file_ != NULL;
-  }
-
-  void ZipWriter::Open()
-  {
-    if (IsOpen())
-    {
-      return;
-    }
-
-    if (path_.size() == 0)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "Please call SetOutputPath() before creating the file");
-    }
-
-    hasFileInZip_ = false;
-
-    int mode = APPEND_STATUS_CREATE;
-    if (append_ && 
-        boost::filesystem::exists(path_))
-    {
-      mode = APPEND_STATUS_ADDINZIP;
-    }
-
-    if (isZip64_)
-    {
-      pimpl_->file_ = zipOpen64(path_.c_str(), mode);
-    }
-    else
-    {
-      pimpl_->file_ = zipOpen(path_.c_str(), mode);
-    }
-
-    if (!pimpl_->file_)
-    {
-      throw OrthancException(ErrorCode_CannotWriteFile,
-                             "Cannot create new ZIP archive: " + path_);
-    }
-  }
-
-  void ZipWriter::SetOutputPath(const char* path)
-  {
-    Close();
-    path_ = path;
-  }
-
-  void ZipWriter::SetZip64(bool isZip64)
-  {
-    Close();
-    isZip64_ = isZip64;
-  }
-
-  void ZipWriter::SetCompressionLevel(uint8_t level)
-  {
-    if (level >= 10)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "ZIP compression level must be between 0 (no compression) "
-                             "and 9 (highest compression)");
-    }
-
-    Close();
-    compressionLevel_ = level;
-  }
-
-  void ZipWriter::OpenFile(const char* path)
-  {
-    Open();
-
-    zip_fileinfo zfi;
-    PrepareFileInfo(zfi);
-
-    int result;
-
-    if (isZip64_)
-    {
-      result = zipOpenNewFileInZip64(pimpl_->file_, path,
-                                     &zfi,
-                                     NULL,   0,
-                                     NULL,   0,
-                                     "",  // Comment
-                                     Z_DEFLATED,
-                                     compressionLevel_, 1);
-    }
-    else
-    {
-      result = zipOpenNewFileInZip(pimpl_->file_, path,
-                                   &zfi,
-                                   NULL,   0,
-                                   NULL,   0,
-                                   "",  // Comment
-                                   Z_DEFLATED,
-                                   compressionLevel_);
-    }
-
-    if (result != 0)
-    {
-      throw OrthancException(ErrorCode_CannotWriteFile,
-                             "Cannot add new file inside ZIP archive: " + std::string(path));
-    }
-
-    hasFileInZip_ = true;
-  }
-
-
-  void ZipWriter::Write(const std::string& data)
-  {
-    if (data.size())
-    {
-      Write(&data[0], data.size());
-    }
-  }
-
-
-  void ZipWriter::Write(const void* data, size_t length)
-  {
-    if (!hasFileInZip_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Call first OpenFile()");
-    }
-
-    const size_t maxBytesInAStep = std::numeric_limits<int32_t>::max();
-
-    const char* p = reinterpret_cast<const char*>(data);
-    
-    while (length > 0)
-    {
-      int bytes = static_cast<int32_t>(length <= maxBytesInAStep ? length : maxBytesInAStep);
-
-      if (zipWriteInFileInZip(pimpl_->file_, p, bytes))
-      {
-        throw OrthancException(ErrorCode_CannotWriteFile,
-                               "Cannot write data to ZIP archive: " + path_);
-      }
-      
-      p += bytes;
-      length -= bytes;
-    }
-  }
-
-
-  void ZipWriter::SetAppendToExisting(bool append)
-  {
-    Close();
-    append_ = append;
-  }
-}
--- a/Core/Compression/ZipWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.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>
-#include <string>
-#include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC ZipWriter : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    bool isZip64_;
-    bool hasFileInZip_;
-    bool append_;
-    uint8_t compressionLevel_;
-    std::string path_;
-
-  public:
-    ZipWriter();
-
-    ~ZipWriter();
-
-    void SetZip64(bool isZip64);
-
-    bool IsZip64() const
-    {
-      return isZip64_;
-    }
-
-    void SetCompressionLevel(uint8_t level);
-
-    uint8_t GetCompressionLevel() const
-    {
-      return compressionLevel_;
-    }
-
-    void SetAppendToExisting(bool append);
-    
-    bool IsAppendToExisting() const
-    {
-      return append_;
-    }
-    
-    void Open();
-
-    void Close();
-
-    bool IsOpen() const;
-
-    void SetOutputPath(const char* path);
-
-    const std::string& GetOutputPath() const
-    {
-      return path_;
-    }
-
-    void OpenFile(const char* path);
-
-    void Write(const void* data, size_t length);
-
-    void Write(const std::string& data);
-  };
-}
--- a/Core/Compression/ZlibCompressor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ZlibCompressor.h"
-
-#include "../OrthancException.h"
-#include "../Logging.h"
-
-#include <stdio.h>
-#include <string.h>
-#include <zlib.h>
-
-namespace Orthanc
-{
-  void ZlibCompressor::Compress(std::string& compressed,
-                                const void* uncompressed,
-                                size_t uncompressedSize)
-  {
-    if (uncompressedSize == 0)
-    {
-      compressed.clear();
-      return;
-    }
-
-    uLongf compressedSize = compressBound(static_cast<uLong>(uncompressedSize))
-      + 1024 /* security margin */;
-    if (compressedSize == 0)
-    {
-      compressedSize = 1;
-    }
-
-    uint8_t* target;
-    if (HasPrefixWithUncompressedSize())
-    {
-      compressed.resize(compressedSize + sizeof(uint64_t));
-      target = reinterpret_cast<uint8_t*>(&compressed[0]) + sizeof(uint64_t);
-    }
-    else
-    {
-      compressed.resize(compressedSize);
-      target = reinterpret_cast<uint8_t*>(&compressed[0]);
-    }
-
-    int error = compress2(target,
-                          &compressedSize,
-                          const_cast<Bytef *>(static_cast<const Bytef *>(uncompressed)), 
-                          static_cast<uLong>(uncompressedSize),
-                          GetCompressionLevel());
-
-    if (error != Z_OK)
-    {
-      compressed.clear();
-
-      switch (error)
-      {
-      case Z_MEM_ERROR:
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-      }  
-    }
-
-    // The compression was successful
-    if (HasPrefixWithUncompressedSize())
-    {
-      uint64_t s = static_cast<uint64_t>(uncompressedSize);
-      memcpy(&compressed[0], &s, sizeof(uint64_t));
-      compressed.resize(compressedSize + sizeof(uint64_t));
-    }
-    else
-    {
-      compressed.resize(compressedSize);
-    }
-  }
-
-
-  void ZlibCompressor::Uncompress(std::string& uncompressed,
-                                  const void* compressed,
-                                  size_t compressedSize)
-  {
-    if (compressedSize == 0)
-    {
-      uncompressed.clear();
-      return;
-    }
-
-    if (!HasPrefixWithUncompressedSize())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot guess the uncompressed size of a zlib-encoded buffer");
-    }
-
-    uint64_t uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
-    
-    try
-    {
-      uncompressed.resize(static_cast<size_t>(uncompressedSize));
-    }
-    catch (...)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    uLongf tmp = static_cast<uLongf>(uncompressedSize);
-    int error = uncompress
-      (reinterpret_cast<uint8_t*>(&uncompressed[0]), 
-       &tmp,
-       reinterpret_cast<const uint8_t*>(compressed) + sizeof(uint64_t),
-        static_cast<uLong>(compressedSize - sizeof(uint64_t)));
-
-    if (error != Z_OK)
-    {
-      uncompressed.clear();
-
-      switch (error)
-      {
-      case Z_DATA_ERROR:
-        throw OrthancException(ErrorCode_CorruptedFile);
-
-      case Z_MEM_ERROR:
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-      }  
-    }
-  }
-}
--- a/Core/Compression/ZlibCompressor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DeflateBaseCompressor.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC ZlibCompressor : public DeflateBaseCompressor
-  {
-  public:
-    ZlibCompressor()
-    {
-      SetPrefixWithUncompressedSize(true);
-    }
-
-    virtual void Compress(std::string& compressed,
-                          const void* uncompressed,
-                          size_t uncompressedSize);
-
-    virtual void Uncompress(std::string& uncompressed,
-                            const void* compressed,
-                            size_t compressedSize);
-  };
-}
--- a/Core/DicomFormat/DicomArray.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomArray.h"
-
-#include <stdio.h>
-
-namespace Orthanc
-{
-  DicomArray::DicomArray(const DicomMap& map)
-  {
-    elements_.reserve(map.content_.size());
-    
-    for (DicomMap::Content::const_iterator it = 
-           map.content_.begin(); it != map.content_.end(); ++it)
-    {
-      elements_.push_back(new DicomElement(it->first, *it->second));
-    }
-  }
-
-
-  DicomArray::~DicomArray()
-  {
-    for (size_t i = 0; i < elements_.size(); i++)
-    {
-      delete elements_[i];
-    }
-  }
-
-
-  void DicomArray::Print(FILE* fp) const
-  {
-    for (size_t  i = 0; i < elements_.size(); i++)
-    {
-      DicomTag t = elements_[i]->GetTag();
-      const DicomValue& v = elements_[i]->GetValue();
-      std::string s = v.IsNull() ? "(null)" : v.GetContent();
-      printf("0x%04x 0x%04x [%s]\n", t.GetGroup(), t.GetElement(), s.c_str());
-    }
-  }
-}
--- a/Core/DicomFormat/DicomArray.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomElement.h"
-#include "DicomMap.h"
-
-#include <vector>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DicomArray : public boost::noncopyable
-  {
-  private:
-    typedef std::vector<DicomElement*>  Elements;
-
-    Elements  elements_;
-
-  public:
-    explicit DicomArray(const DicomMap& map);
-
-    ~DicomArray();
-
-    size_t GetSize() const
-    {
-      return elements_.size();
-    }
-
-    const DicomElement& GetElement(size_t i) const
-    {
-      return *elements_[i];
-    }
-
-    void Print(FILE* fp) const;  // For debugging only
-  };
-}
--- a/Core/DicomFormat/DicomElement.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomValue.h"
-#include "DicomTag.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DicomElement : public boost::noncopyable
-  {
-  private:
-    DicomTag tag_;
-    DicomValue* value_;
-
-  public:
-    DicomElement(uint16_t group,
-                 uint16_t element,
-                 const DicomValue& value) :
-      tag_(group, element),
-      value_(value.Clone())
-    {
-    }
-
-    DicomElement(const DicomTag& tag,
-                 const DicomValue& value) :
-      tag_(tag),
-      value_(value.Clone())
-    {
-    }
-
-    ~DicomElement()
-    {
-      delete value_;
-    }
-
-    const DicomTag& GetTag() const
-    {
-      return tag_;
-    }
-
-    const DicomValue& GetValue() const
-    {
-      return *value_;
-    }
-
-    uint16_t GetTagGroup() const
-    {
-      return tag_.GetGroup();
-    }
-
-    uint16_t GetTagElement() const
-    {
-      return tag_.GetElement();
-    }
-
-    bool operator< (const DicomElement& other) const
-    {
-      return GetTag() < other.GetTag();
-    }
-  };
-}
--- a/Core/DicomFormat/DicomImageInformation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,310 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-#include "DicomImageInformation.h"
-
-#include "../Compatibility.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-#include <boost/lexical_cast.hpp>
-#include <limits>
-#include <cassert>
-#include <stdio.h>
-#include <memory>
-
-namespace Orthanc
-{
-  DicomImageInformation::DicomImageInformation(const DicomMap& values)
-  {
-    unsigned int pixelRepresentation;
-    unsigned int planarConfiguration = 0;
-
-    try
-    {
-      std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).GetContent();
-      Toolbox::ToUpperCase(p);
-
-      if (p == "RGB")
-      {
-        photometric_ = PhotometricInterpretation_RGB;
-      }
-      else if (p == "MONOCHROME1")
-      {
-        photometric_ = PhotometricInterpretation_Monochrome1;
-      }
-      else if (p == "MONOCHROME2")
-      {
-        photometric_ = PhotometricInterpretation_Monochrome2;
-      }
-      else if (p == "PALETTE COLOR")
-      {
-        photometric_ = PhotometricInterpretation_Palette;
-      }
-      else if (p == "HSV")
-      {
-        photometric_ = PhotometricInterpretation_HSV;
-      }
-      else if (p == "ARGB")
-      {
-        photometric_ = PhotometricInterpretation_ARGB;
-      }
-      else if (p == "CMYK")
-      {
-        photometric_ = PhotometricInterpretation_CMYK;
-      }
-      else if (p == "YBR_FULL")
-      {
-        photometric_ = PhotometricInterpretation_YBRFull;
-      }
-      else if (p == "YBR_FULL_422")
-      {
-        photometric_ = PhotometricInterpretation_YBRFull422;
-      }
-      else if (p == "YBR_PARTIAL_420")
-      {
-        photometric_ = PhotometricInterpretation_YBRPartial420;
-      }
-      else if (p == "YBR_PARTIAL_422")
-      {
-        photometric_ = PhotometricInterpretation_YBRPartial422;
-      }
-      else if (p == "YBR_ICT")
-      {
-        photometric_ = PhotometricInterpretation_YBR_ICT;
-      }
-      else if (p == "YBR_RCT")
-      {
-        photometric_ = PhotometricInterpretation_YBR_RCT;
-      }
-      else
-      {
-        photometric_ = PhotometricInterpretation_Unknown;
-      }
-
-      values.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(width_); // in some US images, we've seen tag values of "800\0"; that's why we parse the 'first' value
-      values.GetValue(DICOM_TAG_ROWS).ParseFirstUnsignedInteger(height_);
-
-      bitsAllocated_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_ALLOCATED).GetContent());
-
-      try
-      {
-        samplesPerPixel_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_SAMPLES_PER_PIXEL).GetContent());
-      }
-      catch (OrthancException&)
-      {
-        samplesPerPixel_ = 1;  // Assume 1 color channel
-      }
-
-      try
-      {
-        bitsStored_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_STORED).GetContent());
-      }
-      catch (OrthancException&)
-      {
-        bitsStored_ = bitsAllocated_;
-      }
-
-      try
-      {
-        highBit_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_HIGH_BIT).GetContent());
-      }
-      catch (OrthancException&)
-      {
-        highBit_ = bitsStored_ - 1;
-      }
-
-      try
-      {
-        pixelRepresentation = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_PIXEL_REPRESENTATION).GetContent());
-      }
-      catch (OrthancException&)
-      {
-        pixelRepresentation = 0;  // Assume unsigned pixels
-      }
-
-      if (samplesPerPixel_ > 1)
-      {
-        // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1
-        // 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());
-        }
-        catch (OrthancException&)
-        {
-          planarConfiguration = 0;  // Assume interleaved color channels
-        }
-      }
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-    catch (OrthancException&)
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES))
-    {
-      try
-      {
-        numberOfFrames_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_NotImplemented);
-      }
-    }
-    else
-    {
-      numberOfFrames_ = 1;
-    }
-
-    if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
-        bitsAllocated_ != 24 && bitsAllocated_ != 32)
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
-    }
-    else if (numberOfFrames_ == 0)
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
-    }
-    else if (planarConfiguration != 0 && planarConfiguration != 1)
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration));
-    }
-
-    if (samplesPerPixel_ == 0)
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
-    }
-
-    bytesPerValue_ = bitsAllocated_ / 8;
-
-    isPlanar_ = (planarConfiguration != 0 ? true : false);
-    isSigned_ = (pixelRepresentation != 0 ? true : false);
-  }
-
-  DicomImageInformation* DicomImageInformation::Clone() const
-  {
-    std::unique_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_;
-
-    return target.release();
-  }
-
-  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format,
-                                                 bool ignorePhotometricInterpretation) const
-  {
-    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())
-      {
-        format = PixelFormat_Grayscale8;
-        return true;
-      }
-      
-      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
-      {
-        format = PixelFormat_Grayscale16;
-        return true;
-      }
-
-      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
-      {
-        format = PixelFormat_SignedGrayscale16;
-        return true;
-      }
-      
-      if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned())
-      {
-        format = PixelFormat_Grayscale32;
-        return true;
-      }
-    }
-
-    if (GetBitsStored() == 8 &&
-        GetChannelCount() == 3 &&
-        !IsSigned() &&
-        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
-    {
-      format = PixelFormat_RGB24;
-      return true;
-    }
-
-    return false;
-  }
-
-
-  size_t DicomImageInformation::GetFrameSize() const
-  {
-    return (GetHeight() *
-            GetWidth() *
-            GetBytesPerValue() *
-            GetChannelCount());
-  }
-}
--- a/Core/DicomFormat/DicomImageInformation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,135 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomMap.h"
-
-#include <stdint.h>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DicomImageInformation
-  {
-  private:
-    unsigned int width_;
-    unsigned int height_;
-    unsigned int samplesPerPixel_;
-    unsigned int numberOfFrames_;
-
-    bool isPlanar_;
-    bool isSigned_;
-    size_t bytesPerValue_;
-
-    unsigned int bitsAllocated_;
-    unsigned int bitsStored_;
-    unsigned int highBit_;
-
-    PhotometricInterpretation  photometric_;
-
-  protected:
-    explicit DicomImageInformation()
-    {
-    }
-
-  public:
-    explicit DicomImageInformation(const DicomMap& values);
-
-    DicomImageInformation* Clone() const;
-
-    unsigned int GetWidth() const
-    {
-      return width_;
-    }
-
-    unsigned int GetHeight() const
-    {
-      return height_;
-    }
-
-    unsigned int GetNumberOfFrames() const
-    {
-      return numberOfFrames_;
-    }
-
-    unsigned int GetChannelCount() const
-    {
-      return samplesPerPixel_;
-    }
-
-    unsigned int GetBitsStored() const
-    {
-      return bitsStored_;
-    }
-
-    size_t GetBytesPerValue() const
-    {
-      return bytesPerValue_;
-    }
-
-    bool IsSigned() const
-    {
-      return isSigned_;
-    }
-
-    unsigned int GetBitsAllocated() const
-    {
-      return bitsAllocated_;
-    }
-
-    unsigned int GetHighBit() const
-    {
-      return highBit_;
-    }
-
-    bool IsPlanar() const
-    {
-      return isPlanar_;
-    }
-
-    unsigned int GetShift() const
-    {
-      return highBit_ + 1 - bitsStored_;
-    }
-
-    PhotometricInterpretation GetPhotometricInterpretation() const
-    {
-      return photometric_;
-    }
-
-    bool ExtractPixelFormat(PixelFormat& format,
-                            bool ignorePhotometricInterpretation) const;
-
-    size_t GetFrameSize() const;
-  };
-}
--- a/Core/DicomFormat/DicomInstanceHasher.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomInstanceHasher.h"
-
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-namespace Orthanc
-{
-  void DicomInstanceHasher::Setup(const std::string& patientId,
-                                  const std::string& studyUid,
-                                  const std::string& seriesUid,
-                                  const std::string& instanceUid)
-  {
-    patientId_ = patientId;
-    studyUid_ = studyUid;
-    seriesUid_ = seriesUid;
-    instanceUid_ = instanceUid;
-
-    if (studyUid_.size() == 0 ||
-        seriesUid_.size() == 0 ||
-        instanceUid_.size() == 0)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
-    }
-  }
-
-  DicomInstanceHasher::DicomInstanceHasher(const DicomMap& instance)
-  {
-    const DicomValue* patientId = instance.TestAndGetValue(DICOM_TAG_PATIENT_ID);
-
-    Setup(patientId == NULL ? "" : patientId->GetContent(),
-          instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(),
-          instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(),
-          instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent());
-  }
-
-  const std::string& DicomInstanceHasher::HashPatient()
-  {
-    if (patientHash_.size() == 0)
-    {
-      Toolbox::ComputeSHA1(patientHash_, patientId_);
-    }
-
-    return patientHash_;
-  }
-
-  const std::string& DicomInstanceHasher::HashStudy()
-  {
-    if (studyHash_.size() == 0)
-    {
-      Toolbox::ComputeSHA1(studyHash_, patientId_ + "|" + studyUid_);
-    }
-
-    return studyHash_;
-  }
-
-  const std::string& DicomInstanceHasher::HashSeries()
-  {
-    if (seriesHash_.size() == 0)
-    {
-      Toolbox::ComputeSHA1(seriesHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_);
-    }
-
-    return seriesHash_;
-  }
-
-  const std::string& DicomInstanceHasher::HashInstance()
-  {
-    if (instanceHash_.size() == 0)
-    {
-      Toolbox::ComputeSHA1(instanceHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_ + "|" + instanceUid_);
-    }
-
-    return instanceHash_;
-  }
-}
--- a/Core/DicomFormat/DicomInstanceHasher.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomMap.h"
-
-namespace Orthanc
-{
-  /**
-   * This class implements the hashing mechanism that is used to
-   * convert DICOM unique identifiers to Orthanc identifiers. Any
-   * Orthanc identifier for a DICOM resource corresponds to the SHA-1
-   * hash of the DICOM identifiers. 
-
-   * \note SHA-1 hash is used because it is less sensitive to
-   * collision attacks than MD5. <a
-   * href="http://en.wikipedia.org/wiki/SHA-256#Comparison_of_SHA_functions">[Reference]</a>
-   **/
-  class DicomInstanceHasher
-  {
-  private:
-    std::string patientId_;
-    std::string studyUid_;
-    std::string seriesUid_;
-    std::string instanceUid_;
-
-    std::string patientHash_;
-    std::string studyHash_;
-    std::string seriesHash_;
-    std::string instanceHash_;
-
-    void Setup(const std::string& patientId,
-               const std::string& studyUid,
-               const std::string& seriesUid,
-               const std::string& instanceUid);
-
-  public:
-    DicomInstanceHasher(const DicomMap& instance);
-
-    DicomInstanceHasher(const std::string& patientId,
-                        const std::string& studyUid,
-                        const std::string& seriesUid,
-                        const std::string& instanceUid)
-    {
-      Setup(patientId, studyUid, seriesUid, instanceUid);
-    }
-
-    const std::string& GetPatientId() const
-    {
-      return patientId_;
-    }
-
-    const std::string& GetStudyUid() const
-    {
-      return studyUid_;
-    }
-
-    const std::string& GetSeriesUid() const
-    {
-      return seriesUid_;
-    }
-
-    const std::string& GetInstanceUid() const
-    {
-      return instanceUid_;
-    }
-
-    const std::string& HashPatient();
-
-    const std::string& HashStudy();
-
-    const std::string& HashSeries();
-
-    const std::string& HashInstance();
-  };
-}
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-#include "DicomIntegerPixelAccessor.h"
-
-#include "../OrthancException.h"
-#include <boost/lexical_cast.hpp>
-#include <limits>
-#include <cassert>
-#include <stdio.h>
-
-namespace Orthanc
-{
-  DicomIntegerPixelAccessor::DicomIntegerPixelAccessor(const DicomMap& values,
-                                                       const void* pixelData,
-                                                       size_t size) :
-    information_(values),
-    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_.GetFrameSize();
-
-    if (information_.GetNumberOfFrames() * frameOffset_ > size)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    if (information_.IsSigned())
-    {
-      // Pixels are signed
-      mask_ = (1 << (information_.GetBitsStored() - 1)) - 1;
-      signMask_ = (1 << (information_.GetBitsStored() - 1));
-    }
-    else
-    {
-      // Pixels are unsigned
-      mask_ = (1 << information_.GetBitsStored()) - 1;
-      signMask_ = 0;
-    }
-
-    if (information_.IsPlanar())
-    {
-      /**
-       * Each color plane shall be sent contiguously. For RGB images,
-       * this means the order of the pixel values sent is R1, R2, R3,
-       * ..., G1, G2, G3, ..., B1, B2, B3, etc.
-       **/
-      rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue();
-    }
-    else
-    {
-      /**
-       * The sample values for the first pixel are followed by the
-       * sample values for the second pixel, etc. For RGB images, this
-       * means the order of the pixel values sent shall be R1, G1, B1,
-       * R2, G2, B2, ..., etc.
-       **/
-      rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue() * information_.GetChannelCount();
-    }
-  }
-
-
-  void DicomIntegerPixelAccessor::GetExtremeValues(int32_t& min, 
-                                                   int32_t& max) const
-  {
-    if (information_.GetHeight() == 0 || information_.GetWidth() == 0)
-    {
-      min = max = 0;
-      return;
-    }
-
-    min = std::numeric_limits<int32_t>::max();
-    max = std::numeric_limits<int32_t>::min();
-    
-    for (unsigned int y = 0; y < information_.GetHeight(); y++)
-    {
-      for (unsigned int x = 0; x < information_.GetWidth(); x++)
-      {
-        for (unsigned int c = 0; c < information_.GetChannelCount(); c++)
-        {
-          int32_t v = GetValue(x, y);
-          if (v < min)
-            min = v;
-          if (v > max)
-            max = v;
-        }
-      }
-    }
-  }
-
-
-  int32_t DicomIntegerPixelAccessor::GetValue(unsigned int x, 
-                                              unsigned int y,
-                                              unsigned int channel) const
-  {
-    assert(x < information_.GetWidth() && 
-           y < information_.GetHeight() && 
-           channel < information_.GetChannelCount());
-    
-    const uint8_t* pixel = reinterpret_cast<const uint8_t*>(pixelData_) + 
-      y * rowOffset_ + frame_ * frameOffset_;
-
-    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
-    if (information_.IsPlanar())
-    {
-      /**
-       * Each color plane shall be sent contiguously. For RGB images,
-       * this means the order of the pixel values sent is R1, R2, R3,
-       * ..., G1, G2, G3, ..., B1, B2, B3, etc.
-       **/
-      assert(frameOffset_ % information_.GetChannelCount() == 0);
-      pixel += channel * frameOffset_ / information_.GetChannelCount() + x * information_.GetBytesPerValue();
-    }
-    else
-    {
-      /**
-       * The sample values for the first pixel are followed by the
-       * sample values for the second pixel, etc. For RGB images, this
-       * means the order of the pixel values sent shall be R1, G1, B1,
-       * R2, G2, B2, ..., etc.
-       **/
-      pixel += channel * information_.GetBytesPerValue() + x * information_.GetChannelCount() * information_.GetBytesPerValue();
-    }
-
-    uint32_t v;
-    v = pixel[0];
-    if (information_.GetBytesPerValue() >= 2)
-      v = v + (static_cast<uint32_t>(pixel[1]) << 8);
-    if (information_.GetBytesPerValue() >= 3)
-      v = v + (static_cast<uint32_t>(pixel[2]) << 16);
-    if (information_.GetBytesPerValue() >= 4)
-      v = v + (static_cast<uint32_t>(pixel[3]) << 24);
-
-    v = v >> information_.GetShift();
-
-    if (v & signMask_)
-    {
-      // Signed value
-      // http://en.wikipedia.org/wiki/Two%27s_complement#Subtraction_from_2N
-      return -static_cast<int32_t>(mask_) + static_cast<int32_t>(v & mask_) - 1;
-    }
-    else
-    {
-      // Unsigned value
-      return static_cast<int32_t>(v & mask_);
-    }
-  }
-
-
-  void DicomIntegerPixelAccessor::SetCurrentFrame(unsigned int frame)
-  {
-    if (frame >= information_.GetNumberOfFrames())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    frame_ = frame;
-  }
-
-}
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomMap.h"
-
-#include "DicomImageInformation.h"
-
-#include <stdint.h>
-
-namespace Orthanc
-{
-  class DicomIntegerPixelAccessor
-  {
-  private:
-    DicomImageInformation information_;
-
-    uint32_t signMask_;
-    uint32_t mask_;
-
-    const void* pixelData_;
-    size_t size_;
-    unsigned int frame_;
-    size_t frameOffset_;
-    size_t rowOffset_;
-
-  public:
-    DicomIntegerPixelAccessor(const DicomMap& values,
-                              const void* pixelData,
-                              size_t size);
-
-    const DicomImageInformation GetInformation() const
-    {
-      return information_;
-    }
-
-    unsigned int GetCurrentFrame() const
-    {
-      return frame_;
-    }
-
-    void SetCurrentFrame(unsigned int frame);
-
-    void GetExtremeValues(int32_t& min, 
-                          int32_t& max) const;
-
-    int32_t GetValue(unsigned int x, unsigned int y, unsigned int channel = 0) const;
-
-    const void* GetPixelData() const
-    {
-      return pixelData_;
-    }
-
-    size_t GetSize() const
-    {
-      return size_;
-    }
-  };
-}
--- a/Core/DicomFormat/DicomMap.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1424 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomMap.h"
-
-#include <stdio.h>
-#include <memory>
-
-#include "../Compatibility.h"
-#include "../Endianness.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-#include "DicomArray.h"
-
-
-namespace Orthanc
-{
-  namespace
-  {
-    struct MainDicomTag
-    {
-      const DicomTag tag_;
-      const char*    name_;
-    };
-  }
-
-  static const MainDicomTag PATIENT_MAIN_DICOM_TAGS[] =
-  {
-    // { DicomTag(0x0010, 0x1010), "PatientAge" },
-    // { DicomTag(0x0010, 0x1040), "PatientAddress" },
-    { DicomTag(0x0010, 0x0010), "PatientName" },
-    { DicomTag(0x0010, 0x0030), "PatientBirthDate" },
-    { DicomTag(0x0010, 0x0040), "PatientSex" },
-    { DicomTag(0x0010, 0x1000), "OtherPatientIDs" },
-    { DICOM_TAG_PATIENT_ID, "PatientID" }
-  };
-    
-  static const MainDicomTag STUDY_MAIN_DICOM_TAGS[] =
-  {
-    // { DicomTag(0x0010, 0x1020), "PatientSize" },
-    // { DicomTag(0x0010, 0x1030), "PatientWeight" },
-    { DICOM_TAG_STUDY_DATE, "StudyDate" },
-    { DicomTag(0x0008, 0x0030), "StudyTime" },
-    { DicomTag(0x0020, 0x0010), "StudyID" },
-    { DICOM_TAG_STUDY_DESCRIPTION, "StudyDescription" },
-    { DICOM_TAG_ACCESSION_NUMBER, "AccessionNumber" },
-    { DICOM_TAG_STUDY_INSTANCE_UID, "StudyInstanceUID" },
-
-    // New in db v6
-    { DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION, "RequestedProcedureDescription" },
-    { DICOM_TAG_INSTITUTION_NAME, "InstitutionName" },
-    { DICOM_TAG_REQUESTING_PHYSICIAN, "RequestingPhysician" },
-    { DICOM_TAG_REFERRING_PHYSICIAN_NAME, "ReferringPhysicianName" }
-  };
-    
-  static const MainDicomTag SERIES_MAIN_DICOM_TAGS[] =
-  {
-    // { DicomTag(0x0010, 0x1080), "MilitaryRank" },
-    { DicomTag(0x0008, 0x0021), "SeriesDate" },
-    { DicomTag(0x0008, 0x0031), "SeriesTime" },
-    { DICOM_TAG_MODALITY, "Modality" },
-    { DicomTag(0x0008, 0x0070), "Manufacturer" },
-    { DicomTag(0x0008, 0x1010), "StationName" },
-    { DICOM_TAG_SERIES_DESCRIPTION, "SeriesDescription" },
-    { DicomTag(0x0018, 0x0015), "BodyPartExamined" },
-    { DicomTag(0x0018, 0x0024), "SequenceName" },
-    { DicomTag(0x0018, 0x1030), "ProtocolName" },
-    { DicomTag(0x0020, 0x0011), "SeriesNumber" },
-    { DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES, "CardiacNumberOfImages" },
-    { DICOM_TAG_IMAGES_IN_ACQUISITION, "ImagesInAcquisition" },
-    { DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "NumberOfTemporalPositions" },
-    { DICOM_TAG_NUMBER_OF_SLICES, "NumberOfSlices" },
-    { DICOM_TAG_NUMBER_OF_TIME_SLICES, "NumberOfTimeSlices" },
-    { DICOM_TAG_SERIES_INSTANCE_UID, "SeriesInstanceUID" },
-
-    // New in db v6
-    { DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "ImageOrientationPatient" },
-    { DICOM_TAG_SERIES_TYPE, "SeriesType" },
-    { DICOM_TAG_OPERATOR_NAME, "OperatorsName" },
-    { DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, "PerformedProcedureStepDescription" },
-    { DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, "AcquisitionDeviceProcessingDescription" },
-    { DICOM_TAG_CONTRAST_BOLUS_AGENT, "ContrastBolusAgent" }
-  };
-    
-  static const MainDicomTag INSTANCE_MAIN_DICOM_TAGS[] =
-  {
-    { DicomTag(0x0008, 0x0012), "InstanceCreationDate" },
-    { DicomTag(0x0008, 0x0013), "InstanceCreationTime" },
-    { DicomTag(0x0020, 0x0012), "AcquisitionNumber" },
-    { DICOM_TAG_IMAGE_INDEX, "ImageIndex" },
-    { DICOM_TAG_INSTANCE_NUMBER, "InstanceNumber" },
-    { DICOM_TAG_NUMBER_OF_FRAMES, "NumberOfFrames" },
-    { DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, "TemporalPositionIdentifier" },
-    { DICOM_TAG_SOP_INSTANCE_UID, "SOPInstanceUID" },
-
-    // New in db v6
-    { DICOM_TAG_IMAGE_POSITION_PATIENT, "ImagePositionPatient" },
-    { DICOM_TAG_IMAGE_COMMENTS, "ImageComments" },
-
-    /**
-     * 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, "ImageOrientationPatient" }  // New in Orthanc 1.4.2
-  };
-
-
-  static void LoadMainDicomTags(const MainDicomTag*& tags,
-                                size_t& size,
-                                ResourceType level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        tags = PATIENT_MAIN_DICOM_TAGS;
-        size = sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      case ResourceType_Study:
-        tags = STUDY_MAIN_DICOM_TAGS;
-        size = sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      case ResourceType_Series:
-        tags = SERIES_MAIN_DICOM_TAGS;
-        size = sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      case ResourceType_Instance:
-        tags = INSTANCE_MAIN_DICOM_TAGS;
-        size = sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  static void LoadMainDicomTags(std::map<DicomTag, std::string>& target,
-                                ResourceType level)
-  {
-    const MainDicomTag* tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
-
-    assert(tags != NULL &&
-           size != 0);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      assert(target.find(tags[i].tag_) == target.end());
-      
-      target[tags[i].tag_] = tags[i].name_;
-    }
-  }
-
-
-  namespace
-  {
-    class DicomTag2 : public DicomTag
-    {
-    public:
-      DicomTag2() :
-        DicomTag(0, 0)   // To make std::map<> happy
-      {
-      }
-
-      DicomTag2(const DicomTag& tag) :
-        DicomTag(tag)
-      {
-      }
-    };
-  }
-
-
-  static void LoadMainDicomTags(std::map<std::string, DicomTag2>& target,
-                                ResourceType level)
-  {
-    const MainDicomTag* tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
-
-    assert(tags != NULL &&
-           size != 0);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      assert(target.find(tags[i].name_) == target.end());
-      
-      target[tags[i].name_] = tags[i].tag_;
-    }
-  }
-
-
-  void DicomMap::SetValueInternal(uint16_t group, 
-                                  uint16_t element, 
-                                  DicomValue* value)
-  {
-    DicomTag tag(group, element);
-    Content::iterator it = content_.find(tag);
-
-    if (it != content_.end())
-    {
-      delete it->second;
-      it->second = value;
-    }
-    else
-    {
-      content_.insert(std::make_pair(tag, value));
-    }
-  }
-
-
-  void DicomMap::Clear()
-  {
-    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
-    {
-      assert(it->second != NULL);
-      delete it->second;
-    }
-
-    content_.clear();
-  }
-
-
-  static void ExtractTags(DicomMap& result,
-                          const DicomMap::Content& source,
-                          const MainDicomTag* tags,
-                          size_t count)
-  {
-    result.Clear();
-
-    for (unsigned int i = 0; i < count; i++)
-    {
-      DicomMap::Content::const_iterator it = source.find(tags[i].tag_);
-      if (it != source.end())
-      {
-        result.SetValue(it->first, *it->second /* value will be cloned */);
-      }
-    }
-  }
-
-
-  void DicomMap::ExtractPatientInformation(DicomMap& result) const
-  {
-    ExtractTags(result, content_, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-  }
-
-  void DicomMap::ExtractStudyInformation(DicomMap& result) const
-  {
-    ExtractTags(result, content_, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-  }
-
-  void DicomMap::ExtractSeriesInformation(DicomMap& result) const
-  {
-    ExtractTags(result, content_, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-  }
-
-  void DicomMap::ExtractInstanceInformation(DicomMap& result) const
-  {
-    ExtractTags(result, content_, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-  }
-
-
-
-  DicomMap* DicomMap::Clone() const
-  {
-    std::unique_ptr<DicomMap> result(new DicomMap);
-
-    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
-    {
-      result->content_.insert(std::make_pair(it->first, it->second->Clone()));
-    }
-
-    return result.release();
-  }
-
-
-  void DicomMap::Assign(const DicomMap& other)
-  {
-    Clear();
-
-    for (Content::const_iterator it = other.content_.begin(); it != other.content_.end(); ++it)
-    {
-      content_.insert(std::make_pair(it->first, it->second->Clone()));
-    }
-  }
-
-
-  const DicomValue& DicomMap::GetValue(const DicomTag& tag) const
-  {
-    const DicomValue* value = TestAndGetValue(tag);
-
-    if (value)
-    {
-      return *value;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InexistentTag);
-    }
-  }
-
-
-  const DicomValue* DicomMap::TestAndGetValue(const DicomTag& tag) const
-  {
-    Content::const_iterator it = content_.find(tag);
-
-    if (it == content_.end())
-    {
-      return NULL;
-    }
-    else
-    {
-      return it->second;
-    }
-  }
-
-
-  void DicomMap::Remove(const DicomTag& tag) 
-  {
-    Content::iterator it = content_.find(tag);
-    if (it != content_.end())
-    {
-      delete it->second;
-      content_.erase(it);
-    }
-  }
-
-
-  static void SetupFindTemplate(DicomMap& result,
-                                const MainDicomTag* tags,
-                                size_t count) 
-  {
-    result.Clear();
-
-    for (size_t i = 0; i < count; i++)
-    {
-      result.SetValue(tags[i].tag_, "", false);
-    }
-  }
-
-  void DicomMap::SetupFindPatientTemplate(DicomMap& result)
-  {
-    SetupFindTemplate(result, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-  }
-
-  void DicomMap::SetupFindStudyTemplate(DicomMap& result)
-  {
-    SetupFindTemplate(result, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-    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, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-    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
-    result.Remove(DicomTag(0x0008, 0x1010));  // Station name
-    result.Remove(DicomTag(0x0018, 0x0024));  // Sequence name
-    result.Remove(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES);
-    result.Remove(DICOM_TAG_IMAGES_IN_ACQUISITION);
-    result.Remove(DICOM_TAG_NUMBER_OF_SLICES);
-    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, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
-    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);
-  }
-
-
-  void DicomMap::CopyTagIfExists(const DicomMap& source,
-                                 const DicomTag& tag)
-  {
-    if (source.HasTag(tag))
-    {
-      SetValue(tag, source.GetValue(tag));
-    }
-  }
-
-
-  bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level)
-  {
-    const MainDicomTag *tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      if (tags[i].tag_ == tag)
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  bool DicomMap::IsMainDicomTag(const DicomTag& tag)
-  {
-    return (IsMainDicomTag(tag, ResourceType_Patient) ||
-            IsMainDicomTag(tag, ResourceType_Study) ||
-            IsMainDicomTag(tag, ResourceType_Series) ||
-            IsMainDicomTag(tag, ResourceType_Instance));
-  }
-
-
-  void DicomMap::GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level)
-  {
-    const MainDicomTag *tags = NULL;
-    size_t size;
-    LoadMainDicomTags(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      result.insert(tags[i].tag_);
-    }
-  }
-
-
-  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result, ResourceType level)
-  {
-    result.clear();
-    GetMainDicomTagsInternal(result, level);
-  }
-
-
-  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result)
-  {
-    result.clear();
-    GetMainDicomTagsInternal(result, ResourceType_Patient);
-    GetMainDicomTagsInternal(result, ResourceType_Study);
-    GetMainDicomTagsInternal(result, ResourceType_Series);
-    GetMainDicomTagsInternal(result, ResourceType_Instance);
-  }
-
-
-  void DicomMap::GetTags(std::set<DicomTag>& tags) const
-  {
-    tags.clear();
-
-    for (Content::const_iterator it = content_.begin();
-         it != content_.end(); ++it)
-    {
-      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::IsDicomFile(const void* 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.
-     **/
-
-    const uint8_t* p = reinterpret_cast<const uint8_t*>(dicom);
-
-    return (size >= 132 &&
-            p[128] == 'D' &&
-            p[129] == 'I' &&
-            p[130] == 'C' &&
-            p[131] == 'M');
-  }
-    
-
-  bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
-                                           const void* dicom,
-                                           size_t size)
-  {
-    if (!IsDicomFile(dicom, size))
-    {
-      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, reinterpret_cast<const char*>(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, reinterpret_cast<const char*>(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::LookupStringValue(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::ParseFirstFloat(float& result,
-                                 const DicomTag& tag) const
-  {
-    const DicomValue* value = TestAndGetValue(tag);
-
-    if (value == NULL)
-    {
-      return false;
-    }
-    else
-    {
-      return value->ParseFirstFloat(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::FromDicomAsJson(const Json::Value& dicomAsJson)
-  {
-    if (dicomAsJson.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-    
-    Clear();
-    
-    Json::Value::Members tags = dicomAsJson.getMemberNames();
-    for (Json::Value::Members::const_iterator
-           it = tags.begin(); it != tags.end(); ++it)
-    {
-      DicomTag tag(0, 0);
-      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
-      {
-        throw OrthancException(ErrorCode_CorruptedFile);
-      }
-
-      const Json::Value& value = dicomAsJson[*it];
-
-      if (value.type() != Json::objectValue ||
-          !value.isMember("Type") ||
-          !value.isMember("Value") ||
-          value["Type"].type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_CorruptedFile);
-      }
-
-      if (value["Type"] == "String")
-      {
-        if (value["Value"].type() != Json::stringValue)
-        {
-          throw OrthancException(ErrorCode_CorruptedFile);
-        }
-        else
-        {
-          SetValue(tag, value["Value"].asString(), false /* not binary */);
-        }
-      }
-    }
-  }
-
-
-  void DicomMap::Merge(const DicomMap& other)
-  {
-    for (Content::const_iterator it = other.content_.begin();
-         it != other.content_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (content_.find(it->first) == content_.end())
-      {
-        content_[it->first] = it->second->Clone();
-      }
-    }
-  }
-
-
-  void DicomMap::MergeMainDicomTags(const DicomMap& other,
-                                    ResourceType level)
-  {
-    const MainDicomTag* tags = NULL;
-    size_t size = 0;
-
-    LoadMainDicomTags(tags, size, level);
-    assert(tags != NULL && size > 0);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      Content::const_iterator found = other.content_.find(tags[i].tag_);
-
-      if (found != other.content_.end() &&
-          content_.find(tags[i].tag_) == content_.end())
-      {
-        assert(found->second != NULL);
-        content_[tags[i].tag_] = found->second->Clone();
-      }
-    }
-  }
-    
-
-  void DicomMap::ExtractMainDicomTags(const DicomMap& other)
-  {
-    Clear();
-    MergeMainDicomTags(other, ResourceType_Patient);
-    MergeMainDicomTags(other, ResourceType_Study);
-    MergeMainDicomTags(other, ResourceType_Series);
-    MergeMainDicomTags(other, ResourceType_Instance);
-  }    
-
-
-  bool DicomMap::HasOnlyMainDicomTags() const
-  {
-    // TODO - Speed up possible by making this std::set a global variable
-
-    std::set<DicomTag> mainDicomTags;
-    GetMainDicomTags(mainDicomTags);
-
-    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
-    {
-      if (mainDicomTags.find(it->first) == mainDicomTags.end())
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-    
-
-  void DicomMap::Serialize(Json::Value& target) const
-  {
-    target = Json::objectValue;
-
-    for (Content::const_iterator it = content_.begin(); it != content_.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()) ||
-          content_.find(tag) != content_.end())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      std::unique_ptr<DicomValue> value(new DicomValue);
-      value->Unserialize(source[tags[i]]);
-
-      content_[tag] = value.release();
-    }
-  }
-
-
-  void DicomMap::FromDicomWeb(const Json::Value& source)
-  {
-    static const char* const ALPHABETIC = "Alphabetic";
-    static const char* const IDEOGRAPHIC = "Ideographic";
-    static const char* const INLINE_BINARY = "InlineBinary";
-    static const char* const PHONETIC = "Phonetic";
-    static const char* const VALUE = "Value";
-    static const char* const VR = "vr";
-  
-    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++)
-    {
-      const Json::Value& item = source[tags[i]];
-      DicomTag tag(0, 0);
-
-      if (item.type() != Json::objectValue ||
-          !item.isMember(VR) ||
-          item[VR].type() != Json::stringValue ||
-          !DicomTag::ParseHexadecimal(tag, tags[i].c_str()))
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      ValueRepresentation vr = StringToValueRepresentation(item[VR].asString(), false);
-
-      if (item.isMember(INLINE_BINARY))
-      {
-        const Json::Value& value = item[INLINE_BINARY];
-
-        if (value.type() == Json::stringValue)
-        {
-          std::string decoded;
-          Toolbox::DecodeBase64(decoded, value.asString());
-          SetValue(tag, decoded, true /* binary data */);
-        }
-      }
-      else if (!item.isMember(VALUE))
-      {
-        // Tag is present, but it has a null value
-        SetValue(tag, "", false /* not binary */);
-      }
-      else
-      {
-        const Json::Value& value = item[VALUE];
-
-        if (value.type() == Json::arrayValue)
-        {
-          bool supported = true;
-          
-          std::string s;
-          for (Json::Value::ArrayIndex i = 0; i < value.size() && supported; i++)
-          {
-            if (!s.empty())
-            {
-              s += '\\';
-            }
-
-            switch (value[i].type())
-            {
-              case Json::objectValue:
-                if (vr == ValueRepresentation_PersonName &&
-                    value[i].type() == Json::objectValue)
-                {
-                  if (value[i].isMember(ALPHABETIC) &&
-                      value[i][ALPHABETIC].type() == Json::stringValue)
-                  {
-                    s += value[i][ALPHABETIC].asString();
-                  }
-
-                  bool hasIdeographic = false;
-                  
-                  if (value[i].isMember(IDEOGRAPHIC) &&
-                      value[i][IDEOGRAPHIC].type() == Json::stringValue)
-                  {
-                    s += '=' + value[i][IDEOGRAPHIC].asString();
-                    hasIdeographic = true;
-                  }
-                  
-                  if (value[i].isMember(PHONETIC) &&
-                      value[i][PHONETIC].type() == Json::stringValue)
-                  {
-                    if (!hasIdeographic)
-                    {
-                      s += '=';
-                    }
-                      
-                    s += '=' + value[i][PHONETIC].asString();
-                  }
-                }
-                else
-                {
-                  // This is the case of sequences
-                  supported = false;
-                }
-
-                break;
-            
-              case Json::stringValue:
-                s += value[i].asString();
-                break;
-              
-              case Json::intValue:
-                s += boost::lexical_cast<std::string>(value[i].asInt());
-                break;
-              
-              case Json::uintValue:
-                s += boost::lexical_cast<std::string>(value[i].asUInt());
-                break;
-              
-              case Json::realValue:
-                s += boost::lexical_cast<std::string>(value[i].asDouble());
-                break;
-              
-              default:
-                break;
-            }
-          }
-
-          if (supported)
-          {
-            SetValue(tag, s, false /* not binary */);
-          }
-        }
-      }
-    }
-  }
-
-
-  std::string DicomMap::GetStringValue(const DicomTag& tag,
-                                       const std::string& defaultValue,
-                                       bool allowBinary) const
-  {
-    std::string s;
-    if (LookupStringValue(s, tag, allowBinary))
-    {
-      return s;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  void DicomMap::RemoveBinaryTags()
-  {
-    Content kept;
-
-    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!it->second->IsBinary() &&
-          !it->second->IsNull())
-      {
-        kept[it->first] = it->second;
-      }
-      else
-      {
-        delete it->second;
-      }
-    }
-
-    content_ = kept;
-  }
-
-
-  void DicomMap::DumpMainDicomTags(Json::Value& target,
-                                   ResourceType level) const
-  {
-    std::map<DicomTag, std::string> mainTags;   // TODO - Create a singleton to hold this map
-    LoadMainDicomTags(mainTags, level);
-    
-    target = Json::objectValue;
-
-    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
-    {
-      assert(it->second != NULL);
-      
-      if (!it->second->IsBinary() &&
-          !it->second->IsNull())
-      {
-        std::map<DicomTag, std::string>::const_iterator found = mainTags.find(it->first);
-
-        if (found != mainTags.end())
-        {
-          target[found->second] = it->second->GetContent();
-        }
-      }
-    }    
-  }
-  
-
-  void DicomMap::ParseMainDicomTags(const Json::Value& source,
-                                    ResourceType level)
-  {
-    if (source.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-    
-    std::map<std::string, DicomTag2> mainTags;   // TODO - Create a singleton to hold this map
-    LoadMainDicomTags(mainTags, level);
-    
-    Json::Value::Members members = source.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      std::map<std::string, DicomTag2>::const_iterator found = mainTags.find(members[i]);
-
-      if (found != mainTags.end())
-      {
-        const Json::Value& value = source[members[i]];
-        if (value.type() != Json::stringValue)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat);
-        }
-        else
-        {
-          SetValue(found->second, value.asString(), false);
-        }
-      }
-    }
-  }
-
-
-  void DicomMap::Print(FILE* fp) const
-  {
-    DicomArray a(*this);
-    a.Print(fp);
-  }
-}
--- a/Core/DicomFormat/DicomMap.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,248 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomValue.h"
-#include "../Enumerations.h"
-
-#include <set>
-#include <map>
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DicomMap : public boost::noncopyable
-  {
-  public:
-    typedef std::map<DicomTag, DicomValue*>  Content;
-    
-  private:
-    friend class DicomArray;
-    friend class FromDcmtkBridge;
-    friend class ParsedDicomFile;
-
-    Content content_;
-
-    // Warning: This takes the ownership of "value"
-    void SetValueInternal(uint16_t group, 
-                          uint16_t element, 
-                          DicomValue* value);
-
-    static void GetMainDicomTagsInternal(std::set<DicomTag>& result,
-                                         ResourceType level);
-
-  public:
-    DicomMap()
-    {
-    }
-
-    ~DicomMap()
-    {
-      Clear();
-    }
-
-    size_t GetSize() const
-    {
-      return content_.size();
-    }
-    
-    DicomMap* Clone() const;
-
-    void Assign(const DicomMap& other);
-
-    void Clear();
-
-    void SetNullValue(uint16_t group, 
-                      uint16_t element)
-    {
-      SetValueInternal(group, element, new DicomValue);
-    }
-    
-    void SetNullValue(const DicomTag& tag)
-    {
-      SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue);
-    }
-    
-    void SetValue(uint16_t group, 
-                  uint16_t element, 
-                  const DicomValue& value)
-    {
-      SetValueInternal(group, element, value.Clone());
-    }
-
-    void SetValue(const DicomTag& tag,
-                  const DicomValue& value)
-    {
-      SetValueInternal(tag.GetGroup(), tag.GetElement(), value.Clone());
-    }
-
-    void SetValue(const DicomTag& tag,
-                  const std::string& str,
-                  bool isBinary)
-    {
-      SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(str, isBinary));
-    }
-
-    void SetValue(uint16_t group, 
-                  uint16_t element, 
-                  const std::string& str,
-                  bool isBinary)
-    {
-      SetValueInternal(group, element, new DicomValue(str, isBinary));
-    }
-
-    bool HasTag(uint16_t group, uint16_t element) const
-    {
-      return HasTag(DicomTag(group, element));
-    }
-
-    bool HasTag(const DicomTag& tag) const
-    {
-      return content_.find(tag) != content_.end();
-    }
-
-    const DicomValue& GetValue(uint16_t group, uint16_t element) const
-    {
-      return GetValue(DicomTag(group, element));
-    }
-
-    const DicomValue& GetValue(const DicomTag& tag) const;
-
-    // DO NOT delete the returned value!
-    const DicomValue* TestAndGetValue(uint16_t group, uint16_t element) const
-    {
-      return TestAndGetValue(DicomTag(group, element));
-    }       
-
-    // DO NOT delete the returned value!
-    const DicomValue* TestAndGetValue(const DicomTag& tag) const;
-
-    void Remove(const DicomTag& tag);
-
-    void ExtractPatientInformation(DicomMap& result) const;
-
-    void ExtractStudyInformation(DicomMap& result) const;
-
-    void ExtractSeriesInformation(DicomMap& result) const;
-
-    void ExtractInstanceInformation(DicomMap& result) const;
-
-    static void SetupFindPatientTemplate(DicomMap& result);
-
-    static void SetupFindStudyTemplate(DicomMap& result);
-
-    static void SetupFindSeriesTemplate(DicomMap& result);
-
-    static void SetupFindInstanceTemplate(DicomMap& result);
-
-    void CopyTagIfExists(const DicomMap& source,
-                         const DicomTag& tag);
-
-    static bool IsMainDicomTag(const DicomTag& tag, ResourceType level);
-
-    static bool IsMainDicomTag(const DicomTag& tag);
-
-    static void GetMainDicomTags(std::set<DicomTag>& result, ResourceType level);
-
-    static void GetMainDicomTags(std::set<DicomTag>& result);
-
-    void GetTags(std::set<DicomTag>& tags) const;
-
-    static bool IsDicomFile(const void* dicom,
-                            size_t size);
-    
-    static bool ParseDicomMetaInformation(DicomMap& result,
-                                          const void* dicom,
-                                          size_t size);
-
-    void LogMissingTagsForStore() const;
-
-    bool LookupStringValue(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 ParseFirstFloat(float& result,
-                         const DicomTag& tag) const;
-
-    bool ParseDouble(double& result,
-                     const DicomTag& tag) const;
-
-    void FromDicomAsJson(const Json::Value& dicomAsJson);
-
-    void Merge(const DicomMap& other);
-
-    void MergeMainDicomTags(const DicomMap& other,
-                            ResourceType level);
-
-    void ExtractMainDicomTags(const DicomMap& other);
-
-    bool HasOnlyMainDicomTags() const;
-    
-    void Serialize(Json::Value& target) const;
-
-    void Unserialize(const Json::Value& source);
-
-    void FromDicomWeb(const Json::Value& source);
-
-    std::string GetStringValue(const DicomTag& tag,
-                               const std::string& defaultValue,
-                               bool allowBinary) const;
-
-    void RemoveBinaryTags();
-
-    void DumpMainDicomTags(Json::Value& target,
-                           ResourceType level) const;
-
-    void ParseMainDicomTags(const Json::Value& source,
-                            ResourceType level);
-
-    void Print(FILE* fp) const;  // For debugging only
-  };
-}
--- a/Core/DicomFormat/DicomTag.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,323 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomTag.h"
-
-#include "../OrthancException.h"
-
-#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_)
-      return true;
-
-    if (group_ > other.group_)
-      return false;
-
-    return element_ < other.element_;
-  }
-
-
-  std::ostream& operator<< (std::ostream& o, const DicomTag& tag)
-  {
-    using namespace std;
-    ios_base::fmtflags state = o.flags();
-    o.flags(ios::right | ios::hex);
-    o << "(" << setfill('0') << setw(4) << tag.GetGroup()
-      << "," << setw(4) << tag.GetElement() << ")";
-    o.flags(state);
-    return o;
-  }
-
-
-  std::string DicomTag::Format() const
-  {
-    char b[16];
-    sprintf(b, "%04x,%04x", group_, element_);
-    return std::string(b);
-  }
-
-
-  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)
-      return "AccessionNumber";
-
-    if (*this == DICOM_TAG_SOP_INSTANCE_UID)
-      return "SOPInstanceUID";
-
-    if (*this == DICOM_TAG_PATIENT_ID)
-      return "PatientID";
-
-    if (*this == DICOM_TAG_SERIES_INSTANCE_UID)
-      return "SeriesInstanceUID";
-
-    if (*this == DICOM_TAG_STUDY_INSTANCE_UID)
-      return "StudyInstanceUID"; 
-
-    if (*this == DICOM_TAG_PIXEL_DATA)
-      return "PixelData";
-
-    if (*this == DICOM_TAG_IMAGE_INDEX)
-      return "ImageIndex";
-
-    if (*this == DICOM_TAG_INSTANCE_NUMBER)
-      return "InstanceNumber";
-
-    if (*this == DICOM_TAG_NUMBER_OF_SLICES)
-      return "NumberOfSlices";
-
-    if (*this == DICOM_TAG_NUMBER_OF_FRAMES)
-      return "NumberOfFrames";
-
-    if (*this == DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)
-      return "CardiacNumberOfImages";
-
-    if (*this == DICOM_TAG_IMAGES_IN_ACQUISITION)
-      return "ImagesInAcquisition";
-
-    if (*this == DICOM_TAG_PATIENT_NAME)
-      return "PatientName";
-
-    if (*this == DICOM_TAG_IMAGE_POSITION_PATIENT)
-      return "ImagePositionPatient";
-
-    if (*this == DICOM_TAG_IMAGE_ORIENTATION_PATIENT)
-      return "ImageOrientationPatient";
-
-    // New in Orthanc 1.6.0, as tagged as "RETIRED_" since DCMTK 3.6.4
-    if (*this == DICOM_TAG_OTHER_PATIENT_IDS)
-      return "OtherPatientIDs";
-
-    return "";
-  }
-
-
-  void DicomTag::AddTagsForModule(std::set<DicomTag>& target,
-                                  DicomModule module)
-  {
-    // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions
-
-    switch (module)
-    {
-      case DicomModule_Patient:
-        // This is Table C.7-1 "Patient Module Attributes" (p. 373)
-        target.insert(DicomTag(0x0010, 0x0010));   // Patient's name
-        target.insert(DicomTag(0x0010, 0x0020));   // Patient ID
-        target.insert(DicomTag(0x0010, 0x0030));   // Patient's birth date
-        target.insert(DicomTag(0x0010, 0x0040));   // Patient's sex
-        target.insert(DicomTag(0x0008, 0x1120));   // Referenced patient sequence
-        target.insert(DicomTag(0x0010, 0x0032));   // Patient's birth time
-        target.insert(DicomTag(0x0010, 0x1000));   // Other patient IDs
-        target.insert(DicomTag(0x0010, 0x1002));   // Other patient IDs sequence
-        target.insert(DicomTag(0x0010, 0x1001));   // Other patient names
-        target.insert(DicomTag(0x0010, 0x2160));   // Ethnic group
-        target.insert(DicomTag(0x0010, 0x4000));   // Patient comments
-        target.insert(DicomTag(0x0010, 0x2201));   // Patient species description
-        target.insert(DicomTag(0x0010, 0x2202));   // Patient species code sequence
-        target.insert(DicomTag(0x0010, 0x2292));   // Patient breed description
-        target.insert(DicomTag(0x0010, 0x2293));   // Patient breed code sequence
-        target.insert(DicomTag(0x0010, 0x2294));   // Breed registration sequence
-        target.insert(DicomTag(0x0010, 0x2297));   // Responsible person
-        target.insert(DicomTag(0x0010, 0x2298));   // Responsible person role
-        target.insert(DicomTag(0x0010, 0x2299));   // Responsible organization
-        target.insert(DicomTag(0x0012, 0x0062));   // Patient identity removed
-        target.insert(DicomTag(0x0012, 0x0063));   // De-identification method
-        target.insert(DicomTag(0x0012, 0x0064));   // De-identification method code sequence
-
-        // Table 10-18 ISSUER OF PATIENT ID MACRO (p. 112)
-        target.insert(DicomTag(0x0010, 0x0021));   // Issuer of Patient ID
-        target.insert(DicomTag(0x0010, 0x0024));   // Issuer of Patient ID qualifiers sequence
-        break;
-
-      case DicomModule_Study:
-        // This is Table C.7-3 "General Study Module Attributes" (p. 378)
-        target.insert(DicomTag(0x0020, 0x000d));   // Study instance UID
-        target.insert(DicomTag(0x0008, 0x0020));   // Study date
-        target.insert(DicomTag(0x0008, 0x0030));   // Study time
-        target.insert(DicomTag(0x0008, 0x0090));   // Referring physician's name
-        target.insert(DicomTag(0x0008, 0x0096));   // Referring physician identification sequence
-        target.insert(DicomTag(0x0020, 0x0010));   // Study ID
-        target.insert(DicomTag(0x0008, 0x0050));   // Accession number
-        target.insert(DicomTag(0x0008, 0x0051));   // Issuer of accession number sequence
-        target.insert(DicomTag(0x0008, 0x1030));   // Study description
-        target.insert(DicomTag(0x0008, 0x1048));   // Physician(s) of record
-        target.insert(DicomTag(0x0008, 0x1049));   // Physician(s) of record identification sequence
-        target.insert(DicomTag(0x0008, 0x1060));   // Name of physician(s) reading study
-        target.insert(DicomTag(0x0008, 0x1062));   // Physician(s) reading study identification sequence
-        target.insert(DicomTag(0x0032, 0x1034));   // Requesting service code sequence
-        target.insert(DicomTag(0x0008, 0x1110));   // Referenced study sequence
-        target.insert(DicomTag(0x0008, 0x1032));   // Procedure code sequence
-        target.insert(DicomTag(0x0040, 0x1012));   // Reason for performed procedure code sequence
-        break;
-
-      case DicomModule_Series:
-        // This is Table C.7-5 "General Series Module Attributes" (p. 385)
-        target.insert(DicomTag(0x0008, 0x0060));   // Modality 
-        target.insert(DicomTag(0x0020, 0x000e));   // Series Instance UID 
-        target.insert(DicomTag(0x0020, 0x0011));   // Series Number 
-        target.insert(DicomTag(0x0020, 0x0060));   // Laterality 
-        target.insert(DicomTag(0x0008, 0x0021));   // Series Date 
-        target.insert(DicomTag(0x0008, 0x0031));   // Series Time 
-        target.insert(DicomTag(0x0008, 0x1050));   // Performing Physicians’ Name 
-        target.insert(DicomTag(0x0008, 0x1052));   // Performing Physician Identification Sequence 
-        target.insert(DicomTag(0x0018, 0x1030));   // Protocol Name
-        target.insert(DicomTag(0x0008, 0x103e));   // Series Description 
-        target.insert(DicomTag(0x0008, 0x103f));   // Series Description Code Sequence 
-        target.insert(DicomTag(0x0008, 0x1070));   // Operators' Name 
-        target.insert(DicomTag(0x0008, 0x1072));   // Operator Identification Sequence 
-        target.insert(DicomTag(0x0008, 0x1111));   // Referenced Performed Procedure Step Sequence
-        target.insert(DicomTag(0x0008, 0x1250));   // Related Series Sequence
-        target.insert(DicomTag(0x0018, 0x0015));   // Body Part Examined
-        target.insert(DicomTag(0x0018, 0x5100));   // Patient Position
-        target.insert(DicomTag(0x0028, 0x0108));   // Smallest Pixel Value in Series 
-        target.insert(DicomTag(0x0029, 0x0109));   // Largest Pixel Value in Series 
-        target.insert(DicomTag(0x0040, 0x0275));   // Request Attributes Sequence 
-        target.insert(DicomTag(0x0010, 0x2210));   // Anatomical Orientation Type
-
-        // Table 10-16 PERFORMED PROCEDURE STEP SUMMARY MACRO ATTRIBUTES
-        target.insert(DicomTag(0x0040, 0x0253));   // Performed Procedure Step ID 
-        target.insert(DicomTag(0x0040, 0x0244));   // Performed Procedure Step Start Date 
-        target.insert(DicomTag(0x0040, 0x0245));   // Performed Procedure Step Start Time 
-        target.insert(DicomTag(0x0040, 0x0254));   // Performed Procedure Step Description 
-        target.insert(DicomTag(0x0040, 0x0260));   // Performed Protocol Code Sequence 
-        target.insert(DicomTag(0x0040, 0x0280));   // Comments on the Performed Procedure Step
-        break;
-
-      case DicomModule_Instance:
-        // This is Table C.12-1 "SOP Common Module Attributes" (p. 1207)
-        target.insert(DicomTag(0x0008, 0x0016));   // SOP Class UID
-        target.insert(DicomTag(0x0008, 0x0018));   // SOP Instance UID 
-        target.insert(DicomTag(0x0008, 0x0005));   // Specific Character Set 
-        target.insert(DicomTag(0x0008, 0x0012));   // Instance Creation Date 
-        target.insert(DicomTag(0x0008, 0x0013));   // Instance Creation Time 
-        target.insert(DicomTag(0x0008, 0x0014));   // Instance Creator UID 
-        target.insert(DicomTag(0x0008, 0x001a));   // Related General SOP Class UID 
-        target.insert(DicomTag(0x0008, 0x001b));   // Original Specialized SOP Class UID 
-        target.insert(DicomTag(0x0008, 0x0110));   // Coding Scheme Identification Sequence 
-        target.insert(DicomTag(0x0008, 0x0201));   // Timezone Offset From UTC 
-        target.insert(DicomTag(0x0018, 0xa001));   // Contributing Equipment Sequence
-        target.insert(DicomTag(0x0020, 0x0013));   // Instance Number 
-        target.insert(DicomTag(0x0100, 0x0410));   // SOP Instance Status 
-        target.insert(DicomTag(0x0100, 0x0420));   // SOP Authorization DateTime 
-        target.insert(DicomTag(0x0100, 0x0424));   // SOP Authorization Comment 
-        target.insert(DicomTag(0x0100, 0x0426));   // Authorization Equipment Certification Number
-        target.insert(DicomTag(0x0400, 0x0500));   // Encrypted Attributes Sequence
-        target.insert(DicomTag(0x0400, 0x0561));   // Original Attributes Sequence 
-        target.insert(DicomTag(0x0040, 0xa390));   // HL7 Structured Document Reference Sequence
-        target.insert(DicomTag(0x0028, 0x0303));   // Longitudinal Temporal Information Modified 
-
-        // Table C.12-6 "DIGITAL SIGNATURES MACRO ATTRIBUTES" (p. 1216)
-        target.insert(DicomTag(0x4ffe, 0x0001));   // MAC Parameters sequence
-        target.insert(DicomTag(0xfffa, 0xfffa));   // Digital signatures sequence
-        break;
-
-        // TODO IMAGE MODULE?
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-}
--- a/Core/DicomFormat/DicomTag.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,243 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <set>
-#include <stdint.h>
-
-#include "../Enumerations.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DicomTag
-  {
-    // This must stay a POD (plain old data structure) 
-
-  private:
-    uint16_t group_;
-    uint16_t element_;
-
-  public:
-    DicomTag(uint16_t group,
-             uint16_t element) :
-      group_(group),
-      element_(element)
-    {
-    }
-
-    uint16_t GetGroup() const
-    {
-      return group_;
-    }
-
-    uint16_t GetElement() const
-    {
-      return element_;
-    }
-
-    bool IsPrivate() const
-    {
-      return group_ % 2 == 1;
-    }
-
-    const char* GetMainTagsName() const;
-
-    bool operator< (const DicomTag& other) const;
-
-    bool operator== (const DicomTag& other) const
-    {
-      return group_ == other.group_ && element_ == other.element_;
-    }
-
-    bool operator!= (const DicomTag& other) const
-    {
-      return !(*this == other);
-    }
-
-    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,
-                                 DicomModule module);
-  };
-
-  // Aliases for the most useful tags
-  static const DicomTag DICOM_TAG_ACCESSION_NUMBER(0x0008, 0x0050);
-  static const DicomTag DICOM_TAG_SOP_INSTANCE_UID(0x0008, 0x0018);
-  static const DicomTag DICOM_TAG_PATIENT_ID(0x0010, 0x0020);
-  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);
-
-  static const DicomTag DICOM_TAG_NUMBER_OF_SLICES(0x0054, 0x0081);
-  static const DicomTag DICOM_TAG_NUMBER_OF_TIME_SLICES(0x0054, 0x0101);
-  static const DicomTag DICOM_TAG_NUMBER_OF_FRAMES(0x0028, 0x0008);
-  static const DicomTag DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES(0x0018, 0x1090);
-  static const DicomTag DICOM_TAG_IMAGES_IN_ACQUISITION(0x0020, 0x1002);
-  static const DicomTag DICOM_TAG_PATIENT_NAME(0x0010, 0x0010);
-  static const DicomTag DICOM_TAG_ENCAPSULATED_DOCUMENT(0x0042, 0x0011);
-
-  static const DicomTag DICOM_TAG_STUDY_DESCRIPTION(0x0008, 0x1030);
-  static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e);
-  static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
-
-  // The following is used for "modify/anonymize" operations
-  static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
-  static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002);
-  static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID(0x0002, 0x0003);
-  static const DicomTag DICOM_TAG_DEIDENTIFICATION_METHOD(0x0012, 0x0063);
-
-  // DICOM tags used for fMRI (thanks to Will Ryder)
-  static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105);
-  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);
-
-  // Tags for images
-  static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011);
-  static const DicomTag DICOM_TAG_ROWS(0x0028, 0x0010);
-  static const DicomTag DICOM_TAG_SAMPLES_PER_PIXEL(0x0028, 0x0002);
-  static const DicomTag DICOM_TAG_BITS_ALLOCATED(0x0028, 0x0100);
-  static const DicomTag DICOM_TAG_BITS_STORED(0x0028, 0x0101);
-  static const DicomTag DICOM_TAG_HIGH_BIT(0x0028, 0x0102);
-  static const DicomTag DICOM_TAG_PIXEL_REPRESENTATION(0x0028, 0x0103);
-  static const DicomTag DICOM_TAG_PLANAR_CONFIGURATION(0x0028, 0x0006);
-  static const DicomTag DICOM_TAG_PHOTOMETRIC_INTERPRETATION(0x0028, 0x0004);
-  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_LARGEST_IMAGE_PIXEL_VALUE(0x0028, 0x0107);
-  static const DicomTag DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE(0x0028, 0x0106);
-
-  // Tags related to date and time
-  static const DicomTag DICOM_TAG_ACQUISITION_DATE(0x0008, 0x0022);
-  static const DicomTag DICOM_TAG_ACQUISITION_TIME(0x0008, 0x0032);
-  static const DicomTag DICOM_TAG_CONTENT_DATE(0x0008, 0x0023);
-  static const DicomTag DICOM_TAG_CONTENT_TIME(0x0008, 0x0033);
-  static const DicomTag DICOM_TAG_INSTANCE_CREATION_DATE(0x0008, 0x0012);
-  static const DicomTag DICOM_TAG_INSTANCE_CREATION_TIME(0x0008, 0x0013);
-  static const DicomTag DICOM_TAG_PATIENT_BIRTH_DATE(0x0010, 0x0030);
-  static const DicomTag DICOM_TAG_PATIENT_BIRTH_TIME(0x0010, 0x0032);
-  static const DicomTag DICOM_TAG_SERIES_DATE(0x0008, 0x0021);
-  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_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
-  static const DicomTag DICOM_TAG_CASSETTE_ORIENTATION(0x0018, 0x1402);
-  static const DicomTag DICOM_TAG_CASSETTE_SIZE(0x0018, 0x1403);
-  static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
-  static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010);
-  static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011);
-  static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040);
-  static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060);
-  static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015);
-  static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101);
-  static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070);
-  static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020);
-  static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000);
-  static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201);
-  static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000);
-  static const DicomTag DICOM_TAG_OTHER_PATIENT_IDS(0x0010, 0x1000);
-
-  // 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);
-  static const DicomTag DICOM_TAG_RED_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1201);
-  static const DicomTag DICOM_TAG_GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1202);
-  static const DicomTag DICOM_TAG_BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1203);
-  static const DicomTag DICOM_TAG_RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1101);
-  static const DicomTag DICOM_TAG_GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1102);
-  static const DicomTag DICOM_TAG_BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1103);
-  static const DicomTag DICOM_TAG_CONTOUR_DATA(0x3006, 0x0050);
-                             
-  // 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);
-  static const DicomTag DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE(0x3006, 0x0010);
-  static const DicomTag DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE(0x3006, 0x0012);
-  static const DicomTag DICOM_TAG_RT_REFERENCED_SERIES_SEQUENCE(0x3006, 0x0014);
-
-  // Tags for DICOMDIR
-  static const DicomTag DICOM_TAG_DIRECTORY_RECORD_TYPE(0x0004, 0x1430);
-  static const DicomTag DICOM_TAG_OFFSET_OF_THE_NEXT_DIRECTORY_RECORD(0x0004, 0x1400);
-  static const DicomTag DICOM_TAG_OFFSET_OF_REFERENCED_LOWER_LEVEL_DIRECTORY_ENTITY(0x0004, 0x1420);
-  static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE(0x0004, 0x1511);
-  static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500);
-}
--- a/Core/DicomFormat/DicomValue.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,322 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomValue.h"
-
-#include "../OrthancException.h"
-#include "../SerializationToolbox.h"
-#include "../Toolbox.h"
-
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  DicomValue::DicomValue(const DicomValue& other) : 
-    type_(other.type_),
-    content_(other.content_)
-  {
-  }
-
-
-  DicomValue::DicomValue(const std::string& content,
-                         bool isBinary) :
-    type_(isBinary ? Type_Binary : Type_String),
-    content_(content)
-  {
-  }
-  
-  
-  DicomValue::DicomValue(const char* data,
-                         size_t size,
-                         bool isBinary) :
-    type_(isBinary ? Type_Binary : Type_String)
-  {
-    content_.assign(data, size);
-  }
-    
-  
-  const std::string& DicomValue::GetContent() const
-  {
-    if (type_ == Type_Null)
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-    else
-    {
-      return content_;
-    }
-  }
-
-
-  DicomValue* DicomValue::Clone() const
-  {
-    return new DicomValue(*this);
-  }
-
-  
-#if ORTHANC_ENABLE_BASE64 == 1
-  void DicomValue::FormatDataUriScheme(std::string& target,
-                                       const std::string& mime) const
-  {
-    Toolbox::EncodeBase64(target, GetContent());
-    target.insert(0, "data:" + mime + ";base64,");
-  }
-#endif
-
-  // same as ParseValue but in case the value actually contains a sequence,
-  // it will return the first value
-  // this has been introduced to support invalid "width/height" DICOM tags in some US
-  // images where the width is stored as "800\0" !
-  template <typename T,
-            bool allowSigned>
-  static bool ParseFirstValue(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;
-      }
-
-      if (value.find("\\") == std::string::npos)
-      {
-        result = boost::lexical_cast<T>(value);
-        return true;
-      }
-      else
-      {
-        std::vector<std::string> tokens;
-        Toolbox::TokenizeString(tokens, value, '\\');
-
-        if (tokens.size() >= 1)
-        {
-          result = boost::lexical_cast<T>(tokens[0]);
-          return true;
-        }
-
-        return false;
-      }
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return false;
-    }
-  }
-
-
-  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::ParseFirstFloat(float& result) const
-  {
-    return ParseFirstValue<float, true>(result, *this);
-  }
-
-  bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
-  {
-    return ParseFirstValue<unsigned int, 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <boost/noncopyable.hpp>
-#include <json/value.h>
-
-#if !defined(ORTHANC_ENABLE_BASE64)
-#  error The macro ORTHANC_ENABLE_BASE64 must be defined
-#endif
-
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DicomValue : public boost::noncopyable
-  {
-  private:
-    enum Type
-    {
-      Type_Null,
-      Type_String,
-      Type_Binary
-    };
-
-    Type         type_;
-    std::string  content_;
-
-    DicomValue(const DicomValue& other);
-
-  public:
-    DicomValue() : type_(Type_Null)
-    {
-    }
-    
-    DicomValue(const std::string& content,
-               bool isBinary);
-    
-    DicomValue(const char* data,
-               size_t size,
-               bool isBinary);
-    
-    const std::string& GetContent() const;
-
-    bool IsNull() const
-    {
-      return type_ == Type_Null;
-    }
-
-    bool IsBinary() const
-    {
-      return type_ == Type_Binary;
-    }
-    
-    DicomValue* Clone() const;
-
-#if ORTHANC_ENABLE_BASE64 == 1
-    void FormatDataUriScheme(std::string& target,
-                             const std::string& mime) const;
-
-    void FormatDataUriScheme(std::string& target) const
-    {
-      FormatDataUriScheme(target, MIME_BINARY);
-    }
-#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;
-
-    bool ParseFirstFloat(float& result) const;
-
-    bool ParseFirstUnsignedInteger(unsigned int& result) const;
-
-    void Serialize(Json::Value& target) const;
-
-    void Unserialize(const Json::Value& source);
-  };
-}
--- a/Core/DicomNetworking/DicomAssociation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,860 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomAssociation.h"
-
-#if !defined(DCMTK_VERSION_NUMBER)
-#  error The macro DCMTK_VERSION_NUMBER must be defined
-#endif
-
-#include "../Compatibility.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "NetworkingCompatibility.h"
-
-#include <dcmtk/dcmnet/diutil.h>  // For dcmConnectionTimeout()
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-namespace Orthanc
-{
-  static void FillSopSequence(DcmDataset& dataset,
-                              const DcmTagKey& tag,
-                              const std::vector<std::string>& sopClassUids,
-                              const std::vector<std::string>& sopInstanceUids,
-                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
-                              bool hasFailureReasons)
-  {
-    assert(sopClassUids.size() == sopInstanceUids.size() &&
-           (hasFailureReasons ?
-            failureReasons.size() == sopClassUids.size() :
-            failureReasons.empty()));
-
-    if (sopInstanceUids.empty())
-    {
-      // Add an empty sequence
-      if (!dataset.insertEmptyElement(tag).good())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-    else
-    {
-      for (size_t i = 0; i < sopClassUids.size(); i++)
-      {
-        std::unique_ptr<DcmItem> item(new DcmItem);
-        if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
-            !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
-            (hasFailureReasons &&
-             !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
-            !dataset.insertSequenceItem(tag, item.release()).good())
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-    }
-  }                              
-
-
-  void DicomAssociation::Initialize()
-  {
-    role_ = DicomAssociationRole_Default;
-    isOpen_ = false;
-    net_ = NULL; 
-    params_ = NULL;
-    assoc_ = NULL;      
-
-    // Must be after "isOpen_ = false"
-    ClearPresentationContexts();
-  }
-
-    
-  void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters,
-                                         const OFCondition& cond)
-  {
-    try
-    {
-      CheckCondition(cond, parameters, "connecting");
-    }
-    catch (OrthancException&)
-    {
-      CloseInternal();
-      throw;
-    }
-  }
-
-    
-  void DicomAssociation::CloseInternal()
-  {
-    if (assoc_ != NULL)
-    {
-      ASC_releaseAssociation(assoc_);
-      ASC_destroyAssociation(&assoc_);
-      assoc_ = NULL;
-      params_ = NULL;
-    }
-    else
-    {
-      if (params_ != NULL)
-      {
-        ASC_destroyAssociationParameters(&params_);
-        params_ = NULL;
-      }
-    }
-
-    if (net_ != NULL)
-    {
-      ASC_dropNetwork(&net_);
-      net_ = NULL;
-    }
-
-    accepted_.clear();
-    isOpen_ = false;
-  }
-
-    
-  void DicomAssociation::AddAccepted(const std::string& abstractSyntax,
-                                     DicomTransferSyntax syntax,
-                                     uint8_t presentationContextId)
-  {
-    AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax);
-
-    if (found == accepted_.end())
-    {
-      std::map<DicomTransferSyntax, uint8_t> syntaxes;
-      syntaxes[syntax] = presentationContextId;
-      accepted_[abstractSyntax] = syntaxes;
-    }      
-    else
-    {
-      if (found->second.find(syntax) != found->second.end())
-      {
-        LOG(WARNING) << "The same transfer syntax ("
-                     << GetTransferSyntaxUid(syntax)
-                     << ") was accepted twice for the same abstract syntax UID ("
-                     << abstractSyntax << ")";
-      }
-      else
-      {
-        found->second[syntax] = presentationContextId;
-      }
-    }
-  }
-
-
-  DicomAssociation::~DicomAssociation()
-  {
-    try
-    {
-      Close();
-    }
-    catch (OrthancException& e)
-    {
-      // Don't throw exception in destructors
-      LOG(ERROR) << "Error while destroying a DICOM association: " << e.What();
-    }
-  }
-
-
-  void DicomAssociation::SetRole(DicomAssociationRole role)
-  {
-    if (role_ != role)
-    {
-      Close();
-      role_ = role;
-    }
-  }
-
-  
-  void DicomAssociation::ClearPresentationContexts()
-  {
-    Close();
-    proposed_.clear();
-    proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
-  }
-
-  
-  void DicomAssociation::Open(const DicomAssociationParameters& parameters)
-  {
-    if (isOpen_)
-    {
-      return;  // Already open
-    }
-
-    // Timeout used during association negociation and ASC_releaseAssociation()
-    uint32_t acseTimeout = parameters.GetTimeout();
-    if (acseTimeout == 0)
-    {
-      /**
-       * Timeout is disabled. Global timeout (seconds) for
-       * connecting to remote hosts.  Default value is -1 which
-       * selects infinite timeout, i.e. blocking connect().
-       **/
-      dcmConnectionTimeout.set(-1);
-      acseTimeout = 10;
-    }
-    else
-    {
-      dcmConnectionTimeout.set(acseTimeout);
-    }
-      
-    T_ASC_SC_ROLE dcmtkRole;
-    switch (role_)
-    {
-      case DicomAssociationRole_Default:
-        dcmtkRole = ASC_SC_ROLE_DEFAULT;
-        break;
-
-      case DicomAssociationRole_Scu:
-        dcmtkRole = ASC_SC_ROLE_SCU;
-        break;
-
-      case DicomAssociationRole_Scp:
-        dcmtkRole = ASC_SC_ROLE_SCP;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    assert(net_ == NULL &&
-           params_ == NULL &&
-           assoc_ == NULL);
-
-    if (proposed_.empty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "No presentation context was proposed");
-    }
-
-    LOG(INFO) << "Opening a DICOM SCU connection from AET \""
-              << parameters.GetLocalApplicationEntityTitle() 
-              << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle()
-              << "\" on host " << parameters.GetRemoteModality().GetHost()
-              << ":" << parameters.GetRemoteModality().GetPortNumber() 
-              << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")";
-
-    CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
-    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
-
-    // Set this application's title and the called application's title in the params
-    CheckConnecting(parameters, ASC_setAPTitles(
-                      params_, parameters.GetLocalApplicationEntityTitle().c_str(),
-                      parameters.GetRemoteModality().GetApplicationEntityTitle().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",
-       parameters.GetRemoteModality().GetHost().c_str(),
-       parameters.GetRemoteModality().GetPortNumber());
-
-    CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
-
-    // Set various options
-    CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false));
-
-    // Setup the list of proposed presentation contexts
-    unsigned int presentationContextId = 1;
-    for (size_t i = 0; i < proposed_.size(); i++)
-    {
-      assert(presentationContextId <= 255);
-      const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
-
-      const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
-          
-      std::vector<const char*> transferSyntaxes;
-      transferSyntaxes.reserve(source.size());
-          
-      for (std::set<DicomTransferSyntax>::const_iterator
-             it = source.begin(); it != source.end(); ++it)
-      {
-        transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
-      }
-
-      assert(!transferSyntaxes.empty());
-      CheckConnecting(parameters, ASC_addPresentationContext(
-                        params_, presentationContextId, abstractSyntax,
-                        &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
-
-      presentationContextId += 2;
-    }
-
-    // Do the association
-    CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_));
-    isOpen_ = true;
-
-    // Inspect the accepted transfer syntaxes
-    LST_HEAD **l = &params_->DULparams.acceptedPresentationContext;
-    if (*l != NULL)
-    {
-      DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
-      LST_Position(l, (LST_NODE*)pc);
-      while (pc)
-      {
-        if (pc->result == ASC_P_ACCEPTANCE)
-        {
-          DicomTransferSyntax transferSyntax;
-          if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
-          {
-            AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
-          }
-          else
-          {
-            LOG(WARNING) << "Unknown transfer syntax received from AET \""
-                         << parameters.GetRemoteModality().GetApplicationEntityTitle()
-                         << "\": " << pc->acceptedTransferSyntax;
-          }
-        }
-            
-        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
-      }
-    }
-
-    if (accepted_.empty())
-    {
-      throw OrthancException(ErrorCode_NoPresentationContext,
-                             "Unable to negotiate a presentation context with AET \"" +
-                             parameters.GetRemoteModality().GetApplicationEntityTitle() + "\"");
-    }
-  }
-
-  void DicomAssociation::Close()
-  {
-    if (isOpen_)
-    {
-      CloseInternal();
-    }
-  }
-
-    
-  bool DicomAssociation::LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target,
-                                                           const std::string& abstractSyntax) const
-  {
-    if (!IsOpen())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened");
-    }
-      
-    AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax);
-
-    if (found == accepted_.end())
-    {
-      return false;
-    }
-    else
-    {
-      target = found->second;
-      return true;
-    }
-  }
-
-    
-  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
-  {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(DicomTransferSyntax_LittleEndianImplicit);
-    ts.insert(DicomTransferSyntax_LittleEndianExplicit);
-    ts.insert(DicomTransferSyntax_BigEndianExplicit);  // Retired
-    ProposePresentationContext(abstractSyntax, ts);
-  }
-
-    
-  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
-                                                    DicomTransferSyntax transferSyntax)
-  {
-    std::set<DicomTransferSyntax> ts;
-    ts.insert(transferSyntax);
-    ProposePresentationContext(abstractSyntax, ts);
-  }
-
-    
-  size_t DicomAssociation::GetRemainingPropositions() const
-  {
-    assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
-    return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
-  }
-    
-
-  void DicomAssociation::ProposePresentationContext(
-    const std::string& abstractSyntax,
-    const std::set<DicomTransferSyntax>& transferSyntaxes)
-  {
-    if (transferSyntaxes.empty())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "No transfer syntax provided");
-    }
-      
-    if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Too many proposed presentation contexts");
-    }
-      
-    if (IsOpen())
-    {
-      Close();
-    }
-
-    ProposedPresentationContext context;
-    context.abstractSyntax_ = abstractSyntax;
-    context.transferSyntaxes_ = transferSyntaxes;
-
-    proposed_.push_back(context);
-  }
-
-    
-  T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const
-  {
-    if (isOpen_)
-    {
-      assert(assoc_ != NULL);
-      return *assoc_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "The connection is not open");
-    }
-  }
-
-    
-  T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
-  {
-    if (isOpen_)
-    {
-      assert(net_ != NULL);
-      return *net_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "The connection is not open");
-    }
-  }
-
-    
-  void DicomAssociation::CheckCondition(const OFCondition& cond,
-                                        const DicomAssociationParameters& parameters,
-                                        const std::string& command)
-  {
-    if (cond.bad())
-    {
-      // Reformat the error message from DCMTK by turning multiline
-      // errors into a single line
-      
-      std::string s(cond.text());
-      std::string info;
-      info.reserve(s.size());
-
-      bool isMultiline = false;
-      for (size_t i = 0; i < s.size(); i++)
-      {
-        if (s[i] == '\r')
-        {
-          // Ignore
-        }
-        else if (s[i] == '\n')
-        {
-          if (isMultiline)
-          {
-            info += "; ";
-          }
-          else
-          {
-            info += " (";
-            isMultiline = true;
-          }
-        }
-        else
-        {
-          info.push_back(s[i]);
-        }
-      }
-
-      if (isMultiline)
-      {
-        info += ")";
-      }
-
-      throw OrthancException(ErrorCode_NetworkProtocol,
-                             "DicomAssociation - " + command + " to AET \"" +
-                             parameters.GetRemoteModality().GetApplicationEntityTitle() +
-                             "\": " + info);
-    }
-  }
-    
-
-  void DicomAssociation::ReportStorageCommitment(
-    const DicomAssociationParameters& parameters,
-    const std::string& transactionUid,
-    const std::vector<std::string>& sopClassUids,
-    const std::vector<std::string>& sopInstanceUids,
-    const std::vector<StorageCommitmentFailureReason>& failureReasons)
-  {
-    if (sopClassUids.size() != sopInstanceUids.size() ||
-        sopClassUids.size() != failureReasons.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    
-
-    std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids;
-    std::vector<StorageCommitmentFailureReason> failedReasons;
-
-    successSopClassUids.reserve(sopClassUids.size());
-    successSopInstanceUids.reserve(sopClassUids.size());
-    failedSopClassUids.reserve(sopClassUids.size());
-    failedSopInstanceUids.reserve(sopClassUids.size());
-    failedReasons.reserve(sopClassUids.size());
-
-    for (size_t i = 0; i < sopClassUids.size(); i++)
-    {
-      switch (failureReasons[i])
-      {
-        case StorageCommitmentFailureReason_Success:
-          successSopClassUids.push_back(sopClassUids[i]);
-          successSopInstanceUids.push_back(sopInstanceUids[i]);
-          break;
-
-        case StorageCommitmentFailureReason_ProcessingFailure:
-        case StorageCommitmentFailureReason_NoSuchObjectInstance:
-        case StorageCommitmentFailureReason_ResourceLimitation:
-        case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
-        case StorageCommitmentFailureReason_ClassInstanceConflict:
-        case StorageCommitmentFailureReason_DuplicateTransactionUID:
-          failedSopClassUids.push_back(sopClassUids[i]);
-          failedSopInstanceUids.push_back(sopInstanceUids[i]);
-          failedReasons.push_back(failureReasons[i]);
-          break;
-
-        default:
-        {
-          char buf[16];
-          sprintf(buf, "%04xH", failureReasons[i]);
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Unsupported failure reason for storage commitment: " + std::string(buf));
-        }
-      }
-    }
-    
-    DicomAssociation association;
-
-    {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
-
-      association.SetRole(DicomAssociationRole_Scp);
-      association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
-    }
-      
-    association.Open(parameters);
-
-    /**
-     * N-EVENT-REPORT
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
-     *
-     * Status code:
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
-     **/
-
-    /**
-     * Send the "EVENT_REPORT_RQ" request
-     **/
-
-    LOG(INFO) << "Reporting modality \""
-              << parameters.GetRemoteModality().GetApplicationEntityTitle()
-              << "\" about storage commitment transaction: " << transactionUid
-              << " (" << successSopClassUids.size() << " successes, " 
-              << failedSopClassUids.size() << " failures)";
-    const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
-      
-    {
-      T_DIMSE_Message message;
-      memset(&message, 0, sizeof(message));
-      message.CommandField = DIMSE_N_EVENT_REPORT_RQ;
-
-      T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ;
-      content.MessageID = messageId;
-      strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
-      strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
-      content.DataSetType = DIMSE_DATASET_PRESENT;
-
-      DcmDataset dataset;
-      if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      {
-        std::vector<StorageCommitmentFailureReason> empty;
-        FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
-                        successSopInstanceUids, empty, false);
-      }
-
-      // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
-      if (failedSopClassUids.empty())
-      {
-        content.EventTypeID = 1;  // "Storage Commitment Request Successful"
-      }
-      else
-      {
-        content.EventTypeID = 2;  // "Storage Commitment Request Complete - Failures Exist"
-
-        // Failure reason
-        // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
-        FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids,
-                        failedSopInstanceUids, failedReasons, true);
-      }
-
-      int presID = ASC_findAcceptedPresentationContextID(
-        &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
-      if (presID == 0)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "Unable to send N-EVENT-REPORT request to AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-
-      if (!DIMSE_sendMessageUsingMemoryData(
-            &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
-            &dataset, NULL /* callback */, NULL /* callback context */,
-            NULL /* commandSet */).good())
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol);
-      }
-    }
-
-    /**
-     * Read the "EVENT_REPORT_RSP" response
-     **/
-
-    {
-      T_ASC_PresentationContextID presID = 0;
-      T_DIMSE_Message message;
-
-      if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
-                                (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                                parameters.GetTimeout(), &presID, &message,
-                                NULL /* no statusDetail */).good() ||
-          message.CommandField != DIMSE_N_EVENT_REPORT_RSP)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "Unable to read N-EVENT-REPORT response from AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-
-      const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
-      if (content.MessageIDBeingRespondedTo != messageId ||
-          !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) ||
-          !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) ||
-          //(content.opts & O_NEVENTREPORT_EVENTTYPEID) ||  // Pedantic test - The "content.EventTypeID" is not used by Orthanc
-          std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
-          std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
-          content.DataSetType != DIMSE_DATASET_NULL)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "Badly formatted N-EVENT-REPORT response from AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-
-      if (content.DimseStatus != 0 /* success */)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "The request cannot be handled by remote AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-    }
-
-    association.Close();
-  }
-
-    
-  void DicomAssociation::RequestStorageCommitment(
-    const DicomAssociationParameters& parameters,
-    const std::string& transactionUid,
-    const std::vector<std::string>& sopClassUids,
-    const std::vector<std::string>& sopInstanceUids)
-  {
-    if (sopClassUids.size() != sopInstanceUids.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    for (size_t i = 0; i < sopClassUids.size(); i++)
-    {
-      if (sopClassUids[i].empty() ||
-          sopInstanceUids[i].empty())
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange,
-                               "The SOP class/instance UIDs cannot be empty, found: \"" +
-                               sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\"");
-      }
-    }
-
-    if (transactionUid.size() < 5 ||
-        transactionUid.substr(0, 5) != "2.25.")
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    DicomAssociation association;
-
-    {
-      std::set<DicomTransferSyntax> transferSyntaxes;
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
-      
-      association.SetRole(DicomAssociationRole_Default);
-      association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
-                                             transferSyntaxes);
-    }
-      
-    association.Open(parameters);
-      
-    /**
-     * N-ACTION
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
-     *
-     * Status code:
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
-     **/
-
-    /**
-     * Send the "N_ACTION_RQ" request
-     **/
-
-    LOG(INFO) << "Request to modality \""
-              << parameters.GetRemoteModality().GetApplicationEntityTitle()
-              << "\" about storage commitment for " << sopClassUids.size()
-              << " instances, with transaction UID: " << transactionUid;
-    const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
-      
-    {
-      T_DIMSE_Message message;
-      memset(&message, 0, sizeof(message));
-      message.CommandField = DIMSE_N_ACTION_RQ;
-
-      T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ;
-      content.MessageID = messageId;
-      strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
-      strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
-      content.ActionTypeID = 1;  // "Request Storage Commitment"
-      content.DataSetType = DIMSE_DATASET_PRESENT;
-
-      DcmDataset dataset;
-      if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      {
-        std::vector<StorageCommitmentFailureReason> empty;
-        FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false);
-      }
-          
-      int presID = ASC_findAcceptedPresentationContextID(
-        &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
-      if (presID == 0)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "Unable to send N-ACTION request to AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-
-      if (!DIMSE_sendMessageUsingMemoryData(
-            &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
-            &dataset, NULL /* callback */, NULL /* callback context */,
-            NULL /* commandSet */).good())
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol);
-      }
-    }
-
-    /**
-     * Read the "N_ACTION_RSP" response
-     **/
-
-    {
-      T_ASC_PresentationContextID presID = 0;
-      T_DIMSE_Message message;
-        
-      if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
-                                (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                                parameters.GetTimeout(), &presID, &message,
-                                NULL /* no statusDetail */).good() ||
-          message.CommandField != DIMSE_N_ACTION_RSP)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "Unable to read N-ACTION response from AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-
-      const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
-      if (content.MessageIDBeingRespondedTo != messageId ||
-          !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) ||
-          !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) ||
-          //(content.opts & O_NACTION_ACTIONTYPEID) ||  // Pedantic test - The "content.ActionTypeID" is not used by Orthanc
-          std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
-          std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
-          content.DataSetType != DIMSE_DATASET_NULL)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "Badly formatted N-ACTION response from AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-
-      if (content.DimseStatus != 0 /* success */)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
-                               "The request cannot be handled by remote AET: " +
-                               parameters.GetRemoteModality().GetApplicationEntityTitle());
-      }
-    }
-
-    association.Close();
-  }
-}
--- a/Core/DicomNetworking/DicomAssociation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomAssociationParameters.h"
-
-#include <dcmtk/dcmnet/dimse.h>
-
-#include <stdint.h>   // For uint8_t
-#include <boost/noncopyable.hpp>
-#include <set>
-
-namespace Orthanc
-{
-  class DicomAssociation : public boost::noncopyable
-  {
-  private:
-    // This is the maximum number of presentation context IDs (the
-    // number of odd integers between 1 and 255)
-    // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
-    static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
-    
-    struct ProposedPresentationContext
-    {
-      std::string                    abstractSyntax_;
-      std::set<DicomTransferSyntax>  transferSyntaxes_;
-    };
-
-    typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >
-    AcceptedPresentationContexts;
-
-    DicomAssociationRole                      role_;
-    bool                                      isOpen_;
-    std::vector<ProposedPresentationContext>  proposed_;
-    AcceptedPresentationContexts              accepted_;
-    T_ASC_Network*                            net_;
-    T_ASC_Parameters*                         params_;
-    T_ASC_Association*                        assoc_;
-
-    void Initialize();
-
-    void CheckConnecting(const DicomAssociationParameters& parameters,
-                         const OFCondition& cond);
-    
-    void CloseInternal();
-
-    void AddAccepted(const std::string& abstractSyntax,
-                     DicomTransferSyntax syntax,
-                     uint8_t presentationContextId);
-
-  public:
-    DicomAssociation()
-    {
-      Initialize();
-    }
-
-    ~DicomAssociation();
-
-    bool IsOpen() const
-    {
-      return isOpen_;
-    }
-
-    void SetRole(DicomAssociationRole role);
-
-    void ClearPresentationContexts();
-
-    void Open(const DicomAssociationParameters& parameters);
-    
-    void Close();
-
-    bool LookupAcceptedPresentationContext(
-      std::map<DicomTransferSyntax, uint8_t>& target,
-      const std::string& abstractSyntax) const;
-
-    void ProposeGenericPresentationContext(const std::string& abstractSyntax);
-
-    void ProposePresentationContext(const std::string& abstractSyntax,
-                                    DicomTransferSyntax transferSyntax);
-
-    size_t GetRemainingPropositions() const;
-
-    void ProposePresentationContext(
-      const std::string& abstractSyntax,
-      const std::set<DicomTransferSyntax>& transferSyntaxes);
-    
-    T_ASC_Association& GetDcmtkAssociation() const;
-
-    T_ASC_Network& GetDcmtkNetwork() const;
-
-    static void CheckCondition(const OFCondition& cond,
-                               const DicomAssociationParameters& parameters,
-                               const std::string& command);
-
-    static void ReportStorageCommitment(
-      const DicomAssociationParameters& parameters,
-      const std::string& transactionUid,
-      const std::vector<std::string>& sopClassUids,
-      const std::vector<std::string>& sopInstanceUids,
-      const std::vector<StorageCommitmentFailureReason>& failureReasons);
-    
-    static void RequestStorageCommitment(
-      const DicomAssociationParameters& parameters,
-      const std::string& transactionUid,
-      const std::vector<std::string>& sopClassUids,
-      const std::vector<std::string>& sopInstanceUids);
-  };
-}
--- a/Core/DicomNetworking/DicomAssociationParameters.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomAssociationParameters.h"
-
-#include "../Compatibility.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../SerializationToolbox.h"
-#include "NetworkingCompatibility.h"
-
-#include <boost/thread/mutex.hpp>
-
-// By default, the timeout for client DICOM connections is set to 10 seconds
-static boost::mutex  defaultTimeoutMutex_;
-static uint32_t defaultTimeout_ = 10;
-
-
-namespace Orthanc
-{
-  void DicomAssociationParameters::CheckHost(const std::string& host)
-  {
-    if (host.size() > HOST_NAME_MAX - 10)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Invalid host name (too long): " + host);
-    }
-  }
-
-  
-  uint32_t DicomAssociationParameters::GetDefaultTimeout()
-  {
-    boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
-    return defaultTimeout_;
-  }
-
-
-  DicomAssociationParameters::DicomAssociationParameters() :
-    localAet_("ORTHANC"),
-    timeout_(GetDefaultTimeout())
-  {
-    remote_.SetApplicationEntityTitle("ANY-SCP");
-  }
-
-    
-  DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet,
-                                                         const RemoteModalityParameters& remote) :
-    localAet_(localAet),
-    timeout_(GetDefaultTimeout())
-  {
-    SetRemoteModality(remote);
-  }
-
-    
-  void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& remote)
-  {
-    CheckHost(remote.GetHost());
-    remote_ = remote;
-  }
-
-
-  void DicomAssociationParameters::SetRemoteHost(const std::string& host)
-  {
-    CheckHost(host);
-    remote_.SetHost(host);
-  }
-
-
-  bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const
-  {
-    return (localAet_ == other.localAet_ &&
-            remote_.GetApplicationEntityTitle() == other.remote_.GetApplicationEntityTitle() &&
-            remote_.GetHost() == other.remote_.GetHost() &&
-            remote_.GetPortNumber() == other.remote_.GetPortNumber() &&
-            remote_.GetManufacturer() == other.remote_.GetManufacturer() &&
-            timeout_ == other.timeout_);
-  }
-
-
-  static const char* const LOCAL_AET = "LocalAet";
-  static const char* const REMOTE = "Remote";
-  static const char* const TIMEOUT = "Timeout";  // New in Orthanc in 1.7.0
-
-  
-  void DicomAssociationParameters::SerializeJob(Json::Value& target) const
-  {
-    if (target.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      target[LOCAL_AET] = localAet_;
-      remote_.Serialize(target[REMOTE], true /* force advanced format */);
-      target[TIMEOUT] = timeout_;
-    }
-  }
-
-
-  DicomAssociationParameters DicomAssociationParameters::UnserializeJob(const Json::Value& serialized)
-  {
-    if (serialized.type() == Json::objectValue)
-    {
-      DicomAssociationParameters result;
-    
-      result.remote_ = RemoteModalityParameters(serialized[REMOTE]);
-      result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
-      result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout());
-
-      return result;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-    
-
-  void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds)
-  {
-    LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
-              << seconds << " seconds (0 = no timeout)";
-
-    {
-      boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
-      defaultTimeout_ = seconds;
-    }
-  }
-}
--- a/Core/DicomNetworking/DicomAssociationParameters.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RemoteModalityParameters.h"
-
-#include <json/value.h>
-
-class OFCondition;  // From DCMTK
-
-namespace Orthanc
-{
-  class DicomAssociationParameters
-  {
-  private:
-    std::string               localAet_;
-    RemoteModalityParameters  remote_;
-    uint32_t                  timeout_;
-
-    static void CheckHost(const std::string& host);
-
-  public:
-    DicomAssociationParameters();
-    
-    DicomAssociationParameters(const std::string& localAet,
-                               const RemoteModalityParameters& remote);
-    
-    const std::string& GetLocalApplicationEntityTitle() const
-    {
-      return localAet_;
-    }
-
-    void SetLocalApplicationEntityTitle(const std::string& aet)
-    {
-      localAet_ = aet;
-    }
-
-    const RemoteModalityParameters& GetRemoteModality() const
-    {
-      return remote_;
-    }
-
-    void SetRemoteModality(const RemoteModalityParameters& parameters);
-    
-    void SetRemoteApplicationEntityTitle(const std::string& aet)
-    {
-      remote_.SetApplicationEntityTitle(aet);
-    }
-
-    void SetRemoteHost(const std::string& host);
-
-    void SetRemotePort(uint16_t port)
-    {
-      remote_.SetPortNumber(port);
-    }
-
-    void SetRemoteManufacturer(ModalityManufacturer manufacturer)
-    {
-      remote_.SetManufacturer(manufacturer);
-    }
-
-    bool IsEqual(const DicomAssociationParameters& other) const;
-
-    // Setting it to "0" disables the timeout (infinite wait)
-    void SetTimeout(uint32_t seconds)
-    {
-      timeout_ = seconds;
-    }
-
-    uint32_t GetTimeout() const
-    {
-      return timeout_;
-    }
-
-    bool HasTimeout() const
-    {
-      return timeout_ != 0;
-    }
-
-    void SerializeJob(Json::Value& target) const;
-    
-    static DicomAssociationParameters UnserializeJob(const Json::Value& serialized);
-    
-    static void SetDefaultTimeout(uint32_t seconds);
-
-    static uint32_t GetDefaultTimeout();
-  };
-}
--- a/Core/DicomNetworking/DicomControlUserConnection.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,674 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomControlUserConnection.h"
-
-#include "../Compatibility.h"
-#include "../DicomFormat/DicomArray.h"
-#include "../DicomParsing/FromDcmtkBridge.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "DicomAssociation.h"
-
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmnet/diutil.h>
-
-namespace Orthanc
-{
-  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));
-    }
-  }
-
-
-  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 NormalizeFindQuery(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::unique_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, GetDefaultDicomEncoding(),
-                                   false /* be strict */);
-      }
-
-      default:
-        return new ParsedDicomFile(fields, GetDefaultDicomEncoding(),
-                                   false /* be strict */);
-    }
-  }
-
-
-
-  void DicomControlUserConnection::SetupPresentationContexts()
-  {
-    assert(association_.get() != NULL);
-    association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
-    association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
-    association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
-  }
-    
-
-  void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers,
-                                                DcmDataset* dataset,
-                                                const char* sopClass,
-                                                bool isWorklist,
-                                                const char* level)
-  {
-    assert(isWorklist ^ (level != NULL));
-    assert(association_.get() != NULL);
-
-    association_->Open(parameters_);
-
-    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_->GetDcmtkAssociation(), sopClass);
-    if (presID == 0)
-    {
-      throw OrthancException(ErrorCode_DicomFindUnavailable,
-                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
-    }
-
-    T_DIMSE_C_FindRQ request;
-    memset(&request, 0, sizeof(request));
-    request.MessageID = association_->GetDcmtkAssociation().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;
-
-#if DCMTK_VERSION_NUMBER >= 364
-    int responseCount;
-#endif
-
-    OFCondition cond = DIMSE_findUser(
-      &association_->GetDcmtkAssociation(), presID, &request, dataset,
-#if DCMTK_VERSION_NUMBER >= 364
-      responseCount,
-#endif
-      FindCallback, &payload,
-      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
-      &response, &statusDetail);
-    
-    if (statusDetail)
-    {
-      delete statusDetail;
-    }
-
-    DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
-
-    
-    /**
-     * New in Orthanc 1.6.0: Deal with failures during C-FIND.
-     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
-     **/
-    
-    if (response.DimseStatus != 0x0000 &&  // Success
-        response.DimseStatus != 0xFF00 &&  // Pending - Matches are continuing 
-        response.DimseStatus != 0xFF01)    // Pending - Matches are continuing 
-    {
-      char buf[16];
-      sprintf(buf, "%04X", response.DimseStatus);
-
-      if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               HttpStatus_422_UnprocessableEntity,
-                               "C-FIND SCU to AET \"" +
-                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
-                               "\" has failed with DIMSE status 0x" + buf +
-                               " (unable to process - invalid query ?)");
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
-                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
-                               "\" has failed with DIMSE status 0x" + buf);
-      }
-    }
-  }
-
-    
-  void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
-                                                ResourceType level,
-                                                const DicomMap& fields)
-  {
-    assert(association_.get() != NULL);
-    association_->Open(parameters_);
-
-    std::unique_ptr<ParsedDicomFile> query(
-      ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
-    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
-
-    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
-    switch (level)
-    {
-      case ResourceType_Patient:
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
-        break;
-
-      case ResourceType_Study:
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
-        break;
-
-      case ResourceType_Series:
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
-        break;
-
-      case ResourceType_Instance:
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    // Figure out which of the accepted presentation contexts should be used
-    int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
-    if (presID == 0)
-    {
-      throw OrthancException(ErrorCode_DicomMoveUnavailable,
-                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
-    }
-
-    T_DIMSE_C_MoveRQ request;
-    memset(&request, 0, sizeof(request));
-    request.MessageID = association_->GetDcmtkAssociation().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(
-      &association_->GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
-      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
-      &association_->GetDcmtkNetwork(), NULL, NULL,
-      &response, &statusDetail, &responseIdentifiers);
-
-    if (statusDetail)
-    {
-      delete statusDetail;
-    }
-
-    if (responseIdentifiers)
-    {
-      delete responseIdentifiers;
-    }
-
-    DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
-
-    
-    /**
-     * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
-     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
-     **/
-    
-    if (response.DimseStatus != 0x0000 &&  // Success
-        response.DimseStatus != 0xFF00)    // Pending - Sub-operations are continuing
-    {
-      char buf[16];
-      sprintf(buf, "%04X", response.DimseStatus);
-
-      if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               HttpStatus_422_UnprocessableEntity,
-                               "C-MOVE SCU to AET \"" +
-                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
-                               "\" has failed with DIMSE status 0x" + buf +
-                               " (unable to process - resource not found ?)");
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
-                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
-                               "\" has failed with DIMSE status 0x" + buf);
-      }
-    }
-  }
-    
-
-  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
-    parameters_(params),
-    association_(new DicomAssociation)
-  {
-    SetupPresentationContexts();
-  }
-    
-
-  void DicomControlUserConnection::Close()
-  {
-    assert(association_.get() != NULL);
-    association_->Close();
-  }
-
-
-  bool DicomControlUserConnection::Echo()
-  {
-    assert(association_.get() != NULL);
-    association_->Open(parameters_);
-
-    DIC_US status;
-    DicomAssociation::CheckCondition(
-      DIMSE_echoUser(&association_->GetDcmtkAssociation(),
-                     association_->GetDcmtkAssociation().nextMsgID++, 
-                     /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                     /*opt_dimse_timeout*/ parameters_.GetTimeout(),
-                     &status, NULL),
-      parameters_, "C-ECHO");
-      
-    return status == STATUS_Success;
-  }
-
-
-  void DicomControlUserConnection::Find(DicomFindAnswers& result,
-                                        ResourceType level,
-                                        const DicomMap& originalFields,
-                                        bool normalize)
-  {
-    std::unique_ptr<ParsedDicomFile> query;
-
-    if (normalize)
-    {
-      DicomMap fields;
-      NormalizeFindQuery(fields, level, originalFields);
-      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
-    }
-    else
-    {
-      query.reset(new ParsedDicomFile(originalFields, GetDefaultDicomEncoding(),
-                                      false /* be strict */));
-    }
-    
-    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
-
-    const char* clevel = NULL;
-    const char* sopClass = NULL;
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        clevel = "PATIENT";
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
-        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
-        break;
-
-      case ResourceType_Study:
-        clevel = "STUDY";
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
-        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-        break;
-
-      case ResourceType_Series:
-        clevel = "SERIES";
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
-        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-        break;
-
-      case ResourceType_Instance:
-        clevel = "IMAGE";
-        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
-        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-
-    const char* universal;
-    if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE)
-    {
-      universal = "*";
-    }
-    else
-    {
-      universal = "";
-    }      
-    
-
-    // Add the expected tags for this query level.
-    // WARNING: Do not reorder or add "break" in this switch-case!
-    switch (level)
-    {
-      case ResourceType_Instance:
-        if (!dataset->tagExists(DCM_SOPInstanceUID))
-        {
-          DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
-        }
-
-      case ResourceType_Series:
-        if (!dataset->tagExists(DCM_SeriesInstanceUID))
-        {
-          DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
-        }
-
-      case ResourceType_Study:
-        if (!dataset->tagExists(DCM_AccessionNumber))
-        {
-          DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
-        }
-
-        if (!dataset->tagExists(DCM_StudyInstanceUID))
-        {
-          DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
-        }
-
-      case ResourceType_Patient:
-        if (!dataset->tagExists(DCM_PatientID))
-        {
-          DU_putStringDOElement(dataset, DCM_PatientID, universal);
-        }
-        
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    assert(clevel != NULL && sopClass != NULL);
-    FindInternal(result, dataset, sopClass, false, clevel);
-  }
-    
-
-  void DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
-                                                ParsedDicomFile& query)
-  {
-    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
-    const char* sopClass = UID_FINDModalityWorklistInformationModel;
-
-    FindInternal(result, dataset, sopClass, true, NULL);
-  }
-}
--- a/Core/DicomNetworking/DicomControlUserConnection.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomAssociationParameters.h"
-#include "DicomFindAnswers.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class DicomAssociation;  // Forward declaration for PImpl design pattern
-  
-  class DicomControlUserConnection : public boost::noncopyable
-  {
-  private:
-    DicomAssociationParameters           parameters_;
-    boost::shared_ptr<DicomAssociation>  association_;
-
-    void SetupPresentationContexts();
-
-    void FindInternal(DicomFindAnswers& answers,
-                      DcmDataset* dataset,
-                      const char* sopClass,
-                      bool isWorklist,
-                      const char* level);
-    
-    void MoveInternal(const std::string& targetAet,
-                      ResourceType level,
-                      const DicomMap& fields);
-    
-  public:
-    DicomControlUserConnection(const DicomAssociationParameters& params);
-    
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-
-    void Close();
-
-    bool Echo();
-
-    void Find(DicomFindAnswers& result,
-              ResourceType level,
-              const DicomMap& originalFields,
-              bool normalize);
-
-    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 FindWorklist(DicomFindAnswers& result,
-                      ParsedDicomFile& query);
-  };
-}
--- a/Core/DicomNetworking/DicomFindAnswers.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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::unique_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)
-  {
-    // We use the permissive mode to be tolerant wrt. invalid DICOM
-    // files that contain some tags with out-of-range values (such
-    // tags are removed from the answers)
-    AddAnswerInternal(new ParsedDicomFile(map, encoding_, true /* permissive */));
-                                          //"" /* no private creator */));
-  }
-
-
-  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
-  {
-    // As "DicomFindAnswers" stores its content using class
-    // "ParsedDicomFile" (that internally uses "DcmFileFormat" from
-    // DCMTK), the dataset can contain tags that are reserved if
-    // storing the media on the disk, notably tag
-    // "MediaStorageSOPClassUID" (0002,0002). In this function, we
-    // remove all those tags whose group is below 0x0008. The
-    // resulting data set is clean for emission in the C-FIND SCP.
-
-    // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
-    // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
-
-    DcmDataset& source = *GetAnswer(index).GetDcmtkObject().getDataset();
-
-    std::unique_ptr<DcmDataset> target(new DcmDataset);
-
-    for (unsigned long i = 0; i < source.card(); i++)
-    {
-      const DcmElement* element = source.getElement(i);
-      assert(element != NULL);
-
-      if (element != NULL &&
-          element->getTag().getGroup() >= 0x0008 &&
-          element->getTag().getElement() != 0x0000)
-      {
-        target->insert(dynamic_cast<DcmElement*>(element->clone()));
-      }
-    }
-    
-    return target.release();
-  }
-
-
-  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);
-    }
-  }
-}
--- a/Core/DicomNetworking/DicomFindAnswers.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-    }
-  };
-}
--- a/Core/DicomNetworking/DicomServer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,429 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Logging.h"
-#include "../MultiThreading/RunnableWorkersPool.h"
-#include "../OrthancException.h"
-#include "../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::unique_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::unique_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;
-    getRequestHandlerFactory_ = NULL;
-    storeRequestHandlerFactory_ = NULL;
-    worklistRequestHandlerFactory_ = NULL;
-    storageCommitmentFactory_ = 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::SetGetRequestHandlerFactory(IGetRequestHandlerFactory& factory)
-  {
-    Stop();
-    getRequestHandlerFactory_ = &factory;
-  }
-
-  bool DicomServer::HasGetRequestHandlerFactory() const
-  {
-    return (getRequestHandlerFactory_ != NULL);
-  }
-
-  IGetRequestHandlerFactory& DicomServer::GetGetRequestHandlerFactory() const
-  {
-    if (HasGetRequestHandlerFactory())
-    {
-      return *getRequestHandlerFactory_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NoCGetHandler);
-    }
-  }
-
-  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::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory)
-  {
-    Stop();
-    storageCommitmentFactory_ = &factory;
-  }
-
-  bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const
-  {
-    return (storageCommitmentFactory_ != NULL);
-  }
-
-  IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const
-  {
-    if (HasStorageCommitmentRequestHandlerFactory())
-    {
-      return *storageCommitmentFactory_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NoStorageCommitmentHandler);
-    }
-  }
-
-  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)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "No list of modalities was provided to the DICOM server");
-    }
-    
-    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())
-    {
-      throw OrthancException(ErrorCode_DicomPortInUse,
-                             " (port = " + boost::lexical_cast<std::string>(port_) + ") cannot create network: " + std::string(cond.text()));
-    }
-
-    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());
-    }
-  }
-}
--- a/Core/DicomNetworking/DicomServer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IGetRequestHandlerFactory.h"
-#include "IStoreRequestHandlerFactory.h"
-#include "IWorklistRequestHandlerFactory.h"
-#include "IStorageCommitmentRequestHandlerFactory.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_;
-    IGetRequestHandlerFactory* getRequestHandlerFactory_;
-    IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
-    IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_;
-    IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_;
-    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 SetGetRequestHandlerFactory(IGetRequestHandlerFactory& handler);
-    bool HasGetRequestHandlerFactory() const;
-    IGetRequestHandlerFactory& GetGetRequestHandlerFactory() const;
-
-    void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler);
-    bool HasStoreRequestHandlerFactory() const;
-    IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const;
-
-    void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler);
-    bool HasWorklistRequestHandlerFactory() const;
-    IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const;
-
-    void SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& handler);
-    bool HasStorageCommitmentRequestHandlerFactory() const;
-    IStorageCommitmentRequestHandlerFactory& GetStorageCommitmentRequestHandlerFactory() const;
-
-    void SetApplicationEntityFilter(IApplicationEntityFilter& handler);
-    bool HasApplicationEntityFilter() const;
-    IApplicationEntityFilter& GetApplicationEntityFilter() const;
-
-    void Start();
-  
-    void Stop();
-
-    bool IsMyAETitle(const std::string& aet) const;
-  };
-
-}
--- a/Core/DicomNetworking/DicomStoreUserConnection.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,527 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomStoreUserConnection.h"
-
-#include "../DicomParsing/FromDcmtkBridge.h"
-#include "../DicomParsing/ParsedDicomFile.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "DicomAssociation.h"
-
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-
-namespace Orthanc
-{
-  bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid,
-                                                     const std::set<DicomTransferSyntax>& syntaxes)
-  {
-    // Default transfer syntax for DICOM
-    const bool addLittleEndianImplicit = (
-      proposeUncompressedSyntaxes_ &&
-      syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end());
-    
-    const bool addLittleEndianExplicit = (
-      proposeUncompressedSyntaxes_ &&
-      syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end());
-    
-    const bool addBigEndianExplicit = (
-      proposeUncompressedSyntaxes_ &&
-      proposeRetiredBigEndian_ &&
-      syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end());
-    
-    size_t requiredCount = syntaxes.size();
-    if (addLittleEndianImplicit)
-    {
-      requiredCount += 1;
-    }
-      
-    if (addLittleEndianExplicit ||
-        addBigEndianExplicit)
-    {
-      requiredCount += 1;
-    }
-      
-    if (association_->GetRemainingPropositions() <= requiredCount)
-    {
-      return false;  // Not enough room
-    }
-    else
-    {
-      for (std::set<DicomTransferSyntax>::const_iterator
-             it = syntaxes.begin(); it != syntaxes.end(); ++it)
-      {
-        association_->ProposePresentationContext(sopClassUid, *it);
-        proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *it));
-      }
-
-      if (addLittleEndianImplicit)
-      {
-        association_->ProposePresentationContext(sopClassUid, DicomTransferSyntax_LittleEndianImplicit);
-        proposedOriginalClasses_.insert(std::make_pair(sopClassUid, DicomTransferSyntax_LittleEndianImplicit));
-      }
-
-      if (addLittleEndianExplicit ||
-          addBigEndianExplicit)
-      {
-        std::set<DicomTransferSyntax> uncompressed;
-
-        if (addLittleEndianExplicit)
-        {
-          uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit);
-        }
-
-        if (addBigEndianExplicit)
-        {
-          uncompressed.insert(DicomTransferSyntax_BigEndianExplicit);
-        }
-
-        association_->ProposePresentationContext(sopClassUid, uncompressed);
-
-        assert(!uncompressed.empty());
-        if (addLittleEndianExplicit ^ addBigEndianExplicit)
-        {
-          // Only one transfer syntax was proposed for this presentation context
-          assert(uncompressed.size() == 1);
-          proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *uncompressed.begin()));
-        }
-      }
-
-      return true;
-    }
-  }
-
-
-  bool DicomStoreUserConnection::LookupPresentationContext(
-    uint8_t& presentationContextId,
-    const std::string& sopClassUid,
-    DicomTransferSyntax transferSyntax)
-  {
-    typedef std::map<DicomTransferSyntax, uint8_t>  PresentationContexts;
-
-    PresentationContexts pc;
-    if (association_->IsOpen() &&
-        association_->LookupAcceptedPresentationContext(pc, sopClassUid))
-    {
-      PresentationContexts::const_iterator found = pc.find(transferSyntax);
-      if (found != pc.end())
-      {
-        presentationContextId = found->second;
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  DicomStoreUserConnection::DicomStoreUserConnection(
-    const DicomAssociationParameters& params) :
-    parameters_(params),
-    association_(new DicomAssociation),
-    proposeCommonClasses_(true),
-    proposeUncompressedSyntaxes_(true),
-    proposeRetiredBigEndian_(false)
-  {
-  }
-    
-
-  void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid,
-                                                      DicomTransferSyntax syntax)
-  {
-    RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid);
-
-    if (found == registeredClasses_.end())
-    {
-      std::set<DicomTransferSyntax> ts;
-      ts.insert(syntax);
-      registeredClasses_[sopClassUid] = ts;
-    }
-    else
-    {
-      found->second.insert(syntax);
-    }
-  }
-
-
-  void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid,
-                                                  std::string& sopInstanceUid,
-                                                  DicomTransferSyntax& transferSyntax,
-                                                  DcmFileFormat& dicom)
-  {
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    OFString a, b;
-    if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() ||
-        !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good())
-    {
-      throw OrthancException(ErrorCode_NoSopClassOrInstance,
-                             "Unable to determine the SOP class/instance for C-STORE with AET " +
-                             parameters_.GetRemoteModality().GetApplicationEntityTitle());
-    }
-
-    sopClassUid.assign(a.c_str());
-    sopInstanceUid.assign(b.c_str());
-
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom))
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Unknown transfer syntax from DCMTK");
-    }
-  }
-  
-
-  bool DicomStoreUserConnection::NegotiatePresentationContext(
-    uint8_t& presentationContextId,
-    const std::string& sopClassUid,
-    DicomTransferSyntax transferSyntax)
-  {
-    /**
-     * Step 1: Check whether this presentation context is already
-     * available in the previously negotiated assocation.
-     **/
-
-    if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
-    {
-      return true;
-    }
-
-    // The association must be re-negotiated
-    if (association_->IsOpen())
-    {
-      LOG(INFO) << "Re-negotiating DICOM association with "
-                << parameters_.GetRemoteModality().GetApplicationEntityTitle();
-
-      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) !=
-          proposedOriginalClasses_.end())
-      {
-        LOG(INFO) << "The remote modality has already rejected SOP class UID \""
-                  << sopClassUid << "\" with transfer syntax \""
-                  << GetTransferSyntaxUid(transferSyntax) << "\", don't renegotiate";
-        return false;
-      }
-    }
-
-    association_->ClearPresentationContexts();
-    proposedOriginalClasses_.clear();
-    RegisterStorageClass(sopClassUid, transferSyntax);  // (*)
-
-    
-    /**
-     * Step 2: Propose at least the mandatory SOP class.
-     **/
-
-    {
-      RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid);
-
-      if (mandatory == registeredClasses_.end() ||
-          mandatory->second.find(transferSyntax) == mandatory->second.end())
-      {
-        // Should never fail because of (*)
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (!ProposeStorageClass(sopClassUid, mandatory->second))
-      {
-        // Should never happen in real life: There are no more than
-        // 128 transfer syntaxes in DICOM!
-        throw OrthancException(ErrorCode_InternalError,
-                               "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
-      }
-    }
-
-      
-    /**
-     * Step 3: Propose all the previously spotted SOP classes, as
-     * registered through the "RegisterStorageClass()" method.
-     **/
-      
-    for (RegisteredClasses::const_iterator it = registeredClasses_.begin();
-         it != registeredClasses_.end(); ++it)
-    {
-      if (it->first != sopClassUid)
-      {
-        ProposeStorageClass(it->first, it->second);
-      }
-    }
-      
-
-    /**
-     * Step 4: As long as there is room left in the proposed
-     * presentation contexts, propose the uncompressed transfer syntaxes
-     * for the most common SOP classes, as can be found in the
-     * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
-     * preferred transfer syntax is "LittleEndianImplicit".
-     **/
-
-    if (proposeCommonClasses_)
-    {
-      // The method "ProposeStorageClass()" will automatically add
-      // "LittleEndianImplicit"
-      std::set<DicomTransferSyntax> ts;
-        
-      for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
-      {
-        std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
-          
-        if (c != sopClassUid &&
-            registeredClasses_.find(c) == registeredClasses_.end())
-        {
-          ProposeStorageClass(c, ts);
-        }
-      }
-    }
-
-
-    /**
-     * Step 5: Open the association, and check whether the pair (SOP
-     * class UID, transfer syntax) was accepted by the remote host.
-     **/
-
-    association_->Open(parameters_);
-    return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
-  }
-
-
-  void DicomStoreUserConnection::Store(std::string& sopClassUid,
-                                       std::string& sopInstanceUid,
-                                       DcmFileFormat& dicom,
-                                       bool hasMoveOriginator,
-                                       const std::string& moveOriginatorAET,
-                                       uint16_t moveOriginatorID)
-  {
-    DicomTransferSyntax transferSyntax;
-    LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom);
-
-    uint8_t presID;
-    if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax))
-    {
-      throw OrthancException(ErrorCode_NetworkProtocol,
-                             "No valid presentation context was negotiated for "
-                             "SOP class UID [" + sopClassUid + "] and transfer "
-                             "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] "
-                             "while sending to modality [" +
-                             parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]");
-    }
-    
-    // Prepare the transmission of data
-    T_DIMSE_C_StoreRQ request;
-    memset(&request, 0, sizeof(request));
-    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
-    strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
-    request.Priority = DIMSE_PRIORITY_MEDIUM;
-    request.DataSetType = DIMSE_DATASET_PRESENT;
-    strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
-
-    if (hasMoveOriginator)
-    {    
-      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;
-    }
-
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Finally conduct transmission of data
-    T_DIMSE_C_StoreRSP response;
-    DcmDataset* statusDetail = NULL;
-    DicomAssociation::CheckCondition(
-      DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request,
-                      NULL, dicom.getDataset(), /*progressCallback*/ NULL, NULL,
-                      /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                      /*opt_dimse_timeout*/ GetParameters().GetTimeout(),
-                      &response, &statusDetail, NULL),
-      GetParameters(), "C-STORE");
-
-    if (statusDetail != NULL) 
-    {
-      delete statusDetail;
-    }
-    
-    /**
-     * New in Orthanc 1.6.0: Deal with failures during C-STORE.
-     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
-     **/
-    
-    if (response.DimseStatus != 0x0000 &&  // Success
-        response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
-        response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
-        response.DimseStatus != 0xB006)    // Warning - Elements Discarded
-    {
-      char buf[16];
-      sprintf(buf, "%04X", response.DimseStatus);
-      throw OrthancException(ErrorCode_NetworkProtocol,
-                             "C-STORE SCU to AET \"" +
-                             GetParameters().GetRemoteModality().GetApplicationEntityTitle() +
-                             "\" has failed with DIMSE status 0x" + buf);
-    }
-  }
-
-
-  void DicomStoreUserConnection::Store(std::string& sopClassUid,
-                                       std::string& sopInstanceUid,
-                                       const void* buffer,
-                                       size_t size,
-                                       bool hasMoveOriginator,
-                                       const std::string& moveOriginatorAET,
-                                       uint16_t moveOriginatorID)
-  {
-    std::unique_ptr<DcmFileFormat> dicom(
-      FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
-
-    if (dicom.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
-  }
-
-
-  void DicomStoreUserConnection::LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
-                                                   const std::string& sopClassUid,
-                                                   DicomTransferSyntax sourceSyntax)
-  {
-    acceptedSyntaxes.clear();
-
-    // Make sure a negotiation has already occurred for this transfer
-    // syntax. We don't use the return code: Transcoding is possible
-    // even if the "sourceSyntax" is not supported.
-    uint8_t presID;
-    NegotiatePresentationContext(presID, sopClassUid, sourceSyntax);
-
-    std::map<DicomTransferSyntax, uint8_t> contexts;
-    if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
-    {
-      for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
-             it = contexts.begin(); it != contexts.end(); ++it)
-      {
-        acceptedSyntaxes.insert(it->first);
-      }
-    }
-  }
-
-
-  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
-                                           std::string& sopInstanceUid /* out */,
-                                           IDicomTranscoder& transcoder,
-                                           const void* buffer,
-                                           size_t size,
-                                           bool hasMoveOriginator,
-                                           const std::string& moveOriginatorAET,
-                                           uint16_t moveOriginatorID)
-  {
-    std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
-    if (dicom.get() == NULL ||
-        dicom->getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    DicomTransferSyntax inputSyntax;
-    LookupParameters(sopClassUid, sopInstanceUid, inputSyntax, *dicom);
-
-    std::set<DicomTransferSyntax> accepted;
-    LookupTranscoding(accepted, sopClassUid, inputSyntax);
-
-    if (accepted.find(inputSyntax) != accepted.end())
-    {
-      // No need for transcoding
-      Store(sopClassUid, sopInstanceUid, *dicom,
-            hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
-    }
-    else
-    {
-      // Transcoding is needed
-      std::set<DicomTransferSyntax> uncompressedSyntaxes;
-
-      if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end())
-      {
-        uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
-      }
-
-      if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end())
-      {
-        uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
-      }
-
-      if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end())
-      {
-        uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
-      }
-
-      IDicomTranscoder::DicomImage source;
-      source.AcquireParsed(dicom.release());
-      source.SetExternalBuffer(buffer, size);
-
-      const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
-      
-      IDicomTranscoder::DicomImage transcoded;
-      if (transcoder.Transcode(transcoded, source, uncompressedSyntaxes, false))
-      {
-        if (sourceUid != IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed()))
-        {
-          throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
-                                 "instance UID while transcoding to an uncompressed transfer syntax");
-        }
-        else
-        {
-          DicomTransferSyntax transcodedSyntax;
-          
-          // Sanity check
-          if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) ||
-              accepted.find(transcodedSyntax) == accepted.end())
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-          else
-          {
-            Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(),
-                  hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
-          }
-        }
-      }
-    }
-  }
-}
--- a/Core/DicomNetworking/DicomStoreUserConnection.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_TRANSCODING)
-#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
-#endif
-
-#include "DicomAssociationParameters.h"
-
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-#  include "../DicomParsing/IDicomTranscoder.h"
-#endif
-
-#include <boost/shared_ptr.hpp>
-#include <boost/noncopyable.hpp>
-#include <set>
-#include <stdint.h>  // For uint8_t
-
-
-class DcmFileFormat;
-
-namespace Orthanc
-{
-  /**
-
-     Orthanc < 1.7.0:
-
-     Input        | Output
-     -------------+---------------------------------------------
-     Compressed   | Same transfer syntax
-     Uncompressed | Same transfer syntax, or other uncompressed
-
-     Orthanc >= 1.7.0:
-
-     Input        | Output
-     -------------+---------------------------------------------
-     Compressed   | Same transfer syntax, or uncompressed
-     Uncompressed | Same transfer syntax, or other uncompressed
-
-  **/
-
-  class DicomAssociation;  // Forward declaration for PImpl design pattern
-
-  class DicomStoreUserConnection : public boost::noncopyable
-  {
-  private:
-    typedef std::map<std::string, std::set<DicomTransferSyntax> > RegisteredClasses;
-
-    // "ProposedOriginalClasses" keeps track of the storage classes
-    // that were proposed with a single transfer syntax
-    typedef std::set< std::pair<std::string, DicomTransferSyntax> > ProposedOriginalClasses;
-    
-    DicomAssociationParameters           parameters_;
-    boost::shared_ptr<DicomAssociation>  association_;  // "shared_ptr" is for PImpl
-    RegisteredClasses                    registeredClasses_;
-    ProposedOriginalClasses              proposedOriginalClasses_;
-    bool                                 proposeCommonClasses_;
-    bool                                 proposeUncompressedSyntaxes_;
-    bool                                 proposeRetiredBigEndian_;
-
-    // Return "false" if there is not enough room remaining in the association
-    bool ProposeStorageClass(const std::string& sopClassUid,
-                             const std::set<DicomTransferSyntax>& syntaxes);
-
-    bool LookupPresentationContext(uint8_t& presentationContextId,
-                                   const std::string& sopClassUid,
-                                   DicomTransferSyntax transferSyntax);
-    
-    bool NegotiatePresentationContext(uint8_t& presentationContextId,
-                                      const std::string& sopClassUid,
-                                      DicomTransferSyntax transferSyntax);
-
-    void LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
-                           const std::string& sopClassUid,
-                           DicomTransferSyntax sourceSyntax);
-
-  public:
-    DicomStoreUserConnection(const DicomAssociationParameters& params);
-    
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-
-    void SetCommonClassesProposed(bool proposed)
-    {
-      proposeCommonClasses_ = proposed;
-    }
-
-    bool IsCommonClassesProposed() const
-    {
-      return proposeCommonClasses_;
-    }
-
-    void SetUncompressedSyntaxesProposed(bool proposed)
-    {
-      proposeUncompressedSyntaxes_ = proposed;
-    }
-
-    bool IsUncompressedSyntaxesProposed() const
-    {
-      return proposeUncompressedSyntaxes_;
-    }
-
-    void SetRetiredBigEndianProposed(bool propose)
-    {
-      proposeRetiredBigEndian_ = propose;
-    }
-
-    bool IsRetiredBigEndianProposed() const
-    {
-      return proposeRetiredBigEndian_;
-    }      
-
-    void RegisterStorageClass(const std::string& sopClassUid,
-                              DicomTransferSyntax syntax);
-
-    void Store(std::string& sopClassUid,
-               std::string& sopInstanceUid,
-               DcmFileFormat& dicom,
-               bool hasMoveOriginator,
-               const std::string& moveOriginatorAET,
-               uint16_t moveOriginatorID);
-
-    void Store(std::string& sopClassUid,
-               std::string& sopInstanceUid,
-               const void* buffer,
-               size_t size,
-               bool hasMoveOriginator,
-               const std::string& moveOriginatorAET,
-               uint16_t moveOriginatorID);
-
-    void LookupParameters(std::string& sopClassUid,
-                          std::string& sopInstanceUid,
-                          DicomTransferSyntax& transferSyntax,
-                          DcmFileFormat& dicom);
-
-    void Transcode(std::string& sopClassUid /* out */,
-                   std::string& sopInstanceUid /* out */,
-                   IDicomTranscoder& transcoder,
-                   const void* buffer,
-                   size_t size,
-                   bool hasMoveOriginator,
-                   const std::string& moveOriginatorAET,
-                   uint16_t moveOriginatorID);
-  };
-}
--- a/Core/DicomNetworking/IApplicationEntityFilter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/IFindRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/IFindRequestHandlerFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/IGetRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/dcmnet/assoc.h>
-
-#include "../DicomFormat/DicomMap.h"
-
-#include <string>
-
-
-namespace Orthanc
-{
-  class IGetRequestHandler : boost::noncopyable
-  {
-  public:
-    enum Status
-    {
-      Status_Success,
-      Status_Failure,
-      Status_Warning
-    };
-    
-    virtual ~IGetRequestHandler()
-    {
-    }
-
-    virtual bool Handle(const DicomMap& input,
-                        const std::string& originatorIp,
-                        const std::string& originatorAet,
-                        const std::string& calledAet,
-                        uint32_t timeout) = 0;
-    
-    virtual unsigned int GetSubOperationCount() const = 0;
-    
-    virtual Status DoNext(T_ASC_Association *) = 0;
-    
-    virtual unsigned int GetRemainingCount() const = 0;
-    
-    virtual unsigned int GetCompletedCount() const = 0;
-    
-    virtual unsigned int GetWarningCount() const = 0;
-    
-    virtual unsigned int GetFailedCount() const = 0;
-    
-    virtual const std::string& GetFailedUids() const = 0;
-  };
-}
--- a/Core/DicomNetworking/IGetRequestHandlerFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IGetRequestHandler.h"
-
-namespace Orthanc
-{
-  class IGetRequestHandlerFactory : public boost::noncopyable
-  {
-  public:
-    virtual ~IGetRequestHandlerFactory()
-    {
-    }
-
-    virtual IGetRequestHandler* ConstructGetRequestHandler() = 0;
-  };
-}
--- a/Core/DicomNetworking/IMoveRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/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;
-  };
-
-}
--- a/Core/DicomNetworking/IMoveRequestHandlerFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/IStorageCommitmentRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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>
-#include <string>
-#include <vector>
-
-namespace Orthanc
-{
-  class IStorageCommitmentRequestHandler : public boost::noncopyable
-  {
-  public:
-    virtual ~IStorageCommitmentRequestHandler()
-    {
-    }
-
-    virtual void HandleRequest(const std::string& transactionUid,
-                               const std::vector<std::string>& sopClassUids,
-                               const std::vector<std::string>& sopInstanceUids,
-                               const std::string& remoteIp,
-                               const std::string& remoteAet,
-                               const std::string& calledAet) = 0;
-
-    virtual void HandleReport(const std::string& transactionUid,
-                              const std::vector<std::string>& successSopClassUids,
-                              const std::vector<std::string>& successSopInstanceUids,
-                              const std::vector<std::string>& failedSopClassUids,
-                              const std::vector<std::string>& failedSopInstanceUids,
-                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
-                              const std::string& remoteIp,
-                              const std::string& remoteAet,
-                              const std::string& calledAet) = 0;
-  };
-}
--- a/Core/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IStorageCommitmentRequestHandler.h"
-
-namespace Orthanc
-{
-  class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable
-  {
-  public:
-    virtual ~IStorageCommitmentRequestHandlerFactory()
-    {
-    }
-
-    virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0;
-  };
-}
--- a/Core/DicomNetworking/IStoreRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/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;
-  };
-}
--- a/Core/DicomNetworking/IStoreRequestHandlerFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/IWorklistRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/IWorklistRequestHandlerFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1346 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#if !defined(DCMTK_VERSION_NUMBER)
-#  error The macro DCMTK_VERSION_NUMBER must be defined
-#endif
-
-#include "FindScp.h"
-#include "StoreScp.h"
-#include "MoveScp.h"
-#include "GetScp.h"
-#include "../../Compatibility.h"
-#include "../../Toolbox.h"
-#include "../../Logging.h"
-#include "../../OrthancException.h"
-
-#include <dcmtk/dcmdata/dcdeftag.h>     /* for storage commitment */
-#include <dcmtk/dcmdata/dcsequen.h>     /* for class DcmSequenceOfItems */
-#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
-#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)
-{
-  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
-  {
-    OFCondition AssociationCleanup(T_ASC_Association *assoc)
-    {
-      OFCondition cond = ASC_dropSCPAssociation(assoc);
-      if (cond.bad())
-      {
-        LOG(ERROR) << cond.text();
-        return cond;
-      }
-
-      cond = ASC_destroyAssociation(&assoc);
-      if (cond.bad())
-      {
-        LOG(ERROR) << 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;
-
-      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 (
-#if DCMTK_VERSION_NUMBER >= 364
-          ASC_getAPTitles(assoc->params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).bad() ||
-          ASC_getPresentationAddresses(assoc->params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).bad()
-#else
-          ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
-          ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()
-#endif
-          )
-        {
-          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;
-
-
-      {
-        /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE,
-           and storage commitment, if presented */
-
-        std::vector<const char*> genericTransferSyntaxes;
-        genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
-        genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
-        genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
-
-        std::vector<const char*> knownAbstractSyntaxes;
-
-        // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier
-        // versions, only enabled if C-STORE was also enabled)
-        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);
-        }
-
-        // For C-GET
-        if (server.HasGetRequestHandlerFactory())
-        {
-          knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel);
-          knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel);
-        }
-
-        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
-          assoc->params,
-          &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
-          &genericTransferSyntaxes[0], genericTransferSyntaxes.size());
-        if (cond.bad())
-        {
-          LOG(INFO) << cond.text();
-          AssociationCleanup(assoc);
-          return NULL;
-        }
-
-      
-        /* storage commitment support, new in Orthanc 1.6.0 */
-        if (server.HasStorageCommitmentRequestHandlerFactory())
-        {
-          /**
-           * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept
-           * remote storage commitment requests, and the "SCP" role is
-           * needed to receive storage commitments answers.
-           **/        
-          const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; 
-          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
-            assoc->params, as, 1,
-            &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP);
-          if (cond.bad())
-          {
-            LOG(INFO) << cond.text();
-            AssociationCleanup(assoc);
-            return NULL;
-          }
-        }
-      }
-      
-
-      {
-        /* accept the abstract syntaxes for C-STORE, if presented */
-
-        std::vector<const char*> storageTransferSyntaxes;
-
-        // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
-        storageTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
-        storageTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
-        storageTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
-
-        // New transfer syntaxes supported since Orthanc 0.7.2
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
-        {
-          storageTransferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
-        }
-
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
-        {
-          storageTransferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
-        }
-
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
-        {
-          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
-        }
-
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
-        {
-          storageTransferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
-        }
-
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
-        {
-          storageTransferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
-        }
-
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
-        {
-          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
-        }
-
-#if DCMTK_VERSION_NUMBER >= 361
-        // New in Orthanc 1.6.0
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4))
-        {
-          storageTransferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax);
-          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax);
-          storageTransferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax);
-        }
-#endif
-
-        if (!server.HasApplicationEntityFilter() ||
-            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
-        {
-          storageTransferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
-        }
-
-        /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
-        size_t count = 0;
-        while (dcmAllStorageSOPClassUIDs[count] != NULL)
-        {
-          count++;
-        }
-        
-#if DCMTK_VERSION_NUMBER >= 362
-        // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
-        // only published if DCMTK >= 3.6.2:
-        // https://bitbucket.org/sjodogne/orthanc/issues/137
-        assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
-#endif
-      
-        if (!server.HasGetRequestHandlerFactory())    // dcmqrsrv.cc line 828
-        {
-          // This branch exactly corresponds to Orthanc <= 1.6.1 (in
-          // which C-GET SCP was not supported)
-          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
-            assoc->params, dcmAllStorageSOPClassUIDs, count,
-            &storageTransferSyntaxes[0], storageTransferSyntaxes.size());
-          if (cond.bad())
-          {
-            LOG(INFO) << cond.text();
-            AssociationCleanup(assoc);
-            return NULL;
-          }
-        }
-        else                                         // see dcmqrsrv.cc lines 839 - 876
-        {
-          /* accept storage syntaxes with proposed role */
-          int npc = ASC_countPresentationContexts(assoc->params);
-          for (int i = 0; i < npc; i++)
-          {
-            T_ASC_PresentationContext pc;
-            ASC_getPresentationContext(assoc->params, i, &pc);
-            if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
-            {
-              /**
-               * We are prepared to accept whatever role the caller
-               * proposes.  Normally we can be the SCP of the Storage
-               * Service Class.  When processing the C-GET operation
-               * we can be the SCU of the Storage Service Class.
-               **/
-              const T_ASC_SC_ROLE role = pc.proposedRole;
-            
-              /**
-               * Accept in the order "least wanted" to "most wanted"
-               * transfer syntax.  Accepting a transfer syntax will
-               * override previously accepted transfer syntaxes.
-               **/
-              for (int k = static_cast<int>(storageTransferSyntaxes.size()) - 1; k >= 0; k--)
-              {
-                for (int j = 0; j < static_cast<int>(pc.transferSyntaxCount); j++)
-                {
-                  /**
-                   * If the transfer syntax was proposed then we can accept it
-                   * appears in our supported list of transfer syntaxes
-                   **/
-                  if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxes[k]) == 0)
-                  {
-                    cond = ASC_acceptPresentationContext(
-                      assoc->params, pc.presentationContextID, storageTransferSyntaxes[k], role);
-                    if (cond.bad())
-                    {
-                      LOG(INFO) << cond.text();
-                      AssociationCleanup(assoc);
-                      return NULL;
-                    }
-                  }
-                }
-              }
-            }
-          } /* for */
-        }
-
-        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, &storageTransferSyntaxes[0], storageTransferSyntaxes.size(), ASC_SC_ROLE_DEFAULT);
-          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 */
-#if DCMTK_VERSION_NUMBER >= 364
-      cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf));
-#else
-      cond = ASC_getApplicationContextName(assoc->params, buf);
-#endif
-
-      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_GET_RQ:
-            request = DicomRequestType_Get;
-            supported = true;
-            break;
-
-          case DIMSE_C_FIND_RQ:
-            request = DicomRequestType_Find;
-            supported = true;
-            break;
-
-          case DIMSE_N_ACTION_RQ:
-            request = DicomRequestType_NAction;
-            supported = true;
-            break;
-
-          case DIMSE_N_EVENT_REPORT_RQ:
-            request = DicomRequestType_NEventReport;
-            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::unique_ptr<IStoreRequestHandler> handler
-                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
-
-                if (handler.get() != NULL)
-                {
-                  cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_, associationTimeout_);
-                }
-              }
-              break;
-
-            case DicomRequestType_Move:
-              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
-              {
-                std::unique_ptr<IMoveRequestHandler> handler
-                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
-
-                if (handler.get() != NULL)
-                {
-                  cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
-                }
-              }
-              break;
-              
-            case DicomRequestType_Get:
-              if (server_.HasGetRequestHandlerFactory()) // Should always be true
-              {
-                std::unique_ptr<IGetRequestHandler> handler
-                  (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler());
-                
-                if (handler.get() != NULL)
-                {
-                  cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
-                }
-              }
-              break;
-
-            case DicomRequestType_Find:
-              if (server_.HasFindRequestHandlerFactory() || // Should always be true
-                  server_.HasWorklistRequestHandlerFactory())
-              {
-                std::unique_ptr<IFindRequestHandler> findHandler;
-                if (server_.HasFindRequestHandlerFactory())
-                {
-                  findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
-                }
-
-                std::unique_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_, associationTimeout_);
-              }
-              break;
-
-            case DicomRequestType_NAction:
-              cond = NActionScp(&msg, presID);
-              break;              
-
-            case DicomRequestType_NEventReport:
-              cond = NEventReportScp(&msg, presID);
-              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;
-    }
-
-
-    static DcmDataset* ReadDataset(T_ASC_Association* assoc,
-                                   const char* errorMessage,
-                                   int timeout)
-    {
-      DcmDataset *tmp = NULL;
-      T_ASC_PresentationContextID presIdData;
-    
-      OFCondition cond = DIMSE_receiveDataSetInMemory(
-        assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
-        &presIdData, &tmp, NULL, NULL);
-      if (!cond.good() ||
-          tmp == NULL)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol, errorMessage);
-      }
-
-      return tmp;
-    }
-
-
-    static std::string ReadString(DcmDataset& dataset,
-                                  const DcmTagKey& tag)
-    {
-      const char* s = NULL;
-      if (!dataset.findAndGetString(tag, s).good() ||
-          s == NULL)
-      {
-        char buf[64];
-        sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)",
-                tag.getGroup(), tag.getElement());
-        throw OrthancException(ErrorCode_NetworkProtocol, buf);
-      }
-
-      return std::string(s);
-    }
-
-
-    static void ReadSopSequence(
-      std::vector<std::string>& sopClassUids,
-      std::vector<std::string>& sopInstanceUids,
-      std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL
-      DcmDataset& dataset,
-      const DcmTagKey& tag,
-      bool mandatory)
-    {
-      sopClassUids.clear();
-      sopInstanceUids.clear();
-
-      if (failureReasons)
-      {
-        failureReasons->clear();
-      }
-
-      DcmSequenceOfItems* sequence = NULL;
-      if (!dataset.findAndGetSequence(tag, sequence).good() ||
-          sequence == NULL)
-      {
-        if (mandatory)
-        {        
-          char buf[64];
-          sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
-                  tag.getGroup(), tag.getElement());
-          throw OrthancException(ErrorCode_NetworkProtocol, buf);
-        }
-        else
-        {
-          return;
-        }
-      }
-
-      sopClassUids.reserve(sequence->card());
-      sopInstanceUids.reserve(sequence->card());
-
-      if (failureReasons)
-      {
-        failureReasons->reserve(sequence->card());
-      }
-
-      for (unsigned long i = 0; i < sequence->card(); i++)
-      {
-        const char* a = NULL;
-        const char* b = NULL;
-        if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() ||
-            !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() ||
-            a == NULL ||
-            b == NULL)
-        {
-          throw OrthancException(ErrorCode_NetworkProtocol,
-                                 "Missing Referenced SOP Class/Instance UID "
-                                 "in storage commitment dataset");
-        }
-
-        sopClassUids.push_back(a);
-        sopInstanceUids.push_back(b);
-
-        if (failureReasons != NULL)
-        {
-          Uint16 reason;
-          if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
-          {
-            throw OrthancException(ErrorCode_NetworkProtocol,
-                                   "Missing Failure Reason (0008,1197) "
-                                   "in storage commitment dataset");
-          }
-
-          failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason));
-        }
-      }
-    }
-
-    
-    OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg,
-                                              T_ASC_PresentationContextID presID)
-    {
-      /**
-       * Starting with Orthanc 1.6.0, only storage commitment is
-       * supported with DICOM N-ACTION. This corresponds to the case
-       * where "Action Type ID" equals "1".
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
-       **/
-      
-      if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ ||
-          !server_.HasStorageCommitmentRequestHandlerFactory())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-
-      /**
-       * Check that the storage commitment request is correctly formatted.
-       **/
-      
-      const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ;
-
-      if (request.ActionTypeID != 1)
-      {
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Only storage commitment is implemented for DICOM N-ACTION SCP");
-      }
-
-      if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
-          std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               "Unexpected incoming SOP class or instance UID for storage commitment");
-      }
-
-      if (request.DataSetType != DIMSE_DATASET_PRESENT)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               "Incoming storage commitment request without a dataset");
-      }
-
-
-      /**
-       * Extract the DICOM dataset that is associated with the DIMSE
-       * message. The content of this dataset is documented in "Table
-       * J.3-1. Storage Commitment Request - Action Information":
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
-       **/
-      
-      std::unique_ptr<DcmDataset> dataset(
-        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_));
-
-      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
-
-      std::vector<std::string> sopClassUid, sopInstanceUid;
-      ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
-                      *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
-
-      LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
-
-      for (size_t i = 0; i < sopClassUid.size(); i++)
-      {
-        LOG(INFO) << "  (" << (i + 1) << "/" << sopClassUid.size()
-                  << ") queried SOP Class/Instance UID: "
-                  << sopClassUid[i] << " / " << sopInstanceUid[i];
-      }
-
-
-      /**
-       * Call the Orthanc handler. The list of available DIMSE status
-       * codes can be found at:
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
-       **/
-
-      DIC_US dimseStatus;
-  
-      try
-      {
-        std::unique_ptr<IStorageCommitmentRequestHandler> handler
-          (server_.GetStorageCommitmentRequestHandlerFactory().
-           ConstructStorageCommitmentRequestHandler());
-
-        handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
-                               remoteIp_, remoteAet_, calledAet_);
-        
-        dimseStatus = 0;  // Success
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Error while processing an incoming storage commitment request: " << e.What();
-
-        // Code 0x0110 - "General failure in processing the operation was encountered"
-        dimseStatus = STATUS_N_ProcessingFailure;
-      }
-
-
-      /**
-       * Send the DIMSE status back to the SCU.
-       **/
-
-      {
-        T_DIMSE_Message response;
-        memset(&response, 0, sizeof(response));
-        response.CommandField = DIMSE_N_ACTION_RSP;
-
-        T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP;
-        content.MessageIDBeingRespondedTo = request.MessageID;
-        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
-        content.DimseStatus = dimseStatus;
-        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
-        content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts"
-        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
-        content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID;
-
-        return DIMSE_sendMessageUsingMemoryData(
-          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
-          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
-      }
-    }
-
-
-    OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg,
-                                                   T_ASC_PresentationContextID presID)
-    {
-      /**
-       * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for
-       * storage commitment.
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
-       **/
-
-      if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ ||
-          !server_.HasStorageCommitmentRequestHandlerFactory())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-
-      /**
-       * Check that the storage commitment report is correctly formatted.
-       **/
-      
-      const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ;
-
-      if (report.EventTypeID != 1 /* successful */ &&
-          report.EventTypeID != 2 /* failures exist */)
-      {
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Unknown event for DICOM N-EVENT-REPORT SCP");
-      }
-
-      if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
-          std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               "Unexpected incoming SOP class or instance UID for storage commitment");
-      }
-
-      if (report.DataSetType != DIMSE_DATASET_PRESENT)
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               "Incoming storage commitment report without a dataset");
-      }
-
-
-      /**
-       * Extract the DICOM dataset that is associated with the DIMSE
-       * message. The content of this dataset is documented in "Table
-       * J.3-2. Storage Commitment Result - Event Information":
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2
-       **/
-      
-      std::unique_ptr<DcmDataset> dataset(
-        ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_));
-
-      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
-
-      std::vector<std::string> successSopClassUid, successSopInstanceUid;
-      ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
-                      *dataset, DCM_ReferencedSOPSequence,
-                      (report.EventTypeID == 1) /* mandatory in the case of success */);
-
-      std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
-      std::vector<StorageCommitmentFailureReason> failureReasons;
-
-      if (report.EventTypeID == 2 /* failures exist */)
-      {
-        ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
-                        *dataset, DCM_FailedSOPSequence, true);
-      }
-
-      LOG(INFO) << "Incoming storage commitment report, with transaction UID: " << transactionUid;
-
-      for (size_t i = 0; i < successSopClassUid.size(); i++)
-      {
-        LOG(INFO) << "  (success " << (i + 1) << "/" << successSopClassUid.size()
-                  << ") SOP Class/Instance UID: "
-                  << successSopClassUid[i] << " / " << successSopInstanceUid[i];
-      }
-
-      for (size_t i = 0; i < failedSopClassUid.size(); i++)
-      {
-        LOG(INFO) << "  (failure " << (i + 1) << "/" << failedSopClassUid.size()
-                  << ") SOP Class/Instance UID: "
-                  << failedSopClassUid[i] << " / " << failedSopInstanceUid[i];
-      }
-
-      /**
-       * Call the Orthanc handler. The list of available DIMSE status
-       * codes can be found at:
-       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
-       **/
-
-      DIC_US dimseStatus;
-
-      try
-      {
-        std::unique_ptr<IStorageCommitmentRequestHandler> handler
-          (server_.GetStorageCommitmentRequestHandlerFactory().
-           ConstructStorageCommitmentRequestHandler());
-
-        handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
-                              failedSopClassUid, failedSopInstanceUid, failureReasons,
-                              remoteIp_, remoteAet_, calledAet_);
-        
-        dimseStatus = 0;  // Success
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Error while processing an incoming storage commitment report: " << e.What();
-
-        // Code 0x0110 - "General failure in processing the operation was encountered"
-        dimseStatus = STATUS_N_ProcessingFailure;
-      }
-
-      
-      /**
-       * Send the DIMSE status back to the SCU.
-       **/
-
-      {
-        T_DIMSE_Message response;
-        memset(&response, 0, sizeof(response));
-        response.CommandField = DIMSE_N_EVENT_REPORT_RSP;
-
-        T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP;
-        content.MessageIDBeingRespondedTo = report.MessageID;
-        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
-        content.DimseStatus = dimseStatus;
-        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
-        content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts"
-        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
-        content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID;
-
-        return DIMSE_sendMessageUsingMemoryData(
-          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
-          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
-      }
-    }
-  }
-}
--- a/Core/DicomNetworking/Internals/CommandDispatcher.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-
-      OFCondition NActionScp(T_DIMSE_Message* msg, 
-                             T_ASC_PresentationContextID presID);
-
-      OFCondition NEventReportScp(T_DIMSE_Message* msg, 
-                                  T_ASC_PresentationContextID presID);
-      
-    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();
-    };
-
-    CommandDispatcher* AcceptAssociation(const DicomServer& server, 
-                                         T_ASC_Network *net);
-
-    OFCondition EchoScp(T_ASC_Association* assoc, 
-                        T_DIMSE_Message* msg, 
-                        T_ASC_PresentationContextID presID);
-  }
-}
--- a/Core/DicomNetworking/Internals/FindScp.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,375 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../DicomFormat/DicomArray.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);
-    }
-
-
-    static void FixFindQuery(DicomMap& target,
-                             const DicomMap& source)
-    {
-      // "The definition of a Data Set in PS3.5 specifically excludes
-      // the range of groups below group 0008, and this includes in
-      // particular Meta Information Header elements such as Transfer
-      // Syntax UID (0002,0010)."
-      // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
-      // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
-
-      DicomArray a(source);
-
-      for (size_t i = 0; i < a.GetSize(); i++)
-      {
-        if (a.GetElement(i).GetTag().GetGroup() >= 0x0008)
-        {
-          target.SetValue(a.GetElement(i).GetTag(), a.GetElement(i).GetValue());
-        }
-      }
-    }
-
-
-
-    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_))
-          {
-            throw OrthancException(ErrorCode_UnknownModality,
-                                   "Modality with AET \"" + (*data.remoteAet_) +
-                                   "\" is not defined in the \"DicomModalities\" configuration option");
-          }
-
-          
-          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);
-
-              DicomMap filtered;
-              FixFindQuery(filtered, input);
-
-              data.findHandler_->Handle(data.answers_, filtered, 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,
-                                 int timeout)
-  {
-    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*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                                          /*opt_dimse_timeout*/ timeout);
-
-    // 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/Core/DicomNetworking/Internals/FindScp.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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,
-                        int timeout);
-  }
-}
--- a/Core/DicomNetworking/Internals/GetScp.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,289 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <dcmtk/dcmnet/diutil.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include "GetScp.h"
-
-#include <memory>
-
-#include "../../DicomParsing/FromDcmtkBridge.h"
-#include "../../DicomParsing/ToDcmtkBridge.h"
-#include "../../Logging.h"
-#include "../../OrthancException.h"
-
-#include <boost/lexical_cast.hpp>
-
-
-namespace Orthanc
-{
-  namespace
-  {
-    struct GetScpData
-    {
-      //  Handle returns void.
-      IGetRequestHandler* handler_;
-      DcmDataset* lastRequest_;
-      T_ASC_Association * assoc_;
-
-      std::string remoteIp_;
-      std::string remoteAet_;
-      std::string calledAet_;
-      int timeout_;
-
-      GetScpData()
-      {
-        handler_ = NULL;
-        lastRequest_ = NULL;
-        assoc_ = NULL;
-      };
-    };
-      
-    static DcmDataset *BuildFailedInstanceList(const std::string& failedUIDs)
-    {
-      if (failedUIDs.empty())
-      {
-        return NULL;
-      }
-      else
-      {
-        std::unique_ptr<DcmDataset> rspIds(new DcmDataset());
-        
-        if (!DU_putStringDOElement(rspIds.get(), DCM_FailedSOPInstanceUIDList, failedUIDs.c_str()))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "getSCP: failed to build DCM_FailedSOPInstanceUIDList");
-        }
-
-        return rspIds.release();
-      }
-    }
-
-    static void GetScpCallback(
-      /* in */ 
-      void *callbackData,  
-      OFBool cancelled, 
-      T_DIMSE_C_GetRQ *request, 
-      DcmDataset *requestIdentifiers, 
-      int responseCount,
-      /* out */
-      T_DIMSE_C_GetRSP *response,
-      DcmDataset **responseIdentifiers,
-      DcmDataset **statusDetail)
-    {
-      bzero(response, sizeof(T_DIMSE_C_GetRSP));
-      *statusDetail = NULL;
-      *responseIdentifiers = NULL;   
-
-      GetScpData& data = *reinterpret_cast<GetScpData*>(callbackData);
-      if (data.lastRequest_ == NULL)
-      {
-        DicomMap input;
-        FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers);
-
-        try
-        {
-          if (!data.handler_->Handle(
-                input, data.remoteIp_, data.remoteAet_, data.calledAet_,
-                data.timeout_ < 0 ? 0 : static_cast<uint32_t>(data.timeout_)))
-          {
-            response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
-            return;
-          }
-        }
-        catch (OrthancException& e)
-        {
-          // Internal error!
-          LOG(ERROR) << "IGetRequestHandler Failed: " << e.What();
-          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
-          return;
-        }
-
-        data.lastRequest_ = requestIdentifiers;
-      }
-      else if (data.lastRequest_ != requestIdentifiers)
-      {
-        // Internal error!
-        LOG(ERROR) << "IGetRequestHandler Failed: Internal error lastRequestIdentifier";
-        response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
-        return;
-      }
-
-      if (data.handler_->GetRemainingCount() == 0)
-      {
-        response->DimseStatus = STATUS_Success;
-      }
-      else
-      {
-        IGetRequestHandler::Status status;
-
-        try
-        {
-          status = data.handler_->DoNext(data.assoc_);
-        }
-        catch (OrthancException& e)
-        {
-          // Internal error!
-          LOG(ERROR) << "IGetRequestHandler Failed: " << e.What();
-          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
-          return;
-        }
-        
-        if (status == STATUS_Success)
-        {
-          if (responseCount < static_cast<int>(data.handler_->GetRemainingCount()))
-          {
-            response->DimseStatus = STATUS_Pending;
-          }
-          else
-          {
-            response->DimseStatus = STATUS_Success;
-          }
-        }
-        else
-        {
-          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
-          
-          if (data.handler_->GetFailedCount() > 0 ||
-              data.handler_->GetWarningCount() > 0) 
-          {
-            response->DimseStatus = STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures;
-          }
-          
-          /*
-           * if all the sub-operations failed then we need to generate
-           * a failed or refused status.  cf. DICOM part 4, C.4.3.3.1
-           * we choose to generate a "Refused - Out of Resources -
-           * Unable to perform suboperations" status.
-           */
-          if ((data.handler_->GetFailedCount() > 0) &&
-              ((data.handler_->GetCompletedCount() +
-                data.handler_->GetWarningCount()) == 0)) 
-          {
-            response->DimseStatus = STATUS_GET_Refused_OutOfResourcesSubOperations;
-          }
-          
-          *responseIdentifiers = BuildFailedInstanceList(data.handler_->GetFailedUids());
-        }
-      }
-      
-      response->NumberOfRemainingSubOperations = data.handler_->GetRemainingCount();
-      response->NumberOfCompletedSubOperations = data.handler_->GetCompletedCount();
-      response->NumberOfFailedSubOperations = data.handler_->GetFailedCount();
-      response->NumberOfWarningSubOperations = data.handler_->GetWarningCount();
-    }
-  }
-
-  OFCondition Internals::getScp(T_ASC_Association * assoc,
-                                T_DIMSE_Message * msg, 
-                                T_ASC_PresentationContextID presID,
-                                IGetRequestHandler& handler,
-                                std::string remoteIp,
-                                std::string remoteAet,
-                                std::string calledAet,
-                                int timeout)
-  {
-    GetScpData data;
-    data.lastRequest_ = NULL;
-    data.handler_ = &handler;
-    data.assoc_ = assoc;
-    data.remoteIp_ = remoteIp;
-    data.remoteAet_ = remoteAet;
-    data.calledAet_ = calledAet;
-    data.timeout_ = timeout;
-
-    OFCondition cond = DIMSE_getProvider(assoc, presID, &msg->msg.CGetRQ, 
-                                         GetScpCallback, &data,
-                                         /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                                         /*opt_dimse_timeout*/ timeout);
-    
-    // if some error occured, dump corresponding information and remove the outfile if necessary
-    if (cond.bad())
-    {
-      OFString temp_str;
-      LOG(ERROR) << "Get SCP Failed: " << cond.text();
-    }
-
-    return cond;
-  }
-}
--- a/Core/DicomNetworking/Internals/GetScp.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../IGetRequestHandler.h"
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    OFCondition getScp(T_ASC_Association * assoc,
-                       T_DIMSE_Message * msg, 
-                       T_ASC_PresentationContextID presID,
-                       IGetRequestHandler& handler,
-                       std::string remoteIp,
-                       std::string remoteAet,
-                       std::string calledAet,
-                       int timeout);
-  }
-}
--- a/Core/DicomNetworking/Internals/MoveScp.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,300 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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::unique_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,
-                                 int timeout)
-  {
-    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*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                                          /*opt_dimse_timeout*/ timeout);
-
-    // 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/Core/DicomNetworking/Internals/MoveScp.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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,
-                        int timeout);
-  }
-}
--- a/Core/DicomNetworking/Internals/StoreScp.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,311 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#if !defined(DCMTK_VERSION_NUMBER)
-#  error The macro DCMTK_VERSION_NUMBER must be defined
-#endif
-
-#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 DCMTK_VERSION_NUMBER >= 364
-	    if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sizeof(sopClass),
-						     sopInstance, sizeof(sopInstance), /*opt_correctUIDPadding*/ OFFalse))
-#else
-            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
-#endif
-            {
-		//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,
-                                  int timeout)
-  {
-    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*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
-                               /*opt_dimse_timeout*/ timeout);
-
-    // 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/Core/DicomNetworking/Internals/StoreScp.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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,
-                         int timeout);
-  }
-}
--- a/Core/DicomNetworking/NetworkingCompatibility.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-
-#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
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,378 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_ALLOW_N_ACTION = "AllowNAction";
-static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport";
-static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment";
-static const char* KEY_ALLOW_TRANSCODING = "AllowTranscoding";
-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;
-    allowNAction_ = true;  // For storage commitment
-    allowNEventReport_ = true;  // For storage commitment
-    allowTranscoding_ = 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)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "A TCP port number must be in range [1..65534], but found: " +
-                             boost::lexical_cast<std::string>(value));
-    }
-  }
-
-
-  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);
-    }
-
-    if (serialized.isMember(KEY_ALLOW_N_ACTION))
-    {
-      allowNAction_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_ACTION);
-    }
-
-    if (serialized.isMember(KEY_ALLOW_N_EVENT_REPORT))
-    {
-      allowNEventReport_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_EVENT_REPORT);
-    }
-
-    if (serialized.isMember(KEY_ALLOW_STORAGE_COMMITMENT))
-    {
-      bool allow = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORAGE_COMMITMENT);
-      allowNAction_ = allow;
-      allowNEventReport_ = allow;
-    }
-
-    if (serialized.isMember(KEY_ALLOW_TRANSCODING))
-    {
-      allowTranscoding_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_TRANSCODING);
-    }
-  }
-
-
-  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_;
-
-      case DicomRequestType_NAction:
-        return allowNAction_;
-
-      case DicomRequestType_NEventReport:
-        return allowNEventReport_;
-
-      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;
-
-      case DicomRequestType_NAction:
-        allowNAction_ = allowed;
-        break;
-
-      case DicomRequestType_NEventReport:
-        allowNEventReport_ = allowed;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool RemoteModalityParameters::IsAdvancedFormatNeeded() const
-  {
-    return (!allowEcho_ ||
-            !allowStore_ ||
-            !allowFind_ ||
-            !allowGet_ ||
-            !allowMove_ ||
-            !allowNAction_ ||
-            !allowNEventReport_ ||
-            !allowTranscoding_);
-  }
-
-  
-  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_;
-      target[KEY_ALLOW_N_ACTION] = allowNAction_;
-      target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_;
-      target[KEY_ALLOW_TRANSCODING] = allowTranscoding_;
-    }
-    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);
-    }
-  }
-}
--- a/Core/DicomNetworking/RemoteModalityParameters.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-    bool                  allowNAction_;
-    bool                  allowNEventReport_;
-    bool                  allowTranscoding_;
-    
-    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;
-
-    bool IsTranscodingAllowed() const
-    {
-      return allowTranscoding_;
-    }
-
-    void SetTranscodingAllowed(bool allowed)
-    {
-      allowTranscoding_ = allowed;
-    }
-  };
-}
--- a/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,138 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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();
-  }
-
-
-  TimeoutDicomConnectionManager::Lock::Lock(TimeoutDicomConnectionManager& that,
-                                            const std::string& localAet,
-                                            const RemoteModalityParameters& remote) : 
-    that_(that),
-    lock_(that_.mutex_)
-  {
-    // Calling "Touch()" will be done by the "~Lock()" destructor
-    that_.OpenInternal(localAet, remote);
-  }
-
-  
-  TimeoutDicomConnectionManager::Lock::~Lock()
-  {
-    that_.TouchInternal();
-  }
-
-  
-  DicomStoreUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection()
-  {
-    if (that_.connection_.get() == NULL)
-    {
-      // The allocation should have been done by "that_.Open()" in the constructor
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      return *that_.connection_;
-    }
-  }
-
-
-  // Mutex must be locked
-  void TimeoutDicomConnectionManager::TouchInternal()
-  {
-    lastUse_ = GetNow();
-  }
-
-
-  // Mutex must be locked
-  void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet,
-                                                   const RemoteModalityParameters& remote)
-  {
-    DicomAssociationParameters other(localAet, remote);
-    
-    if (connection_.get() == NULL ||
-        !connection_->GetParameters().IsEqual(other))
-    {
-      connection_.reset(new DicomStoreUserConnection(other));
-    }
-  }
-
-
-  // Mutex must be locked
-  void TimeoutDicomConnectionManager::CloseInternal()
-  {
-    if (connection_.get() != NULL)
-    {
-      LOG(INFO) << "Closing inactive DICOM association with modality: "
-                << connection_->GetParameters().GetRemoteModality().GetApplicationEntityTitle();
-
-      connection_.reset(NULL);
-    }
-  }
-
-
-  void TimeoutDicomConnectionManager::SetInactivityTimeout(unsigned int milliseconds)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    timeout_ = boost::posix_time::milliseconds(milliseconds);
-    CloseInternal();
-  }
-
-
-  unsigned int TimeoutDicomConnectionManager::GetInactivityTimeout()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    return static_cast<unsigned int>(timeout_.total_milliseconds());
-  }
-
-
-  void TimeoutDicomConnectionManager::CloseIfInactive()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (connection_.get() != NULL &&
-        (GetNow() - lastUse_) >= timeout_)
-    {
-      CloseInternal();
-    }
-  }
-}
--- a/Core/DicomNetworking/TimeoutDicomConnectionManager.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 != 1
-#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be 1 to use this file
-#endif
-
-
-#include "../Compatibility.h"
-#include "DicomStoreUserConnection.h"
-
-#include <boost/date_time/posix_time/posix_time.hpp>
-#include <boost/thread/mutex.hpp>
-
-namespace Orthanc
-{
-  /**
-   * This class corresponds to a singleton to a DICOM SCU connection.
-   **/
-  class TimeoutDicomConnectionManager : public boost::noncopyable
-  {
-  private:
-    boost::mutex                               mutex_;
-    std::unique_ptr<DicomStoreUserConnection>  connection_;
-    boost::posix_time::ptime                   lastUse_;
-    boost::posix_time::time_duration           timeout_;
-
-    // Mutex must be locked
-    void TouchInternal();
-
-    // Mutex must be locked
-    void OpenInternal(const std::string& localAet,
-                      const RemoteModalityParameters& remote);
-
-    // Mutex must be locked
-    void CloseInternal();
-
-  public:
-    class Lock : public boost::noncopyable
-    {
-    private:
-      TimeoutDicomConnectionManager&  that_;
-      boost::mutex::scoped_lock       lock_;
-
-    public:
-      Lock(TimeoutDicomConnectionManager& that,
-           const std::string& localAet,
-           const RemoteModalityParameters& remote);
-      
-      ~Lock();
-
-      DicomStoreUserConnection& GetConnection();
-    };
-
-    TimeoutDicomConnectionManager() :
-      timeout_(boost::posix_time::milliseconds(1000))
-    {
-    }
-
-    void SetInactivityTimeout(unsigned int milliseconds);
-
-    unsigned int GetInactivityTimeout();  // In milliseconds
-
-    void Close();
-
-    void CloseIfInactive();
-  };
-}
--- a/Core/DicomParsing/DcmtkTranscoder.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,352 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DcmtkTranscoder.h"
-
-
-#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
-#  error Macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
-#endif
-
-#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
-#  error Macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
-#endif
-
-
-#include "FromDcmtkBridge.h"
-#include "../OrthancException.h"
-
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmjpeg/djrploss.h>  // for DJ_RPLossy
-#include <dcmtk/dcmjpeg/djrplol.h>   // for DJ_RPLossless
-#include <dcmtk/dcmjpls/djrparam.h>  // for DJLSRepresentationParameter
-
-
-namespace Orthanc
-{
-  static bool GetBitsStored(uint16_t& bitsStored,
-                            DcmDataset& dataset)
-  {
-    return dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good();
-  }
-
-  
-  void DcmtkTranscoder::SetLossyQuality(unsigned int quality)
-  {
-    if (quality <= 0 ||
-        quality > 100)
-    {
-      throw OrthancException(
-        ErrorCode_ParameterOutOfRange,
-        "The quality for lossy transcoding must be an integer between 1 and 100, received: " +
-        boost::lexical_cast<std::string>(quality));
-    }
-    else
-    {
-      LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality;
-      lossyQuality_ = quality;
-    }
-  }
-
-    
-  bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
-                                         DcmFileFormat& dicom,
-                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid) 
-  {
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    DicomTransferSyntax syntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Cannot determine the transfer syntax");
-    }
-
-    uint16_t bitsStored;
-    bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset());
-    
-    std::string sourceSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicom);
-    
-    if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end())
-    {
-      // No transcoding is needed
-      return true;
-    }
-      
-    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_LittleEndianImplicit;
-      return true;
-    }
-
-    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_LittleEndianExplicit;
-      return true;
-    }
-      
-    if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_BigEndianExplicit;
-      return true;
-    }
-
-    if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() &&
-        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
-    {
-      selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-      return true;
-    }
-
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() &&
-        allowNewSopInstanceUid &&
-        (!hasBitsStored || bitsStored == 8))
-    {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossy parameters(lossyQuality_);
-        
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
-      {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess1;
-        return true;
-      }
-    }
-#endif
-      
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() &&
-        allowNewSopInstanceUid &&
-        (!hasBitsStored || bitsStored <= 12))
-    {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossy parameters(lossyQuality_);
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
-      {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
-        return true;
-      }
-    }
-#endif
-      
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14) != allowedSyntaxes.end())
-    {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossless parameters(6 /* opt_selection_value */,
-                               0 /* opt_point_transform */);
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, &parameters))
-      {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess14;
-        return true;
-      }
-    }
-#endif
-      
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14SV1) != allowedSyntaxes.end())
-    {
-      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
-      DJ_RPLossless parameters(6 /* opt_selection_value */,
-                               0 /* opt_point_transform */);
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, &parameters))
-      {
-        selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1;
-        return true;
-      }
-    }
-#endif
-      
-#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
-    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossless) != allowedSyntaxes.end())
-    {
-      // Check out "dcmjpls/apps/dcmcjpls.cc"
-      DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */,
-                                             OFTrue /* opt_useLosslessProcess */);
-
-      /**
-       * WARNING: This call results in a segmentation fault if using
-       * the DCMTK package 3.6.2 from Ubuntu 18.04.
-       **/              
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, &parameters))
-      {
-        selectedSyntax = DicomTransferSyntax_JPEGLSLossless;
-        return true;
-      }
-    }
-#endif
-      
-#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
-    if (allowNewSopInstanceUid &&
-        allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossy) != allowedSyntaxes.end())
-    {
-      // Check out "dcmjpls/apps/dcmcjpls.cc"
-      DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */,
-                                             OFFalse /* opt_useLosslessProcess */);
-
-      /**
-       * WARNING: This call results in a segmentation fault if using
-       * the DCMTK package 3.6.2 from Ubuntu 18.04.
-       **/              
-      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, &parameters))
-      {
-        selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
-        return true;
-      }
-    }
-#endif
-
-    return false;
-  }
-
-    
-  bool DcmtkTranscoder::IsSupported(DicomTransferSyntax syntax)
-  {
-    if (syntax == DicomTransferSyntax_LittleEndianImplicit ||
-        syntax == DicomTransferSyntax_LittleEndianExplicit ||
-        syntax == DicomTransferSyntax_BigEndianExplicit ||
-        syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit)
-    {
-      return true;
-    }
-
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    if (syntax == DicomTransferSyntax_JPEGProcess1 ||
-        syntax == DicomTransferSyntax_JPEGProcess2_4 ||
-        syntax == DicomTransferSyntax_JPEGProcess14 ||
-        syntax == DicomTransferSyntax_JPEGProcess14SV1)
-    {
-      return true;
-    }
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
-    if (syntax == DicomTransferSyntax_JPEGLSLossless ||
-        syntax == DicomTransferSyntax_JPEGLSLossy)
-    {
-      return true;
-    }
-#endif
-    
-    return false;
-  }
-
-
-  bool DcmtkTranscoder::Transcode(DicomImage& target,
-                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
-                                  const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                  bool allowNewSopInstanceUid)
-  {
-    target.Clear();
-    
-    DicomTransferSyntax sourceSyntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
-    {
-      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
-      return false;
-    }
-
-    {
-      std::string s;
-      for (std::set<DicomTransferSyntax>::const_iterator
-             it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
-      {
-        if (!s.empty())
-        {
-          s += ", ";
-        }
-
-        s += GetTransferSyntaxUid(*it);
-      }
-
-      if (s.empty())
-      {
-        s = "<none>";
-      }
-      
-      LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
-                << " to one of: " << s;
-    }
-
-#if !defined(NDEBUG)
-    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
-#endif
-
-    DicomTransferSyntax targetSyntax;
-    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
-    {
-      // No transcoding is needed
-      target.AcquireParsed(source);
-      target.AcquireBuffer(source);
-      return true;
-    }
-    else if (InplaceTranscode(targetSyntax, source.GetParsed(),
-                              allowedSyntaxes, allowNewSopInstanceUid))
-    {   
-      // Sanity check
-      DicomTransferSyntax targetSyntax2;
-      if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, source.GetParsed()) &&
-          targetSyntax == targetSyntax2 &&
-          allowedSyntaxes.find(targetSyntax2) != allowedSyntaxes.end())
-      {
-        target.AcquireParsed(source);
-        source.Clear();
-        
-#if !defined(NDEBUG)
-        // Only run the sanity check in debug mode
-        CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
-                         allowedSyntaxes, allowNewSopInstanceUid);
-#endif
-        
-        return true;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }  
-    }
-    else
-    {
-      // Cannot transcode
-      return false;
-    }
-  }
-}
--- a/Core/DicomParsing/DcmtkTranscoder.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_TRANSCODING)
-#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1
-#  error Transcoding is disabled, cannot compile this file
-#endif
-
-#include "IDicomTranscoder.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC DcmtkTranscoder : public IDicomTranscoder
-  {
-  private:
-    unsigned int  lossyQuality_;
-    
-    bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
-                          DcmFileFormat& dicom,
-                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                          bool allowNewSopInstanceUid);
-    
-  public:
-    DcmtkTranscoder() :
-      lossyQuality_(90)
-    {
-    }
-
-    void SetLossyQuality(unsigned int quality);
-
-    unsigned int GetLossyQuality() const
-    {
-      return lossyQuality_;
-    }
-    
-    static bool IsSupported(DicomTransferSyntax syntax);
-
-    virtual bool Transcode(DicomImage& target,
-                           DicomImage& source /* in, "GetParsed()" possibly modified */,
-                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-  };
-}
--- a/Core/DicomParsing/DicomDirWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,602 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.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::unique_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,
-                                bool hasCodeExtensions,
-                                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, hasCodeExtensions);
-          }
-          
-          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,
-                           bool hasCodeExtensions,
-                           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, hasCodeExtensions, 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,
-                                bool hasCodeExtensions,
-                                const DcmTagKey& key)
-    {
-      CopyString(target, source, encoding, hasCodeExtensions, key, false, false);
-    }
-
-    static void CopyStringType1C(DcmDirectoryRecord& target,
-                                 DcmDataset& source,
-                                 Encoding encoding,
-                                 bool hasCodeExtensions,
-                                 const DcmTagKey& key)
-    {
-      CopyString(target, source, encoding, hasCodeExtensions, key, true, false);
-    }
-
-    static void CopyStringType2(DcmDirectoryRecord& target,
-                                DcmDataset& source,
-                                Encoding encoding,
-                                bool hasCodeExtensions,
-                                const DcmTagKey& key)
-    {
-      CopyString(target, source, encoding, hasCodeExtensions, key, false, true);
-    }
-
-    static void CopyStringType3(DcmDirectoryRecord& target,
-                                DcmDataset& source,
-                                Encoding encoding,
-                                bool hasCodeExtensions,
-                                const DcmTagKey& key)
-    {
-      CopyString(target, source, encoding, hasCodeExtensions, 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,
-                     bool hasCodeExtensions)
-    {
-      // cf. "DicomDirInterface::buildPatientRecord()"
-
-      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_PatientID);
-      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_PatientName);
-    }
-
-    void FillStudy(DcmDirectoryRecord& record,
-                   DcmDataset& dicom,
-                   Encoding encoding,
-                   bool hasCodeExtensions)
-    {
-      // cf. "DicomDirInterface::buildStudyRecord()"
-
-      std::string nowDate, nowTime;
-      SystemToolbox::GetNowDicom(nowDate, nowTime, utc_);
-
-      std::string studyDate;
-      if (!GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_StudyDate) &&
-          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_SeriesDate) &&
-          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_AcquisitionDate) &&
-          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_ContentDate))
-      {
-        studyDate = nowDate;
-      }
-          
-      std::string studyTime;
-      if (!GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_StudyTime) &&
-          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_SeriesTime) &&
-          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_AcquisitionTime) &&
-          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, 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, hasCodeExtensions, DCM_StudyDescription);
-      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
-      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
-      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_StudyID);
-      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_AccessionNumber);
-    }
-
-    void FillSeries(DcmDirectoryRecord& record,
-                    DcmDataset& dicom,
-                    Encoding encoding,
-                    bool hasCodeExtensions)
-    {
-      // cf. "DicomDirInterface::buildSeriesRecord()"
-
-      /* copy attribute values from dataset to series record */
-      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_Modality);
-      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
-      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
-      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, 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, hasCodeExtensions, DCM_SeriesDescription);
-      }
-    }
-
-    void FillInstance(DcmDirectoryRecord& record,
-                      DcmDataset& dicom,
-                      Encoding encoding,
-                      bool hasCodeExtensions,
-                      DcmMetaInfo& metaInfo,
-                      const char* path)
-    {
-      // cf. "DicomDirInterface::buildImageRecord()"
-
-      /* copy attribute values from dataset to image record */
-      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_InstanceNumber);
-      //CopyElementType1C(record, dicom, encoding, hasCodeExtensions, DCM_ImageType);
-
-      // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
-
-      std::string sopClassUid, sopInstanceUid, transferSyntaxUid;
-      if (!GetUtf8TagValue(sopClassUid, dicom, encoding, hasCodeExtensions, DCM_SOPClassUID) ||
-          !GetUtf8TagValue(sopInstanceUid, dicom, encoding, hasCodeExtensions, DCM_SOPInstanceUID) ||
-          !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, hasCodeExtensions, 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();
-
-      bool hasCodeExtensions;
-      Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-
-      bool found;
-      std::string id;
-      E_DirRecType type;
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          if (!GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_PatientID))
-          {
-            // Be tolerant about missing patient ID. Fixes issue #124
-            // (GET /studies/ID/media fails for certain dicom file).
-            id = "";
-          }
-
-          found = true;
-          type = ERT_Patient;
-          break;
-
-        case ResourceType_Study:
-          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
-          type = ERT_Study;
-          break;
-
-        case ResourceType_Series:
-          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
-          type = ERT_Series;
-          break;
-
-        case ResourceType_Instance:
-          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, 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::unique_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
-
-      switch (level)
-      {
-        case ResourceType_Patient:
-          FillPatient(*record, dataset, encoding, hasCodeExtensions);
-          break;
-
-        case ResourceType_Study:
-          FillStudy(*record, dataset, encoding, hasCodeExtensions);
-          break;
-
-        case ResourceType_Series:
-          FillSeries(*record, dataset, encoding, hasCodeExtensions);
-          break;
-
-        case ResourceType_Instance:
-          FillInstance(*record, dataset, encoding, hasCodeExtensions, *dicom.GetDcmtkObject().getMetaInfo(), path);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      CopyStringType1C(*record, dataset, encoding, hasCodeExtensions, 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();
-  }
-}
--- a/Core/DicomParsing/DicomDirWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-
-}
--- a/Core/DicomParsing/DicomModification.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1522 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../SerializationToolbox.h"
-#include "FromDcmtkBridge.h"
-#include "ITagVisitor.h"
-
-#include <memory>   // For std::unique_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 VisitNotSupported(const std::vector<DicomTag>& parentTags,
-                                   const std::vector<size_t>& parentIndexes,
-                                   const DicomTag& tag,
-                                   ValueRepresentation vr)
-    {
-    }
-
-    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    const DicomTag& tag)
-    {
-    }
-
-    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 VisitIntegers(const std::vector<DicomTag>& parentTags,
-                               const std::vector<size_t>& parentIndexes,
-                               const DicomTag& tag,
-                               ValueRepresentation vr,
-                               const std::vector<int64_t>& values)
-    {
-    }
-
-    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              const std::vector<double>& value)
-    {
-    }
-
-    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 const std::vector<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 (parentTags.size() == 2 &&
-               parentTags[0] == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE &&
-               parentTags[1] == DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE &&
-               tag == DICOM_TAG_REFERENCED_SOP_INSTANCE_UID)
-      {
-        // in RT-STRUCT, this ReferencedSOPInstanceUID is actually referencing a StudyInstanceUID !!
-        // (observed in many data sets including: https://wiki.cancerimagingarchive.net/display/Public/Lung+CT+Segmentation+Challenge+2017)
-        // tested in test_anonymize_relationships_5
-        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Study);
-        return Action_Replace;
-      }
-      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 if (parentTags.size() == 3 &&
-               parentTags[0] == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE &&
-               parentTags[1] == DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE &&
-               parentTags[2] == DICOM_TAG_RT_REFERENCED_SERIES_SEQUENCE &&
-               tag == DICOM_TAG_SERIES_INSTANCE_UID)
-      {
-        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Series);
-        return Action_Replace;
-      }
-      else if (parentTags.size() == 1 &&
-               parentTags[0] == 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);
-    }
-  }
-
-  void DicomModification::RegisterMappedDicomIdentifier(const std::string& original,
-                                                        const std::string& mapped,
-                                                        ResourceType level)
-  {
-    UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original));
-
-    if (previous == uidMap_.end())
-    {
-      uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped));
-    }
-  }
-
-  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_))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Unable to generate an anonymized ID");
-        }
-      }
-
-      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::unique_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, privateCreator_);
-  }
-
-  
-  DicomModification::DicomModification() :
-    removePrivateTags_(false),
-    level_(ResourceType_Instance),
-    allowManualIdentifiers_(true),
-    keepStudyInstanceUid_(false),
-    keepSeriesInstanceUid_(false),
-    keepSopInstanceUid_(false),
-    updateReferencedRelationships_(true),
-    isAnonymization_(false),
-    //privateCreator_("PrivateCreator"),
-    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 == DICOM_TAG_SOP_INSTANCE_UID)
-    {
-      keepSopInstanceUid_ = 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))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a patient, her PatientID is required to be modified");
-    }
-
-    if (!allowManualIdentifiers_)
-    {
-      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "When modifying a patient, the StudyInstanceUID cannot be manually modified");
-      }
-
-      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "When modifying a patient, the SeriesInstanceUID cannot be manually modified");
-      }
-
-      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "When modifying a patient, the SopInstanceUID cannot be manually modified");
-      }
-    }
-
-
-    // Sanity checks at the study level
-    if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a study, the parent PatientID cannot be manually modified");
-    }
-
-    if (!allowManualIdentifiers_)
-    {
-      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "When modifying a study, the SeriesInstanceUID cannot be manually modified");
-      }
-
-      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "When modifying a study, the SopInstanceUID cannot be manually modified");
-      }
-    }
-
-
-    // Sanity checks at the series level
-    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a series, the parent PatientID cannot be manually modified");
-    }
-
-    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying a series, the parent StudyInstanceUID cannot be manually modified");
-    }
-
-    if (!allowManualIdentifiers_)
-    {
-      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "When modifying a series, the SopInstanceUID cannot be manually modified");
-      }
-    }
-
-
-    // Sanity checks at the instance level
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying an instance, the parent PatientID cannot be manually modified");
-    }
-
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
-    }
-
-    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
-    }
-
-    // (0) Create a summary of the source file, if a custom generator
-    // is provided
-    if (identifierGenerator_ != NULL)
-    {
-      toModify.ExtractDicomSummary(currentSource_);
-    }
-
-    // (1) Make sure the relationships are updated with the ids that we force too
-    // i.e: an RT-STRUCT is referencing its own StudyInstanceUID
-    if (isAnonymization_ && updateReferencedRelationships_)
-    {
-      if (IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-      {
-        std::string original;
-        std::string replacement = GetReplacementAsString(DICOM_TAG_STUDY_INSTANCE_UID);
-        toModify.GetTagValue(original, DICOM_TAG_STUDY_INSTANCE_UID);
-        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Study);
-      }
-
-      if (IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-      {
-        std::string original;
-        std::string replacement = GetReplacementAsString(DICOM_TAG_SERIES_INSTANCE_UID);
-        toModify.GetTagValue(original, DICOM_TAG_SERIES_INSTANCE_UID);
-        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Series);
-      }
-
-      if (IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
-      {
-        std::string original;
-        std::string replacement = GetReplacementAsString(DICOM_TAG_SOP_INSTANCE_UID);
-        toModify.GetTagValue(original, DICOM_TAG_SOP_INSTANCE_UID);
-        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Instance);
-      }
-    }
-
-
-    // (2) Remove the private tags, if need be
-    if (removePrivateTags_)
-    {
-      toModify.RemovePrivateTags(privateTagsToKeep_);
-    }
-
-    // (3) 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 */);
-    }
-
-    // (4) Remove the tags specified by the user
-    for (SetOfTags::const_iterator it = removals_.begin(); 
-         it != removals_.end(); ++it)
-    {
-      toModify.Remove(*it);
-    }
-
-    // (5) 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, privateCreator_);
-    }
-
-    // (6) 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))
-    {
-      if (keepSopInstanceUid_)
-      {
-        LOG(WARNING) << "Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!";
-      }
-      else
-      {
-        MapDicomTags(toModify, ResourceType_Instance);
-      }
-    }
-
-    // (7) 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))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Marking tag \"" + name + "\" as to be " +
-                               (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") +
-                               " requires the \"Force\" option to be set to true");
-      }
-
-      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))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Marking tag \"" + name + "\" as to be replaced " +
-                               "requires the \"Force\" option to be set to true");
-      }
-
-      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
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Member \"" + member + "\" should be a Boolean value");
-    }
-  }
-
-
-  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);
-    }
-
-    // New in Orthanc 1.6.0
-    if (request.isMember("PrivateCreator"))
-    {
-      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
-    }
-  }
-
-
-  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);
-
-    // New in Orthanc 1.6.0
-    if (request.isMember("PrivateCreator"))
-    {
-      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
-    }
-  }
-
-
-
-
-  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* KEEP_SOP_INSTANCE_UID = "KeepSOPInstanceUID";
-  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";
-  static const char* PRIVATE_CREATOR = "PrivateCreator";  // New in Orthanc 1.6.0
-  
-  void DicomModification::Serialize(Json::Value& value) const
-  {
-    if (identifierGenerator_ != NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot serialize a DicomModification with a custom identifier generator");
-    }
-
-    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[KEEP_SOP_INSTANCE_UID] = keepSopInstanceUid_;
-    value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
-    value[IS_ANONYMIZATION] = isAnonymization_;
-    value[PRIVATE_CREATOR] = privateCreator_;
-
-    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);
-    keepSopInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOP_INSTANCE_UID);
-    updateReferencedRelationships_ = SerializationToolbox::ReadBoolean
-      (serialized, UPDATE_REFERENCED_RELATIONSHIPS);
-    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
-
-    if (serialized.isMember(PRIVATE_CREATOR))
-    {
-      privateCreator_ = SerializationToolbox::ReadString(serialized, PRIVATE_CREATOR);
-    }
-
-    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);
-  }
-}
--- a/Core/DicomParsing/DicomModification.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 keepSopInstanceUid_;
-    bool updateReferencedRelationships_;
-    bool isAnonymization_;
-    DicomMap currentSource_;
-    std::string privateCreator_;
-
-    IDicomIdentifierGenerator* identifierGenerator_;
-
-    std::string MapDicomIdentifier(const std::string& original,
-                                   ResourceType level);
-
-    void RegisterMappedDicomIdentifier(const std::string& original,
-                                       const std::string& mapped,
-                                       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;
-
-    void SetPrivateCreator(const std::string& privateCreator)
-    {
-      privateCreator_ = privateCreator;
-    }
-
-    const std::string& GetPrivateCreator()
-    {
-      return privateCreator_;
-    }
-  };
-}
--- a/Core/DicomParsing/DicomWebJsonVisitor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,673 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomWebJsonVisitor.h"
-
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-#include "FromDcmtkBridge.h"
-
-#include <boost/math/special_functions/round.hpp>
-#include <boost/lexical_cast.hpp>
-
-
-static const char* const KEY_ALPHABETIC = "Alphabetic";
-static const char* const KEY_IDEOGRAPHIC = "Ideographic";
-static const char* const KEY_PHONETIC = "Phonetic";
-static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
-static const char* const KEY_INLINE_BINARY = "InlineBinary";
-static const char* const KEY_SQ = "SQ";
-static const char* const KEY_TAG = "tag";
-static const char* const KEY_VALUE = "Value";
-static const char* const KEY_VR = "vr";
-
-
-namespace Orthanc
-{
-#if ORTHANC_ENABLE_PUGIXML == 1
-  static void DecomposeXmlPersonName(pugi::xml_node& target,
-                                     const std::string& source)
-  {
-    std::vector<std::string> tokens;
-    Toolbox::TokenizeString(tokens, source, '^');
-
-    if (tokens.size() >= 1)
-    {
-      target.append_child("FamilyName").text() = tokens[0].c_str();
-    }
-            
-    if (tokens.size() >= 2)
-    {
-      target.append_child("GivenName").text() = tokens[1].c_str();
-    }
-            
-    if (tokens.size() >= 3)
-    {
-      target.append_child("MiddleName").text() = tokens[2].c_str();
-    }
-            
-    if (tokens.size() >= 4)
-    {
-      target.append_child("NamePrefix").text() = tokens[3].c_str();
-    }
-            
-    if (tokens.size() >= 5)
-    {
-      target.append_child("NameSuffix").text() = tokens[4].c_str();
-    }
-  }
-  
-  static void ExploreXmlDataset(pugi::xml_node& target,
-                                const Json::Value& source)
-  {
-    // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
-    assert(source.type() == Json::objectValue);
-
-    Json::Value::Members members = source.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]);
-      const Json::Value& content = source[members[i]];
-
-      assert(content.type() == Json::objectValue &&
-             content.isMember(KEY_VR) &&
-             content[KEY_VR].type() == Json::stringValue);
-      const std::string vr = content[KEY_VR].asString();
-
-      const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
-    
-      pugi::xml_node node = target.append_child("DicomAttribute");
-      node.append_attribute(KEY_TAG).set_value(members[i].c_str());
-      node.append_attribute(KEY_VR).set_value(vr.c_str());
-
-      if (keyword != std::string(DcmTag_ERROR_TagName))
-      {
-        node.append_attribute("keyword").set_value(keyword.c_str());
-      }   
-
-      if (content.isMember(KEY_VALUE))
-      {
-        assert(content[KEY_VALUE].type() == Json::arrayValue);
-        
-        for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++)
-        {
-          std::string number = boost::lexical_cast<std::string>(j + 1);
-
-          if (vr == "SQ")
-          {
-            if (content[KEY_VALUE][j].type() == Json::objectValue)
-            {
-              pugi::xml_node child = node.append_child("Item");
-              child.append_attribute("number").set_value(number.c_str());
-              ExploreXmlDataset(child, content[KEY_VALUE][j]);
-            }
-          }
-          if (vr == "PN")
-          {
-            bool hasAlphabetic = (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
-                                  content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue);
-
-            bool hasIdeographic = (content[KEY_VALUE][j].isMember(KEY_IDEOGRAPHIC) &&
-                                   content[KEY_VALUE][j][KEY_IDEOGRAPHIC].type() == Json::stringValue);
-
-            bool hasPhonetic = (content[KEY_VALUE][j].isMember(KEY_PHONETIC) &&
-                                content[KEY_VALUE][j][KEY_PHONETIC].type() == Json::stringValue);
-
-            if (hasAlphabetic ||
-                hasIdeographic ||
-                hasPhonetic)
-            {
-              pugi::xml_node child = node.append_child("PersonName");
-              child.append_attribute("number").set_value(number.c_str());
-
-              if (hasAlphabetic)
-              {
-                pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
-                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_ALPHABETIC].asString());
-              }
-
-              if (hasIdeographic)
-              {
-                pugi::xml_node name = child.append_child(KEY_IDEOGRAPHIC);
-                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_IDEOGRAPHIC].asString());
-              }
-
-              if (hasPhonetic)
-              {
-                pugi::xml_node name = child.append_child(KEY_PHONETIC);
-                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_PHONETIC].asString());
-              }
-            }
-          }
-          else
-          {
-            pugi::xml_node child = node.append_child("Value");
-            child.append_attribute("number").set_value(number.c_str());
-
-            switch (content[KEY_VALUE][j].type())
-            {
-              case Json::stringValue:
-                child.text() = content[KEY_VALUE][j].asCString();
-                break;
-
-              case Json::realValue:
-                child.text() = content[KEY_VALUE][j].asFloat();
-                break;
-
-              case Json::intValue:
-                child.text() = content[KEY_VALUE][j].asInt();
-                break;
-
-              case Json::uintValue:
-                child.text() = content[KEY_VALUE][j].asUInt();
-                break;
-
-              default:
-                break;
-            }
-          }
-        }
-      }
-      else if (content.isMember(KEY_BULK_DATA_URI) &&
-               content[KEY_BULK_DATA_URI].type() == Json::stringValue)
-      {
-        pugi::xml_node child = node.append_child("BulkData");
-        child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString());
-      }
-      else if (content.isMember(KEY_INLINE_BINARY) &&
-               content[KEY_INLINE_BINARY].type() == Json::stringValue)
-      {
-        pugi::xml_node child = node.append_child("InlineBinary");
-        child.text() = content[KEY_INLINE_BINARY].asCString();
-      }
-    }
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-  static void DicomWebJsonToXml(pugi::xml_document& target,
-                                const Json::Value& source)
-  {
-    pugi::xml_node root = target.append_child("NativeDicomModel");
-    root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
-    root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
-    root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
-
-    ExploreXmlDataset(root, source);
-
-    pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
-    decl.append_attribute("version").set_value("1.0");
-    decl.append_attribute("encoding").set_value("utf-8");
-  }
-#endif
-
-
-  std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag)
-  {
-    char buf[16];
-    sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement());
-    return std::string(buf);
-  }
-
-    
-  Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector<DicomTag>& parentTags,
-                                               const std::vector<size_t>& parentIndexes,
-                                               const DicomTag& tag)
-  {
-    assert(parentTags.size() == parentIndexes.size());      
-
-    Json::Value* node = &result_;
-
-    for (size_t i = 0; i < parentTags.size(); i++)
-    {
-      std::string t = FormatTag(parentTags[i]);
-
-      if (!node->isMember(t))
-      {
-        Json::Value item = Json::objectValue;
-        item[KEY_VR] = KEY_SQ;
-        item[KEY_VALUE] = Json::arrayValue;
-        item[KEY_VALUE].append(Json::objectValue);
-        (*node) [t] = item;
-
-        node = &(*node)[t][KEY_VALUE][0];
-      }
-      else if ((*node)  [t].type() != Json::objectValue ||
-               !(*node) [t].isMember(KEY_VR) ||
-               (*node)  [t][KEY_VR].type() != Json::stringValue ||
-               (*node)  [t][KEY_VR].asString() != KEY_SQ ||
-               !(*node) [t].isMember(KEY_VALUE) ||
-               (*node)  [t][KEY_VALUE].type() != Json::arrayValue)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-      else
-      {
-        size_t currentSize = (*node) [t][KEY_VALUE].size();
-
-        if (parentIndexes[i] < currentSize)
-        {
-          // The node already exists
-        }
-        else if (parentIndexes[i] == currentSize)
-        {
-          (*node) [t][KEY_VALUE].append(Json::objectValue);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-          
-        node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])];
-      }
-    }
-
-    assert(node->type() == Json::objectValue);
-
-    std::string t = FormatTag(tag);
-    if (node->isMember(t))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      (*node) [t] = Json::objectValue;
-      return (*node) [t];
-    }
-  }
-
-    
-  Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value)
-  {
-    if (value < 0)
-    {
-      return Json::Value(static_cast<int32_t>(value));
-    }
-    else
-    {
-      return Json::Value(static_cast<uint32_t>(value));
-    }
-  }
-
-    
-  Json::Value DicomWebJsonVisitor::FormatDouble(double value)
-  {
-    try
-    {
-      long long a = boost::math::llround<double>(value);
-
-      double d = fabs(value - static_cast<double>(a));
-
-      if (d <= std::numeric_limits<double>::epsilon() * 100.0)
-      {
-        return FormatInteger(a);
-      }
-      else
-      {
-        return Json::Value(value);
-      }
-    }
-    catch (boost::math::rounding_error&)
-    {
-      // Can occur if "long long" is too small to receive this value
-      // (e.g. infinity)
-      return Json::Value(value);
-    }
-  }
-
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-  void DicomWebJsonVisitor::FormatXml(std::string& target) const
-  {
-    pugi::xml_document doc;
-    DicomWebJsonToXml(doc, result_);
-    Toolbox::XmlToString(target, doc);
-  }
-#endif
-
-
-  void DicomWebJsonVisitor::VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                               const std::vector<size_t>& parentIndexes,
-                                               const DicomTag& tag)
-  {
-    if (tag.GetElement() != 0x0000)
-    {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
-    }
-  }
-  
-
-  void DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
-                                        const std::vector<size_t>& parentIndexes,
-                                        const DicomTag& tag,
-                                        ValueRepresentation vr,
-                                        const void* data,
-                                        size_t size)
-  {
-    assert(vr == ValueRepresentation_OtherByte ||
-           vr == ValueRepresentation_OtherDouble ||
-           vr == ValueRepresentation_OtherFloat ||
-           vr == ValueRepresentation_OtherLong ||
-           vr == ValueRepresentation_OtherWord ||
-           vr == ValueRepresentation_Unknown);
-
-    if (tag.GetElement() != 0x0000)
-    {
-      BinaryMode mode;
-      std::string bulkDataUri;
-        
-      if (formatter_ == NULL)
-      {
-        mode = BinaryMode_InlineBinary;
-      }
-      else
-      {
-        mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
-      }
-
-      if (mode != BinaryMode_Ignore)
-      {
-        Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-        node[KEY_VR] = EnumerationToString(vr);
-
-        switch (mode)
-        {
-          case BinaryMode_BulkDataUri:
-            node[KEY_BULK_DATA_URI] = bulkDataUri;
-            break;
-
-          case BinaryMode_InlineBinary:
-          {
-            std::string tmp(static_cast<const char*>(data), size);
-          
-            std::string base64;
-            Toolbox::EncodeBase64(base64, tmp);
-
-            node[KEY_INLINE_BINARY] = base64;
-            break;
-          }
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-      }
-    }
-  }
-
-
-  void DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
-                                          const std::vector<size_t>& parentIndexes,
-                                          const DicomTag& tag,
-                                          ValueRepresentation vr,
-                                          const std::vector<int64_t>& values)
-  {
-    if (tag.GetElement() != 0x0000 &&
-        vr != ValueRepresentation_NotSupported)
-    {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(vr);
-
-      if (!values.empty())
-      {
-        Json::Value content = Json::arrayValue;
-        for (size_t i = 0; i < values.size(); i++)
-        {
-          content.append(FormatInteger(values[i]));
-        }
-
-        node[KEY_VALUE] = content;
-      }
-    }
-  }
-
-  void DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
-                                         const std::vector<size_t>& parentIndexes,
-                                         const DicomTag& tag,
-                                         ValueRepresentation vr,
-                                         const std::vector<double>& values)
-  {
-    if (tag.GetElement() != 0x0000 &&
-        vr != ValueRepresentation_NotSupported)
-    {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(vr);
-
-      if (!values.empty())
-      {
-        Json::Value content = Json::arrayValue;
-        for (size_t i = 0; i < values.size(); i++)
-        {
-          content.append(FormatDouble(values[i]));
-        }
-          
-        node[KEY_VALUE] = content;
-      }
-    }
-  }
-
-  
-  void DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                            const std::vector<size_t>& parentIndexes,
-                                            const DicomTag& tag,
-                                            const std::vector<DicomTag>& values)
-  {
-    if (tag.GetElement() != 0x0000)
-    {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag);
-
-      if (!values.empty())
-      {
-        Json::Value content = Json::arrayValue;
-        for (size_t i = 0; i < values.size(); i++)
-        {
-          content.append(FormatTag(values[i]));
-        }
-          
-        node[KEY_VALUE] = content;
-      }
-    }
-  }
-
-  
-  ITagVisitor::Action
-  DicomWebJsonVisitor::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 (tag.GetElement() == 0x0000 ||
-        vr == ValueRepresentation_NotSupported)
-    {
-      return Action_None;
-    }
-    else
-    {
-      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
-      node[KEY_VR] = EnumerationToString(vr);
-
-#if 0
-      /**
-       * TODO - The JSON file has an UTF-8 encoding, thus DCMTK
-       * replaces the specific character set with "ISO_IR 192"
-       * (UNICODE UTF-8). On Google Cloud Healthcare, however, the
-       * source encoding is reported, which seems more logical. We
-       * thus choose the Google convention. Enabling this block will
-       * mimic the DCMTK behavior.
-       **/
-      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
-      {
-        node[KEY_VALUE].append("ISO_IR 192");
-      }
-      else
-#endif
-      {
-        std::string truncated;
-        
-        if (!value.empty() &&
-            value[value.size() - 1] == '\0')
-        {
-          truncated = value.substr(0, value.size() - 1);
-        }
-        else
-        {
-          truncated = value;
-        }
-
-        if (!truncated.empty())
-        {
-          std::vector<std::string> tokens;
-          Toolbox::TokenizeString(tokens, truncated, '\\');
-
-          if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET &&
-              tokens.size() > 1 &&
-              tokens[0].empty())
-          {
-            // Specific character set with code extension: Remove the
-            // first element from the vector of encodings
-            tokens.erase(tokens.begin());
-          }
-
-          node[KEY_VALUE] = Json::arrayValue;
-          for (size_t i = 0; i < tokens.size(); i++)
-          {
-            try
-            {
-              switch (vr)
-              {
-                case ValueRepresentation_PersonName:
-                {
-                  Json::Value value = Json::objectValue;
-                  if (!tokens[i].empty())
-                  {
-                    std::vector<std::string> components;
-                    Toolbox::TokenizeString(components, tokens[i], '=');
-
-                    if (components.size() >= 1)
-                    {
-                      value[KEY_ALPHABETIC] = components[0];
-                    }
-
-                    if (components.size() >= 2)
-                    {
-                      value[KEY_IDEOGRAPHIC] = components[1];
-                    }
-
-                    if (components.size() >= 3)
-                    {
-                      value[KEY_PHONETIC] = components[2];
-                    }
-                  }
-                  
-                  node[KEY_VALUE].append(value);
-                  break;
-                }
-                  
-                case ValueRepresentation_IntegerString:
-                {
-                  /**
-                   * The calls to "StripSpaces()" below fix the
-                   * issue reported by Rana Asim Wajid on 2019-06-05
-                   * ("Error Exception while invoking plugin service
-                   * 32: Bad file format"):
-                   * https://groups.google.com/d/msg/orthanc-users/T32FovWPcCE/-hKFbfRJBgAJ
-                   **/
-
-                  std::string t = Orthanc::Toolbox::StripSpaces(tokens[i]);
-                  if (t.empty())
-                  {
-                    node[KEY_VALUE].append(Json::nullValue);
-                  }
-                  else
-                  {
-                    int64_t value = boost::lexical_cast<int64_t>(t);
-                    node[KEY_VALUE].append(FormatInteger(value));
-                  }
-                 
-                  break;
-                }
-              
-                case ValueRepresentation_DecimalString:
-                {
-                  std::string t = Orthanc::Toolbox::StripSpaces(tokens[i]);
-                  if (t.empty())
-                  {
-                    node[KEY_VALUE].append(Json::nullValue);
-                  }
-                  else
-                  {
-                    double value = boost::lexical_cast<double>(t);
-                    node[KEY_VALUE].append(FormatDouble(value));
-                  }
-
-                  break;
-                }
-
-                default:
-                  if (tokens[i].empty())
-                  {
-                    node[KEY_VALUE].append(Json::nullValue);
-                  }
-                  else
-                  {
-                    node[KEY_VALUE].append(tokens[i]);
-                  }
-                  
-                  break;
-              }
-            }
-            catch (boost::bad_lexical_cast&)
-            {
-              std::string tmp;
-              if (value.size() < 64 &&
-                  Toolbox::IsAsciiString(value))
-              {
-                tmp = ": " + value;
-              }
-              
-              LOG(WARNING) << "Ignoring DICOM tag (" << tag.Format()
-                           << ") with invalid content for VR " << EnumerationToString(vr) << tmp;
-            }
-          }
-        }
-      }
-    }
-      
-    return Action_None;
-  }
-}
--- a/Core/DicomParsing/DicomWebJsonVisitor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_PUGIXML)
-#  error Macro ORTHANC_ENABLE_PUGIXML must be defined to use this file
-#endif
-
-#include "ITagVisitor.h"
-
-#include <json/value.h>
-
-
-namespace Orthanc
-{
-  class DicomWebJsonVisitor : public ITagVisitor
-  {
-  public:
-    enum BinaryMode
-    {
-      BinaryMode_Ignore,
-      BinaryMode_BulkDataUri,
-      BinaryMode_InlineBinary
-    };
-    
-    class IBinaryFormatter : public boost::noncopyable
-    {
-    public:
-      virtual ~IBinaryFormatter()
-      {
-      }
-
-      virtual BinaryMode Format(std::string& bulkDataUri,
-                                const std::vector<DicomTag>& parentTags,
-                                const std::vector<size_t>& parentIndexes,
-                                const DicomTag& tag,
-                                ValueRepresentation vr) = 0;
-    };
-    
-  private:
-    Json::Value        result_;
-    IBinaryFormatter  *formatter_;
-
-    static std::string FormatTag(const DicomTag& tag);
-    
-    Json::Value& CreateNode(const std::vector<DicomTag>& parentTags,
-                            const std::vector<size_t>& parentIndexes,
-                            const DicomTag& tag);
-
-    static Json::Value FormatInteger(int64_t value);
-
-    static Json::Value FormatDouble(double value);
-
-  public:
-    DicomWebJsonVisitor() :
-      formatter_(NULL)
-    {
-      Clear();
-    }
-
-    void SetFormatter(IBinaryFormatter& formatter)
-    {
-      formatter_ = &formatter;
-    }
-    
-    void Clear()
-    {
-      result_ = Json::objectValue;
-    }
-
-    const Json::Value& GetResult() const
-    {
-      return result_;
-    }
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-    void FormatXml(std::string& target) const;
-#endif
-
-    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
-                                   const std::vector<size_t>& parentIndexes,
-                                   const DicomTag& tag,
-                                   ValueRepresentation vr)
-      ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    const DicomTag& tag)
-      ORTHANC_OVERRIDE;
-
-    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)
-      ORTHANC_OVERRIDE;
-
-    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
-                               const std::vector<size_t>& parentIndexes,
-                               const DicomTag& tag,
-                               ValueRepresentation vr,
-                               const std::vector<int64_t>& values)
-      ORTHANC_OVERRIDE;
-
-    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              const std::vector<double>& values)
-      ORTHANC_OVERRIDE;
-
-    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 const std::vector<DicomTag>& values)
-      ORTHANC_OVERRIDE;
-
-    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)
-      ORTHANC_OVERRIDE;
-  };
-}
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2676 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-#if !defined(DCMTK_VERSION_NUMBER)
-#  error The macro DCMTK_VERSION_NUMBER must be defined
-#endif
-
-#include "FromDcmtkBridge.h"
-#include "ToDcmtkBridge.h"
-#include "../Compatibility.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/dcistrmb.h>
-#include <dcmtk/dcmdata/dcostrmb.h>
-#include <dcmtk/dcmdata/dcpixel.h>
-#include <dcmtk/dcmdata/dcuid.h>
-#include <dcmtk/dcmdata/dcxfer.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_VERSION_NUMBER >= 361
-#  include <dcmtk/dcmdata/dcvruc.h>
-#  include <dcmtk/dcmdata/dcvrur.h>
-#endif
-
-#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-#  include <OrthancFrameworkResources.h>
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-#  include <dcmtk/dcmjpeg/djdecode.h>
-#  if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-#    include <dcmtk/dcmjpeg/djencode.h>
-#  endif
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
-#  include <dcmtk/dcmjpls/djdecode.h>
-#  if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-#    include <dcmtk/dcmjpls/djencode.h>
-#  endif
-#endif
-
-
-#include <dcmtk/dcmdata/dcrledrg.h>
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-#  include <dcmtk/dcmdata/dcrleerg.h>
-#  include <dcmtk/dcmimage/diregist.h>  // include to support color images
-#endif
-
-
-namespace Orthanc
-{
-  static bool IsBinaryTag(const DcmTag& key)
-  {
-    return (key.isUnknownVR() ||
-            key.getEVR() == EVR_OB ||
-            key.getEVR() == EVR_OW ||
-            key.getEVR() == EVR_UN ||
-            key.getEVR() == EVR_ox);
-  }
-
-
-#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
-  static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
-                                     FrameworkResources::FileResourceId resource)
-  {
-    std::string content;
-    FrameworkResources::GetFileResource(content, resource);
-
-#if ORTHANC_SANDBOXED == 0
-    TemporaryFile tmp;
-    tmp.Write(content);
-
-    if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot read embedded dictionary. Under Windows, make sure that " 
-                             "your TEMP directory does not contain special characters.");
-    }
-#else
-    if (!dictionary.loadFromMemory(content))
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot read embedded dictionary. Under Windows, make sure that " 
-                             "your TEMP directory does not contain special characters.");
-    }
-#endif
-  }
-#endif
-
-
-  namespace
-  {
-    class DictionaryLocker : public boost::noncopyable
-    {
-    private:
-      DcmDataDictionary& dictionary_;
-
-    public:
-      DictionaryLocker() : dictionary_(dcmDataDict.wrlock())
-      {
-      }
-
-      ~DictionaryLocker()
-      {
-#if DCMTK_VERSION_NUMBER >= 364
-        dcmDataDict.wrunlock();
-#else
-        dcmDataDict.unlock();
-#endif
-      }
-
-      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, FrameworkResources::DICTIONARY_DICONDE);
-
-      LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICOM);
-
-      if (loadPrivateDictionary)
-      {
-        LOG(INFO) << "Loading the embedded dictionary of private tags";
-        LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_PRIVATE);
-      }
-      else
-      {
-        LOG(INFO) << "The dictionary of private tags has not been loaded";
-      }
-
-#else
-      std::vector<std::string> dictionaries;
-      
-      const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
-      if (env != NULL)
-      {
-        // This mimics the behavior of DCMTK:
-        // https://support.dcmtk.org/docs/file_envvars.html
-#if defined(_WIN32)
-        Toolbox::TokenizeString(dictionaries, std::string(env), ';');
-#else
-        Toolbox::TokenizeString(dictionaries, std::string(env), ':');
-#endif
-      }
-      else
-      {
-        boost::filesystem::path base = DCMTK_DICTIONARY_DIR;
-        dictionaries.push_back((base / "dicom.dic").string());
-        dictionaries.push_back((base / "private.dic").string());
-      }
-
-      for (size_t i = 0; i < dictionaries.size(); i++)
-      {
-        LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
-        
-        if (!locker->loadDictionary(dictionaries[i].c_str()))
-        {
-          throw OrthancException(ErrorCode_InexistentFile);
-        }
-      }
-
-#endif
-    }
-
-    /* make sure data dictionary is loaded */
-    if (!dcmDataDict.isDictionaryLoaded())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "No DICOM dictionary loaded, check environment variable: " +
-                             std::string(DCM_DICT_ENVIRONMENT_VARIABLE));
-    }
-
-    {
-      // Test the dictionary with a simple DICOM tag
-      DcmTag key(0x0010, 0x1030); // This is PatientWeight
-      if (key.getEVR() != EVR_DS)
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "The DICOM dictionary has not been correctly read");
-      }
-    }
-  }
-
-
-  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::unique_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());
-        throw OrthancException(ErrorCode_ParameterOutOfRange, std::string(buf));
-      }
-
-      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()))
-      {
-        throw OrthancException(ErrorCode_AlreadyExistingTag,
-                               "Cannot register two tags with the same symbolic name \"" + name + "\"");
-      }
-
-      locker->addEntry(entry.release());
-    }
-  }
-
-
-  Encoding FromDcmtkBridge::DetectEncoding(bool& hasCodeExtensions,
-                                           DcmItem& dataset,
-                                           Encoding defaultEncoding)
-  {
-    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
-
-    OFString tmp;
-    if (dataset.findAndGetOFStringArray(DCM_SpecificCharacterSet, tmp).good())
-    {
-      std::vector<std::string> tokens;
-      Toolbox::TokenizeString(tokens, std::string(tmp.c_str()), '\\');
-
-      hasCodeExtensions = (tokens.size() > 1);
-
-      for (size_t i = 0; i < tokens.size(); i++)
-      {
-        std::string characterSet = Toolbox::StripSpaces(tokens[i]);
-
-        if (!characterSet.empty())
-        {
-          Encoding encoding;
-          
-          if (GetDicomEncoding(encoding, characterSet.c_str()))
-          {
-            // The specific character set is supported by the Orthanc core
-            return encoding;
-          }
-          else
-          {
-            LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
-                         << ", fallback to ASCII (remove all special characters)";
-            return Encoding_Ascii;
-          }
-        }
-      }
-    }
-    else
-    {
-      hasCodeExtensions = false;
-    }
-    
-    // No specific character set tag: Use the default encoding
-    return defaultEncoding;
-  }
-
-
-  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, 
-                                            DcmItem& dataset,
-                                            unsigned int maxStringLength,
-                                            Encoding defaultEncoding,
-                                            const std::set<DicomTag>& ignoreTagLength)
-  {
-    bool hasCodeExtensions;
-    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
-
-    target.Clear();
-    for (unsigned long i = 0; i < dataset.card(); i++)
-    {
-      DcmElement* element = dataset.getElement(i);
-      if (element && element->isLeaf())
-      {
-        target.SetValueInternal(element->getTag().getGTag(),
-                                element->getTag().getETag(),
-                                ConvertLeafElement(*element, DicomToJsonFlags_Default,
-                                                   maxStringLength, encoding, hasCodeExtensions, 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,
-                                                  bool hasCodeExtensions,
-                                                  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, hasCodeExtensions);
-
-        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,
-                                      bool hasCodeExtensions,
-                                      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::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
-                                  (element, flags, 0, encoding, hasCodeExtensions, 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, hasCodeExtensions, ignoreTagLength);
-      }
-    }
-  }
-
-
-  void FromDcmtkBridge::DatasetToJson(Json::Value& parent,
-                                      DcmItem& item,
-                                      DicomToJsonFormat format,
-                                      DicomToJsonFlags flags,
-                                      unsigned int maxStringLength,
-                                      Encoding encoding,
-                                      bool hasCodeExtensions,
-                                      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(), element->getTag().getPrivateCreator()) == NULL)
-        {
-          continue;
-        }
-      }
-
-      if (IsBinaryTag(element->getTag()))
-      {
-        // 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, hasCodeExtensions, ignoreTagLength);
-    }
-  }
-
-
-  void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, 
-                                           DcmDataset& dataset,
-                                           DicomToJsonFormat format,
-                                           DicomToJsonFlags flags,
-                                           unsigned int maxStringLength,
-                                           Encoding defaultEncoding,
-                                           const std::set<DicomTag>& ignoreTagLength)
-  {
-    bool hasCodeExtensions;
-    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
-
-    target = Json::objectValue;
-    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, 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, false, 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::Content::const_iterator 
-           it = values.content_.begin(); it != values.content_.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);
-    }
-  }
-
-
-  
-  static bool SaveToMemoryBufferInternal(std::string& buffer,
-                                         DcmFileFormat& dicom,
-                                         E_TransferSyntax xfer)
-  {
-    E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
-
-    // Create a memory buffer with the proper size
-    {
-      const uint32_t estimatedSize = dicom.calcElementLength(xfer, encodingType);  // (*)
-      buffer.resize(estimatedSize);
-    }
-
-    DcmOutputBufferStream ob(&buffer[0], buffer.size());
-
-    // Fill the memory buffer with the meta-header and the dataset
-    dicom.transferInit();
-    OFCondition c = dicom.write(ob, xfer, encodingType, NULL,
-                                /*opt_groupLength*/ EGL_recalcGL,
-                                /*opt_paddingType*/ EPD_noChange,
-                                /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
-                                EWM_updateMeta /* creates new SOP instance UID on lossy */);
-    dicom.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;
-    }
-  }
-  
-
-  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmDataset& dataSet)
-  {
-    // Determine the transfer syntax which shall be used to write the
-    // information to the file. If not possible, 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.getCurrentXfer();
-    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;
-    }
-
-    // Create the meta-header information
-    DcmFileFormat ff(&dataSet);
-    ff.validateMetaInfo(xfer);
-    ff.removeInvalidGroups();
-
-    return SaveToMemoryBufferInternal(buffer, ff, xfer);
-  }
-
-
-  bool FromDcmtkBridge::Transcode(DcmFileFormat& dicom,
-                                  DicomTransferSyntax syntax,
-                                  const DcmRepresentationParameter* representation)
-  {
-    E_TransferSyntax xfer;
-    if (!LookupDcmtkTransferSyntax(xfer, syntax))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      DicomTransferSyntax sourceSyntax;
-      bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom);
-      
-      if (!dicom.chooseRepresentation(xfer, representation).good() ||
-          !dicom.canWriteXfer(xfer) ||
-          !dicom.validateMetaInfo(xfer, EWM_updateMeta).good())
-      {
-        return false;
-      }
-      else
-      {
-        dicom.removeInvalidGroups();
-
-        if (known)
-        {
-          LOG(INFO) << "Transcoded an image from transfer syntax "
-                    << GetTransferSyntaxUid(sourceSyntax) << " to "
-                    << GetTransferSyntaxUid(syntax);
-        }
-        else
-        {
-          LOG(INFO) << "Transcoded an image from unknown transfer syntax to "
-                    << GetTransferSyntaxUid(syntax);
-        }
-        
-        return true;
-      }
-    }
-  }
-
-
-  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;
-
-#if DCMTK_VERSION_NUMBER >= 361
-        case EVR_OD:
-          return ValueRepresentation_OtherDouble;
-#endif
-
-      case EVR_OF:
-        return ValueRepresentation_OtherFloat;
-
-#if DCMTK_VERSION_NUMBER >= 362
-        case EVR_OL:
-          return ValueRepresentation_OtherLong;
-#endif
-
-      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;
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UC:
-        return ValueRepresentation_UnlimitedCharacters;
-#endif
-
-      case EVR_UI:
-        return ValueRepresentation_UniqueIdentifier;
-
-      case EVR_UL:
-        return ValueRepresentation_UnsignedLong;
-
-      case EVR_UN:
-        return ValueRepresentation_Unknown;
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UR:
-        return ValueRepresentation_UniversalResource;
-#endif
-
-      case EVR_US:
-        return ValueRepresentation_UnsignedShort;
-
-      case EVR_UT:
-        return ValueRepresentation_UnlimitedText;
-
-      default:
-        return ValueRepresentation_NotSupported;
-    }
-  }
-
-
-  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag,
-                                                   const std::string& privateCreator)
-  {
-    if (tag.IsPrivate() &&
-        privateCreator.empty())
-    {
-      // This solves issue 140 (Modifying private tags with REST API
-      // changes VR from LO to UN)
-      // https://bitbucket.org/sjodogne/orthanc/issues/140
-      LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format();
-    }
-    
-#if DCMTK_VERSION_NUMBER >= 362
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-    if (tag.IsPrivate())
-    {
-      return DcmItem::newDicomElement(key, privateCreator.c_str());
-    }
-    else
-    {
-      return DcmItem::newDicomElement(key, NULL);
-    }
-    
-#else
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-    if (tag.IsPrivate())
-    {
-      // https://forum.dcmtk.org/viewtopic.php?t=4527
-      LOG(WARNING) << "You are using DCMTK <= 3.6.1: All the private tags "
-        "are considered as having a binary value representation";
-      key.setPrivateCreator(privateCreator.c_str());
-      return new DcmOtherByteOtherWord(key);
-    }
-    else
-    {
-      return newDicomElement(key);
-    }
-#endif      
-  }
-
-
-
-  void FromDcmtkBridge::FillElementWithString(DcmElement& element,
-                                              const std::string& utf8Value,
-                                              bool decodeDataUriScheme,
-                                              Encoding dicomEncoding)
-  {
-    std::string binary;
-    const std::string* decoded = &utf8Value;
-
-    if (decodeDataUriScheme &&
-        boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
-    {
-      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;
-    }
-
-    if (IsBinaryTag(element.getTag()))
-    {
-      bool ok;
-
-      switch (element.getTag().getEVR())
-      {
-        case EVR_OW:
-          if (decoded->size() % sizeof(Uint16) != 0)
-          {
-            LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
-            ok = false;
-          }
-          else
-          {
-            ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good();
-          }
-          
-          break;
-      
-        default:
-          ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good();
-          break;
-      }
-      
-      if (ok)
-      {
-        return;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    bool ok = false;
-    
-    try
-    {
-      switch (element.getTag().getEVR())
-      {
-        // http://support.dcmtk.org/docs/dcvr_8h-source.html
-
-        /**
-         * TODO.
-         **/
-
-        case EVR_OB:  // other byte
-        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
-#if DCMTK_VERSION_NUMBER >= 361
-        case EVR_UC:  // unlimited characters
-        case EVR_UR:  // URI/URL
-#endif
-        {
-          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
-#if DCMTK_VERSION_NUMBER >= 362
-        case EVR_OL:  // other long (requires byte-swapping)
-#endif
-        {
-          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
-        case EVR_OF:  // other float (requires byte swapping)
-        {
-          ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
-          break;
-        }
-
-        case EVR_FD:  // float double-precision
-#if DCMTK_VERSION_NUMBER >= 361
-        case EVR_OD:  // other double (requires byte-swapping)
-#endif
-        {
-          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)
-    {
-      DicomTag tag(element.getTag().getGroup(), element.getTag().getElement());
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "While creating a DICOM instance, tag (" + tag.Format() +
-                             ") has out-of-range value: \"" + (*decoded) + "\"");
-    }
-  }
-
-
-  DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
-                                        const Json::Value& value,
-                                        bool decodeDataUriScheme,
-                                        Encoding dicomEncoding,
-                                        const std::string& privateCreator)
-  {
-    std::unique_ptr<DcmElement> element;
-
-    switch (value.type())
-    {
-      case Json::stringValue:
-        element.reset(CreateElementForTag(tag, privateCreator));
-        FillElementWithString(*element, value.asString(), decodeDataUriScheme, dicomEncoding);
-        break;
-
-      case Json::nullValue:
-        element.reset(CreateElementForTag(tag, privateCreator));
-        FillElementWithString(*element, "", decodeDataUriScheme, dicomEncoding);
-        break;
-
-      case Json::arrayValue:
-      {
-        const char* p = NULL;
-        if (tag.IsPrivate() &&
-            !privateCreator.empty())
-        {
-          p = privateCreator.c_str();
-        }
-        
-        DcmTag key(tag.GetGroup(), tag.GetElement(), p);
-        if (key.getEVR() != EVR_SQ)
-        {
-          throw OrthancException(ErrorCode_BadParameterType,
-                                 "Bad Parameter type for tag " + tag.Format());
-        }
-
-        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key);
-        element.reset(sequence);
-        
-        for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
-        {
-          std::unique_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, privateCreator));
-              }
-              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, "Bad Parameter type for tag " + tag.Format());
-    }
-
-    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);
-
-    E_TransferSyntax repType;
-    const DcmRepresentationParameter *repParam = NULL;
-    pixelData.getCurrentRepresentationKey(repType, repParam);
-    
-    DcmPixelSequence* pixelSequence = NULL;
-    if (!pixelData.getEncapsulatedRepresentation(repType, repParam, 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())))
-        {
-          throw OrthancException(ErrorCode_BadRequest,
-                                 "Unknown encoding while creating DICOM from JSON: " +
-                                 value.toStyledString());
-        }
-
-        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,
-                                        const std::string& privateCreator)
-  {
-    std::unique_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::unique_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
-        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::unique_ptr<DcmFileFormat> result(new DcmFileFormat);
-
-    result->transferInit();
-
-    /**
-     * New in Orthanc 1.6.0: The "size" is given as an argument to the
-     * "read()" method. This can avoid huge memory consumption if
-     * parsing an invalid DICOM file, which can notably been observed
-     * by executing the integration test "test_upload_compressed" on
-     * valgrind running Orthanc.
-     **/
-    if (!result->read(is, EXS_Unknown, EGL_noChange, size).good())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Cannot parse an invalid DICOM file (size: " +
-                             boost::lexical_cast<std::string>(size) + " bytes)");
-    }
-
-    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,
-                                             bool hasSourceCodeExtensions,
-                                             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, hasSourceCodeExtensions);
-            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, hasSourceCodeExtensions, target);
-          }
-        }
-      }
-    }
-  }
-
-
-#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)
-    {
-      throw OrthancException(ErrorCode_LuaBadOutput,
-                             "Lua: The script must return a table");
-    }
-
-    Json::Value::Members members = output.getMemberNames();
-
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      if (output[members[i]].type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_LuaBadOutput,
-                               "Lua: The script must return a table "
-                               "mapping names of DICOM tags to strings");
-      }
-
-      DicomTag tag(ParseTag(members[i]));
-      target.SetValue(tag, output[members[i]].asString(), false);
-    }
-  }
-#endif
-
-
-  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, 
-                                            DcmItem& dataset,
-                                            const std::set<DicomTag>& ignoreTagLength)
-  {
-    ExtractDicomSummary(target, dataset,
-                        ORTHANC_MAXIMUM_TAG_LENGTH,
-                        GetDefaultDicomEncoding(), ignoreTagLength);
-  }
-
-  
-  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();
-# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DJLSEncoderRegistration::registerCodecs();
-# endif
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    LOG(INFO) << "Registering JPEG codecs in DCMTK";
-    DJDecoderRegistration::registerCodecs(); 
-# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DJEncoderRegistration::registerCodecs();
-# endif
-#endif
-
-    LOG(INFO) << "Registering RLE codecs in DCMTK";
-    DcmRLEDecoderRegistration::registerCodecs(); 
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DcmRLEEncoderRegistration::registerCodecs();
-#endif
-  }
-
-
-  void FromDcmtkBridge::FinalizeCodecs()
-  {
-#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
-    // Unregister JPEG-LS codecs
-    DJLSDecoderRegistration::cleanup();
-# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DJLSEncoderRegistration::cleanup();
-# endif
-#endif
-
-#if ORTHANC_ENABLE_DCMTK_JPEG == 1
-    // Unregister JPEG codecs
-    DJDecoderRegistration::cleanup();
-# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DJEncoderRegistration::cleanup();
-# endif
-#endif
-
-    DcmRLEDecoderRegistration::cleanup(); 
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-    DcmRLEEncoderRegistration::cleanup();
-#endif
-  }
-
-
-
-  // Forward declaration
-  static void ApplyVisitorToElement(DcmElement& element,
-                                    ITagVisitor& visitor,
-                                    const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    Encoding encoding,
-                                    bool hasCodeExtensions);
- 
-  static void ApplyVisitorToDataset(DcmItem& dataset,
-                                    ITagVisitor& visitor,
-                                    const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    Encoding encoding,
-                                    bool hasCodeExtensions)
-  {
-    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, hasCodeExtensions);
-      }      
-    }
-  }
-
-
-  static void ApplyVisitorToLeaf(DcmElement& element,
-                                 ITagVisitor& visitor,
-                                 const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 Encoding encoding,
-                                 bool hasCodeExtensions)
-  {
-    // TODO - Merge this function, that is more recent, with ConvertLeafElement()
-
-    assert(element.isLeaf());
-
-    DcmEVR evr = element.getTag().getEVR();
-
-    
-    /**
-     * Fix the EVR for types internal to DCMTK 
-     **/
-
-    if (evr == EVR_ox)  // OB or OW depending on context
-    {
-      evr = EVR_OB;
-    }
-
-    if (evr == EVR_UNKNOWN ||  // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
-        evr == EVR_UNKNOWN2B)  // used internally for elements with unknown VR with 2-byte length field in explicit VR
-    {
-      evr = EVR_UN;
-    }
-
-    const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
-
-    
-    /**
-     * Deal with binary data (including PixelData).
-     **/
-
-    if (evr == EVR_OB ||  // other byte
-        evr == EVR_OW ||  // other word
-        evr == EVR_UN)    // unknown value representation
-    {
-      Uint16* data16 = NULL;
-      Uint8* data = NULL;
-
-      if (evr == EVR_OW &&
-          element.getUint16Array(data16) == EC_Normal)
-      {
-        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
-      }
-      else if (evr != EVR_OW &&
-               element.getUint8Array(data) == EC_Normal)
-      {
-        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
-      }
-      else
-      {
-        visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
-      }
-
-      return;  // We're done
-    }
-
-
-    /**
-     * Deal with plain strings (and convert them to UTF-8)
-     **/
-
-    char *c = NULL;
-    if (element.isaString() &&
-        element.getString(c).good())
-    {
-      std::string utf8;
-
-      if (c != NULL)  // This case corresponds to the empty string
-      {
-        if (element.getTag() == DCM_SpecificCharacterSet)
-        {
-          utf8.assign(c);
-        }
-        else
-        {
-          std::string s(c);
-          utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
-        }
-      }
-
-      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)
-          {
-            throw OrthancException(ErrorCode_InternalError,
-                                   "Cannot replace value of tag: " + tag.Format());
-          }
-
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      return;  // We're done
-    }
-
-
-    try
-    {
-      // http://support.dcmtk.org/docs/dcvr_8h-source.html
-      switch (evr)
-      {
-
-        /**
-         * Plain string values.
-         **/
-
-        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
-        {
-          Uint8* data = NULL;
-
-          if (element.getUint8Array(data) == EC_Normal)
-          {
-            const Uint32 length = element.getLength();
-            Uint32 l = 0;
-            while (l < length &&
-                   data[l] != 0)
-            {
-              l++;
-            }
-
-            if (l == length)
-            {
-              // Not a null-terminated plain string
-              visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
-            }
-            else
-            {
-              std::string ignored;
-              std::string s(reinterpret_cast<const char*>(data), l);
-              ITagVisitor::Action action = visitor.VisitString
-                (ignored, parentTags, parentIndexes, tag, vr,
-                 Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
-
-              if (action != ITagVisitor::Action_None)
-              {
-                LOG(WARNING) << "Cannot replace this string tag: "
-                             << FromDcmtkBridge::GetTagName(element)
-                             << " (" << tag.Format() << ")";
-              }
-            }
-          }
-          else
-          {
-            visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
-          }
-
-          return;
-        }
-    
-        /**
-         * Numeric types
-         **/ 
-      
-        case EVR_SL:  // signed long
-        {
-          DcmSignedLong& content = dynamic_cast<DcmSignedLong&>(element);
-
-          std::vector<int64_t> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            Sint32 f;
-            if (content.getSint32(f, i).good())
-            {
-              values.push_back(f);
-            }
-          }
-
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
-          break;
-        }
-
-        case EVR_SS:  // signed short
-        {
-          DcmSignedShort& content = dynamic_cast<DcmSignedShort&>(element);
-
-          std::vector<int64_t> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            Sint16 f;
-            if (content.getSint16(f, i).good())
-            {
-              values.push_back(f);
-            }
-          }
-
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
-          break;
-        }
-
-        case EVR_UL:  // unsigned long
-#if DCMTK_VERSION_NUMBER >= 362
-        case EVR_OL:
-#endif
-        {
-          DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
-
-          std::vector<int64_t> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            Uint32 f;
-            if (content.getUint32(f, i).good())
-            {
-              values.push_back(f);
-            }
-          }
-
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
-          break;
-        }
-
-        case EVR_US:  // unsigned short
-        {
-          DcmUnsignedShort& content = dynamic_cast<DcmUnsignedShort&>(element);
-
-          std::vector<int64_t> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            Uint16 f;
-            if (content.getUint16(f, i).good())
-            {
-              values.push_back(f);
-            }
-          }
-
-          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
-          break;
-        }
-
-        case EVR_FL:  // float single-precision
-        case EVR_OF:
-        {
-          DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
-
-          std::vector<double> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            Float32 f;
-            if (content.getFloat32(f, i).good())
-            {
-              values.push_back(f);
-            }
-          }
-
-          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
-          break;
-        }
-
-        case EVR_FD:  // float double-precision
-#if DCMTK_VERSION_NUMBER >= 361
-        case EVR_OD:
-#endif
-        {
-          DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
-
-          std::vector<double> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            Float64 f;
-            if (content.getFloat64(f, i).good())
-            {
-              values.push_back(f);
-            }
-          }
-
-          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
-          break;
-        }
-
-
-        /**
-         * Attribute tag.
-         **/
-
-        case EVR_AT:
-        {
-          DcmAttributeTag& content = dynamic_cast<DcmAttributeTag&>(element);
-
-          std::vector<DicomTag> values;
-          values.reserve(content.getVM());
-
-          for (unsigned long i = 0; i < content.getVM(); i++)
-          {
-            DcmTagKey f;
-            if (content.getTagVal(f, i).good())
-            {
-              DicomTag t(f.getGroup(), f.getElement());
-              values.push_back(t);
-            }
-          }
-
-          assert(vr == ValueRepresentation_AttributeTag);
-          visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
-          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.VisitNotSupported(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,
-                                    bool hasCodeExtensions)
-  {
-    assert(parentTags.size() == parentIndexes.size());
-
-    DicomTag tag(FromDcmtkBridge::Convert(element.getTag()));
-
-    if (element.isLeaf())
-    {
-      ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
-    }
-    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);
-
-      if (sequence.card() == 0)
-      {
-        visitor.VisitEmptySequence(parentTags, parentIndexes, tag);
-      }
-      else
-      {
-        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, hasCodeExtensions);
-        }
-      }
-    }
-  }
-
-
-  void FromDcmtkBridge::Apply(DcmItem& dataset,
-                              ITagVisitor& visitor,
-                              Encoding defaultEncoding)
-  {
-    std::vector<DicomTag> parentTags;
-    std::vector<size_t> parentIndexes;
-    bool hasCodeExtensions;
-    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
-    ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
-  }
-
-
-
-  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
-                                                    DcmFileFormat& dicom)
-  {
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-        
-    DcmDataset& dataset = *dicom.getDataset();
-
-    E_TransferSyntax xfer = dataset.getCurrentXfer();
-    if (xfer == EXS_Unknown)
-    {
-      dataset.updateOriginalXfer();
-      xfer = dataset.getOriginalXfer();
-      if (xfer == EXS_Unknown)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Cannot determine the transfer syntax of the DICOM instance");
-      }
-    }
-
-    return FromDcmtkBridge::LookupOrthancTransferSyntax(target, xfer);
-  }
-}
-
-
-#include "./FromDcmtkBridge_TransferSyntaxes.impl.h"
--- a/Core/DicomParsing/FromDcmtkBridge.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,284 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 ORTHANC_PUBLIC 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,
-                                    const std::set<DicomTag>& ignoreTagLength);
-
-    static void DatasetToJson(Json::Value& parent,
-                              DcmItem& item,
-                              DicomToJsonFormat format,
-                              DicomToJsonFlags flags,
-                              unsigned int maxStringLength,
-                              Encoding encoding,
-                              bool hasCodeExtensions,
-                              const std::set<DicomTag>& ignoreTagLength);
-
-    static void ElementToJson(Json::Value& parent,
-                              DcmElement& element,
-                              DicomToJsonFormat format,
-                              DicomToJsonFlags flags,
-                              unsigned int maxStringLength,
-                              Encoding dicomEncoding,
-                              bool hasCodeExtensions,
-                              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,
-                                     bool hasSourceCodeExtensions,
-                                     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(bool& hasCodeExtensions,
-                                   DcmItem& dataset,
-                                   Encoding defaultEncoding);
-
-    static Encoding DetectEncoding(DcmItem& dataset,
-                                   Encoding defaultEncoding)
-    {
-      // Compatibility wrapper for Orthanc <= 1.5.4
-      bool hasCodeExtensions;  // ignored
-      return DetectEncoding(hasCodeExtensions, dataset, 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,
-                                          bool hasCodeExtensions,
-                                          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)
-    {
-      const DicomTag tag = ParseTag(tagName);
-      target.SetValueInternal(tag.GetGroup(), tag.GetElement(), 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 bool Transcode(DcmFileFormat& dicom,
-                          DicomTransferSyntax syntax,
-                          const DcmRepresentationParameter* representation);
-
-    static ValueRepresentation Convert(DcmEVR vr);
-
-    static ValueRepresentation LookupValueRepresentation(const DicomTag& tag);
-
-    static DcmElement* CreateElementForTag(const DicomTag& tag,
-                                           const std::string& privateCreator);
-    
-    static void FillElementWithString(DcmElement& element,
-                                      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,
-                                const std::string& privateCreator);
-
-    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,
-                                const std::string& privateCreator);
-
-    static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer,
-                                               size_t size);
-
-    static void FromJson(DicomMap& values,
-                         const Json::Value& result);
-
-#if ORTHANC_ENABLE_LUA == 1
-    static void ExecuteToDicom(DicomMap& target,
-                               LuaFunctionCall& call);
-#endif
-
-    static void ExtractDicomSummary(DicomMap& target, 
-                                    DcmItem& dataset,
-                                    const std::set<DicomTag>& ignoreTagLength);
-
-    static void ExtractDicomSummary(DicomMap& target, 
-                                    DcmItem& dataset)
-    {
-      std::set<DicomTag> none;
-      ExtractDicomSummary(target, dataset, none);
-    }
-
-    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);
-
-    static bool LookupDcmtkTransferSyntax(E_TransferSyntax& target,
-                                          DicomTransferSyntax source);
-
-    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
-                                            E_TransferSyntax source);
-
-    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
-                                            DcmFileFormat& dicom);
-  };
-}
--- a/Core/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,549 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
-
-namespace Orthanc
-{
-  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
-                                                  DicomTransferSyntax source)
-  {
-    switch (source)
-    {
-      case DicomTransferSyntax_LittleEndianImplicit:
-        target = EXS_LittleEndianImplicit;
-        return true;
-
-      case DicomTransferSyntax_LittleEndianExplicit:
-        target = EXS_LittleEndianExplicit;
-        return true;
-
-      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
-        target = EXS_DeflatedLittleEndianExplicit;
-        return true;
-
-      case DicomTransferSyntax_BigEndianExplicit:
-        target = EXS_BigEndianExplicit;
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess1:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess1TransferSyntax;
-#  else
-        target = EXS_JPEGProcess1;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess2_4:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess2_4TransferSyntax;
-#  else
-        target = EXS_JPEGProcess2_4;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess3_5:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess3_5TransferSyntax;
-#  else
-        target = EXS_JPEGProcess3_5;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess6_8:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess6_8TransferSyntax;
-#  else
-        target = EXS_JPEGProcess6_8;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess7_9:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess7_9TransferSyntax;
-#  else
-        target = EXS_JPEGProcess7_9;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess10_12:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess10_12TransferSyntax;
-#  else
-        target = EXS_JPEGProcess10_12;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess11_13:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess11_13TransferSyntax;
-#  else
-        target = EXS_JPEGProcess11_13;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess14:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess14TransferSyntax;
-#  else
-        target = EXS_JPEGProcess14;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess15:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess15TransferSyntax;
-#  else
-        target = EXS_JPEGProcess15;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess16_18:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess16_18TransferSyntax;
-#  else
-        target = EXS_JPEGProcess16_18;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess17_19:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess17_19TransferSyntax;
-#  else
-        target = EXS_JPEGProcess17_19;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess20_22:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess20_22TransferSyntax;
-#  else
-        target = EXS_JPEGProcess20_22;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess21_23:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess21_23TransferSyntax;
-#  else
-        target = EXS_JPEGProcess21_23;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess24_26:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess24_26TransferSyntax;
-#  else
-        target = EXS_JPEGProcess24_26;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess25_27:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess25_27TransferSyntax;
-#  else
-        target = EXS_JPEGProcess25_27;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess28:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess28TransferSyntax;
-#  else
-        target = EXS_JPEGProcess28;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess29:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess29TransferSyntax;
-#  else
-        target = EXS_JPEGProcess29;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess14SV1:
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = EXS_JPEGProcess14SV1TransferSyntax;
-#  else
-        target = EXS_JPEGProcess14SV1;
-#  endif
-        return true;
-
-      case DicomTransferSyntax_JPEGLSLossless:
-        target = EXS_JPEGLSLossless;
-        return true;
-
-      case DicomTransferSyntax_JPEGLSLossy:
-        target = EXS_JPEGLSLossy;
-        return true;
-
-      case DicomTransferSyntax_JPEG2000LosslessOnly:
-        target = EXS_JPEG2000LosslessOnly;
-        return true;
-
-      case DicomTransferSyntax_JPEG2000:
-        target = EXS_JPEG2000;
-        return true;
-
-      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
-        target = EXS_JPEG2000MulticomponentLosslessOnly;
-        return true;
-
-      case DicomTransferSyntax_JPEG2000Multicomponent:
-        target = EXS_JPEG2000Multicomponent;
-        return true;
-
-      case DicomTransferSyntax_JPIPReferenced:
-        target = EXS_JPIPReferenced;
-        return true;
-
-      case DicomTransferSyntax_JPIPReferencedDeflate:
-        target = EXS_JPIPReferencedDeflate;
-        return true;
-
-      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
-        target = EXS_MPEG2MainProfileAtMainLevel;
-        return true;
-
-      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
-        target = EXS_MPEG2MainProfileAtHighLevel;
-        return true;
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
-        target = EXS_MPEG4HighProfileLevel4_1;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
-        target = EXS_MPEG4BDcompatibleHighProfileLevel4_1;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
-        target = EXS_MPEG4HighProfileLevel4_2_For2DVideo;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
-        target = EXS_MPEG4HighProfileLevel4_2_For3DVideo;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
-        target = EXS_MPEG4StereoHighProfileLevel4_2;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
-        target = EXS_HEVCMainProfileLevel5_1;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
-        target = EXS_HEVCMain10ProfileLevel5_1;
-        return true;
-#endif
-
-      case DicomTransferSyntax_RLELossless:
-        target = EXS_RLELossless;
-        return true;
-
-      default:
-        return false;
-    }
-  }
-  
-
-  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
-                                                    E_TransferSyntax source)
-  {
-    switch (source)
-    {
-      case EXS_LittleEndianImplicit:
-        target = DicomTransferSyntax_LittleEndianImplicit;
-        return true;
-
-      case EXS_LittleEndianExplicit:
-        target = DicomTransferSyntax_LittleEndianExplicit;
-        return true;
-
-      case EXS_DeflatedLittleEndianExplicit:
-        target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-        return true;
-
-      case EXS_BigEndianExplicit:
-        target = DicomTransferSyntax_BigEndianExplicit;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess1TransferSyntax:
-#  else
-      case EXS_JPEGProcess1:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess1;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess2_4TransferSyntax:
-#  else
-      case EXS_JPEGProcess2_4:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess2_4;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess3_5TransferSyntax:
-#  else
-      case EXS_JPEGProcess3_5:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess3_5;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess6_8TransferSyntax:
-#  else
-      case EXS_JPEGProcess6_8:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess6_8;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess7_9TransferSyntax:
-#  else
-      case EXS_JPEGProcess7_9:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess7_9;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess10_12TransferSyntax:
-#  else
-      case EXS_JPEGProcess10_12:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess10_12;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess11_13TransferSyntax:
-#  else
-      case EXS_JPEGProcess11_13:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess11_13;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess14TransferSyntax:
-#  else
-      case EXS_JPEGProcess14:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess14;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess15TransferSyntax:
-#  else
-      case EXS_JPEGProcess15:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess15;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess16_18TransferSyntax:
-#  else
-      case EXS_JPEGProcess16_18:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess16_18;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess17_19TransferSyntax:
-#  else
-      case EXS_JPEGProcess17_19:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess17_19;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess20_22TransferSyntax:
-#  else
-      case EXS_JPEGProcess20_22:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess20_22;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess21_23TransferSyntax:
-#  else
-      case EXS_JPEGProcess21_23:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess21_23;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess24_26TransferSyntax:
-#  else
-      case EXS_JPEGProcess24_26:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess24_26;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess25_27TransferSyntax:
-#  else
-      case EXS_JPEGProcess25_27:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess25_27;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess28TransferSyntax:
-#  else
-      case EXS_JPEGProcess28:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess28;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess29TransferSyntax:
-#  else
-      case EXS_JPEGProcess29:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess29;
-        return true;
-
-#  if DCMTK_VERSION_NUMBER <= 360
-      case EXS_JPEGProcess14SV1TransferSyntax:
-#  else
-      case EXS_JPEGProcess14SV1:
-#  endif
-        target = DicomTransferSyntax_JPEGProcess14SV1;
-        return true;
-
-      case EXS_JPEGLSLossless:
-        target = DicomTransferSyntax_JPEGLSLossless;
-        return true;
-
-      case EXS_JPEGLSLossy:
-        target = DicomTransferSyntax_JPEGLSLossy;
-        return true;
-
-      case EXS_JPEG2000LosslessOnly:
-        target = DicomTransferSyntax_JPEG2000LosslessOnly;
-        return true;
-
-      case EXS_JPEG2000:
-        target = DicomTransferSyntax_JPEG2000;
-        return true;
-
-      case EXS_JPEG2000MulticomponentLosslessOnly:
-        target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
-        return true;
-
-      case EXS_JPEG2000Multicomponent:
-        target = DicomTransferSyntax_JPEG2000Multicomponent;
-        return true;
-
-      case EXS_JPIPReferenced:
-        target = DicomTransferSyntax_JPIPReferenced;
-        return true;
-
-      case EXS_JPIPReferencedDeflate:
-        target = DicomTransferSyntax_JPIPReferencedDeflate;
-        return true;
-
-      case EXS_MPEG2MainProfileAtMainLevel:
-        target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
-        return true;
-
-      case EXS_MPEG2MainProfileAtHighLevel:
-        target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
-        return true;
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EXS_MPEG4HighProfileLevel4_1:
-        target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EXS_MPEG4BDcompatibleHighProfileLevel4_1:
-        target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EXS_MPEG4HighProfileLevel4_2_For2DVideo:
-        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EXS_MPEG4HighProfileLevel4_2_For3DVideo:
-        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EXS_MPEG4StereoHighProfileLevel4_2:
-        target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case EXS_HEVCMainProfileLevel5_1:
-        target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
-        return true;
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case EXS_HEVCMain10ProfileLevel5_1:
-        target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
-        return true;
-#endif
-
-      case EXS_RLELossless:
-        target = DicomTransferSyntax_RLELossless;
-        return true;
-
-      default:
-        return false;
-    }
-  }
-}
--- a/Core/DicomParsing/IDicomTranscoder.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,438 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IDicomTranscoder.h"
-
-#include "../OrthancException.h"
-#include "FromDcmtkBridge.h"
-#include "ParsedDicomFile.h"
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-namespace Orthanc
-{
-  IDicomTranscoder::TranscodingType IDicomTranscoder::GetTranscodingType(DicomTransferSyntax target,
-                                                                         DicomTransferSyntax source)
-  {
-    if (target == source)
-    {
-      return TranscodingType_Lossless;
-    }
-    else if (target == DicomTransferSyntax_LittleEndianImplicit ||
-             target == DicomTransferSyntax_LittleEndianExplicit ||
-             target == DicomTransferSyntax_BigEndianExplicit ||
-             target == DicomTransferSyntax_DeflatedLittleEndianExplicit ||
-             target == DicomTransferSyntax_JPEGProcess14 ||
-             target == DicomTransferSyntax_JPEGProcess14SV1 ||
-             target == DicomTransferSyntax_JPEGLSLossless ||
-             target == DicomTransferSyntax_JPEG2000LosslessOnly ||
-             target == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly)
-    {
-      return TranscodingType_Lossless;
-    }
-    else if (target == DicomTransferSyntax_JPEGProcess1 ||
-             target == DicomTransferSyntax_JPEGProcess2_4 ||
-             target == DicomTransferSyntax_JPEGLSLossy ||
-             target == DicomTransferSyntax_JPEG2000 ||
-             target == DicomTransferSyntax_JPEG2000Multicomponent)
-    {
-      return TranscodingType_Lossy;
-    }
-    else
-    {
-      return TranscodingType_Unknown;
-    }
-  }
-
-
-  std::string IDicomTranscoder::GetSopInstanceUid(DcmFileFormat& dicom)
-  {
-    if (dicom.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    DcmDataset& dataset = *dicom.getDataset();
-    
-    const char* v = NULL;
-
-    if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() &&
-        v != NULL)
-    {
-      return std::string(v);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID");
-    }
-  }
-
-
-  void IDicomTranscoder::CheckTranscoding(IDicomTranscoder::DicomImage& transcoded,
-                                          DicomTransferSyntax sourceSyntax,
-                                          const std::string& sourceSopInstanceUid,
-                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                          bool allowNewSopInstanceUid)
-  {
-    DcmFileFormat& parsed = transcoded.GetParsed();
-    
-    if (parsed.getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    std::string targetSopInstanceUid = GetSopInstanceUid(parsed);
-
-    if (parsed.getDataset()->tagExists(DCM_PixelData))
-    {
-      if (!allowNewSopInstanceUid && (targetSopInstanceUid != sourceSopInstanceUid))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-    else
-    {
-      if (targetSopInstanceUid != sourceSopInstanceUid)
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "No pixel data: Transcoding must not change the SOP instance UID");
-      }
-    }
-
-    DicomTransferSyntax targetSyntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, parsed))
-    {
-      return;  // Unknown transfer syntax, cannot do further test
-    }
-
-    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
-    {
-      // No transcoding should have happened
-      if (targetSopInstanceUid != sourceSopInstanceUid)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-        
-    if (allowedSyntaxes.find(targetSyntax) == allowedSyntaxes.end())
-    {
-      throw OrthancException(ErrorCode_InternalError, "An incorrect output transfer syntax was chosen");
-    }
-    
-    if (parsed.getDataset()->tagExists(DCM_PixelData))
-    {
-      switch (GetTranscodingType(targetSyntax, sourceSyntax))
-      {
-        case TranscodingType_Lossy:
-          if (targetSopInstanceUid == sourceSopInstanceUid)
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-          break;
-
-        case TranscodingType_Lossless:
-          if (targetSopInstanceUid != sourceSopInstanceUid)
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-          break;
-
-        default:
-          break;
-      }
-    }
-  }
-    
-
-  void IDicomTranscoder::DicomImage::Parse()
-  {
-    if (parsed_.get() != NULL)
-    {
-      // Already parsed
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else if (buffer_.get() != NULL)
-    {
-      if (isExternalBuffer_)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-      else
-      {
-        parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(
-                        buffer_->empty() ? NULL : buffer_->c_str(), buffer_->size()));
-        
-        if (parsed_.get() == NULL)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat);
-        }      
-      }
-    }
-    else if (isExternalBuffer_)
-    {
-      parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(externalBuffer_, externalSize_));
-      
-      if (parsed_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }      
-    }
-    else
-    {
-      // No buffer is available
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-  
-  
-  void IDicomTranscoder::DicomImage::Serialize()
-  {
-    if (parsed_.get() == NULL ||
-        buffer_.get() != NULL ||
-        isExternalBuffer_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else if (parsed_->getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      buffer_.reset(new std::string);
-      FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, *parsed_->getDataset());
-    }
-  }
-
-  
-  IDicomTranscoder::DicomImage::DicomImage() :
-    isExternalBuffer_(false)
-  {
-  }
-
-
-  void IDicomTranscoder::DicomImage::Clear()
-  {
-    parsed_.reset(NULL);
-    buffer_.reset(NULL);
-    isExternalBuffer_ = false;
-  }
-
-  
-  void IDicomTranscoder::DicomImage::AcquireParsed(ParsedDicomFile& parsed)
-  {
-    AcquireParsed(parsed.ReleaseDcmtkObject());
-  }
-  
-      
-  void IDicomTranscoder::DicomImage::AcquireParsed(DcmFileFormat* parsed)
-  {
-    if (parsed == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else if (parsed->getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else if (parsed_.get() != NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parsed_.reset(parsed);
-    }
-  }
-  
-
-  void IDicomTranscoder::DicomImage::AcquireParsed(DicomImage& other)
-  {
-    AcquireParsed(other.ReleaseParsed());
-  }
-  
-
-  void IDicomTranscoder::DicomImage::AcquireBuffer(std::string& buffer /* will be swapped */)
-  {
-    if (buffer_.get() != NULL ||
-        isExternalBuffer_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      buffer_.reset(new std::string);
-      buffer_->swap(buffer);
-    }
-  }
-
-
-  void IDicomTranscoder::DicomImage::AcquireBuffer(DicomImage& other)
-  {
-    if (buffer_.get() != NULL ||
-        isExternalBuffer_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else if (other.isExternalBuffer_)
-    {
-      assert(other.buffer_.get() == NULL);
-      isExternalBuffer_ = true;
-      externalBuffer_ = other.externalBuffer_;
-      externalSize_ = other.externalSize_;
-    }
-    else if (other.buffer_.get() != NULL)
-    {
-      buffer_.reset(other.buffer_.release());
-    }
-    else
-    {
-      buffer_.reset(NULL);
-    }    
-  }
-
-  
-  void IDicomTranscoder::DicomImage::SetExternalBuffer(const void* buffer,
-                                                       size_t size)
-  {
-    if (buffer_.get() != NULL ||
-        isExternalBuffer_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      isExternalBuffer_ = true;
-      externalBuffer_ = buffer;
-      externalSize_ = size;
-    }
-  }
-
-
-  void IDicomTranscoder::DicomImage::SetExternalBuffer(const std::string& buffer)
-  {
-    SetExternalBuffer(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
-  }
-
-
-  DcmFileFormat& IDicomTranscoder::DicomImage::GetParsed()
-  {
-    if (parsed_.get() != NULL)
-    {
-      return *parsed_;
-    }
-    else if (buffer_.get() != NULL ||
-             isExternalBuffer_)
-    {
-      Parse();
-      return *parsed_;
-    }
-    else
-    {
-      throw OrthancException(
-        ErrorCode_BadSequenceOfCalls,
-        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
-    }
-  }
-  
-
-  DcmFileFormat* IDicomTranscoder::DicomImage::ReleaseParsed()
-  {
-    if (parsed_.get() != NULL)
-    {
-      buffer_.reset(NULL);
-      return parsed_.release();
-    }
-    else if (buffer_.get() != NULL ||
-             isExternalBuffer_)
-    {
-      Parse();
-      buffer_.reset(NULL);
-      return parsed_.release();
-    }
-    else
-    {
-      throw OrthancException(
-        ErrorCode_BadSequenceOfCalls,
-        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
-    }
-  }
-
-
-  ParsedDicomFile* IDicomTranscoder::DicomImage::ReleaseAsParsedDicomFile()
-  {
-    return ParsedDicomFile::AcquireDcmtkObject(ReleaseParsed());
-  }
-
-  
-  const void* IDicomTranscoder::DicomImage::GetBufferData()
-  {
-    if (isExternalBuffer_)
-    {
-      assert(buffer_.get() == NULL);
-      return externalBuffer_;
-    }
-    else
-    {    
-      if (buffer_.get() == NULL)
-      {
-        Serialize();
-      }
-
-      assert(buffer_.get() != NULL);
-      return buffer_->empty() ? NULL : buffer_->c_str();
-    }
-  }
-
-  
-  size_t IDicomTranscoder::DicomImage::GetBufferSize()
-  {
-    if (isExternalBuffer_)
-    {
-      assert(buffer_.get() == NULL);
-      return externalSize_;
-    }
-    else
-    {    
-      if (buffer_.get() == NULL)
-      {
-        Serialize();
-      }
-
-      assert(buffer_.get() != NULL);
-      return buffer_->size();
-    }
-  }
-}
--- a/Core/DicomParsing/IDicomTranscoder.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.h"
-#include "../Enumerations.h"
-
-#include <boost/noncopyable.hpp>
-#include <set>
-
-class DcmFileFormat;
-
-namespace Orthanc
-{
-  class ParsedDicomFile;
-  
-  /**
-   * WARNING: This class might be called from several threads at
-   * once. Make sure to implement proper locking.
-   **/
-  class ORTHANC_PUBLIC IDicomTranscoder : public boost::noncopyable
-  {
-  public:
-    class DicomImage : public boost::noncopyable
-    {
-    private:
-      std::unique_ptr<DcmFileFormat>  parsed_;
-      std::unique_ptr<std::string>    buffer_;
-      bool                            isExternalBuffer_;
-      const void*                     externalBuffer_;
-      size_t                          externalSize_;
-
-      void Parse();
-
-      void Serialize();
-
-      DcmFileFormat* ReleaseParsed();
-
-    public:
-      DicomImage();
-      
-      void Clear();
-      
-      // Calling this method will invalidate the "ParsedDicomFile" object
-      void AcquireParsed(ParsedDicomFile& parsed);
-      
-      void AcquireParsed(DcmFileFormat* parsed);
-
-      void AcquireParsed(DicomImage& other);
-
-      void AcquireBuffer(std::string& buffer /* will be swapped */);
-
-      void AcquireBuffer(DicomImage& other);
-
-      void SetExternalBuffer(const void* buffer,
-                             size_t size);
-
-      void SetExternalBuffer(const std::string& buffer);
-
-      DcmFileFormat& GetParsed();
-
-      ParsedDicomFile* ReleaseAsParsedDicomFile();
-
-      const void* GetBufferData();
-
-      size_t GetBufferSize();
-    };
-
-
-  protected:
-    enum TranscodingType
-    {
-      TranscodingType_Lossy,
-      TranscodingType_Lossless,
-      TranscodingType_Unknown
-    };
-
-    static TranscodingType GetTranscodingType(DicomTransferSyntax target,
-                                              DicomTransferSyntax source);
-
-    static void CheckTranscoding(DicomImage& transcoded,
-                                 DicomTransferSyntax sourceSyntax,
-                                 const std::string& sourceSopInstanceUid,
-                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                 bool allowNewSopInstanceUid);
-    
-  public:    
-    virtual ~IDicomTranscoder()
-    {
-    }
-
-    virtual bool Transcode(DicomImage& target,
-                           DicomImage& source /* in, "GetParsed()" possibly modified */,
-                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) = 0;
-
-    static std::string GetSopInstanceUid(DcmFileFormat& dicom);
-  };
-}
--- a/Core/DicomParsing/ITagVisitor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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()
-    {
-    }
-
-    // Visiting a DICOM element that is internal to DCMTK
-    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
-                                   const std::vector<size_t>& parentIndexes,
-                                   const DicomTag& tag,
-                                   ValueRepresentation vr) = 0;
-
-    // SQ
-    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
-                                    const std::vector<size_t>& parentIndexes,
-                                    const DicomTag& tag) = 0;
-
-    // SL, SS, UL, US
-    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
-                               const std::vector<size_t>& parentIndexes,
-                               const DicomTag& tag,
-                               ValueRepresentation vr,
-                               const std::vector<int64_t>& values) = 0;
-
-    // FL, FD, OD, OF
-    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
-                              const std::vector<size_t>& parentIndexes,
-                              const DicomTag& tag,
-                              ValueRepresentation vr,
-                              const std::vector<double>& values) = 0;
-
-    // AT
-    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
-                                 const std::vector<size_t>& parentIndexes,
-                                 const DicomTag& tag,
-                                 const std::vector<DicomTag>& values) = 0;
-
-    // OB, OL, OW, UN
-    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;
-
-    // Visiting an UTF-8 string
-    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;
-  };
-}
--- a/Core/DicomParsing/Internals/DicomFrameIndex.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,403 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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)
-      {
-        // Degenerate case: Empty offset table means only one frame
-        // that overlaps all the fragments
-        table.resize(1);
-        table[0] = 0;
-        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_);
-      }
-    }
-  };
-
-
-  unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom)
-  {
-    const char* tmp = NULL;
-    if (!dicom.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 static_cast<unsigned int>(count);
-    }
-  }
-
-
-  DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom)
-  {
-    countFrames_ = GetFramesCount(dicom);
-    if (countFrames_ == 0)
-    {
-      // The image has no frame. No index is to be built.
-      return;
-    }
-
-    // Test whether this image is composed of a sequence of fragments
-    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom);
-    if (pixelSequence != NULL)
-    {
-      index_.reset(new FragmentIndex(pixelSequence, countFrames_));
-      return;
-    }
-
-    // Extract information about the image structure
-    DicomMap tags;
-    FromDcmtkBridge::ExtractDicomSummary(tags, dicom);
-
-    DicomImageInformation information(tags);
-
-    // Access to the raw pixel data
-    if (DicomImageDecoder::IsPsmctRle1(dicom))
-    {
-      index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize()));
-    }
-    else
-    {
-      index_.reset(new UncompressedIndex(dicom, 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();
-    }
-  }
-}
--- a/Core/DicomParsing/Internals/DicomFrameIndex.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../Compatibility.h"
-#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::unique_ptr<IIndex>  index_;
-    unsigned int             countFrames_;
-
-  public:
-    DicomFrameIndex(DcmDataset& dicom);
-
-    unsigned int GetFramesCount() const
-    {
-      return countFrames_;
-    }
-
-    void GetRawFrame(std::string& frame,
-                     unsigned int index) const;
-
-    static unsigned int GetFramesCount(DcmDataset& dicom);
-  };
-}
--- a/Core/DicomParsing/Internals/DicomImageDecoder.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1042 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#include "../ParsedDicomFile.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"
-
-#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/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::unique_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)
-  {
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    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::unique_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::unique_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::unique_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())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Cannot decode a palette image");
-      }
-
-      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())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Cannot decode a non-palette image");
-      }
-
-      return target.release();
-    }
-  }
-
-
-  ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom,
-                                           unsigned int frame)
-  {
-    if (dicom.GetDcmtkObject().getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      return Decode(*dicom.GetDcmtkObject().getDataset(), frame);
-    }
-  }
-
-
-  ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset,
-                                           unsigned int frame)
-  {
-    E_TransferSyntax syntax = dataset.getCurrentXfer();
-
-    /**
-     * 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::unique_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::unique_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) << "Trying to decode a compressed image by transcoding it to Little Endian Explicit";
-
-      std::unique_ptr<DcmDataset> converted(dynamic_cast<DcmDataset*>(dataset.clone()));
-      converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
-
-      if (converted->canWriteXfer(EXS_LittleEndianExplicit))
-      {
-        return DecodeUncompressedImage(*converted, frame);
-      }
-    }
-
-    DicomTransferSyntax s;
-    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, dataset.getCurrentXfer()))
-    {
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "The built-in DCMTK decoder cannot decode some DICOM instance "
-                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)));
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "The built-in DCMTK decoder cannot decode some DICOM instance");
-    }
-  }
-
-
-  static bool IsColorImage(PixelFormat format)
-  {
-    return (format == PixelFormat_RGB24 ||
-            format == PixelFormat_RGBA32);
-  }
-
-
-  bool DicomImageDecoder::TruncateDecodedImage(std::unique_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::unique_ptr<ImageAccessor> target
-        (new Image(format, image->GetWidth(), image->GetHeight(), false));
-      ImageProcessing::Convert(*target, *image);
-
-#if __cplusplus < 201103L
-      image.reset(target.release());
-#else
-      image = std::move(target);
-#endif
-    }
-
-    return true;
-  }
-
-
-  bool DicomImageDecoder::PreviewDecodedImage(std::unique_ptr<ImageAccessor>& image)
-  {
-    switch (image->GetFormat())
-    {
-      case PixelFormat_RGB24:
-      {
-        // Directly return color images without modification (RGB)
-        return true;
-      }
-
-      case PixelFormat_RGB48:
-      {
-        std::unique_ptr<ImageAccessor> target
-          (new Image(PixelFormat_RGB24, image->GetWidth(), image->GetHeight(), false));
-        ImageProcessing::Convert(*target, *image);
-
-#if __cplusplus < 201103L
-        image.reset(target.release());
-#else
-        image = std::move(target);
-#endif
-
-        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::unique_ptr<ImageAccessor> target
-            (new Image(PixelFormat_Grayscale8, image->GetWidth(), image->GetHeight(), false));
-          ImageProcessing::Convert(*target, *image);
-
-#if __cplusplus < 201103L
-          image.reset(target.release());
-#else
-          image = std::move(target);
-#endif
-        }
-
-        return true;
-      }
-      
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void DicomImageDecoder::ApplyExtractionMode(std::unique_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::unique_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::unique_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::unique_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
-}
--- a/Core/DicomParsing/Internals/DicomImageDecoder.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../Compatibility.h"
-#include "../../Images/ImageAccessor.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 ParsedDicomFile;
-  
-  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::unique_ptr<ImageAccessor>& image,
-                                     PixelFormat format,
-                                     bool allowColorConversion);
-
-    static bool PreviewDecodedImage(std::unique_ptr<ImageAccessor>& image);
-
-    static void ApplyExtractionMode(std::unique_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 ImageAccessor *Decode(DcmDataset& dataset,
-                                 unsigned int frame);
-
-    static void ExtractPamImage(std::string& result,
-                                std::unique_ptr<ImageAccessor>& image,
-                                ImageExtractionMode mode,
-                                bool invert);
-
-#if ORTHANC_ENABLE_PNG == 1
-    static void ExtractPngImage(std::string& result,
-                                std::unique_ptr<ImageAccessor>& image,
-                                ImageExtractionMode mode,
-                                bool invert);
-#endif
-
-#if ORTHANC_ENABLE_JPEG == 1
-    static void ExtractJpegImage(std::string& result,
-                                 std::unique_ptr<ImageAccessor>& image,
-                                 ImageExtractionMode mode,
-                                 bool invert,
-                                 uint8_t quality);
-#endif
-  };
-}
--- a/Core/DicomParsing/MemoryBufferTranscoder.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MemoryBufferTranscoder.h"
-
-#include "../OrthancException.h"
-#include "FromDcmtkBridge.h"
-
-#if !defined(NDEBUG)  // For debugging
-#  include "ParsedDicomFile.h"
-#endif
-
-namespace Orthanc
-{
-  static void CheckTargetSyntax(const std::string& transcoded,
-                                const std::set<DicomTransferSyntax>& allowedSyntaxes)
-  {
-#if !defined(NDEBUG)
-    // Debug mode
-    ParsedDicomFile parsed(transcoded);
-
-    std::string s;
-    DicomTransferSyntax a, b;
-    if (!parsed.LookupTransferSyntax(s) ||
-        !FromDcmtkBridge::LookupOrthancTransferSyntax(a, parsed.GetDcmtkObject()) ||
-        !LookupTransferSyntax(b, s) ||
-        a != b ||
-        allowedSyntaxes.find(a) == allowedSyntaxes.end())
-    {
-      throw OrthancException(
-        ErrorCode_Plugin,
-        "DEBUG - The transcoding plugin has not written to one of the allowed transfer syntaxes");
-    }
-#endif
-  }
-    
-
-  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
-                                         DicomImage& source,
-                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                         bool allowNewSopInstanceUid)
-  {
-    target.Clear();
-    
-#if !defined(NDEBUG)
-    // Don't run this code in release mode, as it implies parsing the DICOM file
-    DicomTransferSyntax sourceSyntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
-    {
-      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
-      return false;
-    }
-    
-    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
-#endif
-
-    std::string buffer;
-    if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(),
-                        allowedSyntaxes, allowNewSopInstanceUid))
-    {
-      CheckTargetSyntax(buffer, allowedSyntaxes);  // For debug only
-
-      target.AcquireBuffer(buffer);
-      
-#if !defined(NDEBUG)
-      // Only run the sanity check in debug mode
-      CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
-                       allowedSyntaxes, allowNewSopInstanceUid);
-#endif
-
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-}
--- a/Core/DicomParsing/MemoryBufferTranscoder.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IDicomTranscoder.h"
-
-namespace Orthanc
-{
-  // This is the basis class for transcoding plugins
-  class MemoryBufferTranscoder : public IDicomTranscoder
-  {
-  protected:
-    virtual bool TranscodeBuffer(std::string& target,
-                                 const void* buffer,
-                                 size_t size,
-                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                 bool allowNewSopInstanceUid) = 0;
-    
-  public:
-    virtual bool Transcode(DicomImage& target /* out */,
-                           DicomImage& source,
-                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-  };
-}
--- a/Core/DicomParsing/ParsedDicomDir.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,195 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ParsedDicomDir.h"
-
-#include "../Compatibility.h"
-#include "../OrthancException.h"
-#include "ParsedDicomFile.h"
-#include "FromDcmtkBridge.h"
-
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-
-namespace Orthanc
-{
-  void ParsedDicomDir::Clear()
-  {
-    for (size_t i = 0; i < content_.size(); i++)
-    {
-      assert(content_[i] != NULL);
-      delete content_[i];
-    }
-  }
-
-  
-  bool ParsedDicomDir::LookupIndexOfOffset(size_t& target,
-                                           unsigned int offset) const
-  {
-    if (offset == 0)
-    {
-      return false;
-    }
-
-    OffsetToIndex::const_iterator found = offsetToIndex_.find(offset);
-    if (found == offsetToIndex_.end())
-    {
-      // Error in the algorithm that computes the offsets
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      target = found->second;
-      return true;
-    }
-  }
-
-
-  ParsedDicomDir::ParsedDicomDir(const std::string content)
-  {
-    ParsedDicomFile dicom(content);
-
-    DcmSequenceOfItems* sequence = NULL;
-    if (dicom.GetDcmtkObject().getDataset() == NULL ||
-        !dicom.GetDcmtkObject().getDataset()->findAndGetSequence(DCM_DirectoryRecordSequence, sequence).good() ||
-        sequence == NULL)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Not a DICOMDIR");
-    }
-
-    content_.resize(sequence->card());
-    nextOffsets_.resize(content_.size());
-    lowerOffsets_.resize(content_.size());
-
-    // Manually reconstruct the list of all the available offsets of
-    // "DcmItem", as "fStartPosition" is a protected member in DCMTK
-    // API
-    std::set<uint32_t> availableOffsets;
-    availableOffsets.insert(0);
-
-
-    for (unsigned long i = 0; i < sequence->card(); i++)
-    {
-      DcmItem* item = sequence->getItem(i);
-      if (item == NULL)
-      {
-        Clear();
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      Uint32 next, lower;
-      if (!item->findAndGetUint32(DCM_OffsetOfTheNextDirectoryRecord, next).good() ||
-          !item->findAndGetUint32(DCM_OffsetOfReferencedLowerLevelDirectoryEntity, lower).good())
-      {
-        item->writeXML(std::cout);
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing offsets in DICOMDIR");
-      }          
-
-      nextOffsets_[i] = next;
-      lowerOffsets_[i] = lower;
-
-      std::unique_ptr<DicomMap> entry(new DicomMap);
-      FromDcmtkBridge::ExtractDicomSummary(*entry, *item);
-
-      if (next != 0)
-      {
-        availableOffsets.insert(next);
-      }
-
-      if (lower != 0)
-      {
-        availableOffsets.insert(lower);
-      }
-
-      content_[i] = entry.release();
-    }
-
-    if (content_.size() != availableOffsets.size())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Inconsistent offsets in DICOMDIR");
-    }
-
-    unsigned int index = 0;
-    for (std::set<uint32_t>::const_iterator it = availableOffsets.begin();
-         it != availableOffsets.end(); ++it)
-    {
-      offsetToIndex_[*it] = index;
-      index ++;
-    }    
-  }
-
-
-  const DicomMap& ParsedDicomDir::GetItem(size_t i) const
-  {
-    if (i >= content_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      assert(content_[i] != NULL);
-      return *content_[i];
-    }
-  }
-
-
-  bool ParsedDicomDir::LookupNext(size_t& target,
-                                  size_t index) const
-  {
-    if (index >= nextOffsets_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return LookupIndexOfOffset(target, nextOffsets_[index]);
-    }
-  }
-
-
-  bool ParsedDicomDir::LookupLower(size_t& target,
-                                   size_t index) const
-  {
-    if (index >= lowerOffsets_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return LookupIndexOfOffset(target, lowerOffsets_[index]);
-    }
-  }
-}
--- a/Core/DicomParsing/ParsedDicomDir.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 to use this file
-#endif
-
-#include "../DicomFormat/DicomMap.h"
-
-namespace Orthanc
-{
-  class ParsedDicomDir : public boost::noncopyable
-  {
-  private:
-    typedef std::map<uint32_t, size_t>  OffsetToIndex;
-
-    std::vector<DicomMap*>  content_;
-    std::vector<size_t>     nextOffsets_;
-    std::vector<size_t>     lowerOffsets_;
-    OffsetToIndex           offsetToIndex_;
-
-    void Clear();
-
-    bool LookupIndexOfOffset(size_t& target,
-                             unsigned int offset) const;
-
-  public:
-    ParsedDicomDir(const std::string content);
-
-    ~ParsedDicomDir()
-    {
-      Clear();
-    }
-
-    size_t GetSize() const
-    {
-      return content_.size();
-    }
-
-    const DicomMap& GetItem(size_t i) const;
-
-    bool LookupNext(size_t& target,
-                    size_t index) const;
-
-    bool LookupLower(size_t& target,
-                     size_t index) const;
-  };
-}
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1742 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Internals/DicomFrameIndex.h"
-#include "ToDcmtkBridge.h"
-
-#include "../Images/PamReader.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::unique_ptr<DcmFileFormat> file_;
-    std::unique_ptr<DicomFrameIndex>  frameIndex_;
-  };
-
-
-#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-  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*/)
-        ORTHANC_OVERRIDE
-      {
-        // No support for compression
-        return HttpCompression_None;
-      }
-
-      virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE
-      {
-        return false;
-      }
-
-      virtual std::string GetContentType() ORTHANC_OVERRIDE
-      {
-        return EnumerationToString(MimeType_Binary);
-      }
-
-      virtual uint64_t  GetContentLength() ORTHANC_OVERRIDE
-      {
-        return length_;
-      }
- 
-      virtual bool ReadNextChunk() ORTHANC_OVERRIDE
-      {
-        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())
-          {
-            throw OrthancException(ErrorCode_InternalError,
-                                   "Error while sending a DICOM field: " +
-                                   std::string(cond.text()));
-          }
-
-          return true;
-        }
-      }
- 
-      virtual const char *GetChunkContent() ORTHANC_OVERRIDE
-      {
-        return chunk_.c_str();
-      }
- 
-      virtual size_t GetChunkSize() ORTHANC_OVERRIDE
-      {
-        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, MimeType_Binary);
-                return true;
-              }
-
-              Uint8* buffer = NULL;
-              if (pixelItem->getUint8Array(buffer).good() && buffer)
-              {
-                output.AnswerBuffer(buffer, pixelItem->getLength(), MimeType_Binary);
-                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 = GetDcmtkObject().getDataset();
-    E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer();
-
-    // 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 = GetDcmtkObject().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 = GetDcmtkObject().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 = *GetDcmtkObject().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,
-                               const std::string& privateCreator)
-  {
-    if (tag.GetElement() == 0x0000)
-    {
-      // Prevent manually modifying generic group length tags: This is
-      // handled by DCMTK serialization
-      return;
-    }
-
-    if (GetDcmtkObject().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();
-
-    bool hasCodeExtensions;
-    Encoding encoding = DetectEncoding(hasCodeExtensions);
-    std::unique_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
-    InsertInternal(*GetDcmtkObject().getDataset(), element.release());
-  }
-
-
-  void ParsedDicomFile::ReplacePlainString(const DicomTag& tag,
-                                           const std::string& utf8Value)
-  {
-    if (tag.IsPrivate())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot apply this function to private tags: " + tag.Format());
-    }
-    else
-    {
-      Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent,
-              "" /* not a private tag, so no private creator */);
-    }
-  }
-
-
-  void ParsedDicomFile::SetIfAbsent(const DicomTag& tag,
-                                    const std::string& utf8Value)
-  {
-    std::string currentValue;
-    if (!GetTagValue(currentValue, tag))
-    {
-      ReplacePlainString(tag, utf8Value);
-    }
-  }
-
-
-  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, URI_SCHEME_PREFIX_BINARY))
-    {
-      std::string mime;
-      if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      decoded = &binary;
-    }
-    else
-    {
-      bool hasCodeExtensions;
-      Encoding encoding = DetectEncoding(hasCodeExtensions);
-      if (encoding != 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,
-                                const std::string& privateCreator)
-  {
-    if (tag.GetElement() == 0x0000)
-    {
-      // Prevent manually modifying generic group length tags: This is
-      // handled by DCMTK serialization
-      return;
-    }
-
-    InvalidateCache();
-
-    DcmDataset& dicom = *GetDcmtkObject().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::unique_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag, privateCreator));
-
-      if (!utf8Value.empty())
-      {
-        bool hasCodeExtensions;
-        Encoding encoding = DetectEncoding(hasCodeExtensions);
-        FromDcmtkBridge::FillElementWithString(*element, utf8Value, decodeDataUriScheme, encoding);
-      }
-
-      InsertInternal(dicom, element.release());
-      UpdateStorageUid(tag, utf8Value, false);
-    }
-  }
-
-    
-  void ParsedDicomFile::Replace(const DicomTag& tag,
-                                const Json::Value& value,
-                                bool decodeDataUriScheme,
-                                DicomReplaceMode mode,
-                                const std::string& privateCreator)
-  {
-    if (tag.GetElement() == 0x0000)
-    {
-      // Prevent manually modifying generic group length tags: This is
-      // handled by DCMTK serialization
-      return;
-    }
-
-    InvalidateCache();
-
-    DcmDataset& dicom = *GetDcmtkObject().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;
-        }
-      }
-
-      bool hasCodeExtensions;
-      Encoding encoding = DetectEncoding(hasCodeExtensions);
-      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
-
-      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, *GetDcmtkObject().getDataset()))
-    {
-      output.AnswerBuffer(serialized, MimeType_Dicom);
-    }
-  }
-#endif
-
-
-  bool ParsedDicomFile::GetTagValue(std::string& value,
-                                    const DicomTag& tag)
-  {
-    DcmTagKey k(tag.GetGroup(), tag.GetElement());
-    DcmDataset& dataset = *GetDcmtkObject().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;
-      }
-
-      bool hasCodeExtensions;
-      Encoding encoding = DetectEncoding(hasCodeExtensions);
-      
-      std::set<DicomTag> tmp;
-      std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
-                                    (*element, DicomToJsonFlags_Default, 
-                                     0, encoding, hasCodeExtensions, 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))
-    {
-      /**
-       * If "PatientID" is absent, be tolerant by considering it
-       * equals the empty string, then proceed. In Orthanc <= 1.5.6,
-       * an exception "Bad file format" was generated.
-       * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ
-       * https://hg.orthanc-server.com/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435
-       **/
-      patientId.clear();
-    }        
-    
-    if (!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, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
-    }
-
-    return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid);
-  }
-
-
-  void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
-  {
-    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().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,
-                                           bool permissive,
-                                           const std::string& defaultPrivateCreator,
-                                           const std::map<uint16_t, std::string>& privateCreators)
-  {
-    pimpl_->file_.reset(new DcmFileFormat);
-    InvalidateCache();
-
-    const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-
-    if (tmp == NULL)
-    {
-      SetEncoding(defaultEncoding);
-    }
-    else if (tmp->IsBinary())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Invalid binary string in the SpecificCharacterSet (0008,0005) tag");
-    }
-    else if (tmp->IsNull() ||
-             tmp->GetContent().empty())
-    {
-      SetEncoding(defaultEncoding);
-    }
-    else
-    {
-      Encoding encoding;
-
-      if (GetDicomEncoding(encoding, tmp->GetContent().c_str()))
-      {
-        SetEncoding(encoding);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange,
-                               "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" +
-                               tmp->GetContent() + "\"");
-      }
-    }
-
-    for (DicomMap::Content::const_iterator 
-           it = source.content_.begin(); it != source.content_.end(); ++it)
-    {
-      if (it->first != DICOM_TAG_SPECIFIC_CHARACTER_SET &&
-          !it->second->IsNull())
-      {
-        try
-        {
-          // Same as "ReplacePlainString()", but with support for private creator
-          const std::string& utf8Value = it->second->GetContent();
-
-          std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(it->first.GetGroup());
-          
-          if (it->first.IsPrivate() &&
-              found != privateCreators.end())
-          {
-            Replace(it->first, utf8Value, false, DicomReplaceMode_InsertIfAbsent, found->second);
-          }
-          else
-          {
-            Replace(it->first, utf8Value, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
-          }
-        }
-        catch (OrthancException&)
-        {
-          if (!permissive)
-          {
-            throw;
-          }
-        }
-      }
-    }
-  }
-
-  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
-                                   Encoding defaultEncoding,
-                                   bool permissive) :
-    pimpl_(new PImpl)
-  {
-    std::map<uint16_t, std::string> noPrivateCreators;
-    CreateFromDicomMap(map, defaultEncoding, permissive, "" /* no default private creator */, noPrivateCreators);
-  }
-
-
-  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
-                                   Encoding defaultEncoding,
-                                   bool permissive,
-                                   const std::string& defaultPrivateCreator,
-                                   const std::map<uint16_t, std::string>& privateCreators) :
-    pimpl_(new PImpl)
-  {
-    CreateFromDicomMap(map, defaultEncoding, permissive, defaultPrivateCreator, privateCreators);
-  }
-
-
-  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.GetDcmtkObject().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));
-  }
-
-
-  ParsedDicomFile::ParsedDicomFile(DcmFileFormat* dicom) : pimpl_(new PImpl)
-  {
-    pimpl_->file_.reset(dicom);  // No cloning
-  }
-
-
-  DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const
-  {
-    if (pimpl_->file_.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "ReleaseDcmtkObject() was called");
-    }
-    else
-    {
-      return *pimpl_->file_;
-    }
-  }
-
-
-  DcmFileFormat* ParsedDicomFile::ReleaseDcmtkObject()
-  {
-    if (pimpl_->file_.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "ReleaseDcmtkObject() was called");
-    }
-    else
-    {
-      pimpl_->frameIndex_.reset(NULL);
-      return pimpl_->file_.release();
-    }
-  }
-
-
-  ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid)
-  {
-    return new ParsedDicomFile(*this, keepSopInstanceUid);
-  }
-
-
-  bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme)
-  {
-    std::string mimeString, content;
-    if (!Toolbox::DecodeDataUriScheme(mimeString, content, dataUriScheme))
-    {
-      return false;
-    }
-
-    Toolbox::ToLowerCase(mimeString);
-    MimeType mime = StringToMimeType(mimeString);
-
-    switch (mime)
-    {
-      case MimeType_Png:
-#if ORTHANC_ENABLE_PNG == 1
-        EmbedImage(mime, content);
-        break;
-#else
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Orthanc was compiled without support of PNG");
-#endif
-
-      case MimeType_Jpeg:
-#if ORTHANC_ENABLE_JPEG == 1
-        EmbedImage(mime, content);
-        break;
-#else
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Orthanc was compiled without support of JPEG");
-#endif
-
-      case MimeType_Pam:
-        EmbedImage(mime, content);
-        break;
-
-      case MimeType_Pdf:
-        EmbedPdf(content);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Unsupported MIME type for the content of a new DICOM file: " +
-                               std::string(EnumerationToString(mime)));
-    }
-
-    return true;
-  }
-
-
-  void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme)
-  {
-    if (!EmbedContentInternal(dataUriScheme))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  void ParsedDicomFile::EmbedImage(MimeType mime,
-                                   const std::string& content)
-  {
-    switch (mime)
-    {
-    
-#if ORTHANC_ENABLE_JPEG == 1
-      case MimeType_Jpeg:
-      {
-        JpegReader reader;
-        reader.ReadFromMemory(content);
-        EmbedImage(reader);
-        break;
-      }
-#endif
-    
-#if ORTHANC_ENABLE_PNG == 1
-      case MimeType_Png:
-      {
-        PngReader reader;
-        reader.ReadFromMemory(content);
-        EmbedImage(reader);
-        break;
-      }
-#endif
-
-      case MimeType_Pam:
-      {
-        PamReader reader;
-        reader.ReadFromMemory(content);
-        EmbedImage(reader);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  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");
-
-    // The "Number of frames" must only be present in multi-frame images
-    //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
-    }
-
-    unsigned int bytesPerPixel = 0;
-
-    switch (accessor.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        // By default, grayscale images are MONOCHROME2
-        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
-
-        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;
-
-        // "Planar configuration" must only present if "Samples per
-        // Pixel" is greater than 1
-        ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
-
-        break;
-
-      case PixelFormat_Grayscale16:
-      case PixelFormat_SignedGrayscale16:
-        // By default, grayscale images are MONOCHROME2
-        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
-
-        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::unique_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 (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }    
-  }
-
-  
-  Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const
-  {
-    return FromDcmtkBridge::DetectEncoding(hasCodeExtensions,
-                                           *GetDcmtkObject().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, *GetDcmtkObject().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, *GetDcmtkObject().getDataset(),
-                                        format, flags, maxStringLength,
-                                        GetDefaultDicomEncoding(), ignoreTagLength);
-  }
-
-
-  void ParsedDicomFile::DatasetToJson(Json::Value& target,
-                                      const std::set<DicomTag>& ignoreTagLength)
-  {
-    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
-  }
-
-
-  void ParsedDicomFile::DatasetToJson(Json::Value& target)
-  {
-    const std::set<DicomTag> ignoreTagLength;
-    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
-  }
-
-
-  void ParsedDicomFile::HeaderToJson(Json::Value& target, 
-                                     DicomToJsonFormat format)
-  {
-    FromDcmtkBridge::ExtractHeaderAsJson(target, *GetDcmtkObject().getMetaInfo(), format, DicomToJsonFlags_None, 0);
-  }
-
-
-  bool ParsedDicomFile::HasTag(const DicomTag& tag) const
-  {
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-    return GetDcmtkObject().getDataset()->tagExists(key);
-  }
-
-
-  void ParsedDicomFile::EmbedPdf(const std::string& pdf)
-  {
-    if (pdf.size() < 5 ||  // (*)
-        strncmp("%PDF-", pdf.c_str(), 5) != 0)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file");
-    }
-
-    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), MIME_PDF);
-    //ReplacePlainString(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
-
-    std::unique_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 = GetDcmtkObject().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 != MIME_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 std::string& privateCreator)
-  {
-    const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false;
-    const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false;
-
-    std::unique_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, privateCreator);
-      }
-    }
-
-    return result.release();
-  }
-
-
-  void ParsedDicomFile::GetRawFrame(std::string& target,
-                                    MimeType& mime,
-                                    unsigned int frameId)
-  {
-    if (pimpl_->frameIndex_.get() == NULL)
-    {
-      assert(pimpl_->file_ != NULL &&
-             GetDcmtkObject().getDataset() != NULL);
-      pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObject().getDataset()));
-    }
-
-    pimpl_->frameIndex_->GetRawFrame(target, frameId);
-
-    E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer();
-    switch (transferSyntax)
-    {
-      case EXS_JPEGProcess1:
-        mime = MimeType_Jpeg;
-        break;
-       
-      case EXS_JPEG2000LosslessOnly:
-      case EXS_JPEG2000:
-        mime = MimeType_Jpeg2000;
-        break;
-
-      default:
-        mime = MimeType_Binary;
-        break;
-    }
-  }
-
-
-  void ParsedDicomFile::InvalidateCache()
-  {
-    pimpl_->frameIndex_.reset(NULL);
-  }
-
-
-  unsigned int ParsedDicomFile::GetFramesCount() const
-  {
-    assert(pimpl_->file_ != NULL &&
-           GetDcmtkObject().getDataset() != NULL);
-    return DicomFrameIndex::GetFramesCount(*GetDcmtkObject().getDataset());
-  }
-
-
-  void ParsedDicomFile::ChangeEncoding(Encoding target)
-  {
-    bool hasCodeExtensions;
-    Encoding source = DetectEncoding(hasCodeExtensions);
-
-    if (source != target)  // Avoid unnecessary conversion
-    {
-      ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target));
-      FromDcmtkBridge::ChangeStringEncoding(*GetDcmtkObject().getDataset(), source, hasCodeExtensions, target);
-    }
-  }
-
-
-  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const
-  {
-    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset());
-  }
-
-
-  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target,
-                                            const std::set<DicomTag>& ignoreTagLength) const
-  {
-    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
-  }
-
-
-  bool ParsedDicomFile::LookupTransferSyntax(std::string& result)
-  {
-#if 0
-    // This was the implementation in Orthanc <= 1.6.1
-
-    // TODO - Shouldn't "dataset.getCurrentXfer()" be used instead of
-    // using the meta header?
-    const char* value = NULL;
-
-    if (GetDcmtkObject().getMetaInfo() != NULL &&
-        GetDcmtkObject().getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() &&
-        value != NULL)
-    {
-      result.assign(value);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-#else
-    DicomTransferSyntax s;
-    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, GetDcmtkObject()))
-    {
-      result.assign(GetTransferSyntaxUid(s));
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-#endif
-  }
-
-
-  bool ParsedDicomFile::LookupPhotometricInterpretation(PhotometricInterpretation& result) const
-  {
-    DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(),
-                DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement());
-
-    DcmDataset& dataset = *GetDcmtkObject().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(*GetDcmtkObject().getDataset(), visitor, GetDefaultDicomEncoding());
-  }
-}
--- a/Core/DicomParsing/ParsedDicomFile.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,270 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-
-#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
-
-#if !defined(ORTHANC_ENABLE_DCMTK)
-#  error The macro ORTHANC_ENABLE_DCMTK must be defined
-#endif
-
-#if ORTHANC_ENABLE_DCMTK != 1
-#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1 to use this file
-#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 ORTHANC_PUBLIC ParsedDicomFile : public IDynamicObject
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    ParsedDicomFile(ParsedDicomFile& other,
-                    bool keepSopInstanceUid);
-
-    void CreateFromDicomMap(const DicomMap& source,
-                            Encoding defaultEncoding,
-                            bool permissive,
-                            const std::string& defaultPrivateCreator,
-                            const std::map<uint16_t, std::string>& privateCreators);
-
-    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);
-
-    ParsedDicomFile(DcmFileFormat* dicom);  // This takes ownership (no clone)
-
-  public:
-    ParsedDicomFile(bool createIdentifiers);  // Create a minimal DICOM instance
-
-    ParsedDicomFile(const DicomMap& map,
-                    Encoding defaultEncoding,
-                    bool permissive
-                    );
-
-    ParsedDicomFile(const DicomMap& map,
-                    Encoding defaultEncoding,
-                    bool permissive,
-                    const std::string& defaultPrivateCreator,
-                    const std::map<uint16_t, std::string>& privateCreators
-                    );
-
-    ParsedDicomFile(const void* content,
-                    size_t size);
-
-    ParsedDicomFile(const std::string& content);
-
-    ParsedDicomFile(DcmDataset& dicom);  // This clones the DCMTK object
-
-    ParsedDicomFile(DcmFileFormat& dicom);  // This clones the DCMTK object
-
-    static ParsedDicomFile* AcquireDcmtkObject(DcmFileFormat* dicom)  // No clone here
-    {
-      return new ParsedDicomFile(dicom);
-    }
-
-    DcmFileFormat& GetDcmtkObject() const;
-
-    // The "ParsedDicomFile" object cannot be used after calling this method
-    DcmFileFormat* ReleaseDcmtkObject();
-
-    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,
-                 const std::string& privateCreator /* used only for private tags */);
-
-    void Replace(const DicomTag& tag,
-                 const Json::Value& value,  // Assumed to be encoded with UTF-8
-                 bool decodeDataUriScheme,
-                 DicomReplaceMode mode,
-                 const std::string& privateCreator /* used only for private tags */);
-
-    void Insert(const DicomTag& tag,
-                const Json::Value& value,   // Assumed to be encoded with UTF-8
-                bool decodeDataUriScheme,
-                const std::string& privateCreator /* used only for private tags */);
-
-    // Cannot be applied to private tags
-    void ReplacePlainString(const DicomTag& tag,
-                            const std::string& utf8Value);
-
-    // Cannot be applied to private tags
-    void SetIfAbsent(const DicomTag& tag,
-                     const std::string& utf8Value);
-
-    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);
-
-    void EmbedImage(MimeType mime,
-                    const std::string& content);
-
-    Encoding DetectEncoding(bool& hasCodeExtensions) 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
-                     MimeType& mime,   // OUT
-                     unsigned int frameId);  // IN
-
-    unsigned int GetFramesCount() const;
-
-    static ParsedDicomFile* CreateFromJson(const Json::Value& value,
-                                           DicomFromJsonFlags flags,
-                                           const std::string& privateCreator);
-
-    void ChangeEncoding(Encoding target);
-
-    void ExtractDicomSummary(DicomMap& target) const;
-
-    void ExtractDicomSummary(DicomMap& target,
-                             const std::set<DicomTag>& ignoreTagLength) const;
-
-    bool LookupTransferSyntax(std::string& result);
-
-    bool LookupPhotometricInterpretation(PhotometricInterpretation& result) const;
-
-    void Apply(ITagVisitor& visitor);
-  };
-}
--- a/Core/DicomParsing/ToDcmtkBridge.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-    }
-  }
-}
--- a/Core/DicomParsing/ToDcmtkBridge.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Core/Endianness.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,220 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancException.h"
-
-#include "Toolbox.h"
-#include <boost/lexical_cast.hpp>
-#include <string>
-#include <map>
-
-namespace Orthanc
-{
-  template <typename Enumeration>
-  class EnumerationDictionary
-  {
-  private:
-    typedef std::map<Enumeration, std::string>  EnumerationToString;
-    typedef std::map<std::string, Enumeration>  StringToEnumeration;
-
-    EnumerationToString enumerationToString_;
-    StringToEnumeration stringToEnumeration_;
-
-  public:
-    void Clear()
-    {
-      enumerationToString_.clear();
-      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
-      if (enumerationToString_.find(value) != enumerationToString_.end() ||
-          stringToEnumeration_.find(str) != stringToEnumeration_.end() ||
-          Toolbox::IsInteger(str) /* Prevent the registration of a number */)
-      {
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-
-      // OK, the string is free and is not a number
-      enumerationToString_[value] = str;
-      stringToEnumeration_[str] = value;
-      stringToEnumeration_[boost::lexical_cast<std::string>(static_cast<int>(value))] = value;
-    }
-
-    Enumeration Translate(const std::string& str) const
-    {
-      if (Toolbox::IsInteger(str))
-      {
-        return static_cast<Enumeration>(boost::lexical_cast<int>(str));
-      }
-
-      typename StringToEnumeration::const_iterator
-        found = stringToEnumeration_.find(str);
-
-      if (found == stringToEnumeration_.end())
-      {
-        throw OrthancException(ErrorCode_InexistentItem);
-      }
-      else
-      {
-        return found->second;
-      }
-    }
-
-    std::string Translate(Enumeration e) const
-    {
-      typename EnumerationToString::const_iterator
-        found = enumerationToString_.find(e);
-
-      if (found == enumerationToString_.end())
-      {
-        // No name for this item
-        return boost::lexical_cast<std::string>(static_cast<int>(e));
-      }
-      else
-      {
-        return found->second;
-      }
-    }
-  };
-}
--- a/Core/Enumerations.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2255 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Enumerations.h"
-
-#include "OrthancException.h"
-#include "Toolbox.h"
-#include "Logging.h"
-
-#include <boost/thread/mutex.hpp>
-#include <string.h>
-#include <cassert>
-
-namespace Orthanc
-{
-  static const char* const MIME_CSS = "text/css";
-  static const char* const MIME_DICOM = "application/dicom";
-  static const char* const MIME_GIF = "image/gif";
-  static const char* const MIME_GZIP = "application/gzip";
-  static const char* const MIME_HTML = "text/html";
-  static const char* const MIME_JAVASCRIPT = "application/javascript";
-  static const char* const MIME_JPEG2000 = "image/jp2";
-  static const char* const MIME_NACL = "application/x-nacl";
-  static const char* const MIME_PLAIN_TEXT = "text/plain";
-  static const char* const MIME_PNACL = "application/x-pnacl";
-  static const char* const MIME_SVG = "image/svg+xml";
-  static const char* const MIME_WEB_ASSEMBLY = "application/wasm";
-  static const char* const MIME_WOFF = "application/x-font-woff";
-  static const char* const MIME_WOFF2 = "font/woff2";
-  static const char* const MIME_XML_2 = "text/xml";
-  static const char* const MIME_ZIP = "application/zip";
-  static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
-  static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
-
-  // This function is autogenerated by the script
-  // "Resources/GenerateErrorCodes.py"
-  const char* EnumerationToString(ErrorCode error)
-  {
-    switch (error)
-    {
-      case ErrorCode_InternalError:
-        return "Internal error";
-
-      case ErrorCode_Success:
-        return "Success";
-
-      case ErrorCode_Plugin:
-        return "Error encountered within the plugin engine";
-
-      case ErrorCode_NotImplemented:
-        return "Not implemented yet";
-
-      case ErrorCode_ParameterOutOfRange:
-        return "Parameter out of range";
-
-      case ErrorCode_NotEnoughMemory:
-        return "The server hosting Orthanc is running out of memory";
-
-      case ErrorCode_BadParameterType:
-        return "Bad type for a parameter";
-
-      case ErrorCode_BadSequenceOfCalls:
-        return "Bad sequence of calls";
-
-      case ErrorCode_InexistentItem:
-        return "Accessing an inexistent item";
-
-      case ErrorCode_BadRequest:
-        return "Bad request";
-
-      case ErrorCode_NetworkProtocol:
-        return "Error in the network protocol";
-
-      case ErrorCode_SystemCommand:
-        return "Error while calling a system command";
-
-      case ErrorCode_Database:
-        return "Error with the database engine";
-
-      case ErrorCode_UriSyntax:
-        return "Badly formatted URI";
-
-      case ErrorCode_InexistentFile:
-        return "Inexistent file";
-
-      case ErrorCode_CannotWriteFile:
-        return "Cannot write to file";
-
-      case ErrorCode_BadFileFormat:
-        return "Bad file format";
-
-      case ErrorCode_Timeout:
-        return "Timeout";
-
-      case ErrorCode_UnknownResource:
-        return "Unknown resource";
-
-      case ErrorCode_IncompatibleDatabaseVersion:
-        return "Incompatible version of the database";
-
-      case ErrorCode_FullStorage:
-        return "The file storage is full";
-
-      case ErrorCode_CorruptedFile:
-        return "Corrupted file (e.g. inconsistent MD5 hash)";
-
-      case ErrorCode_InexistentTag:
-        return "Inexistent tag";
-
-      case ErrorCode_ReadOnly:
-        return "Cannot modify a read-only data structure";
-
-      case ErrorCode_IncompatibleImageFormat:
-        return "Incompatible format of the images";
-
-      case ErrorCode_IncompatibleImageSize:
-        return "Incompatible size of the images";
-
-      case ErrorCode_SharedLibrary:
-        return "Error while using a shared library (plugin)";
-
-      case ErrorCode_UnknownPluginService:
-        return "Plugin invoking an unknown service";
-
-      case ErrorCode_UnknownDicomTag:
-        return "Unknown DICOM tag";
-
-      case ErrorCode_BadJson:
-        return "Cannot parse a JSON document";
-
-      case ErrorCode_Unauthorized:
-        return "Bad credentials were provided to an HTTP request";
-
-      case ErrorCode_BadFont:
-        return "Badly formatted font file";
-
-      case ErrorCode_DatabasePlugin:
-        return "The plugin implementing a custom database back-end does not fulfill the proper interface";
-
-      case ErrorCode_StorageAreaPlugin:
-        return "Error in the plugin implementing a custom storage area";
-
-      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_BadGeometry:
-        return "Geometry error encountered in Stone";
-
-      case ErrorCode_SslInitialization:
-        return "Cannot initialize SSL encryption, check out your certificates";
-
-      case ErrorCode_SQLiteNotOpened:
-        return "SQLite: The database is not opened";
-
-      case ErrorCode_SQLiteAlreadyOpened:
-        return "SQLite: Connection is already open";
-
-      case ErrorCode_SQLiteCannotOpen:
-        return "SQLite: Unable to open the database";
-
-      case ErrorCode_SQLiteStatementAlreadyUsed:
-        return "SQLite: This cached statement is already being referred to";
-
-      case ErrorCode_SQLiteExecute:
-        return "SQLite: Cannot execute a command";
-
-      case ErrorCode_SQLiteRollbackWithoutTransaction:
-        return "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)";
-
-      case ErrorCode_SQLiteCommitWithoutTransaction:
-        return "SQLite: Committing a nonexistent transaction";
-
-      case ErrorCode_SQLiteRegisterFunction:
-        return "SQLite: Unable to register a function";
-
-      case ErrorCode_SQLiteFlush:
-        return "SQLite: Unable to flush the database";
-
-      case ErrorCode_SQLiteCannotRun:
-        return "SQLite: Cannot run a cached statement";
-
-      case ErrorCode_SQLiteCannotStep:
-        return "SQLite: Cannot step over a cached statement";
-
-      case ErrorCode_SQLiteBindOutOfRange:
-        return "SQLite: Bing a value while out of range (serious error)";
-
-      case ErrorCode_SQLitePrepareStatement:
-        return "SQLite: Cannot prepare a cached statement";
-
-      case ErrorCode_SQLiteTransactionAlreadyStarted:
-        return "SQLite: Beginning the same transaction twice";
-
-      case ErrorCode_SQLiteTransactionCommit:
-        return "SQLite: Failure when committing the transaction";
-
-      case ErrorCode_SQLiteTransactionBegin:
-        return "SQLite: Cannot start a transaction";
-
-      case ErrorCode_DirectoryOverFile:
-        return "The directory to be created is already occupied by a regular file";
-
-      case ErrorCode_FileStorageCannotWrite:
-        return "Unable to create a subdirectory or a file in the file storage";
-
-      case ErrorCode_DirectoryExpected:
-        return "The specified path does not point to a directory";
-
-      case ErrorCode_HttpPortInUse:
-        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 privileged or already in use";
-
-      case ErrorCode_BadHttpStatusInRest:
-        return "This HTTP status is not allowed in a REST API";
-
-      case ErrorCode_RegularFileExpected:
-        return "The specified path does not point to a regular file";
-
-      case ErrorCode_PathToExecutable:
-        return "Unable to get the path to the executable";
-
-      case ErrorCode_MakeDirectory:
-        return "Cannot create a directory";
-
-      case ErrorCode_BadApplicationEntityTitle:
-        return "An application entity title (AET) cannot be empty or be longer than 16 characters";
-
-      case ErrorCode_NoCFindHandler:
-        return "No request handler factory for DICOM C-FIND SCP";
-
-      case ErrorCode_NoCMoveHandler:
-        return "No request handler factory for DICOM C-MOVE SCP";
-
-      case ErrorCode_NoCStoreHandler:
-        return "No request handler factory for DICOM C-STORE SCP";
-
-      case ErrorCode_NoApplicationEntityFilter:
-        return "No application entity filter";
-
-      case ErrorCode_NoSopClassOrInstance:
-        return "DicomUserConnection: Unable to find the SOP class and instance";
-
-      case ErrorCode_NoPresentationContext:
-        return "DicomUserConnection: No acceptable presentation context for modality";
-
-      case ErrorCode_DicomFindUnavailable:
-        return "DicomUserConnection: The C-FIND command is not supported by the remote SCP";
-
-      case ErrorCode_DicomMoveUnavailable:
-        return "DicomUserConnection: The C-MOVE command is not supported by the remote SCP";
-
-      case ErrorCode_CannotStoreInstance:
-        return "Cannot store an instance";
-
-      case ErrorCode_CreateDicomNotString:
-        return "Only string values are supported when creating DICOM instances";
-
-      case ErrorCode_CreateDicomOverrideTag:
-        return "Trying to override a value inherited from a parent module";
-
-      case ErrorCode_CreateDicomUseContent:
-        return "Use \"Content\" to inject an image into a new DICOM instance";
-
-      case ErrorCode_CreateDicomNoPayload:
-        return "No payload is present for one instance in the series";
-
-      case ErrorCode_CreateDicomUseDataUriScheme:
-        return "The payload of the DICOM instance must be specified according to Data URI scheme";
-
-      case ErrorCode_CreateDicomBadParent:
-        return "Trying to attach a new DICOM instance to an inexistent resource";
-
-      case ErrorCode_CreateDicomParentIsInstance:
-        return "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)";
-
-      case ErrorCode_CreateDicomParentEncoding:
-        return "Unable to get the encoding of the parent resource";
-
-      case ErrorCode_UnknownModality:
-        return "Unknown modality";
-
-      case ErrorCode_BadJobOrdering:
-        return "Bad ordering of filters in a job";
-
-      case ErrorCode_JsonToLuaTable:
-        return "Cannot convert the given JSON object to a Lua table";
-
-      case ErrorCode_CannotCreateLua:
-        return "Cannot create the Lua context";
-
-      case ErrorCode_CannotExecuteLua:
-        return "Cannot execute a Lua command";
-
-      case ErrorCode_LuaAlreadyExecuted:
-        return "Arguments cannot be pushed after the Lua function is executed";
-
-      case ErrorCode_LuaBadOutput:
-        return "The Lua function does not give the expected number of outputs";
-
-      case ErrorCode_NotLuaPredicate:
-        return "The Lua function is not a predicate (only true/false outputs allowed)";
-
-      case ErrorCode_LuaReturnsNoString:
-        return "The Lua function does not return a string";
-
-      case ErrorCode_StorageAreaAlreadyRegistered:
-        return "Another plugin has already registered a custom storage area";
-
-      case ErrorCode_DatabaseBackendAlreadyRegistered:
-        return "Another plugin has already registered a custom database back-end";
-
-      case ErrorCode_DatabaseNotInitialized:
-        return "Plugin trying to call the database during its initialization";
-
-      case ErrorCode_SslDisabled:
-        return "Orthanc has been built without SSL support";
-
-      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";
-
-      case ErrorCode_NoCGetHandler:
-        return "No request handler factory for DICOM C-GET SCP";
-
-      case ErrorCode_NoStorageCommitmentHandler:
-        return "No request handler factory for DICOM N-ACTION SCP (storage commitment)";
-
-      case ErrorCode_UnsupportedMediaType:
-        return "Unsupported media type";
-
-      default:
-        if (error >= ErrorCode_START_PLUGINS)
-        {
-          return "Error encountered within some plugin";
-        }
-        else
-        {
-          return "Unknown error code";
-        }
-    }
-  }
-
-
-  const char* EnumerationToString(HttpMethod method)
-  {
-    switch (method)
-    {
-      case HttpMethod_Get:
-        return "GET";
-
-      case HttpMethod_Post:
-        return "POST";
-
-      case HttpMethod_Delete:
-        return "DELETE";
-
-      case HttpMethod_Put:
-        return "PUT";
-
-      default:
-        return "?";
-    }
-  }
-
-
-  const char* EnumerationToString(HttpStatus status)
-  {
-    switch (status)
-    {
-    case HttpStatus_100_Continue:
-      return "Continue";
-
-    case HttpStatus_101_SwitchingProtocols:
-      return "Switching Protocols";
-
-    case HttpStatus_102_Processing:
-      return "Processing";
-
-    case HttpStatus_200_Ok:
-      return "OK";
-
-    case HttpStatus_201_Created:
-      return "Created";
-
-    case HttpStatus_202_Accepted:
-      return "Accepted";
-
-    case HttpStatus_203_NonAuthoritativeInformation:
-      return "Non-Authoritative Information";
-
-    case HttpStatus_204_NoContent:
-      return "No Content";
-
-    case HttpStatus_205_ResetContent:
-      return "Reset Content";
-
-    case HttpStatus_206_PartialContent:
-      return "Partial Content";
-
-    case HttpStatus_207_MultiStatus:
-      return "Multi-Status";
-
-    case HttpStatus_208_AlreadyReported:
-      return "Already Reported";
-
-    case HttpStatus_226_IMUsed:
-      return "IM Used";
-
-    case HttpStatus_300_MultipleChoices:
-      return "Multiple Choices";
-
-    case HttpStatus_301_MovedPermanently:
-      return "Moved Permanently";
-
-    case HttpStatus_302_Found:
-      return "Found";
-
-    case HttpStatus_303_SeeOther:
-      return "See Other";
-
-    case HttpStatus_304_NotModified:
-      return "Not Modified";
-
-    case HttpStatus_305_UseProxy:
-      return "Use Proxy";
-
-    case HttpStatus_307_TemporaryRedirect:
-      return "Temporary Redirect";
-
-    case HttpStatus_400_BadRequest:
-      return "Bad Request";
-
-    case HttpStatus_401_Unauthorized:
-      return "Unauthorized";
-
-    case HttpStatus_402_PaymentRequired:
-      return "Payment Required";
-
-    case HttpStatus_403_Forbidden:
-      return "Forbidden";
-
-    case HttpStatus_404_NotFound:
-      return "Not Found";
-
-    case HttpStatus_405_MethodNotAllowed:
-      return "Method Not Allowed";
-
-    case HttpStatus_406_NotAcceptable:
-      return "Not Acceptable";
-
-    case HttpStatus_407_ProxyAuthenticationRequired:
-      return "Proxy Authentication Required";
-
-    case HttpStatus_408_RequestTimeout:
-      return "Request Timeout";
-
-    case HttpStatus_409_Conflict:
-      return "Conflict";
-
-    case HttpStatus_410_Gone:
-      return "Gone";
-
-    case HttpStatus_411_LengthRequired:
-      return "Length Required";
-
-    case HttpStatus_412_PreconditionFailed:
-      return "Precondition Failed";
-
-    case HttpStatus_413_RequestEntityTooLarge:
-      return "Request Entity Too Large";
-
-    case HttpStatus_414_RequestUriTooLong:
-      return "Request-URI Too Long";
-
-    case HttpStatus_415_UnsupportedMediaType:
-      return "Unsupported Media Type";
-
-    case HttpStatus_416_RequestedRangeNotSatisfiable:
-      return "Requested Range Not Satisfiable";
-
-    case HttpStatus_417_ExpectationFailed:
-      return "Expectation Failed";
-
-    case HttpStatus_422_UnprocessableEntity:
-      return "Unprocessable Entity";
-
-    case HttpStatus_423_Locked:
-      return "Locked";
-
-    case HttpStatus_424_FailedDependency:
-      return "Failed Dependency";
-
-    case HttpStatus_426_UpgradeRequired:
-      return "Upgrade Required";
-
-    case HttpStatus_500_InternalServerError:
-      return "Internal Server Error";
-
-    case HttpStatus_501_NotImplemented:
-      return "Not Implemented";
-
-    case HttpStatus_502_BadGateway:
-      return "Bad Gateway";
-
-    case HttpStatus_503_ServiceUnavailable:
-      return "Service Unavailable";
-
-    case HttpStatus_504_GatewayTimeout:
-      return "Gateway Timeout";
-
-    case HttpStatus_505_HttpVersionNotSupported:
-      return "HTTP Version Not Supported";
-
-    case HttpStatus_506_VariantAlsoNegotiates:
-      return "Variant Also Negotiates";
-
-    case HttpStatus_507_InsufficientStorage:
-      return "Insufficient Storage";
-
-    case HttpStatus_509_BandwidthLimitExceeded:
-      return "Bandwidth Limit Exceeded";
-
-    case HttpStatus_510_NotExtended:
-      return "Not Extended";
-
-    default:
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(ResourceType type)
-  {
-    switch (type)
-    {
-      case ResourceType_Patient:
-        return "Patient";
-
-      case ResourceType_Study:
-        return "Study";
-
-      case ResourceType_Series:
-        return "Series";
-
-      case ResourceType_Instance:
-        return "Instance";
-      
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(ImageFormat format)
-  {
-    switch (format)
-    {
-      case ImageFormat_Png:
-        return "Png";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(Encoding encoding)
-  {
-    switch (encoding)
-    {
-      case Encoding_Ascii:
-        return "Ascii";
-
-      case Encoding_Utf8:
-        return "Utf8";
-
-      case Encoding_Latin1:
-        return "Latin1";
-
-      case Encoding_Latin2:
-        return "Latin2";
-
-      case Encoding_Latin3:
-        return "Latin3";
-
-      case Encoding_Latin4:
-        return "Latin4";
-
-      case Encoding_Latin5:
-        return "Latin5";
-
-      case Encoding_Cyrillic:
-        return "Cyrillic";
-
-      case Encoding_Windows1251:
-        return "Windows1251";
-
-      case Encoding_Arabic:
-        return "Arabic";
-
-      case Encoding_Greek:
-        return "Greek";
-
-      case Encoding_Hebrew:
-        return "Hebrew";
-
-      case Encoding_Thai:
-        return "Thai";
-
-      case Encoding_Japanese:
-        return "Japanese";
-
-      case Encoding_Chinese:
-        return "Chinese";
-
-      case Encoding_Korean:
-        return "Korean";
-
-      case Encoding_JapaneseKanji:
-        return "JapaneseKanji";
-
-      case Encoding_SimplifiedChinese:
-        return "SimplifiedChinese";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(PhotometricInterpretation photometric)
-  {
-    switch (photometric)
-    {
-      case PhotometricInterpretation_RGB:
-        return "RGB";
-
-      case PhotometricInterpretation_Monochrome1:
-        return "MONOCHROME1";
-
-      case PhotometricInterpretation_Monochrome2:
-        return "MONOCHROME2";
-
-      case PhotometricInterpretation_ARGB:
-        return "ARGB";
-
-      case PhotometricInterpretation_CMYK:
-        return "CMYK";
-
-      case PhotometricInterpretation_HSV:
-        return "HSV";
-
-      case PhotometricInterpretation_Palette:
-        return "PALETTE COLOR";
-
-      case PhotometricInterpretation_YBRFull:
-        return "YBR_FULL";
-
-      case PhotometricInterpretation_YBRFull422:
-        return "YBR_FULL_422";
-
-      case PhotometricInterpretation_YBRPartial420:
-        return "YBR_PARTIAL_420"; 
-
-      case PhotometricInterpretation_YBRPartial422:
-        return "YBR_PARTIAL_422"; 
-
-      case PhotometricInterpretation_YBR_ICT:
-        return "YBR_ICT"; 
-
-      case PhotometricInterpretation_YBR_RCT:
-        return "YBR_RCT"; 
-
-      case PhotometricInterpretation_Unknown:
-        return "Unknown";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(RequestOrigin origin)
-  {
-    switch (origin)
-    {
-      case RequestOrigin_Unknown:
-        return "Unknown";
-
-      case RequestOrigin_DicomProtocol:
-        return "DicomProtocol";
-
-      case RequestOrigin_RestApi:
-        return "RestApi";
-
-      case RequestOrigin_Plugins:
-        return "Plugins";
-
-      case RequestOrigin_Lua:
-        return "Lua";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  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_Vitrea:
-        return "Vitrea";
-      
-      case ModalityManufacturer_GE:
-        return "GE";
-      
-      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;
-
-      case DicomRequestType_NAction:
-        return "N-ACTION";
-        break;
-
-      case DicomRequestType_NEventReport:
-        return "N-EVENT-REPORT";
-        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_Mpeg4:
-        return "MPEG4";
-
-      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);
-    }
-  }
-
-
-  const char* EnumerationToString(MimeType mime)
-  {
-    switch (mime)
-    {
-      case MimeType_Binary:
-        return MIME_BINARY;
-        
-      case MimeType_Dicom:
-        return MIME_DICOM;
-        
-      case MimeType_Jpeg:
-        return MIME_JPEG;
-        
-      case MimeType_Jpeg2000:
-        return MIME_JPEG2000;
-        
-      case MimeType_Json:
-        return MIME_JSON;
-        
-      case MimeType_Pdf:
-        return MIME_PDF;
-        
-      case MimeType_Png:
-        return MIME_PNG;
-        
-      case MimeType_Xml:
-        return MIME_XML;
-        
-      case MimeType_PlainText:
-        return MIME_PLAIN_TEXT;
-                
-      case MimeType_Pam:
-        return MIME_PAM;
-                
-      case MimeType_Html:
-        return MIME_HTML;
-                
-      case MimeType_Gzip:
-        return MIME_GZIP;
-                
-      case MimeType_JavaScript:
-        return MIME_JAVASCRIPT;
-                
-      case MimeType_Css:
-        return MIME_CSS;
-                
-      case MimeType_WebAssembly:
-        return MIME_WEB_ASSEMBLY;
-                
-      case MimeType_Gif:
-        return MIME_GIF;
-                
-      case MimeType_Zip:
-        return MIME_ZIP;
-                
-      case MimeType_NaCl:
-        return MIME_NACL;
-                
-      case MimeType_PNaCl:
-        return MIME_PNACL;
-                
-      case MimeType_Svg:
-        return MIME_SVG;
-                
-      case MimeType_Woff:
-        return MIME_WOFF;
-
-      case MimeType_Woff2:
-        return MIME_WOFF2;
-
-      case MimeType_PrometheusText:
-        // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
-        return "text/plain; version=0.0.4";
-
-      case MimeType_DicomWebJson:
-        return MIME_DICOM_WEB_JSON;
-                
-      case MimeType_DicomWebXml:
-        return MIME_DICOM_WEB_XML;
-                
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-  
-
-  const char* EnumerationToString(Endianness endianness)
-  {
-    switch (endianness)
-    {
-      case Endianness_Little:
-        return "Little-endian";
-
-      case Endianness_Big:
-        return "Big-endian";
-
-      case Endianness_Unknown:
-        return "Unknown endianness";
-                
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(StorageCommitmentFailureReason reason)
-  {
-    switch (reason)
-    {
-      case StorageCommitmentFailureReason_Success:
-        return "Success";
-
-      case StorageCommitmentFailureReason_ProcessingFailure:
-        return "A general failure in processing the operation was encountered";
-
-      case StorageCommitmentFailureReason_NoSuchObjectInstance:
-        return "One or more of the elements in the Referenced SOP "
-          "Instance Sequence was not available";
-        
-      case StorageCommitmentFailureReason_ResourceLimitation:
-        return "The SCP does not currently have enough resources to "
-          "store the requested SOP Instance(s)";
-
-      case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
-        return "Storage Commitment has been requested for a SOP Instance "
-          "with a SOP Class that is not supported by the SCP";
-
-      case StorageCommitmentFailureReason_ClassInstanceConflict:
-        return "The SOP Class of an element in the Referenced SOP Instance Sequence "
-          "did not correspond to the SOP class registered for this SOP Instance at the SCP";
-
-      case StorageCommitmentFailureReason_DuplicateTransactionUID:
-        return "The Transaction UID of the Storage Commitment Request is already in use";
-
-      default:
-        return "Unknown failure reason";
-    }
-  }
-
-
-  Encoding StringToEncoding(const char* encoding)
-  {
-    std::string s(encoding);
-    Toolbox::ToUpperCase(s);
-
-    if (s == "UTF8")
-    {
-      return Encoding_Utf8;
-    }
-
-    if (s == "ASCII")
-    {
-      return Encoding_Ascii;
-    }
-
-    if (s == "LATIN1")
-    {
-      return Encoding_Latin1;
-    }
-
-    if (s == "LATIN2")
-    {
-      return Encoding_Latin2;
-    }
-
-    if (s == "LATIN3")
-    {
-      return Encoding_Latin3;
-    }
-
-    if (s == "LATIN4")
-    {
-      return Encoding_Latin4;
-    }
-
-    if (s == "LATIN5")
-    {
-      return Encoding_Latin5;
-    }
-
-    if (s == "CYRILLIC")
-    {
-      return Encoding_Cyrillic;
-    }
-
-    if (s == "WINDOWS1251")
-    {
-      return Encoding_Windows1251;
-    }
-
-    if (s == "ARABIC")
-    {
-      return Encoding_Arabic;
-    }
-
-    if (s == "GREEK")
-    {
-      return Encoding_Greek;
-    }
-
-    if (s == "HEBREW")
-    {
-      return Encoding_Hebrew;
-    }
-
-    if (s == "THAI")
-    {
-      return Encoding_Thai;
-    }
-
-    if (s == "JAPANESE")
-    {
-      return Encoding_Japanese;
-    }
-
-    if (s == "CHINESE")
-    {
-      return Encoding_Chinese;
-    }
-
-    if (s == "KOREAN")
-    {
-      return Encoding_Korean;
-    }
-
-    if (s == "JAPANESEKANJI")
-    {
-      return Encoding_JapaneseKanji;
-    }
-
-    if (s == "SIMPLIFIEDCHINESE")
-    {
-      return Encoding_SimplifiedChinese;
-    }
-
-    throw OrthancException(ErrorCode_ParameterOutOfRange);
-  }
-
-
-  ResourceType StringToResourceType(const char* type)
-  {
-    std::string s(type);
-    Toolbox::ToUpperCase(s);
-
-    if (s == "PATIENT" || s == "PATIENTS")
-    {
-      return ResourceType_Patient;
-    }
-    else if (s == "STUDY" || s == "STUDIES")
-    {
-      return ResourceType_Study;
-    }
-    else if (s == "SERIES")
-    {
-      return ResourceType_Series;
-    }
-    else if (s == "INSTANCE"  || s == "IMAGE" || 
-             s == "INSTANCES" || s == "IMAGES")
-    {
-      return ResourceType_Instance;
-    }
-
-    throw OrthancException(ErrorCode_ParameterOutOfRange);
-  }
-
-
-  ImageFormat StringToImageFormat(const char* format)
-  {
-    std::string s(format);
-    Toolbox::ToUpperCase(s);
-
-    if (s == "PNG")
-    {
-      return ImageFormat_Png;
-    }
-
-    throw OrthancException(ErrorCode_ParameterOutOfRange);
-  }
-
-
-  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)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange, s);
-      }
-      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 == "StoreScp")
-    {
-      return ModalityManufacturer_StoreScp;
-    }
-    else if (manufacturer == "Vitrea")
-    {
-      return ModalityManufacturer_Vitrea;
-    }
-    else if (manufacturer == "GE")
-    {
-      return ModalityManufacturer_GE;
-    }
-    else if (manufacturer == "AgfaImpax" ||
-             manufacturer == "SyngoVia")
-    {
-      result = ModalityManufacturer_GenericNoWildcardInDates;
-      obsolete = true;
-    }
-    else if (manufacturer == "EFilm2" ||
-             manufacturer == "MedInria" ||
-             manufacturer == "ClearCanvas" ||
-             manufacturer == "Dcm4Chee"
-             )
-    {
-      result = ModalityManufacturer_Generic;
-      obsolete = true;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Unknown modality manufacturer: \"" + manufacturer + "\"");
-    }
-
-    if (obsolete)
-    {
-      LOG(WARNING) << "The \"" << manufacturer << "\" manufacturer is now obsolete. "
-                   << "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);
-    }
-  }
-
-
-  MimeType StringToMimeType(const std::string& mime)
-  {
-    if (mime == MIME_BINARY)
-    {
-      return MimeType_Binary;
-    }
-    else if (mime == MIME_DICOM)
-    {
-      return MimeType_Dicom;
-    }
-    else if (mime == MIME_JPEG)
-    {
-      return MimeType_Jpeg;
-    }
-    else if (mime == MIME_JPEG2000)
-    {
-      return MimeType_Jpeg2000;
-    }
-    else if (mime == MIME_JSON)
-    {
-      return MimeType_Json;
-    }
-    else if (mime == MIME_PDF)
-    {
-      return MimeType_Pdf;
-    }
-    else if (mime == MIME_PNG)
-    {
-      return MimeType_Png;
-    }
-    else if (mime == MIME_XML ||
-             mime == MIME_XML_2)
-    {
-      return MimeType_Xml;
-    }
-    else if (mime == MIME_PLAIN_TEXT)
-    {
-      return MimeType_PlainText;
-    }
-    else if (mime == MIME_PAM)
-    {
-      return MimeType_Pam;
-    }
-    else if (mime == MIME_HTML)
-    {
-      return MimeType_Html;
-    }
-    else if (mime == MIME_GZIP)
-    {
-      return MimeType_Gzip;
-    }
-    else if (mime == MIME_JAVASCRIPT)
-    {
-      return MimeType_JavaScript;
-    }
-    else if (mime == MIME_CSS)
-    {
-      return MimeType_Css;
-    }
-    else if (mime == MIME_WEB_ASSEMBLY)
-    {
-      return MimeType_WebAssembly;
-    }
-    else if (mime == MIME_GIF)
-    {
-      return MimeType_Gif;
-    }
-    else if (mime == MIME_ZIP)
-    {
-      return MimeType_Zip;
-    }
-    else if (mime == MIME_NACL)
-    {
-      return MimeType_NaCl;
-    }
-    else if (mime == MIME_PNACL)
-    {
-      return MimeType_PNaCl;
-    }
-    else if (mime == MIME_SVG)
-    {
-      return MimeType_Svg;
-    }
-    else if (mime == MIME_WOFF)
-    {
-      return MimeType_Woff;
-    }
-    else if (mime == MIME_WOFF2)
-    {
-      return MimeType_Woff2;
-    }
-    else if (mime == MIME_DICOM_WEB_JSON)
-    {
-      return MimeType_DicomWebJson;
-    }
-    else if (mime == MIME_DICOM_WEB_XML)
-    {
-      return MimeType_DicomWebXml;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-  
-
-  unsigned int GetBytesPerPixel(PixelFormat format)
-  {
-    switch (format)
-    {
-      case PixelFormat_Grayscale8:
-        return 1;
-
-      case PixelFormat_Grayscale16:
-      case PixelFormat_SignedGrayscale16:
-        return 2;
-
-      case PixelFormat_RGB24:
-        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);
-    }
-  }
-
-
-  bool GetDicomEncoding(Encoding& encoding,
-                        const char* specificCharacterSet)
-  {
-    std::string s = Toolbox::StripSpaces(specificCharacterSet);
-    Toolbox::ToUpperCase(s);
-
-    // 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 2022 IR 6")
-    {
-      encoding = Encoding_Ascii;
-    }
-    else if (s == "ISO_IR 192")
-    {
-      encoding = Encoding_Utf8;
-    }
-    else if (s == "ISO_IR 100" ||
-             s == "ISO 2022 IR 100")
-    {
-      encoding = Encoding_Latin1;
-    }
-    else if (s == "ISO_IR 101" ||
-             s == "ISO 2022 IR 101")
-    {
-      encoding = Encoding_Latin2;
-    }
-    else if (s == "ISO_IR 109" ||
-             s == "ISO 2022 IR 109")
-    {
-      encoding = Encoding_Latin3;
-    }
-    else if (s == "ISO_IR 110" ||
-             s == "ISO 2022 IR 110")
-    {
-      encoding = Encoding_Latin4;
-    }
-    else if (s == "ISO_IR 148" ||
-             s == "ISO 2022 IR 148")
-    {
-      encoding = Encoding_Latin5;
-    }
-    else if (s == "ISO_IR 144" ||
-             s == "ISO 2022 IR 144")
-    {
-      encoding = Encoding_Cyrillic;
-    }
-    else if (s == "ISO_IR 127" ||
-             s == "ISO 2022 IR 127")
-    {
-      encoding = Encoding_Arabic;
-    }
-    else if (s == "ISO_IR 126" ||
-             s == "ISO 2022 IR 126")
-    {
-      encoding = Encoding_Greek;
-    }
-    else if (s == "ISO_IR 138" ||
-             s == "ISO 2022 IR 138")
-    {
-      encoding = Encoding_Hebrew;
-    }
-    else if (s == "ISO_IR 166" ||
-             s == "ISO 2022 IR 166")
-    {
-      encoding = Encoding_Thai;
-    }
-    else if (s == "ISO_IR 13" ||
-             s == "ISO 2022 IR 13")
-    {
-      encoding = Encoding_Japanese;
-    }
-    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;
-    }
-    else if (s == "ISO 2022 IR 149")
-    {
-      encoding = Encoding_Korean;
-    }
-    else if (s == "ISO 2022 IR 87")
-    {
-      encoding = Encoding_JapaneseKanji;
-    }
-    else if (s == "ISO 2022 IR 58")
-    {
-      encoding = Encoding_SimplifiedChinese;
-    }
-    /*
-      else if (s == "ISO 2022 IR 159")
-      {
-      TODO - Supplementary Kanji set
-      }
-    */
-    else
-    {
-      return false;
-    }
-
-    // The encoding was properly detected
-    return true;
-  }
-
-
-  ResourceType GetChildResourceType(ResourceType type)
-  {
-    switch (type)
-    {
-      case ResourceType_Patient:
-        return ResourceType_Study;
-
-      case ResourceType_Study:
-        return ResourceType_Series;
-        
-      case ResourceType_Series:
-        return ResourceType_Instance;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  ResourceType GetParentResourceType(ResourceType type)
-  {
-    switch (type)
-    {
-      case ResourceType_Study:
-        return ResourceType_Patient;
-        
-      case ResourceType_Series:
-        return ResourceType_Study;
-
-      case ResourceType_Instance:
-        return ResourceType_Series;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool IsResourceLevelAboveOrEqual(ResourceType level,
-                                   ResourceType reference)
-  {
-    switch (reference)
-    {
-      case ResourceType_Patient:
-        return (level == ResourceType_Patient);
-
-      case ResourceType_Study:
-        return (level == ResourceType_Patient ||
-                level == ResourceType_Study);
-
-      case ResourceType_Series:
-        return (level == ResourceType_Patient ||
-                level == ResourceType_Study ||
-                level == ResourceType_Series);
-
-      case ResourceType_Instance:
-        return (level == ResourceType_Patient ||
-                level == ResourceType_Study ||
-                level == ResourceType_Series ||
-                level == ResourceType_Instance);
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  DicomModule GetModule(ResourceType type)
-  {
-    switch (type)
-    {
-      case ResourceType_Patient:
-        return DicomModule_Patient;
-
-      case ResourceType_Study:
-        return DicomModule_Study;
-        
-      case ResourceType_Series:
-        return DicomModule_Series;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-
-  const char* GetDicomSpecificCharacterSet(Encoding encoding)
-  {
-    // 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:
-        return "ISO_IR 192";
-
-      case Encoding_Latin1:
-        return "ISO_IR 100";
-
-      case Encoding_Latin2:
-        return "ISO_IR 101";
-
-      case Encoding_Latin3:
-        return "ISO_IR 109";
-
-      case Encoding_Latin4:
-        return "ISO_IR 110";
-
-      case Encoding_Latin5:
-        return "ISO_IR 148";
-
-      case Encoding_Cyrillic:
-        return "ISO_IR 144";
-
-      case Encoding_Arabic:
-        return "ISO_IR 127";
-
-      case Encoding_Greek:
-        return "ISO_IR 126";
-
-      case Encoding_Hebrew:
-        return "ISO_IR 138";
-
-      case Encoding_Japanese:
-        return "ISO_IR 13";
-
-      case Encoding_Chinese:
-        return "GB18030";
-
-      case Encoding_Thai:
-        return "ISO_IR 166";
-
-      case Encoding_Korean:
-        return "ISO 2022 IR 149";
-
-      case Encoding_JapaneseKanji:
-        return "ISO 2022 IR 87";
-
-      case Encoding_SimplifiedChinese:
-        return "ISO 2022 IR 58";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  // This function is autogenerated by the script
-  // "Resources/GenerateErrorCodes.py"
-  HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error)
-  {
-    switch (error)
-    {
-      case ErrorCode_Success:
-        return HttpStatus_200_Ok;
-
-      case ErrorCode_ParameterOutOfRange:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_BadParameterType:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_InexistentItem:
-        return HttpStatus_404_NotFound;
-
-      case ErrorCode_BadRequest:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_UriSyntax:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_InexistentFile:
-        return HttpStatus_404_NotFound;
-
-      case ErrorCode_BadFileFormat:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_UnknownResource:
-        return HttpStatus_404_NotFound;
-
-      case ErrorCode_InexistentTag:
-        return HttpStatus_404_NotFound;
-
-      case ErrorCode_BadJson:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_Unauthorized:
-        return HttpStatus_401_Unauthorized;
-
-      case ErrorCode_NotAcceptable:
-        return HttpStatus_406_NotAcceptable;
-
-      case ErrorCode_DatabaseUnavailable:
-        return HttpStatus_503_ServiceUnavailable;
-
-      case ErrorCode_CreateDicomNotString:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_CreateDicomOverrideTag:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_CreateDicomUseContent:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_CreateDicomNoPayload:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_CreateDicomUseDataUriScheme:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_CreateDicomBadParent:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_CreateDicomParentIsInstance:
-        return HttpStatus_400_BadRequest;
-
-      case ErrorCode_UnsupportedMediaType:
-        return HttpStatus_415_UnsupportedMediaType;
-
-      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;
-  }
-}
-
-
-#include "./Enumerations_TransferSyntaxes.impl.h"
--- a/Core/Enumerations.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,934 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#include <string>
-
-
-// Macro "ORTHANC_FORCE_INLINE" forces a function/method to be inlined
-#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
-
-
-// Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override"
-// and "final" keywords introduced in C++11, to do compile-time
-// checking of virtual methods
-// The __cplusplus macro is broken in Visual Studio up to 15.6 and, in
-// later versions, require the usage of the /Zc:__cplusplus flag
-// We thus use an alternate way of checking for 'override' support
-#ifdef ORTHANC_OVERRIDE_SUPPORTED
-#error ORTHANC_OVERRIDE_SUPPORTED cannot be defined at this point
-#endif 
-
-#if __cplusplus >= 201103L
-#  define ORTHANC_OVERRIDE_SUPPORTED 1
-#else
-#  ifdef _MSC_VER
-#    if _MSC_VER >= 1600
-#      define ORTHANC_OVERRIDE_SUPPORTED 1
-#    endif
-#  endif
-#endif
-
-#if ORTHANC_OVERRIDE_SUPPORTED
-// The override keyword (C++11) is enabled
-#  define ORTHANC_OVERRIDE  override 
-#  define ORTHANC_FINAL     final
-#else
-// The override keyword (C++11) is not available
-#  define ORTHANC_OVERRIDE
-#  define ORTHANC_FINAL
-#endif
-
-namespace Orthanc
-{
-  static const char* const URI_SCHEME_PREFIX_BINARY = "data:application/octet-stream;base64,";
-
-  static const char* const MIME_BINARY = "application/octet-stream";
-  static const char* const MIME_JPEG = "image/jpeg";
-  static const char* const MIME_JSON = "application/json";
-  static const char* const MIME_JSON_UTF8 = "application/json; charset=utf-8";
-  static const char* const MIME_PDF = "application/pdf";
-  static const char* const MIME_PNG = "image/png";
-  static const char* const MIME_XML = "application/xml";
-  static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8";
-
-  /**
-   * "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
-   **/
-  static const char* const MIME_PAM = "image/x-portable-arbitrarymap";
-
-
-  enum MimeType
-  {
-    MimeType_Binary,
-    MimeType_Css,
-    MimeType_Dicom,
-    MimeType_Gif,
-    MimeType_Gzip,
-    MimeType_Html,
-    MimeType_JavaScript,
-    MimeType_Jpeg,
-    MimeType_Jpeg2000,
-    MimeType_Json,
-    MimeType_NaCl,
-    MimeType_PNaCl,
-    MimeType_Pam,
-    MimeType_Pdf,
-    MimeType_PlainText,
-    MimeType_Png,
-    MimeType_Svg,
-    MimeType_WebAssembly,
-    MimeType_Xml,
-    MimeType_Woff,            // Web Open Font Format
-    MimeType_Woff2,
-    MimeType_Zip,
-    MimeType_PrometheusText,  // Prometheus text-based exposition format (for metrics)
-    MimeType_DicomWebJson,
-    MimeType_DicomWebXml
-  };
-
-  
-  enum Endianness
-  {
-    Endianness_Unknown,
-    Endianness_Big,
-    Endianness_Little
-  };
-
-  // This enumeration is autogenerated by the script
-  // "Resources/GenerateErrorCodes.py"
-  enum ErrorCode
-  {
-    ErrorCode_InternalError = -1    /*!< Internal error */,
-    ErrorCode_Success = 0    /*!< Success */,
-    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    /*!< 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 */,
-    ErrorCode_BadRequest = 8    /*!< Bad request */,
-    ErrorCode_NetworkProtocol = 9    /*!< Error in the network protocol */,
-    ErrorCode_SystemCommand = 10    /*!< Error while calling a system command */,
-    ErrorCode_Database = 11    /*!< Error with the database engine */,
-    ErrorCode_UriSyntax = 12    /*!< Badly formatted URI */,
-    ErrorCode_InexistentFile = 13    /*!< Inexistent file */,
-    ErrorCode_CannotWriteFile = 14    /*!< Cannot write to file */,
-    ErrorCode_BadFileFormat = 15    /*!< Bad file format */,
-    ErrorCode_Timeout = 16    /*!< Timeout */,
-    ErrorCode_UnknownResource = 17    /*!< Unknown resource */,
-    ErrorCode_IncompatibleDatabaseVersion = 18    /*!< Incompatible version of the database */,
-    ErrorCode_FullStorage = 19    /*!< The file storage is full */,
-    ErrorCode_CorruptedFile = 20    /*!< Corrupted file (e.g. inconsistent MD5 hash) */,
-    ErrorCode_InexistentTag = 21    /*!< Inexistent tag */,
-    ErrorCode_ReadOnly = 22    /*!< Cannot modify a read-only data structure */,
-    ErrorCode_IncompatibleImageFormat = 23    /*!< Incompatible format of the images */,
-    ErrorCode_IncompatibleImageSize = 24    /*!< Incompatible size of the images */,
-    ErrorCode_SharedLibrary = 25    /*!< Error while using a shared library (plugin) */,
-    ErrorCode_UnknownPluginService = 26    /*!< Plugin invoking an unknown service */,
-    ErrorCode_UnknownDicomTag = 27    /*!< Unknown DICOM tag */,
-    ErrorCode_BadJson = 28    /*!< Cannot parse a JSON document */,
-    ErrorCode_Unauthorized = 29    /*!< Bad credentials were provided to an HTTP request */,
-    ErrorCode_BadFont = 30    /*!< Badly formatted font file */,
-    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_BadGeometry = 38    /*!< Geometry error encountered in Stone */,
-    ErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
-    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 */,
-    ErrorCode_SQLiteStatementAlreadyUsed = 1003    /*!< SQLite: This cached statement is already being referred to */,
-    ErrorCode_SQLiteExecute = 1004    /*!< SQLite: Cannot execute a command */,
-    ErrorCode_SQLiteRollbackWithoutTransaction = 1005    /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */,
-    ErrorCode_SQLiteCommitWithoutTransaction = 1006    /*!< SQLite: Committing a nonexistent transaction */,
-    ErrorCode_SQLiteRegisterFunction = 1007    /*!< SQLite: Unable to register a function */,
-    ErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
-    ErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
-    ErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
-    ErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
-    ErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
-    ErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
-    ErrorCode_SQLiteTransactionBegin = 1015    /*!< SQLite: Cannot start a transaction */,
-    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 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 */,
-    ErrorCode_MakeDirectory = 2008    /*!< Cannot create a directory */,
-    ErrorCode_BadApplicationEntityTitle = 2009    /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */,
-    ErrorCode_NoCFindHandler = 2010    /*!< No request handler factory for DICOM C-FIND SCP */,
-    ErrorCode_NoCMoveHandler = 2011    /*!< No request handler factory for DICOM C-MOVE SCP */,
-    ErrorCode_NoCStoreHandler = 2012    /*!< No request handler factory for DICOM C-STORE SCP */,
-    ErrorCode_NoApplicationEntityFilter = 2013    /*!< No application entity filter */,
-    ErrorCode_NoSopClassOrInstance = 2014    /*!< DicomUserConnection: Unable to find the SOP class and instance */,
-    ErrorCode_NoPresentationContext = 2015    /*!< DicomUserConnection: No acceptable presentation context for modality */,
-    ErrorCode_DicomFindUnavailable = 2016    /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */,
-    ErrorCode_DicomMoveUnavailable = 2017    /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */,
-    ErrorCode_CannotStoreInstance = 2018    /*!< Cannot store an instance */,
-    ErrorCode_CreateDicomNotString = 2019    /*!< Only string values are supported when creating DICOM instances */,
-    ErrorCode_CreateDicomOverrideTag = 2020    /*!< Trying to override a value inherited from a parent module */,
-    ErrorCode_CreateDicomUseContent = 2021    /*!< Use \"Content\" to inject an image into a new DICOM instance */,
-    ErrorCode_CreateDicomNoPayload = 2022    /*!< No payload is present for one instance in the series */,
-    ErrorCode_CreateDicomUseDataUriScheme = 2023    /*!< The payload of the DICOM instance must be specified according to Data URI scheme */,
-    ErrorCode_CreateDicomBadParent = 2024    /*!< Trying to attach a new DICOM instance to an inexistent resource */,
-    ErrorCode_CreateDicomParentIsInstance = 2025    /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */,
-    ErrorCode_CreateDicomParentEncoding = 2026    /*!< Unable to get the encoding of the parent resource */,
-    ErrorCode_UnknownModality = 2027    /*!< Unknown modality */,
-    ErrorCode_BadJobOrdering = 2028    /*!< Bad ordering of filters in a job */,
-    ErrorCode_JsonToLuaTable = 2029    /*!< Cannot convert the given JSON object to a Lua table */,
-    ErrorCode_CannotCreateLua = 2030    /*!< Cannot create the Lua context */,
-    ErrorCode_CannotExecuteLua = 2031    /*!< Cannot execute a Lua command */,
-    ErrorCode_LuaAlreadyExecuted = 2032    /*!< Arguments cannot be pushed after the Lua function is executed */,
-    ErrorCode_LuaBadOutput = 2033    /*!< The Lua function does not give the expected number of outputs */,
-    ErrorCode_NotLuaPredicate = 2034    /*!< The Lua function is not a predicate (only true/false outputs allowed) */,
-    ErrorCode_LuaReturnsNoString = 2035    /*!< The Lua function does not return a string */,
-    ErrorCode_StorageAreaAlreadyRegistered = 2036    /*!< Another plugin has already registered a custom storage area */,
-    ErrorCode_DatabaseBackendAlreadyRegistered = 2037    /*!< Another plugin has already registered a custom database back-end */,
-    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_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
-    ErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
-    ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
-    ErrorCode_START_PLUGINS = 1000000
-  };
-
-  // This enumeration is autogenerated by the script
-  // "Resources/GenerateTransferSyntaxes.py"
-  enum DicomTransferSyntax
-  {
-    DicomTransferSyntax_LittleEndianImplicit    /*!< Implicit VR Little Endian */,
-    DicomTransferSyntax_LittleEndianExplicit    /*!< Explicit VR Little Endian */,
-    DicomTransferSyntax_DeflatedLittleEndianExplicit    /*!< Deflated Explicit VR Little Endian */,
-    DicomTransferSyntax_BigEndianExplicit    /*!< Explicit VR Big Endian */,
-    DicomTransferSyntax_JPEGProcess1    /*!< JPEG Baseline (process 1, lossy) */,
-    DicomTransferSyntax_JPEGProcess2_4    /*!< JPEG Extended Sequential (processes 2 & 4) */,
-    DicomTransferSyntax_JPEGProcess3_5    /*!< JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess6_8    /*!< JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit) */,
-    DicomTransferSyntax_JPEGProcess7_9    /*!< JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess10_12    /*!< JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit) */,
-    DicomTransferSyntax_JPEGProcess11_13    /*!< JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess14    /*!< JPEG Lossless, Nonhierarchical with any selection value (process 14) */,
-    DicomTransferSyntax_JPEGProcess15    /*!< JPEG Lossless with any selection value, arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess16_18    /*!< JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit) */,
-    DicomTransferSyntax_JPEGProcess17_19    /*!< JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess20_22    /*!< JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit) */,
-    DicomTransferSyntax_JPEGProcess21_23    /*!< JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess24_26    /*!< JPEG Full Progression, Hierarchical (lossy, 8/12 bit) */,
-    DicomTransferSyntax_JPEGProcess25_27    /*!< JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess28    /*!< JPEG Lossless, Hierarchical */,
-    DicomTransferSyntax_JPEGProcess29    /*!< JPEG Lossless, Hierarchical, arithmetic coding */,
-    DicomTransferSyntax_JPEGProcess14SV1    /*!< JPEG Lossless, Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1]) */,
-    DicomTransferSyntax_JPEGLSLossless    /*!< JPEG-LS (lossless) */,
-    DicomTransferSyntax_JPEGLSLossy    /*!< JPEG-LS (lossy or near-lossless) */,
-    DicomTransferSyntax_JPEG2000LosslessOnly    /*!< JPEG 2000 (lossless) */,
-    DicomTransferSyntax_JPEG2000    /*!< JPEG 2000 (lossless or lossy) */,
-    DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly    /*!< JPEG 2000 part 2 multicomponent extensions (lossless) */,
-    DicomTransferSyntax_JPEG2000Multicomponent    /*!< JPEG 2000 part 2 multicomponent extensions (lossless or lossy) */,
-    DicomTransferSyntax_JPIPReferenced    /*!< JPIP Referenced */,
-    DicomTransferSyntax_JPIPReferencedDeflate    /*!< JPIP Referenced Deflate */,
-    DicomTransferSyntax_MPEG2MainProfileAtMainLevel    /*!< MPEG2 Main Profile / Main Level */,
-    DicomTransferSyntax_MPEG2MainProfileAtHighLevel    /*!< MPEG2 Main Profile / High Level */,
-    DicomTransferSyntax_MPEG4HighProfileLevel4_1    /*!< MPEG4 AVC/H.264 High Profile / Level 4.1 */,
-    DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1    /*!< MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1 */,
-    DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo    /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video */,
-    DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo    /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video */,
-    DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2    /*!< MPEG4 AVC/H.264 Stereo High Profile / Level 4.2 */,
-    DicomTransferSyntax_HEVCMainProfileLevel5_1    /*!< HEVC/H.265 Main Profile / Level 5.1 */,
-    DicomTransferSyntax_HEVCMain10ProfileLevel5_1    /*!< HEVC/H.265 Main 10 Profile / Level 5.1 */,
-    DicomTransferSyntax_RLELossless    /*!< RLE - Run Length Encoding (lossless) */,
-    DicomTransferSyntax_RFC2557MimeEncapsulation    /*!< RFC 2557 MIME Encapsulation */,
-    DicomTransferSyntax_XML    /*!< XML Encoding */
-  };
-
-
-  /**
-   * {summary}{The memory layout of the pixels (resp. voxels) of a 2D (resp. 3D) image.}
-   **/
-  enum PixelFormat
-  {
-    /**
-     * {summary}{Color image in RGB24 format.}
-     * {description}{This format describes a color image. The pixels are stored in 3
-     * consecutive bytes. The memory layout is RGB.}
-     **/
-    PixelFormat_RGB24 = 1,
-
-    /**
-     * {summary}{Color image in RGBA32 format.}
-     * {description}{This format describes a color image. The pixels are stored in 4
-     * consecutive bytes. The memory layout is RGBA.}
-     **/
-    PixelFormat_RGBA32 = 2,
-
-    /**
-     * {summary}{Graylevel 8bpp image.}
-     * {description}{The image is graylevel. Each pixel is unsigned and stored in one byte.}
-     **/
-    PixelFormat_Grayscale8 = 3,
-      
-    /**
-     * {summary}{Graylevel, unsigned 16bpp image.}
-     * {description}{The image is graylevel. Each pixel is unsigned and stored in two bytes.}
-     **/
-    PixelFormat_Grayscale16 = 4,
-      
-    /**
-     * {summary}{Graylevel, signed 16bpp image.}
-     * {description}{The image is graylevel. Each pixel is signed and stored in two bytes.}
-     **/
-    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 8 bytes.}
-     **/
-    PixelFormat_Grayscale64 = 10
-  };
-
-
-  /**
-   * {summary}{The extraction mode specifies the way the values of the pixels are scaled when downloading a 2D image.}
-   **/
-  enum ImageExtractionMode
-  {
-    /**
-     * {summary}{Rescaled to 8bpp.}
-     * {description}{The minimum value of the image is set to 0, and its maximum value is set to 255.}
-     **/
-    ImageExtractionMode_Preview = 1,
-
-    /**
-     * {summary}{Truncation to the [0, 255] range.}
-     **/
-    ImageExtractionMode_UInt8 = 2,
-
-    /**
-     * {summary}{Truncation to the [0, 65535] range.}
-     **/
-    ImageExtractionMode_UInt16 = 3,
-
-    /**
-     * {summary}{Truncation to the [-32768, 32767] range.}
-     **/
-    ImageExtractionMode_Int16 = 4
-  };
-
-
-  /**
-   * Most common, non-joke and non-experimental HTTP status codes
-   * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
-   **/
-  enum HttpStatus
-  {
-    HttpStatus_None = -1,
-
-    // 1xx Informational
-    HttpStatus_100_Continue = 100,
-    HttpStatus_101_SwitchingProtocols = 101,
-    HttpStatus_102_Processing = 102,
-
-    // 2xx Success
-    HttpStatus_200_Ok = 200,
-    HttpStatus_201_Created = 201,
-    HttpStatus_202_Accepted = 202,
-    HttpStatus_203_NonAuthoritativeInformation = 203,
-    HttpStatus_204_NoContent = 204,
-    HttpStatus_205_ResetContent = 205,
-    HttpStatus_206_PartialContent = 206,
-    HttpStatus_207_MultiStatus = 207,
-    HttpStatus_208_AlreadyReported = 208,
-    HttpStatus_226_IMUsed = 226,
-
-    // 3xx Redirection
-    HttpStatus_300_MultipleChoices = 300,
-    HttpStatus_301_MovedPermanently = 301,
-    HttpStatus_302_Found = 302,
-    HttpStatus_303_SeeOther = 303,
-    HttpStatus_304_NotModified = 304,
-    HttpStatus_305_UseProxy = 305,
-    HttpStatus_307_TemporaryRedirect = 307,
-
-    // 4xx Client Error
-    HttpStatus_400_BadRequest = 400,
-    HttpStatus_401_Unauthorized = 401,
-    HttpStatus_402_PaymentRequired = 402,
-    HttpStatus_403_Forbidden = 403,
-    HttpStatus_404_NotFound = 404,
-    HttpStatus_405_MethodNotAllowed = 405,
-    HttpStatus_406_NotAcceptable = 406,
-    HttpStatus_407_ProxyAuthenticationRequired = 407,
-    HttpStatus_408_RequestTimeout = 408,
-    HttpStatus_409_Conflict = 409,
-    HttpStatus_410_Gone = 410,
-    HttpStatus_411_LengthRequired = 411,
-    HttpStatus_412_PreconditionFailed = 412,
-    HttpStatus_413_RequestEntityTooLarge = 413,
-    HttpStatus_414_RequestUriTooLong = 414,
-    HttpStatus_415_UnsupportedMediaType = 415,
-    HttpStatus_416_RequestedRangeNotSatisfiable = 416,
-    HttpStatus_417_ExpectationFailed = 417,
-    HttpStatus_422_UnprocessableEntity = 422,
-    HttpStatus_423_Locked = 423,
-    HttpStatus_424_FailedDependency = 424,
-    HttpStatus_426_UpgradeRequired = 426,
-
-    // 5xx Server Error
-    HttpStatus_500_InternalServerError = 500,
-    HttpStatus_501_NotImplemented = 501,
-    HttpStatus_502_BadGateway = 502,
-    HttpStatus_503_ServiceUnavailable = 503,
-    HttpStatus_504_GatewayTimeout = 504,
-    HttpStatus_505_HttpVersionNotSupported = 505,
-    HttpStatus_506_VariantAlsoNegotiates = 506,
-    HttpStatus_507_InsufficientStorage = 507,
-    HttpStatus_509_BandwidthLimitExceeded = 509,
-    HttpStatus_510_NotExtended = 510
-  };
-
-
-  enum HttpMethod
-  {
-    HttpMethod_Get = 0,
-    HttpMethod_Post = 1,
-    HttpMethod_Delete = 2,
-    HttpMethod_Put = 3
-  };
-
-
-  enum ImageFormat
-  {
-    ImageFormat_Png = 1
-  };
-
-
-  // https://en.wikipedia.org/wiki/HTTP_compression
-  enum HttpCompression
-  {
-    HttpCompression_None,
-    HttpCompression_Deflate,
-    HttpCompression_Gzip
-  };
-
-
-  // Specific Character Sets
-  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
-  enum Encoding
-  {
-    Encoding_Ascii,
-    Encoding_Utf8,
-    Encoding_Latin1,
-    Encoding_Latin2,
-    Encoding_Latin3,
-    Encoding_Latin4,
-    Encoding_Latin5,                        // Turkish
-    Encoding_Cyrillic,
-    Encoding_Windows1251,                   // Windows-1251 (commonly used for Cyrillic)
-    Encoding_Arabic,
-    Encoding_Greek,
-    Encoding_Hebrew,
-    Encoding_Thai,                          // TIS 620-2533
-    Encoding_Japanese,                      // JIS X 0201 (Shift JIS): Katakana
-    Encoding_Chinese,                       // GB18030 - Chinese simplified
-    Encoding_JapaneseKanji,                 // Multibyte - JIS X 0208: Kanji
-    //Encoding_JapaneseSupplementaryKanji,  // Multibyte - JIS X 0212: Supplementary Kanji set
-    Encoding_Korean,                        // Multibyte - KS X 1001: Hangul and Hanja
-    Encoding_SimplifiedChinese              // ISO 2022 IR 58
-  };
-
-
-  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.2
-  enum PhotometricInterpretation
-  {
-    PhotometricInterpretation_ARGB,  // Retired
-    PhotometricInterpretation_CMYK,  // Retired
-    PhotometricInterpretation_HSV,   // Retired
-    PhotometricInterpretation_Monochrome1,
-    PhotometricInterpretation_Monochrome2,
-    PhotometricInterpretation_Palette,
-    PhotometricInterpretation_RGB,
-    PhotometricInterpretation_YBRFull,
-    PhotometricInterpretation_YBRFull422,
-    PhotometricInterpretation_YBRPartial420,
-    PhotometricInterpretation_YBRPartial422,
-    PhotometricInterpretation_YBR_ICT,
-    PhotometricInterpretation_YBR_RCT,
-    PhotometricInterpretation_Unknown
-  };
-
-  enum DicomModule
-  {
-    DicomModule_Patient,
-    DicomModule_Study,
-    DicomModule_Series,
-    DicomModule_Instance,
-    DicomModule_Image
-  };
-
-  enum RequestOrigin
-  {
-    RequestOrigin_Unknown,
-    RequestOrigin_DicomProtocol,
-    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_Vitrea,
-    ModalityManufacturer_GE
-  };
-
-  enum DicomRequestType
-  {
-    DicomRequestType_Echo,
-    DicomRequestType_Find,
-    DicomRequestType_Get,
-    DicomRequestType_Move,
-    DicomRequestType_Store,
-    DicomRequestType_NAction,
-    DicomRequestType_NEventReport
-  };
-
-  enum TransferSyntax
-  {
-    TransferSyntax_Deflated,
-    TransferSyntax_Jpeg,
-    TransferSyntax_Jpeg2000,
-    TransferSyntax_JpegLossless,
-    TransferSyntax_Jpip,
-    TransferSyntax_Mpeg2,
-    TransferSyntax_Mpeg4,  // New in Orthanc 1.6.0
-    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
-  };
-
-  
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
-  enum StorageCommitmentFailureReason
-  {
-    StorageCommitmentFailureReason_Success = 0,
-
-    // A general failure in processing the operation was encountered
-    StorageCommitmentFailureReason_ProcessingFailure = 0x0110,
-
-    // One or more of the elements in the Referenced SOP Instance
-    // Sequence was not available
-    StorageCommitmentFailureReason_NoSuchObjectInstance = 0x0112,
-
-    // The SCP does not currently have enough resources to store the
-    // requested SOP Instance(s)
-    StorageCommitmentFailureReason_ResourceLimitation = 0x0213,
-
-    // Storage Commitment has been requested for a SOP Instance with a
-    // SOP Class that is not supported by the SCP
-    StorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 0x0122,
-
-    // The SOP Class of an element in the Referenced SOP Instance
-    // Sequence did not correspond to the SOP class registered for
-    // this SOP Instance at the SCP
-    StorageCommitmentFailureReason_ClassInstanceConflict = 0x0119,
-
-    // The Transaction UID of the Storage Commitment Request is already in use
-    StorageCommitmentFailureReason_DuplicateTransactionUID = 0x0131
-  };
-
-
-  enum DicomAssociationRole
-  {
-    DicomAssociationRole_Default,
-    DicomAssociationRole_Scu,
-    DicomAssociationRole_Scp
-  };
-
-
-  /**
-   * WARNING: Do not change the explicit values in the enumerations
-   * below this point. This would result in incompatible databases
-   * between versions of Orthanc!
-   **/
-
-  enum CompressionType
-  {
-    /**
-     * Buffer/file that is stored as-is, in a raw fashion, without
-     * compression.
-     **/
-    CompressionType_None = 1,
-
-    /**
-     * Buffer that is compressed using the "deflate" algorithm (RFC
-     * 1951), wrapped inside the zlib data format (RFC 1950), prefixed
-     * with a "uint64_t" (8 bytes) that encodes the size of the
-     * uncompressed buffer. If the compressed buffer is empty, its
-     * represents an empty uncompressed buffer. This format is
-     * internal to Orthanc. If the 8 first bytes are skipped AND the
-     * buffer is non-empty, the buffer is compatible with the
-     * "deflate" HTTP compression.
-     **/
-    CompressionType_ZlibWithSize = 2
-  };
-
-  enum FileContentType
-  {
-    // If you add a value below, insert it in "PluginStorageArea" in
-    // the file "Plugins/Engine/OrthancPlugins.cpp"
-    FileContentType_Unknown = 0,
-    FileContentType_Dicom = 1,
-    FileContentType_DicomAsJson = 2,
-
-    // Make sure that the value "65535" can be stored into this enumeration
-    FileContentType_StartUser = 1024,
-    FileContentType_EndUser = 65535
-  };
-
-  enum ResourceType
-  {
-    ResourceType_Patient = 1,
-    ResourceType_Study = 2,
-    ResourceType_Series = 3,
-    ResourceType_Instance = 4
-  };
-
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(ErrorCode code);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(HttpMethod method);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(HttpStatus status);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(ResourceType type);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(ImageFormat format);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(Encoding encoding);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(PhotometricInterpretation photometric);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(RequestOrigin origin);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(PixelFormat format);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(ModalityManufacturer manufacturer);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(DicomRequestType type);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(TransferSyntax syntax);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(DicomVersion version);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(ValueRepresentation vr);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(JobState state);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(MimeType mime);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(Endianness endianness);
-
-  ORTHANC_PUBLIC
-  const char* EnumerationToString(StorageCommitmentFailureReason reason);
-
-  ORTHANC_PUBLIC
-  Encoding StringToEncoding(const char* encoding);
-
-  ORTHANC_PUBLIC
-  ResourceType StringToResourceType(const char* type);
-
-  ORTHANC_PUBLIC
-  ImageFormat StringToImageFormat(const char* format);
-
-  ORTHANC_PUBLIC
-  ValueRepresentation StringToValueRepresentation(const std::string& vr,
-                                                  bool throwIfUnsupported);
-
-  ORTHANC_PUBLIC
-  PhotometricInterpretation StringToPhotometricInterpretation(const char* value);
-
-  ORTHANC_PUBLIC
-  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
-
-  ORTHANC_PUBLIC
-  DicomVersion StringToDicomVersion(const std::string& version);
-
-  ORTHANC_PUBLIC
-  JobState StringToJobState(const std::string& state);
-  
-  ORTHANC_PUBLIC
-  RequestOrigin StringToRequestOrigin(const std::string& origin);
-
-  ORTHANC_PUBLIC
-  MimeType StringToMimeType(const std::string& mime);
-  
-  ORTHANC_PUBLIC
-  unsigned int GetBytesPerPixel(PixelFormat format);
-
-  ORTHANC_PUBLIC
-  bool GetDicomEncoding(Encoding& encoding,
-                        const char* specificCharacterSet);
-
-  ORTHANC_PUBLIC
-  ResourceType GetChildResourceType(ResourceType type);
-
-  ORTHANC_PUBLIC
-  ResourceType GetParentResourceType(ResourceType type);
-
-  ORTHANC_PUBLIC
-  bool IsResourceLevelAboveOrEqual(ResourceType level,
-                                   ResourceType reference);
-
-  ORTHANC_PUBLIC
-  DicomModule GetModule(ResourceType type);
-
-  ORTHANC_PUBLIC
-  const char* GetDicomSpecificCharacterSet(Encoding encoding);
-
-  ORTHANC_PUBLIC
-  HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error);
-
-  ORTHANC_PUBLIC
-  bool IsUserContentType(FileContentType type);
-
-  ORTHANC_PUBLIC
-  bool IsBinaryValueRepresentation(ValueRepresentation vr);
-  
-  ORTHANC_PUBLIC
-  Encoding GetDefaultDicomEncoding();
-
-  ORTHANC_PUBLIC
-  void SetDefaultDicomEncoding(Encoding encoding);
-
-  ORTHANC_PUBLIC
-  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax);
-
-  ORTHANC_PUBLIC
-  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax);
-
-  ORTHANC_PUBLIC
-  bool LookupTransferSyntax(DicomTransferSyntax& target,
-                            const std::string& uid);
-}
--- a/Core/Enumerations_TransferSyntaxes.impl.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,566 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
-
-namespace Orthanc
-{
-  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax)
-  {
-    switch (syntax)
-    {
-      case DicomTransferSyntax_LittleEndianImplicit:
-        return "1.2.840.10008.1.2";
-
-      case DicomTransferSyntax_LittleEndianExplicit:
-        return "1.2.840.10008.1.2.1";
-
-      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
-        return "1.2.840.10008.1.2.1.99";
-
-      case DicomTransferSyntax_BigEndianExplicit:
-        return "1.2.840.10008.1.2.2";
-
-      case DicomTransferSyntax_JPEGProcess1:
-        return "1.2.840.10008.1.2.4.50";
-
-      case DicomTransferSyntax_JPEGProcess2_4:
-        return "1.2.840.10008.1.2.4.51";
-
-      case DicomTransferSyntax_JPEGProcess3_5:
-        return "1.2.840.10008.1.2.4.52";
-
-      case DicomTransferSyntax_JPEGProcess6_8:
-        return "1.2.840.10008.1.2.4.53";
-
-      case DicomTransferSyntax_JPEGProcess7_9:
-        return "1.2.840.10008.1.2.4.54";
-
-      case DicomTransferSyntax_JPEGProcess10_12:
-        return "1.2.840.10008.1.2.4.55";
-
-      case DicomTransferSyntax_JPEGProcess11_13:
-        return "1.2.840.10008.1.2.4.56";
-
-      case DicomTransferSyntax_JPEGProcess14:
-        return "1.2.840.10008.1.2.4.57";
-
-      case DicomTransferSyntax_JPEGProcess15:
-        return "1.2.840.10008.1.2.4.58";
-
-      case DicomTransferSyntax_JPEGProcess16_18:
-        return "1.2.840.10008.1.2.4.59";
-
-      case DicomTransferSyntax_JPEGProcess17_19:
-        return "1.2.840.10008.1.2.4.60";
-
-      case DicomTransferSyntax_JPEGProcess20_22:
-        return "1.2.840.10008.1.2.4.61";
-
-      case DicomTransferSyntax_JPEGProcess21_23:
-        return "1.2.840.10008.1.2.4.62";
-
-      case DicomTransferSyntax_JPEGProcess24_26:
-        return "1.2.840.10008.1.2.4.63";
-
-      case DicomTransferSyntax_JPEGProcess25_27:
-        return "1.2.840.10008.1.2.4.64";
-
-      case DicomTransferSyntax_JPEGProcess28:
-        return "1.2.840.10008.1.2.4.65";
-
-      case DicomTransferSyntax_JPEGProcess29:
-        return "1.2.840.10008.1.2.4.66";
-
-      case DicomTransferSyntax_JPEGProcess14SV1:
-        return "1.2.840.10008.1.2.4.70";
-
-      case DicomTransferSyntax_JPEGLSLossless:
-        return "1.2.840.10008.1.2.4.80";
-
-      case DicomTransferSyntax_JPEGLSLossy:
-        return "1.2.840.10008.1.2.4.81";
-
-      case DicomTransferSyntax_JPEG2000LosslessOnly:
-        return "1.2.840.10008.1.2.4.90";
-
-      case DicomTransferSyntax_JPEG2000:
-        return "1.2.840.10008.1.2.4.91";
-
-      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
-        return "1.2.840.10008.1.2.4.92";
-
-      case DicomTransferSyntax_JPEG2000Multicomponent:
-        return "1.2.840.10008.1.2.4.93";
-
-      case DicomTransferSyntax_JPIPReferenced:
-        return "1.2.840.10008.1.2.4.94";
-
-      case DicomTransferSyntax_JPIPReferencedDeflate:
-        return "1.2.840.10008.1.2.4.95";
-
-      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
-        return "1.2.840.10008.1.2.4.100";
-
-      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
-        return "1.2.840.10008.1.2.4.101";
-
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
-        return "1.2.840.10008.1.2.4.102";
-
-      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
-        return "1.2.840.10008.1.2.4.103";
-
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
-        return "1.2.840.10008.1.2.4.104";
-
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
-        return "1.2.840.10008.1.2.4.105";
-
-      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
-        return "1.2.840.10008.1.2.4.106";
-
-      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
-        return "1.2.840.10008.1.2.4.107";
-
-      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
-        return "1.2.840.10008.1.2.4.108";
-
-      case DicomTransferSyntax_RLELossless:
-        return "1.2.840.10008.1.2.5";
-
-      case DicomTransferSyntax_RFC2557MimeEncapsulation:
-        return "1.2.840.10008.1.2.6.1";
-
-      case DicomTransferSyntax_XML:
-        return "1.2.840.10008.1.2.6.2";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax)
-  {
-    switch (syntax)
-    {
-      case DicomTransferSyntax_LittleEndianImplicit:
-        return false;
-
-      case DicomTransferSyntax_LittleEndianExplicit:
-        return false;
-
-      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
-        return false;
-
-      case DicomTransferSyntax_BigEndianExplicit:
-        return false;
-
-      case DicomTransferSyntax_JPEGProcess1:
-        return false;
-
-      case DicomTransferSyntax_JPEGProcess2_4:
-        return false;
-
-      case DicomTransferSyntax_JPEGProcess3_5:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess6_8:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess7_9:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess10_12:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess11_13:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess14:
-        return false;
-
-      case DicomTransferSyntax_JPEGProcess15:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess16_18:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess17_19:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess20_22:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess21_23:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess24_26:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess25_27:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess28:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess29:
-        return true;
-
-      case DicomTransferSyntax_JPEGProcess14SV1:
-        return false;
-
-      case DicomTransferSyntax_JPEGLSLossless:
-        return false;
-
-      case DicomTransferSyntax_JPEGLSLossy:
-        return false;
-
-      case DicomTransferSyntax_JPEG2000LosslessOnly:
-        return false;
-
-      case DicomTransferSyntax_JPEG2000:
-        return false;
-
-      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
-        return false;
-
-      case DicomTransferSyntax_JPEG2000Multicomponent:
-        return false;
-
-      case DicomTransferSyntax_JPIPReferenced:
-        return false;
-
-      case DicomTransferSyntax_JPIPReferencedDeflate:
-        return false;
-
-      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
-        return false;
-
-      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
-        return false;
-
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
-        return false;
-
-      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
-        return false;
-
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
-        return false;
-
-      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
-        return false;
-
-      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
-        return false;
-
-      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
-        return false;
-
-      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
-        return false;
-
-      case DicomTransferSyntax_RLELossless:
-        return false;
-
-      case DicomTransferSyntax_RFC2557MimeEncapsulation:
-        return true;
-
-      case DicomTransferSyntax_XML:
-        return true;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool LookupTransferSyntax(DicomTransferSyntax& target,
-                            const std::string& uid)
-  {
-    if (uid == "1.2.840.10008.1.2")
-    {
-      target = DicomTransferSyntax_LittleEndianImplicit;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.1")
-    {
-      target = DicomTransferSyntax_LittleEndianExplicit;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.1.99")
-    {
-      target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.2")
-    {
-      target = DicomTransferSyntax_BigEndianExplicit;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.50")
-    {
-      target = DicomTransferSyntax_JPEGProcess1;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.51")
-    {
-      target = DicomTransferSyntax_JPEGProcess2_4;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.52")
-    {
-      target = DicomTransferSyntax_JPEGProcess3_5;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.53")
-    {
-      target = DicomTransferSyntax_JPEGProcess6_8;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.54")
-    {
-      target = DicomTransferSyntax_JPEGProcess7_9;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.55")
-    {
-      target = DicomTransferSyntax_JPEGProcess10_12;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.56")
-    {
-      target = DicomTransferSyntax_JPEGProcess11_13;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.57")
-    {
-      target = DicomTransferSyntax_JPEGProcess14;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.58")
-    {
-      target = DicomTransferSyntax_JPEGProcess15;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.59")
-    {
-      target = DicomTransferSyntax_JPEGProcess16_18;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.60")
-    {
-      target = DicomTransferSyntax_JPEGProcess17_19;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.61")
-    {
-      target = DicomTransferSyntax_JPEGProcess20_22;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.62")
-    {
-      target = DicomTransferSyntax_JPEGProcess21_23;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.63")
-    {
-      target = DicomTransferSyntax_JPEGProcess24_26;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.64")
-    {
-      target = DicomTransferSyntax_JPEGProcess25_27;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.65")
-    {
-      target = DicomTransferSyntax_JPEGProcess28;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.66")
-    {
-      target = DicomTransferSyntax_JPEGProcess29;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.70")
-    {
-      target = DicomTransferSyntax_JPEGProcess14SV1;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.80")
-    {
-      target = DicomTransferSyntax_JPEGLSLossless;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.81")
-    {
-      target = DicomTransferSyntax_JPEGLSLossy;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.90")
-    {
-      target = DicomTransferSyntax_JPEG2000LosslessOnly;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.91")
-    {
-      target = DicomTransferSyntax_JPEG2000;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.92")
-    {
-      target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.93")
-    {
-      target = DicomTransferSyntax_JPEG2000Multicomponent;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.94")
-    {
-      target = DicomTransferSyntax_JPIPReferenced;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.95")
-    {
-      target = DicomTransferSyntax_JPIPReferencedDeflate;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.100")
-    {
-      target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.101")
-    {
-      target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.102")
-    {
-      target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.103")
-    {
-      target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.104")
-    {
-      target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.105")
-    {
-      target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.106")
-    {
-      target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.107")
-    {
-      target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.4.108")
-    {
-      target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.5")
-    {
-      target = DicomTransferSyntax_RLELossless;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.6.1")
-    {
-      target = DicomTransferSyntax_RFC2557MimeEncapsulation;
-      return true;
-    }
-    
-    if (uid == "1.2.840.10008.1.2.6.2")
-    {
-      target = DicomTransferSyntax_XML;
-      return true;
-    }
-    
-    return false;
-  }
-}
--- a/Core/FileBuffer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "FileBuffer.h"
-
-#include "TemporaryFile.h"
-#include "OrthancException.h"
-
-#include <boost/filesystem/fstream.hpp>
-
-
-namespace Orthanc
-{
-  class FileBuffer::PImpl
-  {
-  private:
-    TemporaryFile                file_;
-    boost::filesystem::ofstream  stream_;
-    bool                         isWriting_;
-
-  public:
-    PImpl() :
-      isWriting_(true)
-    {
-      stream_.open(file_.GetPath(), std::ofstream::out | std::ofstream::binary);
-      if (!stream_.good())
-      {
-        throw OrthancException(ErrorCode_CannotWriteFile);
-      }
-    }
-
-    ~PImpl()
-    {
-      if (isWriting_)
-      {
-        stream_.close();
-      }
-    }
-
-    void Append(const char* buffer,
-                size_t size)
-    {
-      if (!isWriting_)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      if (size > 0)
-      {
-        stream_.write(buffer, size);
-        if (!stream_.good())
-        {
-          stream_.close();
-          throw OrthancException(ErrorCode_FileStorageCannotWrite);
-        }
-      }
-    }
-
-    void Read(std::string& target)
-    {
-      if (isWriting_)
-      {
-        stream_.close();
-        isWriting_ = false;
-      }
-
-      file_.Read(target);
-    }
-  };
-
-    
-  FileBuffer::FileBuffer() :
-    pimpl_(new PImpl)
-  {
-  }
-
-
-  void FileBuffer::Append(const char* buffer,
-                          size_t size)
-  {
-    assert(pimpl_.get() != NULL);
-    pimpl_->Append(buffer, size);
-  }
-
-
-  void FileBuffer::Read(std::string& target)
-  {
-    assert(pimpl_.get() != NULL);
-    pimpl_->Read(target);
-  }
-}
--- a/Core/FileBuffer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#if !defined(ORTHANC_SANDBOXED)
-#  error The macro ORTHANC_SANDBOXED must be defined
-#endif
-
-#if ORTHANC_SANDBOXED == 1
-#  error The class FileBuffer cannot be used in sandboxed environments
-#endif
-
-#include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
-
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC FileBuffer : public boost::noncopyable
-  {
-  private:
-    class PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-  public:
-    FileBuffer();
-
-    void Append(const char* buffer,
-                size_t size);
-
-    void Read(std::string& target);
-  };
-}
--- a/Core/FileStorage/FileInfo.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,132 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <stdint.h>
-#include "../Enumerations.h"
-
-namespace Orthanc
-{
-  struct FileInfo
-  {
-  private:
-    std::string uuid_;
-    FileContentType contentType_;
-
-    uint64_t uncompressedSize_;
-    std::string uncompressedMD5_;
-
-    CompressionType compressionType_;
-    uint64_t compressedSize_;
-    std::string compressedMD5_;
-
-  public:
-    FileInfo()
-    {
-    }
-
-    /**
-     * Constructor for an uncompressed attachment.
-     **/
-    FileInfo(const std::string& uuid,
-             FileContentType contentType,
-             uint64_t size,
-             const std::string& md5) :
-      uuid_(uuid),
-      contentType_(contentType),
-      uncompressedSize_(size),
-      uncompressedMD5_(md5),
-      compressionType_(CompressionType_None),
-      compressedSize_(size),
-      compressedMD5_(md5)
-    {
-    }
-
-    /**
-     * Constructor for a compressed attachment.
-     **/
-    FileInfo(const std::string& uuid,
-             FileContentType contentType,
-             uint64_t uncompressedSize,
-             const std::string& uncompressedMD5,
-             CompressionType compressionType,
-             uint64_t compressedSize,
-             const std::string& compressedMD5) :
-      uuid_(uuid),
-      contentType_(contentType),
-      uncompressedSize_(uncompressedSize),
-      uncompressedMD5_(uncompressedMD5),
-      compressionType_(compressionType),
-      compressedSize_(compressedSize),
-      compressedMD5_(compressedMD5)
-    {
-    }
-
-    const std::string& GetUuid() const
-    {
-      return uuid_;
-    }
-
-    FileContentType GetContentType() const
-    {
-      return contentType_;
-    }
-
-    uint64_t GetUncompressedSize() const
-    {
-      return uncompressedSize_;
-    }
-
-    CompressionType GetCompressionType() const
-    {
-      return compressionType_;
-    }
-
-    uint64_t GetCompressedSize() const
-    {
-      return compressedSize_;
-    }
-
-    const std::string& GetCompressedMD5() const
-    {
-      return compressedMD5_;
-    }
-
-    const std::string& GetUncompressedMD5() const
-    {
-      return uncompressedMD5_;
-    }
-  };
-}
--- a/Core/FileStorage/FilesystemStorage.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,274 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "FilesystemStorage.h"
-
-// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
-// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-#include "../SystemToolbox.h"
-
-#include <boost/filesystem/fstream.hpp>
-
-
-static std::string ToString(const boost::filesystem::path& p)
-{
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-  return p.filename().string();
-#else
-  return p.filename();
-#endif
-}
-
-
-namespace Orthanc
-{
-  boost::filesystem::path FilesystemStorage::GetPath(const std::string& uuid) const
-  {
-    namespace fs = boost::filesystem;
-
-    if (!Toolbox::IsUuid(uuid))
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    fs::path path = root_;
-
-    path /= std::string(&uuid[0], &uuid[2]);
-    path /= std::string(&uuid[2], &uuid[4]);
-    path /= uuid;
-
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-    path.make_preferred();
-#endif
-
-    return path;
-  }
-
-  FilesystemStorage::FilesystemStorage(std::string root)
-  {
-    //root_ = boost::filesystem::absolute(root).string();
-    root_ = 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)
-  {
-    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
-              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
-
-    boost::filesystem::path path;
-    
-    path = GetPath(uuid);
-
-    if (boost::filesystem::exists(path))
-    {
-      // Extremely unlikely case: This Uuid has already been created
-      // in the past.
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (boost::filesystem::exists(path.parent_path()))
-    {
-      if (!boost::filesystem::is_directory(path.parent_path()))
-      {
-        throw OrthancException(ErrorCode_DirectoryOverFile);
-      }
-    }
-    else
-    {
-      if (!boost::filesystem::create_directories(path.parent_path()))
-      {
-        throw OrthancException(ErrorCode_FileStorageCannotWrite);
-      }
-    }
-
-    SystemToolbox::WriteFile(content, size, path.string());
-  }
-
-
-  void FilesystemStorage::Read(std::string& content,
-                               const std::string& uuid,
-                               FileContentType type)
-  {
-    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
-              << "\" content type";
-
-    content.clear();
-    SystemToolbox::ReadFile(content, GetPath(uuid).string());
-  }
-
-
-  uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const
-  {
-    boost::filesystem::path path = GetPath(uuid);
-    return boost::filesystem::file_size(path);
-  }
-
-
-
-  void FilesystemStorage::ListAllFiles(std::set<std::string>& result) const
-  {
-    namespace fs = boost::filesystem;
-
-    result.clear();
-
-    if (fs::exists(root_) && fs::is_directory(root_))
-    {
-      for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current)
-      {
-        if (SystemToolbox::IsRegularFile(current->path().string()))
-        {
-          try
-          {
-            fs::path d = current->path();
-            std::string uuid = ToString(d);
-            if (Toolbox::IsUuid(uuid))
-            {
-              fs::path p0 = d.parent_path().parent_path().parent_path();
-              std::string p1 = ToString(d.parent_path().parent_path());
-              std::string p2 = ToString(d.parent_path());
-              if (p1.length() == 2 &&
-                  p2.length() == 2 &&
-                  p1 == uuid.substr(0, 2) &&
-                  p2 == uuid.substr(2, 2) &&
-                  p0 == root_)
-              {
-                result.insert(uuid);
-              }
-            }
-          }
-          catch (fs::filesystem_error&)
-          {
-          }
-        }
-      }
-    }
-  }
-
-
-  void FilesystemStorage::Clear()
-  {
-    namespace fs = boost::filesystem;
-    typedef std::set<std::string> List;
-
-    List result;
-    ListAllFiles(result);
-
-    for (List::const_iterator it = result.begin(); it != result.end(); ++it)
-    {
-      Remove(*it, FileContentType_Unknown /*ignored in this class*/);
-    }
-  }
-
-
-  void FilesystemStorage::Remove(const std::string& uuid,
-                                 FileContentType type)
-  {
-    LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
-
-    namespace fs = boost::filesystem;
-
-    fs::path p = GetPath(uuid);
-
-    try
-    {
-      fs::remove(p);
-    }
-    catch (...)
-    {
-      // Ignore the error
-    }
-
-    // Remove the two parent directories, ignoring the error code if
-    // these directories are not empty
-
-    try
-    {
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-      boost::system::error_code err;
-      fs::remove(p.parent_path(), err);
-      fs::remove(p.parent_path().parent_path(), err);
-#else
-      fs::remove(p.parent_path());
-      fs::remove(p.parent_path().parent_path());
-#endif
-    }
-    catch (...)
-    {
-      // Ignore the error
-    }
-  }
-
-
-  uintmax_t FilesystemStorage::GetCapacity() const
-  {
-    return boost::filesystem::space(root_).capacity;
-  }
-
-  uintmax_t FilesystemStorage::GetAvailableSpace() const
-  {
-    return boost::filesystem::space(root_).available;
-  }
-}
--- a/Core/FileStorage/FilesystemStorage.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-
-#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>
-#include <boost/filesystem.hpp>
-#include <set>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea
-  {
-    // TODO REMOVE THIS
-    friend class FilesystemHttpSender;
-    friend class FileStorageAccessor;
-
-  private:
-    boost::filesystem::path root_;
-
-    boost::filesystem::path GetPath(const std::string& uuid) const;
-
-  public:
-    explicit FilesystemStorage(std::string root);
-
-    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);
-
-    void ListAllFiles(std::set<std::string>& result) const;
-
-    uintmax_t GetSize(const std::string& uuid) const;
-
-    void Clear();
-
-    uintmax_t GetCapacity() const;
-
-    uintmax_t GetAvailableSpace() const;
-  };
-}
--- a/Core/FileStorage/IStorageArea.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class IStorageArea : public boost::noncopyable
-  {
-  public:
-    virtual ~IStorageArea()
-    {
-    }
-
-    virtual void Create(const std::string& uuid,
-                        const void* content,
-                        size_t size,
-                        FileContentType type) = 0;
-
-    virtual void Read(std::string& content,
-                      const std::string& uuid,
-                      FileContentType type) = 0;
-
-    virtual void Remove(const std::string& uuid,
-                        FileContentType type) = 0;
-  };
-}
--- a/Core/FileStorage/MemoryStorageArea.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,128 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-    }
-  }
-}
--- a/Core/FileStorage/MemoryStorageArea.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,246 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "StorageAccessor.h"
-
-#include "../Compatibility.h"
-#include "../Compression/ZlibCompressor.h"
-#include "../MetricsRegistry.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-#  include "../HttpServer/HttpStreamTranscoder.h"
-#endif
-
-
-static const std::string METRICS_CREATE = "orthanc_storage_create_duration_ms";
-static const std::string METRICS_READ = "orthanc_storage_read_duration_ms";
-static const std::string METRICS_REMOVE = "orthanc_storage_remove_duration_ms";
-
-
-namespace Orthanc
-{
-  class StorageAccessor::MetricsTimer : public boost::noncopyable
-  {
-  private:
-    std::unique_ptr<MetricsRegistry::Timer>  timer_;
-
-  public:
-    MetricsTimer(StorageAccessor& that,
-                 const std::string& name)
-    {
-      if (that.metrics_ != NULL)
-      {
-        timer_.reset(new MetricsRegistry::Timer(*that.metrics_, name));
-      }
-    }
-  };
-
-
-  FileInfo StorageAccessor::Write(const void* data,
-                                  size_t size,
-                                  FileContentType type,
-                                  CompressionType compression,
-                                  bool storeMd5)
-  {
-    std::string uuid = Toolbox::GenerateUuid();
-
-    std::string md5;
-
-    if (storeMd5)
-    {
-      Toolbox::ComputeMD5(md5, data, size);
-    }
-
-    switch (compression)
-    {
-      case CompressionType_None:
-      {
-        MetricsTimer timer(*this, METRICS_CREATE);
-
-        area_.Create(uuid, data, size, type);
-        return FileInfo(uuid, type, size, md5);
-      }
-
-      case CompressionType_ZlibWithSize:
-      {
-        ZlibCompressor zlib;
-
-        std::string compressed;
-        zlib.Compress(compressed, data, size);
-
-        std::string compressedMD5;
-      
-        if (storeMd5)
-        {
-          Toolbox::ComputeMD5(compressedMD5, compressed);
-        }
-
-        {
-          MetricsTimer timer(*this, METRICS_CREATE);
-
-          if (compressed.size() > 0)
-          {
-            area_.Create(uuid, &compressed[0], compressed.size(), type);
-          }
-          else
-          {
-            area_.Create(uuid, NULL, 0, type);
-          }
-        }
-
-        return FileInfo(uuid, type, size, md5,
-                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void StorageAccessor::Read(std::string& content,
-                             const FileInfo& info)
-  {
-    switch (info.GetCompressionType())
-    {
-      case CompressionType_None:
-      {
-        MetricsTimer timer(*this, METRICS_READ);
-        area_.Read(content, info.GetUuid(), info.GetContentType());
-        break;
-      }
-
-      case CompressionType_ZlibWithSize:
-      {
-        ZlibCompressor zlib;
-
-        std::string compressed;
-
-        {
-          MetricsTimer timer(*this, METRICS_READ);
-          area_.Read(compressed, info.GetUuid(), info.GetContentType());
-        }
-
-        IBufferCompressor::Uncompress(content, zlib, compressed);
-        break;
-      }
-
-      default:
-      {
-        throw OrthancException(ErrorCode_NotImplemented);
-      }
-    }
-
-    // TODO Check the validity of the uncompressed MD5?
-  }
-
-
-  void StorageAccessor::ReadRaw(std::string& content,
-                                const FileInfo& info)
-  {
-    MetricsTimer timer(*this, METRICS_READ);
-    area_.Read(content, info.GetUuid(), info.GetContentType());
-  }
-
-
-  void StorageAccessor::Remove(const std::string& fileUuid,
-                               FileContentType type)
-  {
-    MetricsTimer timer(*this, METRICS_REMOVE);
-    area_.Remove(fileUuid, type);
-  }
-
-
-#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-  void StorageAccessor::SetupSender(BufferHttpSender& sender,
-                                    const FileInfo& info,
-                                    const std::string& mime)
-  {
-    {
-      MetricsTimer timer(*this, METRICS_READ);
-      area_.Read(sender.GetBuffer(), info.GetUuid(), 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 std::string& mime)
-  {
-    BufferHttpSender sender;
-    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 std::string& mime)
-  {
-    BufferHttpSender sender;
-    SetupSender(sender, info, mime);
-  
-    HttpStreamTranscoder transcoder(sender, info.GetCompressionType());
-    output.AnswerStream(transcoder);
-  }
-#endif
-}
--- a/Core/FileStorage/StorageAccessor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 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"
-
-#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-#  include "../HttpServer/BufferHttpSender.h"
-#  include "../RestApi/RestApiOutput.h"
-#endif
-
-#include <vector>
-#include <string>
-#include <boost/noncopyable.hpp>
-#include <stdint.h>
-
-namespace Orthanc
-{
-  class MetricsRegistry;
-
-  /**
-   * This class handles the compression/decompression of the raw files
-   * contained in the storage area, and monitors timing metrics (if
-   * enabled).
-   **/
-  class StorageAccessor : boost::noncopyable
-  {
-  private:
-    class MetricsTimer;
-
-    IStorageArea&     area_;
-    MetricsRegistry*  metrics_;
-
-#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-    void SetupSender(BufferHttpSender& sender,
-                     const FileInfo& info,
-                     const std::string& mime);
-#endif
-
-  public:
-    StorageAccessor(IStorageArea& area) : 
-      area_(area),
-      metrics_(NULL)
-    {
-    }
-
-    StorageAccessor(IStorageArea& area,
-                    MetricsRegistry& metrics) : 
-      area_(area),
-      metrics_(&metrics)
-    {
-    }
-
-    FileInfo Write(const void* data,
-                   size_t size,
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5);
-
-    FileInfo Write(const std::string& data, 
-                   FileContentType type,
-                   CompressionType compression,
-                   bool storeMd5)
-    {
-      return Write((data.size() == 0 ? NULL : data.c_str()),
-                   data.size(), type, compression, storeMd5);
-    }
-
-    void Read(std::string& content,
-              const FileInfo& info);
-
-    void ReadRaw(std::string& content,
-                 const FileInfo& info);
-
-    void Remove(const std::string& fileUuid,
-                FileContentType type);
-
-    void Remove(const FileInfo& info)
-    {
-      Remove(info.GetUuid(), info.GetContentType());
-    }
-
-#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
-    void AnswerFile(HttpOutput& output,
-                    const FileInfo& info,
-                    MimeType mime)
-    {
-      AnswerFile(output, info, EnumerationToString(mime));
-    }
-
-    void AnswerFile(HttpOutput& output,
-                    const FileInfo& info,
-                    const std::string& mime);
-
-    void AnswerFile(RestApiOutput& output,
-                    const FileInfo& info,
-                    MimeType mime)
-    {
-      AnswerFile(output, info, EnumerationToString(mime));
-    }
-
-    void AnswerFile(RestApiOutput& output,
-                    const FileInfo& info,
-                    const std::string& mime);
-#endif
-  };
-}
--- a/Core/HttpClient.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1214 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpClient.h"
-
-#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>
-
-// Default timeout = 60 seconds (in Orthanc <= 1.5.6, it was 10 seconds)
-static const unsigned int DEFAULT_HTTP_TIMEOUT = 60;
-
-
-#if ORTHANC_ENABLE_PKCS11 == 1
-#  include "Pkcs11.h"
-#endif
-
-
-extern "C"
-{
-  static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status)
-  {
-    if (code == CURLE_OK)
-    {
-      code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
-      return code;
-    }
-    else
-    {
-      LOG(ERROR) << "Error code " << static_cast<int>(code)
-                 << " in libcurl: " << curl_easy_strerror(code);
-      *status = 0;
-      return code;
-    }
-  }
-}
-
-// This is a dummy wrapper function to suppress any OpenSSL-related
-// problem in valgrind. Inlining is prevented.
-#if defined(__GNUC__) || defined(__clang__)
-  __attribute__((noinline)) 
-#endif
-static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status)
-{
-#if ORTHANC_ENABLE_SSL == 1
-  return GetHttpStatus(curl_easy_perform(curl), curl, status);
-#else
-  throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
-                                  "Orthanc was compiled without SSL support, "
-                                  "cannot make HTTPS request");
-#endif
-}
-
-
-
-namespace Orthanc
-{
-  static CURLcode CheckCode(CURLcode code)
-  {
-    if (code == CURLE_NOT_BUILT_IN)
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Your libcurl does not contain a required feature, "
-                             "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
-    }
-
-    if (code != CURLE_OK)
-    {
-      throw OrthancException(ErrorCode_NetworkProtocol,
-                             "libCURL error: " + std::string(curl_easy_strerror(code)));
-    }
-
-    return code;
-  }
-
-
-  // RAII pattern around a "curl_slist"
-  class HttpClient::CurlHeaders : public boost::noncopyable
-  {
-  private:
-    struct curl_slist *content_;
-    bool               isChunkedTransfer_;
-    bool               hasExpect_;
-
-  public:
-    CurlHeaders() :
-      content_(NULL),
-      isChunkedTransfer_(false),
-      hasExpect_(false)
-    {
-    }
-
-    CurlHeaders(const HttpClient::HttpHeaders& headers)
-    {
-      for (HttpClient::HttpHeaders::const_iterator
-             it = headers.begin(); it != headers.end(); ++it)
-      {
-        AddHeader(it->first, it->second);
-      }
-    }
-
-    ~CurlHeaders()
-    {
-      Clear();
-    }
-
-    bool IsEmpty() const
-    {
-      return content_ == NULL;
-    }
-
-    void Clear()
-    {
-      if (content_ != NULL)
-      {
-        curl_slist_free_all(content_);
-        content_ = NULL;
-      }
-
-      isChunkedTransfer_ = false;
-      hasExpect_ = false;
-    }
-
-    void AddHeader(const std::string& key,
-                   const std::string& value)
-    {
-      if (boost::iequals(key, "Expect"))
-      {
-        hasExpect_ = true;
-      }
-
-      if (boost::iequals(key, "Transfer-Encoding") &&
-          value == "chunked")
-      {
-        isChunkedTransfer_ = true;
-      }
-        
-      std::string item = key + ": " + value;
-
-      struct curl_slist *tmp = curl_slist_append(content_, item.c_str());
-        
-      if (tmp == NULL)
-      {
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-      else
-      {
-        content_ = tmp;
-      }
-    }
-
-    void Assign(CURL* curl) const
-    {
-      CheckCode(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, content_));
-    }
-
-    bool HasExpect() const
-    {
-      return hasExpect_;
-    }
-
-    bool IsChunkedTransfer() const
-    {
-      return isChunkedTransfer_;
-    }
-  };
-
-
-  class HttpClient::CurlRequestBody : public boost::noncopyable
-  {
-  private:
-    HttpClient::IRequestBody*  body_;
-    std::string                sourceBuffer_;
-    size_t                     sourceBufferTransmittedSize_;
-
-    size_t CallbackInternal(char* curlBuffer,
-                            size_t curlBufferSize)
-    {
-      if (body_ == NULL)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      if (curlBufferSize == 0)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      // Read chunks from the body stream so as to fill the target buffer
-      size_t curlBufferFilledSize = 0;
-      size_t sourceRemainingSize = sourceBuffer_.size() - sourceBufferTransmittedSize_;
-      bool hasMore = true;
-      
-      while (sourceRemainingSize < curlBufferSize && hasMore)
-      {
-        if (sourceRemainingSize > 0)
-        {
-          // transmit the end of current source buffer
-          memcpy(curlBuffer + curlBufferFilledSize,
-                 sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize);
-
-          curlBufferFilledSize += sourceRemainingSize;
-        }
-
-        // start filling a new source buffer
-        sourceBufferTransmittedSize_ = 0;
-        sourceBuffer_.clear();
-
-        hasMore = body_->ReadNextChunk(sourceBuffer_);
-
-        sourceRemainingSize = sourceBuffer_.size();
-      }
-
-      if (sourceRemainingSize > 0 &&
-          curlBufferSize > curlBufferFilledSize)
-      {
-        size_t s = std::min(sourceRemainingSize, curlBufferSize - curlBufferFilledSize);
-
-        memcpy(curlBuffer + curlBufferFilledSize,
-               sourceBuffer_.data() + sourceBufferTransmittedSize_, s);
-
-        sourceBufferTransmittedSize_ += s;
-        curlBufferFilledSize += s;
-      }
-
-      return curlBufferFilledSize;
-    }
-    
-  public:
-    CurlRequestBody() :
-      body_(NULL),
-      sourceBufferTransmittedSize_(0)
-    {
-    }
-
-    void SetBody(HttpClient::IRequestBody& body)
-    {
-      body_ = &body;
-      sourceBufferTransmittedSize_ = 0;
-      sourceBuffer_.clear();
-    }
-
-    void Clear()
-    {
-      body_ = NULL;
-      sourceBufferTransmittedSize_ = 0;
-      sourceBuffer_.clear();
-    }
-
-    bool IsValid() const
-    {
-      return body_ != NULL;
-    }
-
-    static size_t Callback(char *buffer, size_t size, size_t nitems, void *userdata)
-    {
-      try
-      {
-        assert(userdata != NULL);
-        return reinterpret_cast<HttpClient::CurlRequestBody*>(userdata)->
-          CallbackInternal(buffer, size * nitems);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
-        return CURL_READFUNC_ABORT;
-      }
-      catch (...)
-      {
-        LOG(ERROR) << "Native exception while streaming HTTP body";
-        return CURL_READFUNC_ABORT;
-      }
-    }
-  };
-
-
-  class HttpClient::CurlAnswer : public boost::noncopyable
-  {
-  private:
-    HttpClient::IAnswer&  answer_;
-    bool                  headersLowerCase_;
-
-  public:
-    CurlAnswer(HttpClient::IAnswer& answer,
-               bool headersLowerCase) :
-      answer_(answer),
-      headersLowerCase_(headersLowerCase)
-    {
-    }
-
-    static size_t HeaderCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
-    {
-      try
-      {
-        assert(userdata != NULL);
-        CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
-
-        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 (that.headersLowerCase_)
-            {
-              Toolbox::ToLowerCase(tmp);
-            }
-
-            std::string key = Toolbox::StripSpaces(tmp);
-
-            if (!key.empty())
-            {
-              std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
-              that.answer_.AddHeader(key, value);
-            }
-          }
-
-          return length;
-        }
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
-        return CURL_READFUNC_ABORT;
-      }
-      catch (...)
-      {
-        LOG(ERROR) << "Native exception while streaming HTTP body";
-        return CURL_READFUNC_ABORT;
-      }
-    }
-
-    static size_t BodyCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
-    {
-      try
-      {
-        assert(userdata != NULL);
-        CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
-
-        size_t length = size * nmemb;
-        if (length == 0)
-        {
-          return 0;
-        }
-        else
-        {
-          that.answer_.AddChunk(buffer, length);
-          return length;
-        }
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
-        return CURL_READFUNC_ABORT;
-      }
-      catch (...)
-      {
-        LOG(ERROR) << "Native exception while streaming HTTP body";
-        return CURL_READFUNC_ABORT;
-      }
-    }
-  };
-
-
-  class HttpClient::DefaultAnswer : public HttpClient::IAnswer
-  {
-  private:
-    ChunkedBuffer   answer_;
-    HttpHeaders*    headers_;
-
-  public:
-    DefaultAnswer() : headers_(NULL)
-    {
-    }
-
-    void SetHeaders(HttpHeaders& headers)
-    {
-      headers_ = &headers;
-      headers_->clear();
-    }
-
-    void FlattenBody(std::string& target)
-    {
-      answer_.Flatten(target);
-    }
-
-    virtual void AddHeader(const std::string& key,
-                           const std::string& value)
-    {
-      if (headers_ != NULL)
-      {
-        (*headers_) [key] = value;
-      }
-    }
-      
-    virtual void AddChunk(const void* data,
-                          size_t size)
-    {
-      answer_.AddChunk(data, size);
-    }
-  };
-
-
-  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_;
-    CurlHeaders defaultPostHeaders_;
-    CurlHeaders defaultChunkedHeaders_;
-    CurlHeaders userHeaders_;
-    CurlRequestBody requestBody_;
-  };
-
-
-  void HttpClient::ThrowException(HttpStatus status)
-  {
-    switch (status)
-    {
-      case HttpStatus_400_BadRequest:
-        throw OrthancException(ErrorCode_BadRequest);
-
-      case HttpStatus_401_Unauthorized:
-      case HttpStatus_403_Forbidden:
-        throw OrthancException(ErrorCode_Unauthorized);
-
-      case HttpStatus_404_NotFound:
-        throw OrthancException(ErrorCode_UnknownResource);
-
-      default:
-        throw OrthancException(ErrorCode_NetworkProtocol);
-    }
-  }
-
-
-  /*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;
-    }*/
-
-
-  void HttpClient::Setup()
-  {
-    pimpl_->defaultPostHeaders_.AddHeader("Expect", "");
-    pimpl_->defaultChunkedHeaders_.AddHeader("Expect", "");
-    pimpl_->defaultChunkedHeaders_.AddHeader("Transfer-Encoding", "chunked");
-
-    pimpl_->curl_ = curl_easy_init();
-
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlAnswer::HeaderCallback));
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlAnswer::BodyCallback));
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0));
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1));
-
-    // This fixes the "longjmp causes uninitialized stack frame" crash
-    // that happens on modern Linux versions.
-    // http://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOSIGNAL, 1));
-
-    url_ = "";
-    method_ = HttpMethod_Get;
-    lastStatus_ = HttpStatus_None;
-    SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose());
-    timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout();
-    GlobalParameters::GetInstance().GetDefaultProxy(proxy_);
-    GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_);    
-  }
-
-
-  HttpClient::HttpClient() : 
-    pimpl_(new PImpl),
-    verifyPeers_(true),
-    pkcs11Enabled_(false),
-    headersToLowerCase_(true),
-    redirectionFollowed_(true)
-  {
-    Setup();
-  }
-
-
-  HttpClient::HttpClient(const WebServiceParameters& service,
-                         const std::string& uri) : 
-    pimpl_(new PImpl),
-    verifyPeers_(true),
-    headersToLowerCase_(true),
-    redirectionFollowed_(true)
-  {
-    Setup();
-
-    if (service.GetUsername().size() != 0 && 
-        service.GetPassword().size() != 0)
-    {
-      SetCredentials(service.GetUsername().c_str(), 
-                     service.GetPassword().c_str());
-    }
-
-    if (!service.GetCertificateFile().empty())
-    {
-      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);
-    }
-  }
-
-
-  HttpClient::~HttpClient()
-  {
-    curl_easy_cleanup(pimpl_->curl_);
-  }
-
-
-  void HttpClient::SetBody(const std::string& data)
-  {
-    body_ = data;
-    pimpl_->requestBody_.Clear();
-  }
-
-
-  void HttpClient::SetBody(IRequestBody& body)
-  {
-    body_.clear();
-    pimpl_->requestBody_.SetBody(body);
-  }
-
-  
-  void HttpClient::ClearBody()
-  {
-    body_.clear();
-    pimpl_->requestBody_.Clear();
-  }
-
-
-  void HttpClient::SetVerbose(bool isVerbose)
-  {
-    isVerbose_ = isVerbose;
-
-    if (isVerbose_)
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1));
-      //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback));
-    }
-    else
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 0));
-    }
-  }
-
-
-  void HttpClient::AddHeader(const std::string& key,
-                             const std::string& value)
-  {
-    if (key.empty())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      pimpl_->userHeaders_.AddHeader(key, value);
-    }
-  }
-
-
-  void HttpClient::ClearHeaders()
-  {
-    pimpl_->userHeaders_.Clear();
-  }
-
-
-  bool HttpClient::ApplyInternal(CurlAnswer& answer)
-  {
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &answer));
-
-#if ORTHANC_ENABLE_SSL == 1
-    // Setup HTTPS-related options
-
-    if (verifyPeers_)
-    {
-      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_)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Cannot enable both client certificates and PKCS#11 authentication");
-    }
-
-    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
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "Cannot use PKCS#11 for a HTTPS request, "
-                               "because it has not been initialized");
-      }
-#else
-      throw OrthancException(ErrorCode_InternalError,
-                             "This version of Orthanc is compiled without support for PKCS#11");
-#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
-      throw OrthancException(ErrorCode_InternalError,
-                             "This version of Orthanc is compiled without OpenSSL support, "
-                             "cannot use HTTPS client authentication");
-#endif
-    }
-
-    // Reset the parameters from previous calls to Apply()
-    pimpl_->userHeaders_.Assign(pimpl_->curl_);
-    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, 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)
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, DEFAULT_HTTP_TIMEOUT));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, DEFAULT_HTTP_TIMEOUT));
-    }
-    else
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_));
-    }
-
-    if (credentials_.size() != 0)
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str()));
-    }
-
-    if (proxy_.size() != 0)
-    {
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str()));
-    }
-
-    switch (method_)
-    {
-    case HttpMethod_Get:
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 1L));
-      break;
-
-    case HttpMethod_Post:
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
-
-      break;
-
-    case HttpMethod_Delete:
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L));
-      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
-      break;
-
-    case HttpMethod_Put:
-      // http://stackoverflow.com/a/7570281/881731: Don't use
-      // CURLOPT_PUT if there is a body
-
-      // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
-
-      curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */
-      break;
-
-    default:
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (method_ == HttpMethod_Post ||
-        method_ == HttpMethod_Put)
-    {
-      if (!pimpl_->userHeaders_.IsEmpty() &&
-          !pimpl_->userHeaders_.HasExpect())
-      {
-        LOG(INFO) << "For performance, the HTTP header \"Expect\" should be set to empty string in POST/PUT requests";
-      }
-
-      if (pimpl_->requestBody_.IsValid())
-      {
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestBody::Callback));
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->requestBody_));
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, -1L));
-    
-        if (pimpl_->userHeaders_.IsEmpty())
-        {
-          pimpl_->defaultChunkedHeaders_.Assign(pimpl_->curl_);
-        }
-        else if (!pimpl_->userHeaders_.IsChunkedTransfer())
-        {
-          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" "
-                       << "if streaming a chunked body in POST/PUT requests";
-        }
-      }
-      else
-      {
-        // Disable possible previous stream transfers
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, NULL));
-        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_UPLOAD, 0));
-
-        if (pimpl_->userHeaders_.IsChunkedTransfer())
-        {
-          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must only be set "
-                       << "if streaming a chunked body in POST/PUT requests";
-        }
-
-        if (pimpl_->userHeaders_.IsEmpty())
-        {
-          pimpl_->defaultPostHeaders_.Assign(pimpl_->curl_);
-        }
-
-        if (body_.size() > 0)
-        {
-          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, body_.c_str()));
-          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, body_.size()));
-        }
-        else
-        {
-          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
-          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
-        }
-      }
-    }
-
-
-    // Do the actual request
-    CURLcode code;
-    long status = 0;
-
-    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
-
-    const boost::posix_time::ptime start = boost::posix_time::microsec_clock::universal_time();
-    
-    if (boost::starts_with(url_, "https://"))
-    {
-      code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status);
-    }
-    else
-    {
-      code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status);
-    }
-
-    const boost::posix_time::ptime end = boost::posix_time::microsec_clock::universal_time();
-    
-    LOG(INFO) << "HTTP status code " << status << " in "
-              << ((end - start).total_milliseconds()) << " ms after "
-              << EnumerationToString(method_) << " request on: " << url_;
-
-    if (isVerbose_)
-    {
-      LOG(INFO) << "cURL status code: " << code;
-    }
-
-    CheckCode(code);
-
-    if (status == 0)
-    {
-      // This corresponds to a call to an inexistent host
-      lastStatus_ = HttpStatus_500_InternalServerError;
-    }
-    else
-    {
-      lastStatus_ = static_cast<HttpStatus>(status);
-    }
-
-    if (status >= 200 && status < 300)
-    {
-      return true;   // Success
-    }
-    else
-    {
-      LOG(ERROR) << "Error in HTTP request, received HTTP status " << status 
-                 << " (" << EnumerationToString(lastStatus_) << ")";
-      return false;
-    }
-  }
-
-
-  bool HttpClient::ApplyInternal(std::string& answerBody,
-                                 HttpHeaders* answerHeaders)
-  {
-    answerBody.clear();
-
-    DefaultAnswer answer;
-
-    if (answerHeaders != NULL)
-    {
-      answer.SetHeaders(*answerHeaders);
-    }
-
-    CurlAnswer wrapper(answer, headersToLowerCase_);
-
-    if (ApplyInternal(wrapper))
-    {
-      answer.FlattenBody(answerBody);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  bool HttpClient::ApplyInternal(Json::Value& answerBody,
-                                 HttpClient::HttpHeaders* answerHeaders)
-  {
-    std::string s;
-    if (ApplyInternal(s, answerHeaders))
-    {
-      Json::Reader reader;
-      return reader.parse(s, answerBody);
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  void HttpClient::SetCredentials(const char* username,
-                                  const char* password)
-  {
-    credentials_ = std::string(username) + ":" + std::string(password);
-  }
-
-
-  void HttpClient::ConfigureSsl(bool httpsVerifyPeers,
-                                const std::string& httpsVerifyCertificates)
-  {
-#if ORTHANC_ENABLE_SSL == 1
-    if (httpsVerifyPeers)
-    {
-      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: " << httpsVerifyCertificates;
-      }
-    }
-    else
-    {
-      LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled";
-    }
-#endif
-
-    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);
-  }
-
-
-  bool HttpClient::Apply(IAnswer& answer)
-  {
-    CurlAnswer wrapper(answer, headersToLowerCase_);
-    return ApplyInternal(wrapper);
-  }
-
-
-  void HttpClient::ApplyAndThrowException(IAnswer& answer)
-  {
-    CurlAnswer wrapper(answer, headersToLowerCase_);
-
-    if (!ApplyInternal(wrapper))
-    {
-      ThrowException(GetLastStatus());
-    }
-  }
-
-
-  void HttpClient::ApplyAndThrowException(std::string& answerBody)
-  {
-    if (!Apply(answerBody))
-    {
-      ThrowException(GetLastStatus());
-    }
-  }
-
-  
-  void HttpClient::ApplyAndThrowException(Json::Value& answerBody)
-  {
-    if (!Apply(answerBody))
-    {
-      ThrowException(GetLastStatus());
-    }
-  }
-
-
-  void HttpClient::ApplyAndThrowException(std::string& answerBody,
-                                          HttpHeaders& answerHeaders)
-  {
-    if (!Apply(answerBody, answerHeaders))
-    {
-      ThrowException(GetLastStatus());
-    }
-  }
-  
-
-  void HttpClient::ApplyAndThrowException(Json::Value& answerBody,
-                                          HttpHeaders& answerHeaders)
-  {
-    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))
-    {
-      throw OrthancException(ErrorCode_InexistentFile,
-                             "Cannot open certificate file: " + certificateFile);
-    }
-
-    if (!certificateKeyFile.empty() && 
-        !SystemToolbox::IsRegularFile(certificateKeyFile))
-    {
-      throw OrthancException(ErrorCode_InexistentFile,
-                             "Cannot open key file: " + certificateKeyFile);
-    }
-
-    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
-    throw OrthancException(ErrorCode_InternalError,
-                           "This version of Orthanc is compiled without support for PKCS#11");
-#endif
-  }
-}
--- a/Core/HttpClient.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,341 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-#include "WebServiceParameters.h"
-
-#include <string>
-#include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
-#include <json/json.h>
-
-#if !defined(ORTHANC_ENABLE_CURL)
-#  error The macro ORTHANC_ENABLE_CURL must be defined
-#endif
-
-#if ORTHANC_ENABLE_CURL != 1
-#  error Support for curl is disabled, cannot use this file
-#endif
-
-#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 ORTHANC_PUBLIC HttpClient : public boost::noncopyable
-  {
-  public:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-
-    class IRequestBody : public boost::noncopyable
-    {
-    public:
-      virtual ~IRequestBody()
-      {
-      }
-      
-      virtual bool ReadNextChunk(std::string& chunk) = 0;
-    };
-
-    class IAnswer : public boost::noncopyable
-    {
-    public:
-      virtual ~IAnswer()
-      {
-      }
-
-      virtual void AddHeader(const std::string& key,
-                             const std::string& value) = 0;
-      
-      virtual void AddChunk(const void* data,
-                            size_t size) = 0;
-    };
-
-  private:
-    class CurlHeaders;
-    class CurlRequestBody;
-    class CurlAnswer;
-    class DefaultAnswer;
-    class GlobalParameters;
-
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    std::string url_;
-    std::string credentials_;
-    HttpMethod method_;
-    HttpStatus lastStatus_;
-    std::string body_;  // This only makes sense for POST and PUT requests
-    bool isVerbose_;
-    long timeout_;
-    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&);  // Assignment forbidden
-    HttpClient(const HttpClient& base);  // Copy forbidden
-
-    bool ApplyInternal(CurlAnswer& answer);
-
-    bool ApplyInternal(std::string& answerBody,
-                       HttpHeaders* answerHeaders);
-
-    bool ApplyInternal(Json::Value& answerBody,
-                       HttpHeaders* answerHeaders);
-
-  public:
-    HttpClient();
-
-    HttpClient(const WebServiceParameters& service,
-               const std::string& uri);
-
-    ~HttpClient();
-
-    void SetUrl(const char* url)
-    {
-      url_ = std::string(url);
-    }
-
-    void SetUrl(const std::string& url)
-    {
-      url_ = url;
-    }
-
-    const std::string& GetUrl() const
-    {
-      return url_;
-    }
-
-    void SetMethod(HttpMethod method)
-    {
-      method_ = method;
-    }
-
-    HttpMethod GetMethod() const
-    {
-      return method_;
-    }
-
-    void SetTimeout(long seconds)
-    {
-      timeout_ = seconds;
-    }
-
-    long GetTimeout() const
-    {
-      return timeout_;
-    }
-
-    void SetBody(const std::string& data);
-
-    std::string& GetBody()
-    {
-      return body_;
-    }
-
-    const std::string& GetBody() const
-    {
-      return body_;
-    }
-
-    void SetBody(IRequestBody& body);
-
-    void ClearBody();
-
-    void SetVerbose(bool isVerbose);
-
-    bool IsVerbose() const
-    {
-      return isVerbose_;
-    }
-
-    void AddHeader(const std::string& key,
-                   const std::string& value);
-
-    void ClearHeaders();
-
-    bool Apply(IAnswer& answer);
-
-    bool Apply(std::string& answerBody)
-    {
-      return ApplyInternal(answerBody, NULL);
-    }
-
-    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
-    {
-      return lastStatus_;
-    }
-
-    void SetCredentials(const char* username,
-                        const char* password);
-
-    void SetProxy(const std::string& proxy)
-    {
-      proxy_ = proxy;
-    }
-
-    void SetHttpsVerifyPeers(bool verify)
-    {
-      verifyPeers_ = verify;
-    }
-
-    bool IsHttpsVerifyPeers() const
-    {
-      return verifyPeers_;
-    }
-
-    void SetHttpsCACertificates(const std::string& certificates)
-    {
-      caCertificates_ = certificates;
-    }
-
-    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_;
-    }
-
-    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(IAnswer& answer);
-
-    void ApplyAndThrowException(std::string& answerBody);
-
-    void ApplyAndThrowException(Json::Value& answerBody);
-
-    void ApplyAndThrowException(std::string& answerBody,
-                                HttpHeaders& answerHeaders);
-
-    void ApplyAndThrowException(Json::Value& answerBody,
-                                HttpHeaders& answerHeaders);
-
-    static void ThrowException(HttpStatus status);
-  };
-}
--- a/Core/HttpServer/BufferHttpSender.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "BufferHttpSender.h"
-
-#include "../OrthancException.h"
-
-#include <cassert>
-
-namespace Orthanc
-{
-  BufferHttpSender::BufferHttpSender() :
-    position_(0), 
-    chunkSize_(0),
-    currentChunkSize_(0)
-  {
-  }
-
-
-  bool BufferHttpSender::ReadNextChunk()
-  {
-    assert(position_ + currentChunkSize_ <= buffer_.size());
-
-    position_ += currentChunkSize_;
-
-    if (position_ == buffer_.size())
-    {
-      return false;
-    }
-    else
-    {
-      currentChunkSize_ = buffer_.size() - position_;
-
-      if (chunkSize_ != 0 &&
-          currentChunkSize_ > chunkSize_)
-      {
-        currentChunkSize_ = chunkSize_;
-      }
-
-      return true;
-    }
-  }
-
-
-  const char* BufferHttpSender::GetChunkContent()
-  {
-    return buffer_.c_str() + position_;
-  }
-
-
-  size_t BufferHttpSender::GetChunkSize()
-  {
-    return currentChunkSize_;
-  }
-}
--- a/Core/HttpServer/BufferHttpSender.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpFileSender.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC BufferHttpSender : public HttpFileSender
-  {
-  private:
-    std::string  buffer_;
-    size_t       position_;
-    size_t       chunkSize_;
-    size_t       currentChunkSize_;
-
-  public:
-    BufferHttpSender();
-
-    std::string& GetBuffer() 
-    {
-      return buffer_;
-    }
-
-    const std::string& GetBuffer() const
-    {
-      return buffer_;
-    }
-
-    // This is for test purpose. If "chunkSize" is set to "0" (the
-    // default), the entire buffer is consumed at once.
-    void SetChunkSize(size_t chunkSize)
-    {
-      chunkSize_ = chunkSize;
-    }
-
-
-    /**
-     * Implementation of the IHttpStreamAnswer interface.
-     **/
-
-    virtual uint64_t GetContentLength()
-    {
-      return buffer_.size();
-    }
-
-    virtual bool ReadNextChunk();
-
-    virtual const char* GetChunkContent();
-
-    virtual size_t GetChunkSize();
-  };
-}
--- a/Core/HttpServer/FilesystemHttpHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,182 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "FilesystemHttpHandler.h"
-
-#include "../OrthancException.h"
-#include "../SystemToolbox.h"
-#include "FilesystemHttpSender.h"
-
-#include <boost/filesystem.hpp>
-
-
-namespace Orthanc
-{
-  struct FilesystemHttpHandler::PImpl
-  {
-    UriComponents baseUri_;
-    boost::filesystem::path root_;
-  };
-
-
-
-  static void OutputDirectoryContent(HttpOutput& output,
-                                     const IHttpHandler::Arguments& headers,
-                                     const UriComponents& uri,
-                                     const boost::filesystem::path& p)
-  {
-    namespace fs = boost::filesystem;
-
-    std::string s;
-    s += "<html>";
-    s += "  <body>";
-    s += "    <h1>Subdirectories</h1>";
-    s += "    <ul>";
-
-    if (uri.size() > 0)
-    {
-      std::string h = Toolbox::FlattenUri(uri) + "/..";
-      s += "<li><a href=\"" + h + "\">..</a></li>";
-    }
-
-    fs::directory_iterator end;
-    for (fs::directory_iterator it(p) ; it != end; ++it)
-    {
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-      std::string f = it->path().filename().string();
-#else
-      std::string f = it->path().filename();
-#endif
-
-      std::string h = Toolbox::FlattenUri(uri) + "/" + f;
-      if (fs::is_directory(it->status()))
-        s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
-    }      
-
-    s += "    </ul>";      
-    s += "    <h1>Files</h1>";
-    s += "    <ul>";
-
-    for (fs::directory_iterator it(p) ; it != end; ++it)
-    {
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-      std::string f = it->path().filename().string();
-#else
-      std::string f = it->path().filename();
-#endif
-
-      std::string h = Toolbox::FlattenUri(uri) + "/" + f;
-      if (SystemToolbox::IsRegularFile(it->path().string()))
-      {
-        s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
-      }
-    }      
-
-    s += "    </ul>";
-    s += "  </body>";
-    s += "</html>";
-
-    output.SetContentType(MimeType_Html);
-    output.Answer(s);
-  }
-
-
-  FilesystemHttpHandler::FilesystemHttpHandler(const std::string& baseUri,
-                                               const std::string& root) : pimpl_(new PImpl)
-  {
-    Toolbox::SplitUriComponents(pimpl_->baseUri_, baseUri);
-    pimpl_->root_ = root;
-    listDirectoryContent_ = false;
-    
-    namespace fs = boost::filesystem;
-    if (!fs::exists(pimpl_->root_) || 
-        !fs::is_directory(pimpl_->root_))
-    {
-      throw OrthancException(ErrorCode_DirectoryExpected);
-    }
-  }
-
-
-  bool FilesystemHttpHandler::Handle(
-    HttpOutput& output,
-    RequestOrigin /*origin*/,
-    const char* /*remoteIp*/,
-    const char* /*username*/,
-    HttpMethod method,
-    const UriComponents& uri,
-    const Arguments& headers,
-    const GetArguments& arguments,
-    const void* /*bodyData*/,
-    size_t /*bodySize*/)
-  {
-    if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri))
-    {
-      // This URI is not served by this handler
-      return false;
-    }
-
-    if (method != HttpMethod_Get)
-    {
-      output.SendMethodNotAllowed("GET");
-      return true;
-    }
-
-    namespace fs = boost::filesystem;
-
-    fs::path p = pimpl_->root_;
-    for (size_t i = pimpl_->baseUri_.size(); i < uri.size(); i++)
-    {
-      p /= uri[i];
-    }
-
-    if (SystemToolbox::IsRegularFile(p.string()))
-    {
-      FilesystemHttpSender sender(p);
-      sender.SetContentType(SystemToolbox::AutodetectMimeType(p.string()));
-      output.Answer(sender);   // TODO COMPRESSION
-    }
-    else if (listDirectoryContent_ &&
-             fs::exists(p) && 
-             fs::is_directory(p))
-    {
-      OutputDirectoryContent(output, headers, uri, p);
-    }
-    else
-    {
-      output.SendStatus(HttpStatus_404_NotFound);
-    }
-
-    return true;
-  } 
-}
--- a/Core/HttpServer/FilesystemHttpHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class FilesystemHttpHandler : public IHttpHandler
-  {
-  private:
-    // PImpl idiom to avoid the inclusion of boost::filesystem
-    // throughout the software
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    bool listDirectoryContent_;
-
-  public:
-    FilesystemHttpHandler(const std::string& baseUri,
-                          const std::string& root);
-
-    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                            RequestOrigin origin,
-                                            const char* remoteIp,
-                                            const char* username,
-                                            HttpMethod method,
-                                            const UriComponents& uri,
-                                            const Arguments& headers)
-    {
-      return false;
-    }
-
-    virtual bool Handle(
-      HttpOutput& output,
-      RequestOrigin origin,
-      const char* remoteIp,
-      const char* username,
-      HttpMethod method,
-      const UriComponents& uri,
-      const Arguments& headers,
-      const GetArguments& arguments,
-      const void* /*bodyData*/,
-      size_t /*bodySize*/);
-
-    bool IsListDirectoryContent() const
-    {
-      return listDirectoryContent_;
-    }
-
-    void SetListDirectoryContent(bool enabled)
-    {
-      listDirectoryContent_ = enabled;
-    }
-  };
-}
--- a/Core/HttpServer/FilesystemHttpSender.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "FilesystemHttpSender.h"
-
-#include "../OrthancException.h"
-
-static const size_t  CHUNK_SIZE = 64 * 1024;   // Use 64KB chunks
-
-namespace Orthanc
-{
-  void FilesystemHttpSender::Initialize(const boost::filesystem::path& path)
-  {
-    SetContentFilename(path.filename().string());
-    file_.open(path.string().c_str(), std::ifstream::binary);
-
-    if (!file_.is_open())
-    {
-      throw OrthancException(ErrorCode_InexistentFile);
-    }
-
-    file_.seekg(0, file_.end);
-    size_ = file_.tellg();
-    file_.seekg(0, file_.beg);
-  }
-
-
-  bool FilesystemHttpSender::ReadNextChunk()
-  {
-    if (chunk_.size() == 0)
-    {
-      chunk_.resize(CHUNK_SIZE);
-    }
-
-    file_.read(&chunk_[0], chunk_.size());
-
-    if ((file_.flags() & std::istream::failbit) ||
-        file_.gcount() < 0)
-    {
-      throw OrthancException(ErrorCode_CorruptedFile);
-    }
-
-    chunkSize_ = static_cast<size_t>(file_.gcount());
-
-    return chunkSize_ > 0;
-  }
-}
--- a/Core/HttpServer/FilesystemHttpSender.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpFileSender.h"
-#include "BufferHttpSender.h"
-#include "../FileStorage/FilesystemStorage.h"
-
-#include <fstream>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC FilesystemHttpSender : public HttpFileSender
-  {
-  private:
-    std::ifstream    file_;
-    uint64_t         size_;
-    std::string      chunk_;
-    size_t           chunkSize_;
-
-    void Initialize(const boost::filesystem::path& path);
-
-  public:
-    explicit FilesystemHttpSender(const std::string& path)
-    {
-      Initialize(path);
-    }
-
-    explicit FilesystemHttpSender(const boost::filesystem::path& path)
-    {
-      Initialize(path);
-    }
-
-    FilesystemHttpSender(const std::string& path,
-                         MimeType contentType)
-    {
-      SetContentType(contentType);
-      Initialize(path);
-    }
-
-    FilesystemHttpSender(const FilesystemStorage& storage,
-                         const std::string& uuid)
-    {
-      Initialize(storage.GetPath(uuid));
-    }
-
-    /**
-     * Implementation of the IHttpStreamAnswer interface.
-     **/
-
-    virtual uint64_t GetContentLength()
-    {
-      return size_;
-    }
-
-    virtual bool ReadNextChunk();
-
-    virtual const char* GetChunkContent()
-    {
-      return chunk_.c_str();
-    }
-
-    virtual size_t GetChunkSize()
-    {
-      return chunkSize_;
-    }
-  };
-}
--- a/Core/HttpServer/HttpContentNegociation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,271 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-        {
-          throw OrthancException(
-            ErrorCode_BadRequest,
-            "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + value);
-        }
-      }
-    }
-
-    return 1.0f;  // Default quality
-  }
-
-
-  void HttpContentNegociation::SelectBestMatch(std::unique_ptr<Reference>& best,
-                                               const Handler& handler,
-                                               const std::string& type,
-                                               const std::string& subtype,
-                                               float quality)
-  {
-    std::unique_ptr<Reference> match(new Reference(handler, type, subtype, quality));
-
-    if (best.get() == NULL ||
-        *best < *match)
-    {
-#if __cplusplus < 201103L
-      best.reset(match.release());
-#else
-      best = std::move(match);
-#endif
-    }
-  }
-
-
-  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::unique_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;
-    }
-  }
-}
--- a/Core/HttpServer/HttpContentNegociation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-#include "../Compatibility.h"
-
-#include <memory>
-#include <boost/noncopyable.hpp>
-#include <map>
-#include <list>
-#include <string>
-#include <vector>
-#include <stdint.h>
-
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC 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::unique_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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpFileSender.h"
-
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-#include "../SystemToolbox.h"
-
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  void HttpFileSender::SetContentFilename(const std::string& filename)
-  {
-    filename_ = filename;
-
-    if (contentType_.empty())
-    {
-      contentType_ = SystemToolbox::AutodetectMimeType(filename);
-    }
-  }
-
-
-  bool HttpFileSender::HasContentFilename(std::string& filename)
-  {
-    if (filename_.empty())
-    {
-      return false;
-    }
-    else
-    {
-      filename = filename_;
-      return true;
-    }
-  }
-    
-  std::string HttpFileSender::GetContentType()
-  {
-    if (contentType_.empty())
-    {
-      return MIME_BINARY;
-    }
-    else
-    {
-      return contentType_;
-    }
-  }
-}
--- a/Core/HttpServer/HttpFileSender.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpOutput.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC HttpFileSender : public IHttpStreamAnswer
-  {
-  private:
-    std::string contentType_;
-    std::string filename_;
-
-  public:
-    void SetContentType(MimeType contentType)
-    {
-      contentType_ = EnumerationToString(contentType);
-    }
-
-    void SetContentType(const std::string& contentType)
-    {
-      contentType_ = contentType;
-    }
-
-    const std::string& GetContentType() const
-    {
-      return contentType_;
-    }
-
-    void SetContentFilename(const std::string& filename);
-
-    const std::string& GetContentFilename() const
-    {
-      return filename_;
-    }
-
-
-    /**
-     * Implementation of the IHttpStreamAnswer interface.
-     **/
-
-    virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/, 
-                                                 bool /*deflateAllowed*/)
-    {
-      return HttpCompression_None;
-    }
-
-    virtual bool HasContentFilename(std::string& filename);
-    
-    virtual std::string GetContentType();
-  };
-}
--- a/Core/HttpServer/HttpOutput.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,772 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpOutput.h"
-
-#include "../ChunkedBuffer.h"
-#include "../Compression/GzipCompressor.h"
-#include "../Compression/ZlibCompressor.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-#include <iostream>
-#include <vector>
-#include <stdio.h>
-#include <boost/lexical_cast.hpp>
-
-
-#if ORTHANC_ENABLE_CIVETWEB == 1
-#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
-#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
-#  endif
-#endif
-
-
-namespace Orthanc
-{
-  HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
-                                         bool isKeepAlive) : 
-    stream_(stream),
-    state_(State_WritingHeader),
-    status_(HttpStatus_200_Ok),
-    hasContentLength_(false),
-    contentPosition_(0),
-    keepAlive_(isKeepAlive)
-  {
-  }
-
-  HttpOutput::StateMachine::~StateMachine()
-  {
-    if (state_ != State_Done)
-    {
-      //asm volatile ("int3;");
-      //LOG(ERROR) << "This HTTP answer does not contain any body";
-    }
-
-    if (hasContentLength_ && contentPosition_ != contentLength_)
-    {
-      LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body";
-    }
-  }
-
-
-  void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status)
-  {
-    if (state_ != State_WritingHeader)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    status_ = status;
-  }
-
-
-  void HttpOutput::StateMachine::SetContentLength(uint64_t length)
-  {
-    if (state_ != State_WritingHeader)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    hasContentLength_ = true;
-    contentLength_ = length;
-  }
-
-  void HttpOutput::StateMachine::SetContentType(const char* contentType)
-  {
-    AddHeader("Content-Type", contentType);
-  }
-
-  void HttpOutput::StateMachine::SetContentFilename(const char* filename)
-  {
-    // TODO Escape double quotes
-    AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\"");
-  }
-
-  void HttpOutput::StateMachine::SetCookie(const std::string& cookie,
-                                           const std::string& value)
-  {
-    if (state_ != State_WritingHeader)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    // TODO Escape "=" characters
-    AddHeader("Set-Cookie", cookie + "=" + value);
-  }
-
-
-  void HttpOutput::StateMachine::AddHeader(const std::string& header,
-                                           const std::string& value)
-  {
-    if (state_ != State_WritingHeader)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    headers_.push_back(header + ": " + value + "\r\n");
-  }
-
-  void HttpOutput::StateMachine::ClearHeaders()
-  {
-    if (state_ != State_WritingHeader)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    headers_.clear();
-  }
-
-  void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length)
-  {
-    if (state_ == State_Done)
-    {
-      if (length == 0)
-      {
-        return;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "Because of keep-alive connections, the entire body must "
-                               "be sent at once or Content-Length must be given");
-      }
-    }
-
-    if (state_ == State_WritingMultipart)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (state_ == State_WritingHeader)
-    {
-      // Send the HTTP header before writing the body
-
-      stream_.OnHttpStatusReceived(status_);
-
-      std::string s = "HTTP/1.1 " + 
-        boost::lexical_cast<std::string>(status_) +
-        " " + std::string(EnumerationToString(status_)) +
-        "\r\n";
-
-      if (keepAlive_)
-      {
-        s += "Connection: keep-alive\r\n";
-      }
-      else
-      {
-        s += "Connection: close\r\n";
-      }
-
-      for (std::list<std::string>::const_iterator
-             it = headers_.begin(); it != headers_.end(); ++it)
-      {
-        s += *it;
-      }
-
-      if (status_ != HttpStatus_200_Ok)
-      {
-        hasContentLength_ = false;
-      }
-
-      uint64_t contentLength = (hasContentLength_ ? contentLength_ : length);
-      s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n\r\n";
-
-      stream_.Send(true, s.c_str(), s.size());
-      state_ = State_WritingBody;
-    }
-
-    if (hasContentLength_ &&
-        contentPosition_ + length > contentLength_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "The body size exceeds what was declared with SetContentSize()");
-    }
-
-    if (length > 0)
-    {
-      stream_.Send(false, buffer, length);
-      contentPosition_ += length;
-    }
-
-    if (!hasContentLength_ ||
-        contentPosition_ == contentLength_)
-    {
-      state_ = State_Done;
-    }
-  }
-
-
-  void HttpOutput::StateMachine::CloseBody()
-  {
-    switch (state_)
-    {
-      case State_WritingHeader:
-        SetContentLength(0);
-        SendBody(NULL, 0);
-        break;
-
-      case State_WritingBody:
-        if (!hasContentLength_ ||
-            contentPosition_ == contentLength_)
-        {
-          state_ = State_Done;
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                                 "The body size has not reached what was declared with SetContentSize()");
-        }
-
-        break;
-
-      case State_WritingMultipart:
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "Cannot invoke CloseBody() with multipart outputs");
-
-      case State_Done:
-        return;  // Ignore
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }      
-  }
-
-
-  HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const
-  {
-#if 0
-    // TODO Do not compress small files?
-    if (bodySize < 512)
-    {
-      return HttpCompression_None;
-    }
-#endif
-
-    // Prefer "gzip" over "deflate" if the choice is offered
-
-    if (isGzipAllowed_)
-    {
-      return HttpCompression_Gzip;
-    }
-    else if (isDeflateAllowed_)
-    {
-      return HttpCompression_Deflate;
-    }
-    else
-    {
-      return HttpCompression_None;
-    }
-  }
-
-
-  void HttpOutput::SendMethodNotAllowed(const std::string& allowed)
-  {
-    stateMachine_.ClearHeaders();
-    stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed);
-    stateMachine_.AddHeader("Allow", allowed);
-    stateMachine_.SendBody(NULL, 0);
-  }
-
-
-  void HttpOutput::SendStatus(HttpStatus status,
-			      const char* message,
-			      size_t messageSize)
-  {
-    if (status == HttpStatus_301_MovedPermanently ||
-        //status == HttpStatus_401_Unauthorized ||
-        status == HttpStatus_405_MethodNotAllowed)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Please use the dedicated methods to this HTTP status code in HttpOutput");
-    }
-    
-    stateMachine_.SetHttpStatus(status);
-    stateMachine_.SendBody(message, messageSize);
-  }
-
-
-  void HttpOutput::Redirect(const std::string& path)
-  {
-    stateMachine_.ClearHeaders();
-    stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently);
-    stateMachine_.AddHeader("Location", path);
-    stateMachine_.SendBody(NULL, 0);
-  }
-
-
-  void HttpOutput::SendUnauthorized(const std::string& realm)
-  {
-    stateMachine_.ClearHeaders();
-    stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized);
-    stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
-    stateMachine_.SendBody(NULL, 0);
-  }
-
-  
-  void HttpOutput::Answer(const void* buffer, 
-                          size_t length)
-  {
-    if (length == 0)
-    {
-      AnswerEmpty();
-      return;
-    }
-
-    HttpCompression compression = GetPreferredCompression(length);
-
-    if (compression == HttpCompression_None)
-    {
-      stateMachine_.SetContentLength(length);
-      stateMachine_.SendBody(buffer, length);
-      return;
-    }
-
-    std::string compressed, encoding;
-
-    switch (compression)
-    {
-      case HttpCompression_Deflate:
-      {
-        encoding = "deflate";
-        ZlibCompressor compressor;
-        // Do not prefix the buffer with its uncompressed size, to be compatible with "deflate"
-        compressor.SetPrefixWithUncompressedSize(false);  
-        compressor.Compress(compressed, buffer, length);
-        break;
-      }
-
-      case HttpCompression_Gzip:
-      {
-        encoding = "gzip";
-        GzipCompressor compressor;
-        compressor.Compress(compressed, buffer, length);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    LOG(TRACE) << "Compressing a HTTP answer using " << encoding;
-
-    // The body is empty, do not use HTTP compression
-    if (compressed.size() == 0)
-    {
-      AnswerEmpty();
-    }
-    else
-    {
-      stateMachine_.AddHeader("Content-Encoding", encoding);
-      stateMachine_.SetContentLength(compressed.size());
-      stateMachine_.SendBody(compressed.c_str(), compressed.size());
-    }
-
-    stateMachine_.CloseBody();
-  }
-
-
-  void HttpOutput::Answer(const std::string& str)
-  {
-    Answer(str.size() == 0 ? NULL : str.c_str(), str.size());
-  }
-
-
-  void HttpOutput::AnswerEmpty()
-  {
-    stateMachine_.CloseBody();
-  }
-
-
-  void HttpOutput::StateMachine::CheckHeadersCompatibilityWithMultipart() const
-  {
-    for (std::list<std::string>::const_iterator
-           it = headers_.begin(); it != headers_.end(); ++it)
-    {
-      if (!Toolbox::StartsWith(*it, "Set-Cookie: "))
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "The only headers that can be set in multipart answers "
-                               "are Set-Cookie (here: " + *it + " is set)");
-      }
-    }
-  }
-
-
-  static void PrepareMultipartMainHeader(std::string& boundary,
-                                         std::string& contentTypeHeader,
-                                         const std::string& subType,
-                                         const std::string& contentType)
-  {
-    if (subType != "mixed" &&
-        subType != "related")
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    /**
-     * 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 + "\"";
-    }
-
-    boundary = Toolbox::GenerateUuid() + "-" + Toolbox::GenerateUuid();
-
-    /**
-     * Fix for issue #165: "Encapsulation boundaries must not appear
-     * within the encapsulations, and must be no longer than 70
-     * characters, not counting the two leading hyphens."
-     * https://tools.ietf.org/html/rfc1521
-     * https://bitbucket.org/sjodogne/orthanc/issues/165/
-     **/
-    if (boundary.size() != 36 + 1 + 36)  // one UUID contains 36 characters
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    boundary = boundary.substr(0, 70);
-    
-    contentTypeHeader = ("multipart/" + subType + "; type=" + tmp + "; boundary=" + boundary);
-  }
-
-
-  void HttpOutput::StateMachine::StartMultipart(const std::string& subType,
-                                                const std::string& contentType)
-  {
-    if (state_ != State_WritingHeader)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    if (status_ != HttpStatus_200_Ok)
-    {
-      SendBody(NULL, 0);
-      return;
-    }
-
-    stream_.OnHttpStatusReceived(status_);
-
-    std::string header = "HTTP/1.1 200 OK\r\n";
-
-    if (keepAlive_)
-    {
-#if ORTHANC_ENABLE_MONGOOSE == 1
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "Multipart answers are not implemented together "
-                             "with keep-alive connections if using Mongoose");
-      
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
-      // Turn off Keep-Alive for multipart answers
-      // https://github.com/civetweb/civetweb/issues/727
-      stream_.DisableKeepAlive();
-      header += "Connection: close\r\n";
-#  else
-      // The function "mg_disable_keep_alive()" is not available,
-      // let's continue with Keep-Alive. Performance of WADO-RS will
-      // decrease.
-      header += "Connection: keep-alive\r\n";
-#  endif   
-
-#else
-#  error Please support your embedded Web server here
-#endif
-    }
-    else
-    {
-      header += "Connection: close\r\n";
-    }
-
-    // Possibly add the cookies
-    CheckHeadersCompatibilityWithMultipart();
-
-    for (std::list<std::string>::const_iterator
-           it = headers_.begin(); it != headers_.end(); ++it)
-    {
-      header += *it;
-    }
-
-    std::string contentTypeHeader;
-    PrepareMultipartMainHeader(multipartBoundary_, contentTypeHeader, subType, contentType);
-    multipartContentType_ = contentType;
-    header += ("Content-Type: " + contentTypeHeader + "\r\n\r\n");
-
-    stream_.Send(true, header.c_str(), header.size());
-    state_ = State_WritingMultipart;
-  }
-
-
-  static void PrepareMultipartItemHeader(std::string& target,
-                                         size_t length,
-                                         const std::map<std::string, std::string>& headers,
-                                         const std::string& boundary,
-                                         const std::string& contentType)
-  {
-    target = "--" + boundary + "\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)
-    {
-      target += 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)
-    {
-      target += "Content-Type: " + contentType + "\r\n";
-    }
-
-    if (!hasContentLength)
-    {
-      target += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n";
-    }
-
-    if (!hasMimeVersion)
-    {
-      target += "MIME-Version: 1.0\r\n\r\n";
-    }
-  }
-
-
-  void HttpOutput::StateMachine::SendMultipartItem(const void* item,
-                                                   size_t length,
-                                                   const std::map<std::string, std::string>& headers)
-  {
-    if (state_ != State_WritingMultipart)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    std::string header;
-    PrepareMultipartItemHeader(header, length, headers, multipartBoundary_, multipartContentType_);
-    stream_.Send(false, header.c_str(), header.size());
-
-    if (length > 0)
-    {
-      stream_.Send(false, item, length);
-    }
-
-    stream_.Send(false, "\r\n", 2);    
-  }
-
-
-  void HttpOutput::StateMachine::CloseMultipart()
-  {
-    if (state_ != State_WritingMultipart)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    // The two lines below might throw an exception, if the client has
-    // closed the connection. Such an error is ignored.
-    try
-    {
-      std::string header = "--" + multipartBoundary_ + "--\r\n";
-      stream_.Send(false, header.c_str(), header.size());
-    }
-    catch (OrthancException&)
-    {
-    }
-
-    state_ = State_Done;
-  }
-
-
-  static void AnswerStreamAsBuffer(HttpOutput& output,
-                                   IHttpStreamAnswer& stream)
-  {
-    ChunkedBuffer buffer;
-
-    while (stream.ReadNextChunk())
-    {
-      if (stream.GetChunkSize() > 0)
-      {
-        buffer.AddChunk(stream.GetChunkContent(), stream.GetChunkSize());
-      }
-    }
-
-    std::string s;
-    buffer.Flatten(s);
-
-    output.SetContentType(stream.GetContentType());
-    
-    std::string filename;
-    if (stream.HasContentFilename(filename))
-    {
-      output.SetContentFilename(filename.c_str());
-    }
-
-    output.Answer(s);
-  }
-
-
-  void HttpOutput::Answer(IHttpStreamAnswer& stream)
-  {
-    HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_);
-
-    switch (compression)
-    {
-      case HttpCompression_None:
-      {
-        if (isGzipAllowed_ || isDeflateAllowed_)
-        {
-          // New in Orthanc 1.5.7: Compress streams without built-in
-          // compression, if requested by the "Accept-Encoding" HTTP
-          // header
-          AnswerStreamAsBuffer(*this, stream);
-          return;
-        }
-        
-        break;
-      }
-
-      case HttpCompression_Gzip:
-        stateMachine_.AddHeader("Content-Encoding", "gzip");
-        break;
-
-      case HttpCompression_Deflate:
-        stateMachine_.AddHeader("Content-Encoding", "deflate");
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    stateMachine_.SetContentLength(stream.GetContentLength());
-
-    std::string contentType = stream.GetContentType();
-    if (contentType.empty())
-    {
-      contentType = MIME_BINARY;
-    }
-
-    stateMachine_.SetContentType(contentType.c_str());
-
-    std::string filename;
-    if (stream.HasContentFilename(filename))
-    {
-      SetContentFilename(filename.c_str());
-    }
-
-    while (stream.ReadNextChunk())
-    {
-      stateMachine_.SendBody(stream.GetChunkContent(),
-                             stream.GetChunkSize());
-    }
-
-    stateMachine_.CloseBody();
-  }
-
-
-  void HttpOutput::AnswerMultipartWithoutChunkedTransfer(
-    const std::string& subType,
-    const std::string& contentType,
-    const std::vector<const void*>& parts,
-    const std::vector<size_t>& sizes,
-    const std::vector<const std::map<std::string, std::string>*>& headers)
-  {
-    if (parts.size() != sizes.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    stateMachine_.CheckHeadersCompatibilityWithMultipart();
-
-    std::string boundary, contentTypeHeader;
-    PrepareMultipartMainHeader(boundary, contentTypeHeader, subType, contentType);
-    SetContentType(contentTypeHeader);
-
-    std::map<std::string, std::string> empty;
-
-    ChunkedBuffer chunked;
-    for (size_t i = 0; i < parts.size(); i++)
-    {
-      std::string partHeader;
-      PrepareMultipartItemHeader(partHeader, sizes[i], headers[i] == NULL ? empty : *headers[i], 
-                                 boundary, contentType);
-
-      chunked.AddChunk(partHeader);
-      chunked.AddChunk(parts[i], sizes[i]);
-      chunked.AddChunk("\r\n");    
-    }
-
-    chunked.AddChunk("--" + boundary + "--\r\n");
-
-    std::string body;
-    chunked.Flatten(body);
-    Answer(body);
-  }
-}
--- a/Core/HttpServer/HttpOutput.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,250 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IHttpOutputStream.h"
-#include "IHttpStreamAnswer.h"
-
-#include <list>
-#include <string>
-#include <stdint.h>
-#include <map>
-#include <vector>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC HttpOutput : public boost::noncopyable
-  {
-  private:
-    typedef std::list< std::pair<std::string, std::string> >  Header;
-
-    class StateMachine : public boost::noncopyable
-    {
-    public:
-      enum State
-      {
-        State_WritingHeader,      
-        State_WritingBody,
-        State_WritingMultipart,
-        State_Done
-      };
-
-    private:
-      IHttpOutputStream& stream_;
-      State state_;
-
-      HttpStatus status_;
-      bool hasContentLength_;
-      uint64_t contentLength_;
-      uint64_t contentPosition_;
-      bool keepAlive_;
-      std::list<std::string> headers_;
-
-      std::string multipartBoundary_;
-      std::string multipartContentType_;
-
-    public:
-      StateMachine(IHttpOutputStream& stream,
-                   bool isKeepAlive);
-
-      ~StateMachine();
-
-      void SetHttpStatus(HttpStatus status);
-
-      void SetContentLength(uint64_t length);
-
-      void SetContentType(const char* contentType);
-
-      void SetContentFilename(const char* filename);
-
-      void SetCookie(const std::string& cookie,
-                     const std::string& value);
-
-      void AddHeader(const std::string& header,
-                     const std::string& value);
-
-      void ClearHeaders();
-
-      void SendBody(const void* buffer, size_t length);
-
-      void StartMultipart(const std::string& subType,
-                          const std::string& contentType);
-
-      void SendMultipartItem(const void* item, 
-                             size_t length,
-                             const std::map<std::string, std::string>& headers);
-
-      void CloseMultipart();
-
-      void CloseBody();
-
-      State GetState() const
-      {
-        return state_;
-      }
-
-      void CheckHeadersCompatibilityWithMultipart() const;
-    };
-
-    StateMachine stateMachine_;
-    bool         isDeflateAllowed_;
-    bool         isGzipAllowed_;
-
-    HttpCompression GetPreferredCompression(size_t bodySize) const;
-
-  public:
-    HttpOutput(IHttpOutputStream& stream,
-               bool isKeepAlive) : 
-      stateMachine_(stream, isKeepAlive),
-      isDeflateAllowed_(false),
-      isGzipAllowed_(false)
-    {
-    }
-
-    void SetDeflateAllowed(bool allowed)
-    {
-      isDeflateAllowed_ = allowed;
-    }
-
-    bool IsDeflateAllowed() const
-    {
-      return isDeflateAllowed_;
-    }
-
-    void SetGzipAllowed(bool allowed)
-    {
-      isGzipAllowed_ = allowed;
-    }
-
-    bool IsGzipAllowed() const
-    {
-      return isGzipAllowed_;
-    }
-
-    void SendStatus(HttpStatus status,
-		    const char* message,
-		    size_t messageSize);
-
-    void SendStatus(HttpStatus status)
-    {
-      SendStatus(status, NULL, 0);
-    }
-
-    void SendStatus(HttpStatus status,
-		    const std::string& message)
-    {
-      SendStatus(status, message.c_str(), message.size());
-    }
-
-    void SetContentType(MimeType contentType)
-    {
-      stateMachine_.SetContentType(EnumerationToString(contentType));
-    }
-    
-    void SetContentType(const std::string& contentType)
-    {
-      stateMachine_.SetContentType(contentType.c_str());
-    }
-
-    void SetContentFilename(const char* filename)
-    {
-      stateMachine_.SetContentFilename(filename);
-    }
-
-    void SetCookie(const std::string& cookie,
-                   const std::string& value)
-    {
-      stateMachine_.SetCookie(cookie, value);
-    }
-
-    void AddHeader(const std::string& key,
-                   const std::string& value)
-    {
-      stateMachine_.AddHeader(key, value);
-    }
-
-    void Answer(const void* buffer, 
-                size_t length);
-
-    void Answer(const std::string& str);
-
-    void AnswerEmpty();
-
-    void SendMethodNotAllowed(const std::string& allowed);
-
-    void Redirect(const std::string& path);
-
-    void SendUnauthorized(const std::string& realm);
-
-    void StartMultipart(const std::string& subType,
-                        const std::string& contentType)
-    {
-      stateMachine_.StartMultipart(subType, contentType);
-    }
-
-    void SendMultipartItem(const void* item, 
-                           size_t size,
-                           const std::map<std::string, std::string>& headers)
-    {
-      stateMachine_.SendMultipartItem(item, size, headers);
-    }
-
-    void CloseMultipart()
-    {
-      stateMachine_.CloseMultipart();
-    }
-
-    bool IsWritingMultipart() const
-    {
-      return stateMachine_.GetState() == StateMachine::State_WritingMultipart;
-    }
-
-    void Answer(IHttpStreamAnswer& stream);
-
-    /**
-     * This method is a replacement to the combination
-     * "StartMultipart()" + "SendMultipartItem()". It generates the
-     * same answer, but it gives a chance to compress the body if
-     * "Accept-Encoding: gzip" is provided by the client, which is not
-     * possible in chunked transfers.
-     **/
-    void AnswerMultipartWithoutChunkedTransfer(
-      const std::string& subType,
-      const std::string& contentType,
-      const std::vector<const void*>& parts,
-      const std::vector<size_t>& sizes,
-      const std::vector<const std::map<std::string, std::string>*>& headers);
-  };
-}
--- a/Core/HttpServer/HttpServer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1385 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-// http://en.highscore.de/cpp/boost/stringhandling.html
-
-#include "../PrecompiledHeaders.h"
-#include "HttpServer.h"
-
-#include "../ChunkedBuffer.h"
-#include "../FileBuffer.h"
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../TemporaryFile.h"
-#include "HttpToolbox.h"
-
-#if ORTHANC_ENABLE_MONGOOSE == 1
-#  include <mongoose.h>
-
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-#  include <civetweb.h>
-#  define MONGOOSE_USE_CALLBACKS 1
-#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
-#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
-#  endif
-
-#else
-#  error "Either Mongoose or Civetweb must be enabled to compile this file"
-#endif
-
-#include <algorithm>
-#include <string.h>
-#include <boost/lexical_cast.hpp>
-#include <boost/algorithm/string.hpp>
-#include <boost/filesystem.hpp>
-#include <iostream>
-#include <string.h>
-#include <stdio.h>
-#include <boost/thread.hpp>
-
-#if !defined(ORTHANC_ENABLE_SSL)
-#  error The macro ORTHANC_ENABLE_SSL must be defined
-#endif
-
-#if ORTHANC_ENABLE_SSL == 1
-#  include <openssl/opensslv.h>
-#  include <openssl/err.h>
-#endif
-
-#define ORTHANC_REALM "Orthanc Secure Area"
-
-
-namespace Orthanc
-{
-  static const char MULTIPART_FORM[] = "multipart/form-data; boundary=";
-  static unsigned int MULTIPART_FORM_LENGTH = sizeof(MULTIPART_FORM) / sizeof(char) - 1;
-
-
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-    class MongooseOutputStream : public IHttpOutputStream
-    {
-    private:
-      struct mg_connection* connection_;
-
-    public:
-      MongooseOutputStream(struct mg_connection* connection) : connection_(connection)
-      {
-      }
-
-      virtual void Send(bool isHeader, const void* buffer, size_t length)
-      {
-        if (length > 0)
-        {
-          int status = mg_write(connection_, buffer, length);
-          if (status != static_cast<int>(length))
-          {
-            // status == 0 when the connection has been closed, -1 on error
-            throw OrthancException(ErrorCode_NetworkProtocol);
-          }
-        }
-      }
-
-      virtual void OnHttpStatusReceived(HttpStatus status)
-      {
-        // Ignore this
-      }
-
-      virtual void DisableKeepAlive()
-      {
-#if ORTHANC_ENABLE_MONGOOSE == 1
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Only available if using CivetWeb");
-
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
-        mg_disable_keep_alive(connection_);
-#  else
-#       warning The function "mg_disable_keep_alive()" is not available, DICOMweb might run slowly
-        throw OrthancException(ErrorCode_NotImplemented,
-                               "Only available if using a patched version of CivetWeb");
-#  endif
-
-#else
-#  error Please support your embedded Web server here
-#endif
-      }
-    };
-
-
-    enum PostDataStatus
-    {
-      PostDataStatus_Success,
-      PostDataStatus_NoLength,
-      PostDataStatus_Pending,
-      PostDataStatus_Failure
-    };
-  }
-
-
-// TODO Move this to external file
-
-
-  class ChunkedFile : public ChunkedBuffer
-  {
-  private:
-    std::string filename_;
-
-  public:
-    ChunkedFile(const std::string& filename) :
-      filename_(filename)
-    {
-    }
-
-    const std::string& GetFilename() const
-    {
-      return filename_;
-    }
-  };
-
-
-
-  class ChunkStore : public boost::noncopyable
-  {
-  private:
-    typedef std::list<ChunkedFile*>  Content;
-    Content  content_;
-    unsigned int numPlaces_;
-
-    boost::mutex mutex_;
-    std::set<std::string> discardedFiles_;
-
-    void Clear()
-    {
-      for (Content::iterator it = content_.begin();
-           it != content_.end(); ++it)
-      {
-        delete *it;
-      }
-    }
-
-    Content::iterator Find(const std::string& filename)
-    {
-      for (Content::iterator it = content_.begin();
-           it != content_.end(); ++it)
-      {
-        if ((*it)->GetFilename() == filename)
-        {
-          return it;
-        }
-      }
-
-      return content_.end();
-    }
-
-    void Remove(const std::string& filename)
-    {
-      Content::iterator it = Find(filename);
-      if (it != content_.end())
-      {
-        delete *it;
-        content_.erase(it);
-      }
-    }
-
-  public:
-    ChunkStore()
-    {
-      numPlaces_ = 10;
-    }
-
-    ~ChunkStore()
-    {
-      Clear();
-    }
-
-    PostDataStatus Store(std::string& completed,
-                         const char* chunkData,
-                         size_t chunkSize,
-                         const std::string& filename,
-                         size_t filesize)
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      std::set<std::string>::iterator wasDiscarded = discardedFiles_.find(filename);
-      if (wasDiscarded != discardedFiles_.end())
-      {
-        discardedFiles_.erase(wasDiscarded);
-        return PostDataStatus_Failure;
-      }
-
-      ChunkedFile* f;
-      Content::iterator it = Find(filename);
-      if (it == content_.end())
-      {
-        f = new ChunkedFile(filename);
-
-        // Make some room
-        if (content_.size() >= numPlaces_)
-        {
-          discardedFiles_.insert(content_.front()->GetFilename());
-          delete content_.front();
-          content_.pop_front();
-        }
-
-        content_.push_back(f);
-      }
-      else
-      {
-        f = *it;
-      }
-
-      f->AddChunk(chunkData, chunkSize);
-
-      if (f->GetNumBytes() > filesize)
-      {
-        Remove(filename);
-      }
-      else if (f->GetNumBytes() == filesize)
-      {
-        f->Flatten(completed);
-        Remove(filename);
-        return PostDataStatus_Success;
-      }
-
-      return PostDataStatus_Pending;
-    }
-
-    /*void Print() 
-      {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      printf("ChunkStore status:\n");
-      for (Content::const_iterator i = content_.begin();
-      i != content_.end(); i++)
-      {
-      printf("  [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes());
-      }
-      printf("-----\n");
-      }*/
-  };
-
-
-  struct HttpServer::PImpl
-  {
-    struct mg_context *context_;
-    ChunkStore chunkStore_;
-  };
-
-
-  ChunkStore& HttpServer::GetChunkStore()
-  {
-    return pimpl_->chunkStore_;
-  }
-
-
-  static PostDataStatus ReadBodyWithContentLength(std::string& body,
-                                                  struct mg_connection *connection,
-                                                  const std::string& contentLength)
-  {
-    int length;
-    try
-    {
-      length = boost::lexical_cast<int>(contentLength);
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return PostDataStatus_NoLength;
-    }
-
-    if (length < 0)
-    {
-      length = 0;
-    }
-
-    body.resize(length);
-
-    size_t pos = 0;
-    while (length > 0)
-    {
-      int r = mg_read(connection, &body[pos], length);
-      if (r <= 0)
-      {
-        return PostDataStatus_Failure;
-      }
-
-      assert(r <= length);
-      length -= r;
-      pos += r;
-    }
-
-    return PostDataStatus_Success;
-  }
-                                                  
-
-  static PostDataStatus ReadBodyToString(std::string& body,
-                                         struct mg_connection *connection,
-                                         const IHttpHandler::Arguments& headers)
-  {
-    IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length");
-
-    if (contentLength != headers.end())
-    {
-      // "Content-Length" is available
-      return ReadBodyWithContentLength(body, connection, contentLength->second);
-    }
-    else
-    {
-      // No Content-Length. Store the individual chunks in a temporary
-      // file, then read it back into the memory buffer "body"
-      FileBuffer buffer;
-
-      std::string tmp(1024 * 1024, 0);
-      
-      for (;;)
-      {
-        int r = mg_read(connection, &tmp[0], tmp.size());
-        if (r < 0)
-        {
-          return PostDataStatus_Failure;
-        }
-        else if (r == 0)
-        {
-          break;
-        }
-        else
-        {
-          buffer.Append(tmp.c_str(), r);
-        }
-      }
-
-      buffer.Read(body);
-
-      return PostDataStatus_Success;
-    }
-  }
-
-
-  static PostDataStatus ReadBodyToStream(IHttpHandler::IChunkedRequestReader& stream,
-                                         struct mg_connection *connection,
-                                         const IHttpHandler::Arguments& headers)
-  {
-    IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length");
-
-    if (contentLength != headers.end())
-    {
-      // "Content-Length" is available
-      std::string body;
-      PostDataStatus status = ReadBodyWithContentLength(body, connection, contentLength->second);
-
-      if (status == PostDataStatus_Success &&
-          !body.empty())
-      {
-        stream.AddBodyChunk(body.c_str(), body.size());
-      }
-
-      return status;
-    }
-    else
-    {
-      // No Content-Length: This is a chunked transfer. Stream the HTTP connection.
-      std::string tmp(1024 * 1024, 0);
-      
-      for (;;)
-      {
-        int r = mg_read(connection, &tmp[0], tmp.size());
-        if (r < 0)
-        {
-          return PostDataStatus_Failure;
-        }
-        else if (r == 0)
-        {
-          break;
-        }
-        else
-        {
-          stream.AddBodyChunk(tmp.c_str(), r);
-        }
-      }
-
-      return PostDataStatus_Success;
-    }
-  }
-
-
-  static PostDataStatus ParseMultipartForm(std::string &completedFile,
-                                           struct mg_connection *connection,
-                                           const IHttpHandler::Arguments& headers,
-                                           const std::string& contentType,
-                                           ChunkStore& chunkStore)
-  {
-    std::string boundary = "--" + contentType.substr(MULTIPART_FORM_LENGTH);
-
-    std::string body;
-    PostDataStatus status = ReadBodyToString(body, connection, headers);
-
-    if (status != PostDataStatus_Success)
-    {
-      return status;
-    }
-
-    /*for (IHttpHandler::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++)
-      {
-      std::cout << "Header [" << i->first << "] = " << i->second << "\n";
-      }
-      printf("CHUNK\n");*/
-
-    typedef IHttpHandler::Arguments::const_iterator ArgumentIterator;
-
-    ArgumentIterator requestedWith = headers.find("x-requested-with");
-    ArgumentIterator fileName = headers.find("x-file-name");
-    ArgumentIterator fileSizeStr = headers.find("x-file-size");
-
-    if (requestedWith != headers.end() &&
-        requestedWith->second != "XMLHttpRequest")
-    {
-      return PostDataStatus_Failure; 
-    }
-
-    size_t fileSize = 0;
-    if (fileSizeStr != headers.end())
-    {
-      try
-      {
-        fileSize = boost::lexical_cast<size_t>(fileSizeStr->second);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return PostDataStatus_Failure;
-      }
-    }
-
-    typedef boost::find_iterator<std::string::iterator> FindIterator;
-    typedef boost::iterator_range<char*> Range;
-
-    //chunkStore.Print();
-
-    // TODO - Refactor using class "MultipartStreamReader"
-    try
-    {
-      FindIterator last;
-      for (FindIterator it =
-             make_find_iterator(body, boost::first_finder(boundary));
-           it!=FindIterator();
-           ++it)
-      {
-        if (last != FindIterator())
-        {
-          Range part(&last->back(), &it->front());
-          Range content = boost::find_first(part, "\r\n\r\n");
-          if (/*content != Range()*/!content.empty())
-          {
-            Range c(&content.back() + 1, &it->front() - 2);
-            size_t chunkSize = c.size();
-
-            if (chunkSize > 0)
-            {
-              const char* chunkData = &c.front();
-
-              if (fileName == headers.end())
-              {
-                // This file is stored in a single chunk
-                completedFile.resize(chunkSize);
-                if (chunkSize > 0)
-                {
-                  memcpy(&completedFile[0], chunkData, chunkSize);
-                }
-                return PostDataStatus_Success;
-              }
-              else
-              {
-                return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize);
-              }
-            }
-          }
-        }
-
-        last = it;
-      }
-    }
-    catch (std::length_error&)
-    {
-      return PostDataStatus_Failure;
-    }
-
-    return PostDataStatus_Pending;
-  }
-
-
-  static bool IsAccessGranted(const HttpServer& that,
-                              const IHttpHandler::Arguments& headers)
-  {
-    bool granted = false;
-
-    IHttpHandler::Arguments::const_iterator auth = headers.find("authorization");
-    if (auth != headers.end())
-    {
-      std::string s = auth->second;
-      if (s.size() > 6 &&
-          s.substr(0, 6) == "Basic ")
-      {
-        std::string b64 = s.substr(6);
-        granted = that.IsValidBasicHttpAuthentication(b64);
-      }
-    }
-
-    return granted;
-  }
-
-
-  static std::string GetAuthenticatedUsername(const IHttpHandler::Arguments& headers)
-  {
-    IHttpHandler::Arguments::const_iterator auth = headers.find("authorization");
-
-    if (auth == headers.end())
-    {
-      return "";
-    }
-
-    std::string s = auth->second;
-    if (s.size() <= 6 ||
-        s.substr(0, 6) != "Basic ")
-    {
-      return "";
-    }
-
-    std::string b64 = s.substr(6);
-    std::string decoded;
-    Toolbox::DecodeBase64(decoded, b64);
-    size_t semicolons = decoded.find(':');
-
-    if (semicolons == std::string::npos)
-    {
-      // Bad-formatted request
-      return "";
-    }
-    else
-    {
-      return decoded.substr(0, semicolons);
-    }
-  }
-
-
-  static bool ExtractMethod(HttpMethod& method,
-                            const struct mg_request_info *request,
-                            const IHttpHandler::Arguments& headers,
-                            const IHttpHandler::GetArguments& argumentsGET)
-  {
-    std::string overriden;
-
-    // Check whether some PUT/DELETE faking is done
-
-    // 1. Faking with Google's approach
-    IHttpHandler::Arguments::const_iterator methodOverride =
-      headers.find("x-http-method-override");
-
-    if (methodOverride != headers.end())
-    {
-      overriden = methodOverride->second;
-    }
-    else if (!strcmp(request->request_method, "GET"))
-    {
-      // 2. Faking with Ruby on Rail's approach
-      // GET /my/resource?_method=delete <=> DELETE /my/resource
-      for (size_t i = 0; i < argumentsGET.size(); i++)
-      {
-        if (argumentsGET[i].first == "_method")
-        {
-          overriden = argumentsGET[i].second;
-          break;
-        }
-      }
-    }
-
-    if (overriden.size() > 0)
-    {
-      // A faking has been done within this request
-      Toolbox::ToUpperCase(overriden);
-
-      LOG(INFO) << "HTTP method faking has been detected for " << overriden;
-
-      if (overriden == "PUT")
-      {
-        method = HttpMethod_Put;
-        return true;
-      }
-      else if (overriden == "DELETE")
-      {
-        method = HttpMethod_Delete;
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-    // No PUT/DELETE faking was present
-    if (!strcmp(request->request_method, "GET"))
-    {
-      method = HttpMethod_Get;
-    }
-    else if (!strcmp(request->request_method, "POST"))
-    {
-      method = HttpMethod_Post;
-    }
-    else if (!strcmp(request->request_method, "DELETE"))
-    {
-      method = HttpMethod_Delete;
-    }
-    else if (!strcmp(request->request_method, "PUT"))
-    {
-      method = HttpMethod_Put;
-    }
-    else
-    {
-      return false;
-    }    
-
-    return true;
-  }
-
-
-  static void ConfigureHttpCompression(HttpOutput& output,
-                                       const IHttpHandler::Arguments& headers)
-  {
-    // Look if the client wishes HTTP compression
-    // https://en.wikipedia.org/wiki/HTTP_compression
-    IHttpHandler::Arguments::const_iterator it = headers.find("accept-encoding");
-    if (it != headers.end())
-    {
-      std::vector<std::string> encodings;
-      Toolbox::TokenizeString(encodings, it->second, ',');
-
-      for (size_t i = 0; i < encodings.size(); i++)
-      {
-        std::string s = Toolbox::StripSpaces(encodings[i]);
-
-        if (s == "deflate")
-        {
-          output.SetDeflateAllowed(true);
-        }
-        else if (s == "gzip")
-        {
-          output.SetGzipAllowed(true);
-        }
-      }
-    }
-  }
-
-
-  static void InternalCallback(HttpOutput& output /* out */,
-                               HttpMethod& method /* out */,
-                               HttpServer& server,
-                               struct mg_connection *connection,
-                               const struct mg_request_info *request)
-  {
-    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 (!server.IsRemoteAccessAllowed() &&
-        !localhost)
-    {
-      output.SendUnauthorized(server.GetRealm());
-      return;
-    }
-
-
-    // Extract the HTTP headers
-    IHttpHandler::Arguments headers;
-    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, value));
-      VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]";
-    }
-
-    if (server.IsHttpCompressionEnabled())
-    {
-      ConfigureHttpCompression(output, headers);
-    }
-
-
-    // Extract the GET arguments
-    IHttpHandler::GetArguments argumentsGET;
-    if (!strcmp(request->request_method, "GET"))
-    {
-      HttpToolbox::ParseGetArguments(argumentsGET, request->query_string);
-    }
-
-
-    // Compute the HTTP method, taking method faking into consideration
-    method = HttpMethod_Get;
-    if (!ExtractMethod(method, request, headers, argumentsGET))
-    {
-      output.SendStatus(HttpStatus_400_BadRequest);
-      return;
-    }
-
-
-    // Authenticate this connection
-    if (server.IsAuthenticationEnabled() && 
-        !IsAccessGranted(server, headers))
-    {
-      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", 
-            reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], 
-            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]);
-
-    const char* requestUri = request->uri;
-      
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-    const char* remoteIp = request->remote_addr;
-    const char* requestUri = request->local_uri;
-#else
-#  error
-#endif
-
-    if (requestUri == NULL)
-    {
-      requestUri = "";
-    }
-      
-    std::string username = GetAuthenticatedUsername(headers);
-
-    IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter();
-    if (filter != NULL)
-    {
-      if (!filter->IsAllowed(method, requestUri, remoteIp,
-                             username.c_str(), headers, argumentsGET))
-      {
-        //output.SendUnauthorized(server.GetRealm());
-        output.SendStatus(HttpStatus_403_Forbidden);
-        return;
-      }
-    }
-
-
-    // Decompose the URI into its components
-    UriComponents uri;
-    try
-    {
-      Toolbox::SplitUriComponents(uri, requestUri);
-    }
-    catch (OrthancException&)
-    {
-      output.SendStatus(HttpStatus_400_BadRequest);
-      return;
-    }
-
-    LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
-
-
-    bool found = false;
-
-    // Extract the body of the request for PUT and POST, or process
-    // the body as a stream
-
-    // TODO Avoid unneccessary memcopy of the body
-
-    std::string body;
-    if (method == HttpMethod_Post ||
-        method == HttpMethod_Put)
-    {
-      PostDataStatus status;
-
-      bool isMultipartForm = false;
-
-      IHttpHandler::Arguments::const_iterator ct = headers.find("content-type");
-      if (ct != headers.end() &&
-          ct->second.size() >= MULTIPART_FORM_LENGTH &&
-          !memcmp(ct->second.c_str(), MULTIPART_FORM, MULTIPART_FORM_LENGTH))
-      {
-        /** 
-         * The user uses the "upload" form of Orthanc Explorer, for
-         * file uploads through a HTML form.
-         **/
-        status = ParseMultipartForm(body, connection, headers, ct->second, server.GetChunkStore());
-        isMultipartForm = true;
-      }
-
-      if (!isMultipartForm)
-      {
-        std::unique_ptr<IHttpHandler::IChunkedRequestReader> stream;
-
-        if (server.HasHandler())
-        {
-          found = server.GetHandler().CreateChunkedRequestReader
-            (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers);
-        }
-        
-        if (found)
-        {
-          if (stream.get() == NULL)
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          status = ReadBodyToStream(*stream, connection, headers);
-
-          if (status == PostDataStatus_Success)
-          {
-            stream->Execute(output);
-          }
-        }
-        else
-        {
-          status = ReadBodyToString(body, connection, headers);
-        }
-      }
-
-      switch (status)
-      {
-        case PostDataStatus_NoLength:
-          output.SendStatus(HttpStatus_411_LengthRequired);
-          return;
-
-        case PostDataStatus_Failure:
-          output.SendStatus(HttpStatus_400_BadRequest);
-          return;
-
-        case PostDataStatus_Pending:
-          output.AnswerEmpty();
-          return;
-
-        case PostDataStatus_Success:
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    if (!found && 
-        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
-    {
-#if ORTHANC_ENABLE_MONGOOSE == 1
-      void *that = request->user_data;
-      const char* requestUri = request->uri;
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-      // https://github.com/civetweb/civetweb/issues/409
-      void *that = mg_get_user_data(mg_get_context(connection));
-      const char* requestUri = request->local_uri;
-#else
-#  error
-#endif
-
-      if (requestUri == NULL)
-      {
-        requestUri = "";
-      }
-      
-      HttpServer* server = reinterpret_cast<HttpServer*>(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
-      {
-        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&)
-        {
-          throw OrthancException(ErrorCode_BadParameterType,
-                                 "Syntax error in some user-supplied data");
-        }
-        catch (boost::filesystem::filesystem_error& e)
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Error while accessing the filesystem: " + e.path1().string());
-        }
-        catch (std::runtime_error&)
-        {
-          throw OrthancException(ErrorCode_BadRequest,
-                                 "Presumably an error while parsing the JSON body");
-        }
-        catch (std::bad_alloc&)
-        {
-          throw OrthancException(ErrorCode_NotEnoughMemory,
-                                 "The server hosting Orthanc is running out of memory");
-        }
-        catch (...)
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "An unhandled exception was generated inside the HTTP server");
-        }
-      }
-      catch (OrthancException& e)
-      {
-        assert(server != NULL);
-
-        // 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, requestUri);
-          }
-        }
-        catch (OrthancException&)
-        {
-          // An exception here reflects the fact that the status code
-          // was already set by the HTTP handler.
-        }
-      }
-    }
-    catch (...)
-    {
-      // 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";
-    }
-  }
-
-
-#if MONGOOSE_USE_CALLBACKS == 0
-  static void* Callback(enum mg_event event,
-                        struct mg_connection *connection,
-                        const struct mg_request_info *request)
-  {
-    if (event == MG_NEW_REQUEST) 
-    {
-      ProtectedCallback(connection, request);
-
-      // Mark as processed
-      return (void*) "";
-    }
-    else
-    {
-      return NULL;
-    }
-  }
-
-#elif MONGOOSE_USE_CALLBACKS == 1
-  static int Callback(struct mg_connection *connection)
-  {
-    const struct mg_request_info *request = mg_get_request_info(connection);
-
-    ProtectedCallback(connection, request);
-
-    return 1;  // Do not let Mongoose handle the request by itself
-  }
-
-#else
-#  error Please set MONGOOSE_USE_CALLBACKS
-#endif
-
-
-
-
-
-  bool HttpServer::IsRunning() const
-  {
-    return (pimpl_->context_ != NULL);
-  }
-
-
-  HttpServer::HttpServer() : pimpl_(new PImpl)
-  {
-    pimpl_->context_ = NULL;
-    handler_ = NULL;
-    remoteAllowed_ = false;
-    authentication_ = false;
-    ssl_ = false;
-    port_ = 8000;
-    filter_ = NULL;
-    keepAlive_ = false;
-    httpCompression_ = true;
-    exceptionFormatter_ = NULL;
-    realm_ = ORTHANC_REALM;
-    threadsCount_ = 50;  // Default value in mongoose
-    tcpNoDelay_ = true;
-    requestTimeout_ = 30;  // Default value in mongoose/civetweb (30 seconds)
-
-#if ORTHANC_ENABLE_MONGOOSE == 1
-    LOG(INFO) << "This Orthanc server uses Mongoose as its embedded HTTP server";
-#endif
-
-#if ORTHANC_ENABLE_CIVETWEB == 1
-    LOG(INFO) << "This Orthanc server uses CivetWeb as its embedded HTTP server";
-#endif
-
-#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 */ &&
-        OPENSSL_VERSION_NUMBER >= 0x1000100fL  /* openssl-1.0.1 */) 
-    {
-      LOG(WARNING) << "This version of OpenSSL is vulnerable to the Heartbleed exploit";
-    }
-#endif
-  }
-
-
-  HttpServer::~HttpServer()
-  {
-    Stop();
-  }
-
-
-  void HttpServer::SetPortNumber(uint16_t port)
-  {
-    Stop();
-    port_ = port;
-  }
-
-  void HttpServer::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_);
-      std::string numThreads = boost::lexical_cast<std::string>(threadsCount_);
-      std::string requestTimeoutMilliseconds = boost::lexical_cast<std::string>(requestTimeout_ * 1000);
-
-      if (ssl_)
-      {
-        port += "s";
-      }
-
-      const char *options[] = {
-        // Set the TCP port for the HTTP server
-        "listening_ports", port.c_str(), 
-        
-        // Optimization reported by Chris Hafey
-        // 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
-
-#if ORTHANC_ENABLE_CIVETWEB == 1
-        // Disable TCP Nagle's algorithm to maximize speed (this
-        // option is not available in Mongoose).
-        // https://groups.google.com/d/topic/civetweb/35HBR9seFjU/discussion
-        // https://eklitzke.org/the-caveats-of-tcp-nodelay
-        "tcp_nodelay", (tcpNoDelay_ ? "1" : "0"),
-#endif
-
-        // Set the number of threads
-        "num_threads", numThreads.c_str(),
-        
-        // Set the timeout for the HTTP server
-        "request_timeout_ms", requestTimeoutMilliseconds.c_str(),
-
-        // Set the SSL certificate, if any. This must be the last option.
-        ssl_ ? "ssl_certificate" : NULL,
-        certificate_.c_str(),
-        NULL
-      };
-
-#if MONGOOSE_USE_CALLBACKS == 0
-      pimpl_->context_ = mg_start(&Callback, this, options);
-
-#elif MONGOOSE_USE_CALLBACKS == 1
-      struct mg_callbacks callbacks;
-      memset(&callbacks, 0, sizeof(callbacks));
-      callbacks.begin_request = Callback;
-      pimpl_->context_ = mg_start(&callbacks, this, options);
-
-#else
-#  error Please set MONGOOSE_USE_CALLBACKS
-#endif
-
-      if (!pimpl_->context_)
-      {
-        bool isSslError = false;
-
-#if ORTHANC_ENABLE_SSL == 1
-        for (;;)
-        {
-          unsigned long code = ERR_get_error();
-          if (code == 0)
-          {
-            break;
-          }
-          else
-          {
-            isSslError = true;
-            char message[1024];
-            ERR_error_string_n(code, message, sizeof(message) - 1);
-            LOG(ERROR) << "OpenSSL error: " << message;
-          }
-        }        
-#endif
-
-        if (isSslError)
-        {
-          throw OrthancException(ErrorCode_SslInitialization);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_HttpPortInUse,
-                                 " (port = " + boost::lexical_cast<std::string>(port_) + ")");
-        }
-      }
-
-      LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber()
-                   << " (HTTPS encryption is "
-                   << (IsSslEnabled() ? "enabled" : "disabled")
-                   << ", remote access is "
-                   << (IsRemoteAccessAllowed() ? "" : "not ")
-                   << "allowed)";
-    }
-  }
-
-  void HttpServer::Stop()
-  {
-    if (IsRunning())
-    {
-      mg_stop(pimpl_->context_);
-      pimpl_->context_ = NULL;
-    }
-  }
-
-
-  void HttpServer::ClearUsers()
-  {
-    Stop();
-    registeredUsers_.clear();
-  }
-
-
-  void HttpServer::RegisterUser(const char* username,
-                                const char* password)
-  {
-    Stop();
-
-    std::string tag = std::string(username) + ":" + std::string(password);
-    std::string encoded;
-    Toolbox::EncodeBase64(encoded, tag);
-    registeredUsers_.insert(encoded);
-  }
-
-  void HttpServer::SetSslEnabled(bool enabled)
-  {
-    Stop();
-
-#if ORTHANC_ENABLE_SSL == 0
-    if (enabled)
-    {
-      throw OrthancException(ErrorCode_SslDisabled);
-    }
-    else
-    {
-      ssl_ = false;
-    }
-#else
-    ssl_ = enabled;
-#endif
-  }
-
-
-  void HttpServer::SetKeepAliveEnabled(bool enabled)
-  {
-    Stop();
-    keepAlive_ = enabled;
-    LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled");
-
-#if ORTHANC_ENABLE_MONGOOSE == 1
-    if (enabled)
-    {
-      LOG(WARNING) << "You should disable HTTP keep alive, as you are using Mongoose";
-    }
-#endif
-  }
-
-
-  void HttpServer::SetAuthenticationEnabled(bool enabled)
-  {
-    Stop();
-    authentication_ = enabled;
-  }
-
-  void HttpServer::SetSslCertificate(const char* path)
-  {
-    Stop();
-    certificate_ = path;
-  }
-
-  void HttpServer::SetRemoteAccessAllowed(bool allowed)
-  {
-    Stop();
-    remoteAllowed_ = allowed;
-  }
-
-  void HttpServer::SetHttpCompressionEnabled(bool enabled)
-  {
-    Stop();
-    httpCompression_ = enabled;
-    LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled");
-  }
-  
-  void HttpServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter)
-  {
-    Stop();
-    filter_ = &filter;
-  }
-
-
-  void HttpServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter)
-  {
-    Stop();
-    exceptionFormatter_ = &formatter;
-  }
-
-
-  bool HttpServer::IsValidBasicHttpAuthentication(const std::string& basic) const
-  {
-    return registeredUsers_.find(basic) != registeredUsers_.end();
-  }
-
-
-  void HttpServer::Register(IHttpHandler& handler)
-  {
-    Stop();
-    handler_ = &handler;
-  }
-
-
-  IHttpHandler& HttpServer::GetHandler() const
-  {
-    if (handler_ == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return *handler_;
-  }
-
-
-  void HttpServer::SetThreadsCount(unsigned int threads)
-  {
-    if (threads <= 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    
-    Stop();
-    threadsCount_ = threads;
-
-    LOG(INFO) << "The embedded HTTP server will use " << threads << " threads";
-  }
-
-  
-  void HttpServer::SetTcpNoDelay(bool tcpNoDelay)
-  {
-    Stop();
-    tcpNoDelay_ = tcpNoDelay;
-    LOG(INFO) << "TCP_NODELAY for the HTTP sockets is set to "
-              << (tcpNoDelay ? "true" : "false");
-  }
-
-
-  void HttpServer::SetRequestTimeout(unsigned int seconds)
-  {
-    if (seconds <= 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Request timeout must be a stricly positive integer");
-    }
-
-    Stop();
-    requestTimeout_ = seconds;
-    LOG(INFO) << "Request timeout in the HTTP server is set to " << seconds << " seconds";
-  }
-}
--- a/Core/HttpServer/HttpServer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,230 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-// To have ORTHANC_ENABLE_xxx defined if using the shared library
-#include "../OrthancFramework.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 <list>
-#include <map>
-#include <set>
-#include <stdint.h>
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class ChunkStore;
-  class OrthancException;
-
-  class IHttpExceptionFormatter : public boost::noncopyable
-  {
-  public:
-    virtual ~IHttpExceptionFormatter()
-    {
-    }
-
-    virtual void Format(HttpOutput& output,
-                        const OrthancException& exception,
-                        HttpMethod method,
-                        const char* uri) = 0;
-  };
-
-
-  class ORTHANC_PUBLIC HttpServer : public boost::noncopyable
-  {
-  private:
-    // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    IHttpHandler *handler_;
-
-    typedef std::set<std::string> RegisteredUsers;
-    RegisteredUsers registeredUsers_;
-
-    bool remoteAllowed_;
-    bool authentication_;
-    bool ssl_;
-    std::string certificate_;
-    uint16_t port_;
-    IIncomingHttpRequestFilter* filter_;
-    bool keepAlive_;
-    bool httpCompression_;
-    IHttpExceptionFormatter* exceptionFormatter_;
-    std::string realm_;
-    unsigned int threadsCount_;
-    bool tcpNoDelay_;
-    unsigned int requestTimeout_;  // In seconds
-  
-    bool IsRunning() const;
-
-  public:
-    HttpServer();
-
-    ~HttpServer();
-
-    void SetPortNumber(uint16_t port);
-
-    uint16_t GetPortNumber() const
-    {
-      return port_;
-    }
-
-    void Start();
-
-    void Stop();
-
-    void ClearUsers();
-
-    void RegisterUser(const char* username,
-                      const char* password);
-
-    bool IsAuthenticationEnabled() const
-    {
-      return authentication_;
-    }
-
-    void SetAuthenticationEnabled(bool enabled);
-
-    bool IsSslEnabled() const
-    {
-      return ssl_;
-    }
-
-    void SetSslEnabled(bool enabled);
-
-    bool IsKeepAliveEnabled() const
-    {
-      return keepAlive_;
-    }
-
-    void SetKeepAliveEnabled(bool enabled);
-
-    const std::string& GetSslCertificate() const
-    {
-      return certificate_;
-    }
-
-    void SetSslCertificate(const char* path);
-
-    bool IsRemoteAccessAllowed() const
-    {
-      return remoteAllowed_;
-    }
-
-    void SetRemoteAccessAllowed(bool allowed);
-
-    bool IsHttpCompressionEnabled() const
-    {
-      return httpCompression_;;
-    }
-
-    void SetHttpCompressionEnabled(bool enabled);
-
-    IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
-    {
-      return filter_;
-    }
-
-    void SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter);
-
-    ChunkStore& GetChunkStore();
-
-    bool IsValidBasicHttpAuthentication(const std::string& basic) const;
-
-    void Register(IHttpHandler& handler);
-
-    bool HasHandler() const
-    {
-      return handler_ != NULL;
-    }
-
-    IHttpHandler& GetHandler() const;
-
-    void SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter);
-
-    IHttpExceptionFormatter* GetExceptionFormatter()
-    {
-      return exceptionFormatter_;
-    }
-
-    const std::string& GetRealm() const
-    {
-      return realm_;
-    }
-
-    void SetRealm(const std::string& realm)
-    {
-      realm_ = realm;
-    }
-
-    void SetThreadsCount(unsigned int threads);
-
-    unsigned int GetThreadsCount() const
-    {
-      return threadsCount_;
-    }
-
-    // New in Orthanc 1.5.2, not available for Mongoose
-    void SetTcpNoDelay(bool tcpNoDelay);
-
-    bool IsTcpNoDelay() const
-    {
-      return tcpNoDelay_;
-    }
-
-    void SetRequestTimeout(unsigned int seconds);
-
-    unsigned int GetRequestTimeout() const
-    {
-      return requestTimeout_;
-    }
-  };
-}
--- a/Core/HttpServer/HttpStreamTranscoder.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,251 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpStreamTranscoder.h"
-
-#include "../OrthancException.h"
-#include "../Compression/ZlibCompressor.h"
-
-#include <string.h>   // For memcpy()
-#include <cassert>
-
-#include <stdio.h>
-
-namespace Orthanc
-{
-  void HttpStreamTranscoder::ReadSource(std::string& buffer)
-  {
-    if (source_.SetupHttpCompression(false, false) != HttpCompression_None)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    uint64_t size = source_.GetContentLength();
-    if (static_cast<uint64_t>(static_cast<size_t>(size)) != size)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    buffer.resize(static_cast<size_t>(size));
-    size_t offset = 0;
-
-    while (source_.ReadNextChunk())
-    {
-      size_t chunkSize = static_cast<size_t>(source_.GetChunkSize());
-      memcpy(&buffer[offset], source_.GetChunkContent(), chunkSize);
-      offset += chunkSize;
-    }
-
-    if (offset != size)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  HttpCompression HttpStreamTranscoder::SetupZlibCompression(bool deflateAllowed)
-  {
-    uint64_t size = source_.GetContentLength();
-
-    if (size == 0)
-    {
-      return HttpCompression_None;
-    }
-
-    if (size < sizeof(uint64_t))
-    {
-      throw OrthancException(ErrorCode_CorruptedFile);
-    }
-
-    if (deflateAllowed)
-    {
-      bytesToSkip_ = sizeof(uint64_t);
-
-      return HttpCompression_Deflate;
-    }
-    else
-    {
-      // TODO Use stream-based zlib decoding to reduce memory usage
-      std::string compressed;
-      ReadSource(compressed);
-
-      uncompressed_.reset(new BufferHttpSender);
-
-      ZlibCompressor compressor;
-      IBufferCompressor::Uncompress(uncompressed_->GetBuffer(), compressor, compressed);
-
-      return HttpCompression_None;
-    }
-  }
-
-
-  HttpCompression HttpStreamTranscoder::SetupHttpCompression(bool gzipAllowed,
-                                                             bool deflateAllowed)
-  {
-    if (ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    ready_ = true;
-
-    switch (sourceCompression_)
-    {
-      case CompressionType_None:
-        return HttpCompression_None;
-
-      case CompressionType_ZlibWithSize:
-        return SetupZlibCompression(deflateAllowed);
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  uint64_t HttpStreamTranscoder::GetContentLength()
-  {
-    if (!ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    if (uncompressed_.get() != NULL)
-    {
-      return uncompressed_->GetContentLength();
-    }
-    else
-    {
-      uint64_t length = source_.GetContentLength();
-      if (length < bytesToSkip_)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      return length - bytesToSkip_;
-    }
-  }
-
-
-  bool HttpStreamTranscoder::ReadNextChunk()
-  {
-    if (!ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    if (uncompressed_.get() != NULL)
-    {
-      return uncompressed_->ReadNextChunk();
-    }
-
-    assert(skipped_ <= bytesToSkip_);
-    if (skipped_ == bytesToSkip_)
-    {
-      // We have already skipped the first bytes of the stream
-      currentChunkOffset_ = 0;
-      return source_.ReadNextChunk();
-    }
-
-    // This condition can only be true on the first call to "ReadNextChunk()"
-    for (;;)
-    {
-      assert(skipped_ < bytesToSkip_);
-
-      bool ok = source_.ReadNextChunk();
-      if (!ok)
-      {
-        throw OrthancException(ErrorCode_CorruptedFile);
-      }
-
-      size_t remaining = static_cast<size_t>(bytesToSkip_ - skipped_);
-      size_t s = source_.GetChunkSize();
-
-      if (s < remaining)
-      {
-        skipped_ += s;
-      }
-      else if (s == remaining)
-      {
-        // We have skipped enough bytes, but we must read a new chunk
-        currentChunkOffset_ = 0;            
-        skipped_ = bytesToSkip_;
-        return source_.ReadNextChunk();
-      }
-      else
-      {
-        // We have skipped enough bytes, and we have enough data in the current chunk
-        assert(s > remaining);
-        currentChunkOffset_ = remaining;
-        skipped_ = bytesToSkip_;
-        return true;
-      }
-    }
-  }
-
-
-  const char* HttpStreamTranscoder::GetChunkContent()
-  {
-    if (!ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    if (uncompressed_.get() != NULL)
-    {
-      return uncompressed_->GetChunkContent();
-    }
-    else
-    {
-      return source_.GetChunkContent() + currentChunkOffset_;
-    }
-  }
-
-  size_t HttpStreamTranscoder::GetChunkSize()
-  {
-    if (!ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    if (uncompressed_.get() != NULL)
-    {
-      return uncompressed_->GetChunkSize();
-    }
-    else
-    {
-      return static_cast<size_t>(source_.GetChunkSize() - currentChunkOffset_);
-    }
-  }
-}
--- a/Core/HttpServer/HttpStreamTranscoder.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "BufferHttpSender.h"
-
-#include "../Compatibility.h"
-
-#include <memory>  // For std::unique_ptr
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC HttpStreamTranscoder : public IHttpStreamAnswer
-  {
-  private:
-    IHttpStreamAnswer& source_;
-    CompressionType    sourceCompression_;
-    uint64_t           bytesToSkip_;
-    uint64_t           skipped_;
-    uint64_t           currentChunkOffset_;
-    bool               ready_;
-
-    std::unique_ptr<BufferHttpSender>  uncompressed_;
-
-    void ReadSource(std::string& buffer);
-
-    HttpCompression SetupZlibCompression(bool deflateAllowed);
-
-  public:
-    HttpStreamTranscoder(IHttpStreamAnswer& source,
-                         CompressionType compression) : 
-      source_(source),
-      sourceCompression_(compression),
-      bytesToSkip_(0),
-      skipped_(0),
-      currentChunkOffset_(0),
-      ready_(false)
-    {
-    }
-
-    // This is the first method to be called
-    virtual HttpCompression SetupHttpCompression(bool gzipAllowed,
-                                                 bool deflateAllowed);
-
-    virtual bool HasContentFilename(std::string& filename)
-    {
-      return source_.HasContentFilename(filename);
-    }
-
-    virtual std::string GetContentType()
-    {
-      return source_.GetContentType();
-    }
-
-    virtual uint64_t GetContentLength();
-
-    virtual bool ReadNextChunk();
-
-    virtual const char* GetChunkContent();
-
-    virtual size_t GetChunkSize();
-  };
-}
--- a/Core/HttpServer/HttpToolbox.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,298 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "HttpToolbox.h"
-
-#include <stdio.h>
-#include <string.h>
-#include <iostream>
-
-#include "HttpOutput.h"
-#include "StringHttpOutput.h"
-
-
-static const char* LOCALHOST = "127.0.0.1";
-
-
-
-namespace Orthanc
-{
-  static void SplitGETNameValue(IHttpHandler::GetArguments& result,
-                                const char* start,
-                                const char* end)
-  {
-    std::string name, value;
-    
-    const char* equal = strchr(start, '=');
-    if (equal == NULL || equal >= end)
-    {
-      name = std::string(start, end - start);
-      //value = "";
-    }
-    else
-    {
-      name = std::string(start, equal - start);
-      value = std::string(equal + 1, end);
-    }
-
-    Toolbox::UrlDecode(name);
-    Toolbox::UrlDecode(value);
-
-    result.push_back(std::make_pair(name, value));
-  }
-
-
-  void HttpToolbox::ParseGetArguments(IHttpHandler::GetArguments& result, 
-                                      const char* query)
-  {
-    const char* pos = query;
-
-    while (pos != NULL)
-    {
-      const char* ampersand = strchr(pos, '&');
-      if (ampersand)
-      {
-        SplitGETNameValue(result, pos, ampersand);
-        pos = ampersand + 1;
-      }
-      else
-      {
-        // No more ampersand, this is the last argument
-        SplitGETNameValue(result, pos, pos + strlen(pos));
-        pos = NULL;
-      }
-    }
-  }
-
-
-  void  HttpToolbox::ParseGetQuery(UriComponents& uri,
-                                   IHttpHandler::GetArguments& getArguments, 
-                                   const char* query)
-  {
-    const char *questionMark = ::strchr(query, '?');
-    if (questionMark == NULL)
-    {
-      // No question mark in the string
-      Toolbox::SplitUriComponents(uri, query);
-      getArguments.clear();
-    }
-    else
-    {
-      Toolbox::SplitUriComponents(uri, std::string(query, questionMark));
-      HttpToolbox::ParseGetArguments(getArguments, questionMark + 1);
-    }    
-  }
-
- 
-  std::string HttpToolbox::GetArgument(const IHttpHandler::Arguments& getArguments,
-                                       const std::string& name,
-                                       const std::string& defaultValue)
-  {
-    IHttpHandler::Arguments::const_iterator it = getArguments.find(name);
-    if (it == getArguments.end())
-    {
-      return defaultValue;
-    }
-    else
-    {
-      return it->second;
-    }
-  }
-
-
-  std::string HttpToolbox::GetArgument(const IHttpHandler::GetArguments& getArguments,
-                                       const std::string& name,
-                                       const std::string& defaultValue)
-  {
-    for (size_t i = 0; i < getArguments.size(); i++)
-    {
-      if (getArguments[i].first == name)
-      {
-        return getArguments[i].second;
-      }
-    }
-
-    return defaultValue;
-  }
-
-
-
-  void HttpToolbox::ParseCookies(IHttpHandler::Arguments& result, 
-                                 const IHttpHandler::Arguments& httpHeaders)
-  {
-    result.clear();
-
-    IHttpHandler::Arguments::const_iterator it = httpHeaders.find("cookie");
-    if (it != httpHeaders.end())
-    {
-      const std::string& cookies = it->second;
-
-      size_t pos = 0;
-      while (pos != std::string::npos)
-      {
-        size_t nextSemicolon = cookies.find(";", pos);
-        std::string cookie;
-
-        if (nextSemicolon == std::string::npos)
-        {
-          cookie = cookies.substr(pos);
-          pos = std::string::npos;
-        }
-        else
-        {
-          cookie = cookies.substr(pos, nextSemicolon - pos);
-          pos = nextSemicolon + 1;
-        }
-
-        size_t equal = cookie.find("=");
-        if (equal != std::string::npos)
-        {
-          std::string name = Toolbox::StripSpaces(cookie.substr(0, equal));
-          std::string value = Toolbox::StripSpaces(cookie.substr(equal + 1));
-          result[name] = value;
-        }
-      }
-    }
-  }
-
-
-  void HttpToolbox::CompileGetArguments(IHttpHandler::Arguments& compiled,
-                                        const IHttpHandler::GetArguments& source)
-  {
-    compiled.clear();
-
-    for (size_t i = 0; i < source.size(); i++)
-    {
-      compiled[source[i].first] = source[i].second;
-    }
-  }
-
-
-  bool HttpToolbox::SimpleGet(std::string& result,
-                              IHttpHandler& handler,
-                              RequestOrigin origin,
-                              const std::string& uri,
-                              const IHttpHandler::Arguments& httpHeaders)
-  {
-    UriComponents curi;
-    IHttpHandler::GetArguments getArguments;
-    ParseGetQuery(curi, getArguments, uri.c_str());
-
-    StringHttpOutput stream;
-    HttpOutput http(stream, false /* no keep alive */);
-
-    if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, 
-                       httpHeaders, getArguments, NULL /* no body for GET */, 0))
-    {
-      stream.GetOutput(result);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  static bool SimplePostOrPut(std::string& result,
-                              IHttpHandler& handler,
-                              RequestOrigin origin,
-                              HttpMethod method,
-                              const std::string& uri,
-                              const void* bodyData,
-                              size_t bodySize,
-                              const IHttpHandler::Arguments& httpHeaders)
-  {
-    IHttpHandler::GetArguments getArguments;  // No GET argument for POST/PUT
-
-    UriComponents curi;
-    Toolbox::SplitUriComponents(curi, uri);
-
-    StringHttpOutput stream;
-    HttpOutput http(stream, false /* no keep alive */);
-
-    if (handler.Handle(http, origin, LOCALHOST, "", method, curi, 
-                       httpHeaders, getArguments, bodyData, bodySize))
-    {
-      stream.GetOutput(result);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  bool HttpToolbox::SimplePost(std::string& result,
-                               IHttpHandler& handler,
-                               RequestOrigin origin,
-                               const std::string& uri,
-                               const void* bodyData,
-                               size_t bodySize,
-                               const IHttpHandler::Arguments& httpHeaders)
-  {
-    return SimplePostOrPut(result, handler, origin, HttpMethod_Post, uri, bodyData, bodySize, httpHeaders);
-  }
-
-
-  bool HttpToolbox::SimplePut(std::string& result,
-                              IHttpHandler& handler,
-                              RequestOrigin origin,
-                              const std::string& uri,
-                              const void* bodyData,
-                              size_t bodySize,
-                              const IHttpHandler::Arguments& httpHeaders)
-  {
-    return SimplePostOrPut(result, handler, origin, HttpMethod_Put, uri, bodyData, bodySize, httpHeaders);
-  }
-
-
-  bool HttpToolbox::SimpleDelete(IHttpHandler& handler,
-                                 RequestOrigin origin,
-                                 const std::string& uri,
-                                 const IHttpHandler::Arguments& httpHeaders)
-  {
-    UriComponents curi;
-    Toolbox::SplitUriComponents(curi, uri);
-
-    IHttpHandler::GetArguments getArguments;  // No GET argument for DELETE
-
-    StringHttpOutput stream;
-    HttpOutput http(stream, false /* no keep alive */);
-
-    return handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, 
-                          httpHeaders, getArguments, NULL /* no body for DELETE */, 0);
-  }
-}
--- a/Core/HttpServer/HttpToolbox.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-#include "IHttpHandler.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC HttpToolbox : public boost::noncopyable
-  {
-  public:
-    static void ParseGetArguments(IHttpHandler::GetArguments& result, 
-                                  const char* query);
-
-    static void ParseGetQuery(UriComponents& uri,
-                              IHttpHandler::GetArguments& getArguments, 
-                              const char* query);
-
-    static std::string GetArgument(const IHttpHandler::Arguments& getArguments,
-                                   const std::string& name,
-                                   const std::string& defaultValue);
-
-    static std::string GetArgument(const IHttpHandler::GetArguments& getArguments,
-                                   const std::string& name,
-                                   const std::string& defaultValue);
-
-    static void ParseCookies(IHttpHandler::Arguments& result, 
-                             const IHttpHandler::Arguments& httpHeaders);
-
-    static void CompileGetArguments(IHttpHandler::Arguments& compiled,
-                                    const IHttpHandler::GetArguments& source);
-
-    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,
-                           const std::string& uri,
-                           const void* bodyData,
-                           size_t bodySize,
-                          const IHttpHandler::Arguments& httpHeaders);
-
-    static bool SimplePut(std::string& result,
-                          IHttpHandler& handler,
-                          RequestOrigin origin,
-                          const std::string& uri,
-                          const void* bodyData,
-                          size_t bodySize,
-                          const IHttpHandler::Arguments& httpHeaders);
-
-    static bool SimpleDelete(IHttpHandler& handler,
-                             RequestOrigin origin,
-                             const std::string& uri,
-                             const IHttpHandler::Arguments& httpHeaders);
-  };
-}
--- a/Core/HttpServer/IHttpHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,96 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.h"
-#include "../Toolbox.h"
-#include "HttpOutput.h"
-
-#include <map>
-#include <set>
-#include <vector>
-#include <string>
-#include <memory>
-
-namespace Orthanc
-{
-  class IHttpHandler : public boost::noncopyable
-  {
-  public:
-    typedef std::map<std::string, std::string>                  Arguments;
-    typedef std::vector< std::pair<std::string, std::string> >  GetArguments;
-
-
-    class IChunkedRequestReader : public boost::noncopyable
-    {
-    public:
-      virtual ~IChunkedRequestReader()
-      {
-      }
-
-      virtual void AddBodyChunk(const void* data,
-                                size_t size) = 0;
-
-      virtual void Execute(HttpOutput& output) = 0;
-    };
-
-
-    virtual ~IHttpHandler()
-    {
-    }
-
-    /**
-     * This function allows one to deal with chunked transfers (new in
-     * Orthanc 1.5.7). It is only called if "method" is POST or PUT.
-     **/
-    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                            RequestOrigin origin,
-                                            const char* remoteIp,
-                                            const char* username,
-                                            HttpMethod method,
-                                            const UriComponents& uri,
-                                            const Arguments& headers) = 0;
-
-    virtual bool Handle(HttpOutput& output,
-                        RequestOrigin origin,
-                        const char* remoteIp,
-                        const char* username,
-                        HttpMethod method,
-                        const UriComponents& uri,
-                        const Arguments& headers,
-                        const GetArguments& getArguments,
-                        const void* bodyData,
-                        size_t bodySize) = 0;
-  };
-}
--- a/Core/HttpServer/IHttpOutputStream.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class IHttpOutputStream : public boost::noncopyable
-  {
-  public:
-    virtual ~IHttpOutputStream()
-    {
-    }
-
-    virtual void OnHttpStatusReceived(HttpStatus status) = 0;
-
-    virtual void Send(bool isHeader, const void* buffer, size_t length) = 0;
-
-    // Disable HTTP keep alive for this single HTTP connection. Must
-    // be called before sending the "HTTP/1.1 200 OK" header.
-    virtual void DisableKeepAlive() = 0;
-  };
-}
--- a/Core/HttpServer/IHttpStreamAnswer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <boost/noncopyable.hpp>
-#include <string>
-
-namespace Orthanc
-{
-  class IHttpStreamAnswer : public boost::noncopyable
-  {
-  public:
-    virtual ~IHttpStreamAnswer()
-    {
-    }
-
-    // This is the first method to be called
-    virtual HttpCompression SetupHttpCompression(bool gzipAllowed,
-                                                 bool deflateAllowed) = 0;
-
-    virtual bool HasContentFilename(std::string& filename) = 0;
-
-    virtual std::string GetContentType() = 0;
-
-    virtual uint64_t GetContentLength() = 0;
-
-    virtual bool ReadNextChunk() = 0;
-
-    virtual const char* GetChunkContent() = 0;
-
-    virtual size_t GetChunkSize() = 0;
-  };
-}
--- a/Core/HttpServer/IIncomingHttpRequestFilter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/MultipartStreamReader.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,357 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MultipartStreamReader.h"
-
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-#include <boost/algorithm/string/predicate.hpp>
-
-#if defined(_MSC_VER)
-#  include <BaseTsd.h>   // Definition of ssize_t
-#endif
-
-namespace Orthanc
-{
-  static void ParseHeaders(MultipartStreamReader::HttpHeaders& headers,
-                           StringMatcher::Iterator start,
-                           StringMatcher::Iterator end)
-  {
-    std::string tmp(start, end);
-
-    std::vector<std::string> lines;
-    Toolbox::TokenizeString(lines, tmp, '\n');
-
-    headers.clear();
-
-    for (size_t i = 0; i < lines.size(); i++)
-    {
-      size_t separator = lines[i].find(':');
-      if (separator != std::string::npos)
-      {
-        std::string key = Toolbox::StripSpaces(lines[i].substr(0, separator));
-        std::string value = Toolbox::StripSpaces(lines[i].substr(separator + 1));
-
-        Toolbox::ToLowerCase(key);
-        headers[key] = value;
-      }
-    }
-  }
-
-
-  static bool LookupHeaderSizeValue(size_t& target,
-                                    const MultipartStreamReader::HttpHeaders& headers,
-                                    const std::string& key)
-  {
-    MultipartStreamReader::HttpHeaders::const_iterator it = headers.find(key);
-    if (it == headers.end())
-    {
-      return false;
-    }
-    else
-    {
-      int64_t value;
-        
-      try
-      {
-        value = boost::lexical_cast<int64_t>(it->second);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      if (value < 0)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        target = static_cast<size_t>(value);
-        return true;
-      }
-    }
-  }
-
-
-  void MultipartStreamReader::ParseStream()
-  {
-    if (handler_ == NULL ||
-        state_ == State_Done)
-    {
-      return;
-    }
-      
-    std::string corpus;
-    buffer_.Flatten(corpus);
-
-    StringMatcher::Iterator current = corpus.begin();
-    StringMatcher::Iterator corpusEnd = corpus.end();
-
-    if (state_ == State_UnusedArea)
-    {
-      /**
-       * "Before the first boundary is an area that is ignored by
-       * MIME-compliant clients. This area is generally used to put
-       * a message to users of old non-MIME clients."
-       * https://en.wikipedia.org/wiki/MIME#Multipart_messages
-       **/
-
-      if (boundaryMatcher_.Apply(current, corpusEnd))
-      {
-        current = boundaryMatcher_.GetMatchBegin();
-        state_ = State_Content;
-      }
-      else
-      {
-        // We have not seen the end of the unused area yet
-        std::string reminder(current, corpusEnd);
-        buffer_.AddChunkDestructive(reminder);
-        return;
-      }          
-    } 
-      
-    for (;;)
-    {
-      size_t patternSize = boundaryMatcher_.GetPattern().size();
-      size_t remainingSize = std::distance(current, corpusEnd);
-      if (remainingSize < patternSize + 2)
-      {
-        break;  // Not enough data available
-      }
-        
-      std::string boundary(current, current + patternSize + 2);
-      if (boundary == boundaryMatcher_.GetPattern() + "--")
-      {
-        state_ = State_Done;
-        return;
-      }
-        
-      if (boundary != boundaryMatcher_.GetPattern() + "\r\n")
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               "Garbage between two items in a multipart stream");
-      }
-
-      StringMatcher::Iterator start = current + patternSize + 2;
-        
-      if (!headersMatcher_.Apply(start, corpusEnd))
-      {
-        break;  // Not enough data available
-      }
-
-      HttpHeaders headers;
-      ParseHeaders(headers, start, headersMatcher_.GetMatchBegin());
-
-      size_t contentLength = 0;
-      if (!LookupHeaderSizeValue(contentLength, headers, "content-length"))
-      {
-        if (boundaryMatcher_.Apply(headersMatcher_.GetMatchEnd(), corpusEnd))
-        {
-          size_t d = std::distance(headersMatcher_.GetMatchEnd(), boundaryMatcher_.GetMatchBegin());
-          if (d <= 1)
-          {
-            throw OrthancException(ErrorCode_NetworkProtocol);
-          }
-          else
-          {
-            contentLength = d - 2;
-          }
-        }
-        else
-        {
-          break;  // Not enough data available to have a full part
-        }
-      }
-
-      // Explicit conversion to avoid warning about signed vs. unsigned comparison
-      std::iterator_traits<StringMatcher::Iterator>::difference_type d = contentLength + 2;
-      if (d > std::distance(headersMatcher_.GetMatchEnd(), corpusEnd))
-      {
-        break;  // Not enough data available to have a full part
-      }
-
-      const char* p = headersMatcher_.GetPointerEnd() + contentLength;
-      if (p[0] != '\r' ||
-          p[1] != '\n')
-      {
-        throw OrthancException(ErrorCode_NetworkProtocol,
-                               "No endline at the end of a part");
-      }
-          
-      handler_->HandlePart(headers, headersMatcher_.GetPointerEnd(), contentLength);
-      current = headersMatcher_.GetMatchEnd() + contentLength + 2;
-    }
-
-    if (current != corpusEnd)
-    {
-      std::string reminder(current, corpusEnd);
-      buffer_.AddChunkDestructive(reminder);
-    }
-  }
-
-
-  MultipartStreamReader::MultipartStreamReader(const std::string& boundary) :
-    state_(State_UnusedArea),
-    handler_(NULL),
-    headersMatcher_("\r\n\r\n"),
-    boundaryMatcher_("--" + boundary),
-    blockSize_(10 * 1024 * 1024)
-  {
-  }
-
-
-  void MultipartStreamReader::SetBlockSize(size_t size)
-  {
-    if (size == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      blockSize_ = size;
-    }        
-  }
-
-    
-  void MultipartStreamReader::AddChunk(const void* chunk,
-                                       size_t size)
-  {
-    if (state_ != State_Done &&
-        size != 0)
-    {
-      size_t oldSize = buffer_.GetNumBytes();
-      
-      buffer_.AddChunk(chunk, size);
-
-      if (oldSize / blockSize_ != buffer_.GetNumBytes() / blockSize_)
-      {
-        ParseStream();
-      }
-    }
-  }
-
-
-  void MultipartStreamReader::AddChunk(const std::string& chunk)
-  {
-    if (!chunk.empty())
-    {
-      AddChunk(chunk.c_str(), chunk.size());
-    }
-  }
-
-
-  void MultipartStreamReader::CloseStream()
-  {
-    if (buffer_.GetNumBytes() != 0)
-    {
-      ParseStream();
-    }
-  }
-
-
-  bool MultipartStreamReader::GetMainContentType(std::string& contentType,
-                                                 const HttpHeaders& headers)
-  {
-    HttpHeaders::const_iterator it = headers.find("content-type");
-
-    if (it == headers.end())
-    {
-      return false;
-    }
-    else
-    {
-      contentType = it->second;
-      return true;
-    }
-  }
-
-
-  bool MultipartStreamReader::ParseMultipartContentType(std::string& contentType,
-                                                        std::string& subType,
-                                                        std::string& boundary,
-                                                        const std::string& contentTypeHeader)
-  {
-    std::vector<std::string> tokens;
-    Orthanc::Toolbox::TokenizeString(tokens, contentTypeHeader, ';');
-
-    if (tokens.empty())
-    {
-      return false;
-    }
-
-    contentType = Orthanc::Toolbox::StripSpaces(tokens[0]);
-    Orthanc::Toolbox::ToLowerCase(contentType);
-
-    if (contentType.empty())
-    {
-      return false;
-    }
-
-    bool valid = false;
-    subType.clear();
-
-    for (size_t i = 0; i < tokens.size(); i++)
-    {
-      std::vector<std::string> items;
-      Orthanc::Toolbox::TokenizeString(items, tokens[i], '=');
-
-      if (items.size() == 2)
-      {
-        if (boost::iequals("boundary", Orthanc::Toolbox::StripSpaces(items[0])))
-        {
-          boundary = Orthanc::Toolbox::StripSpaces(items[1]);
-          valid = !boundary.empty();
-        }
-        else if (boost::iequals("type", Orthanc::Toolbox::StripSpaces(items[0])))
-        {
-          subType = Orthanc::Toolbox::StripSpaces(items[1]);
-          Orthanc::Toolbox::ToLowerCase(subType);
-
-          // https://bitbucket.org/sjodogne/orthanc/issues/54/decide-what-to-do-wrt-quoting-of-multipart
-          // https://tools.ietf.org/html/rfc7231#section-3.1.1.1
-          if (subType.size() >= 2 &&
-              subType[0] == '"' &&
-              subType[subType.size() - 1] == '"')
-          {
-            subType = subType.substr(1, subType.size() - 2);
-          }
-        }
-      }
-    }
-
-    return valid;
-  }
-}
--- a/Core/HttpServer/MultipartStreamReader.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "StringMatcher.h"
-#include "../ChunkedBuffer.h"
-
-#include <map>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC MultipartStreamReader : public boost::noncopyable
-  {
-  public:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-    
-    class IHandler : public boost::noncopyable
-    {
-    public:
-      virtual ~IHandler()
-      {
-      }
-      
-      virtual void HandlePart(const HttpHeaders& headers,
-                              const void* part,
-                              size_t size) = 0;
-    };
-    
-  private:
-    enum State
-    {
-      State_UnusedArea,
-      State_Content,
-      State_Done
-    };
-    
-    State          state_;
-    IHandler*      handler_;
-    StringMatcher  headersMatcher_;
-    StringMatcher  boundaryMatcher_;
-    ChunkedBuffer  buffer_;
-    size_t         blockSize_;
-
-    void ParseStream();
-
-  public:
-    MultipartStreamReader(const std::string& boundary);
-
-    void SetBlockSize(size_t size);
-
-    size_t GetBlockSize() const
-    {
-      return blockSize_;
-    }
-
-    void SetHandler(IHandler& handler)
-    {
-      handler_ = &handler;
-    }
-    
-    void AddChunk(const void* chunk,
-                  size_t size);
-
-    void AddChunk(const std::string& chunk);
-
-    void CloseStream();
-
-    static bool GetMainContentType(std::string& contentType,
-                                   const HttpHeaders& headers);
-
-    static bool ParseMultipartContentType(std::string& contentType,
-                                          std::string& subType,  // Possibly empty
-                                          std::string& boundary,
-                                          const std::string& contentTypeHeader);
-  };
-}
--- a/Core/HttpServer/StringHttpOutput.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "StringHttpOutput.h"
-
-#include "../OrthancException.h"
-
-namespace Orthanc
-{
-  void StringHttpOutput::OnHttpStatusReceived(HttpStatus status)
-  {
-    switch (status)
-    {
-      case HttpStatus_200_Ok:
-        found_ = true;
-        break;
-
-      case HttpStatus_404_NotFound:
-        found_ = false;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_BadRequest);
-    }
-  }
-
-  void StringHttpOutput::Send(bool isHeader, const void* buffer, size_t length)
-  {
-    if (!isHeader)
-    {
-      buffer_.AddChunk(buffer, length);
-    }
-  }
-
-  void StringHttpOutput::GetOutput(std::string& output)
-  {
-    if (found_)
-    {
-      buffer_.Flatten(output);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-}
--- a/Core/HttpServer/StringHttpOutput.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IHttpOutputStream.h"
-
-#include "../ChunkedBuffer.h"
-
-namespace Orthanc
-{
-  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);
-
-    virtual void DisableKeepAlive()
-    {
-    }
-
-    void GetOutput(std::string& output);
-  };
-}
--- a/Core/HttpServer/StringMatcher.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "StringMatcher.h"
-
-#include "../OrthancException.h"
-
-#include <boost/algorithm/searching/boyer_moore.hpp>
-//#include <boost/algorithm/searching/boyer_moore_horspool.hpp>
-//#include <boost/algorithm/searching/knuth_morris_pratt.hpp>
-
-namespace Orthanc
-{
-  class StringMatcher::Search
-  {
-  private:
-    typedef boost::algorithm::boyer_moore<Iterator>  Algorithm;
-    //typedef boost::algorithm::boyer_moore_horspool<std::string::const_iterator>  Algorithm;
-
-    Algorithm algorithm_;
-
-  public:
-    // WARNING - The lifetime of "pattern_" must be larger than
-    // "search_", as the latter internally keeps a pointer to "pattern" (*)
-    Search(const std::string& pattern) :
-      algorithm_(pattern.begin(), pattern.end())
-    {
-    }
-
-    Iterator Apply(Iterator start,
-                   Iterator end) const
-    {
-#if BOOST_VERSION >= 106200
-      return algorithm_(start, end).first;
-#else
-      return algorithm_(start, end);
-#endif
-    }
-  };
-    
-
-  StringMatcher::StringMatcher(const std::string& pattern) :
-    pattern_(pattern),
-    valid_(false)
-  {
-    // WARNING - Don't use "pattern" (local variable, will be
-    // destroyed once exiting the constructor) but "pattern_"
-    // (variable member, will last as long as the algorithm),
-    // otherwise lifetime is bad! (*)
-    search_.reset(new Search(pattern_));
-  }
-  
-
-  bool StringMatcher::Apply(Iterator start,
-                            Iterator end)
-  {
-    assert(search_.get() != NULL);
-    matchBegin_ = search_->Apply(start, end);
-
-    if (matchBegin_ == end)
-    {
-      valid_ = false;
-    }
-    else
-    {
-      matchEnd_ = matchBegin_ + pattern_.size();
-      assert(matchEnd_ <= end);
-      valid_ = true;
-    }
-
-    return valid_;
-  }
-
-
-  StringMatcher::Iterator StringMatcher::GetMatchBegin() const
-  {
-    if (valid_)
-    {
-      return matchBegin_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-
-  StringMatcher::Iterator StringMatcher::GetMatchEnd() const
-  {
-    if (valid_)
-    {
-      return matchEnd_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-
-  const char* StringMatcher::GetPointerBegin() const
-  {
-    return &GetMatchBegin()[0];
-  }
-
-
-  const char* StringMatcher::GetPointerEnd() const
-  {
-    return GetPointerBegin() + pattern_.size();
-  }
-}
--- a/Core/HttpServer/StringMatcher.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-
-#include <boost/noncopyable.hpp>
-#include <boost/shared_ptr.hpp>
-#include <string>
-
-namespace Orthanc
-{
-  // Convenience class that wraps a Boost algorithm for string matching
-  class ORTHANC_PUBLIC StringMatcher : public boost::noncopyable
-  {
-  public:
-    typedef std::string::const_iterator Iterator;
-
-  private:
-    class Search;
-      
-    boost::shared_ptr<Search>  search_;  // PImpl pattern
-    std::string                pattern_;
-    bool                       valid_;
-    Iterator                   matchBegin_;
-    Iterator                   matchEnd_;
-    
-  public:
-    StringMatcher(const std::string& pattern);
-
-    const std::string& GetPattern() const
-    {
-      return pattern_;
-    }
-
-    bool IsValid() const
-    {
-      return valid_;
-    }
-
-    bool Apply(Iterator start,
-               Iterator end);
-
-    bool Apply(const std::string& corpus)
-    {
-      return Apply(corpus.begin(), corpus.end());
-    }
-
-    Iterator GetMatchBegin() const;
-
-    Iterator GetMatchEnd() const;
-
-    const char* GetPointerBegin() const;
-
-    const char* GetPointerEnd() const;
-  };
-}
--- a/Core/IDynamicObject.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  /**
-   * This class should be the ancestor to any class whose type is
-   * determined at the runtime, and that can be dynamically allocated.
-   * Being a child of IDynamicObject only implies the existence of a
-   * virtual destructor.
-   **/
-  class ORTHANC_PUBLIC IDynamicObject : public boost::noncopyable
-  {
-  public:
-    virtual ~IDynamicObject()
-    {
-    }
-  };
-  
-
-  /**
-   * This class is a simple implementation of a IDynamicObject that
-   * stores a single typed value.
-   */
-  template <typename T>
-  class SingleValueObject : public IDynamicObject
-  {
-  private:
-    T  value_;
-    
-  public:
-    explicit SingleValueObject(const T& value) :
-      value_(value)
-    {
-    }
-
-    const T& GetValue() const
-    {
-      return value_;
-    }
-  };
-}
--- a/Core/Images/Font.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,419 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "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 "../OrthancException.h"
-#include "../Toolbox.h"
-#include "Image.h"
-#include "ImageProcessing.h"
-
-#include <stdio.h>
-#include <memory>
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  Font::~Font()
-  {
-    for (Characters::iterator it = characters_.begin();
-         it != characters_.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-
-  void Font::LoadFromMemory(const std::string& font)
-  {
-    Json::Value v;
-    Json::Reader reader;
-    if (!reader.parse(font, v) ||
-        v.type() != Json::objectValue ||
-        !v.isMember("Name") ||
-        !v.isMember("Size") ||
-        !v.isMember("Characters") ||
-        v["Name"].type() != Json::stringValue ||
-        v["Size"].type() != Json::intValue ||
-        v["Characters"].type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFont);
-    }
-
-    name_ = v["Name"].asString();
-    size_ = v["Size"].asUInt();
-    maxHeight_ = 0;
-
-    Json::Value::Members characters = v["Characters"].getMemberNames();
-
-    for (size_t i = 0; i < characters.size(); i++)
-    {
-      const Json::Value& info = v["Characters"][characters[i]];
-      if (info.type() != Json::objectValue ||
-          !info.isMember("Advance") ||
-          !info.isMember("Bitmap") ||
-          !info.isMember("Height") ||
-          !info.isMember("Top") ||
-          !info.isMember("Width") ||
-          info["Advance"].type() != Json::intValue ||
-          info["Bitmap"].type() != Json::arrayValue ||
-          info["Height"].type() != Json::intValue ||
-          info["Top"].type() != Json::intValue ||
-          info["Width"].type() != Json::intValue)
-      {
-        throw OrthancException(ErrorCode_BadFont);
-      }
-
-      std::unique_ptr<Character> c(new Character);
-      
-      c->advance_ = info["Advance"].asUInt();
-      c->height_ = info["Height"].asUInt();
-      c->top_ = info["Top"].asUInt();
-      c->width_ = info["Width"].asUInt();
-      c->bitmap_.resize(info["Bitmap"].size());
-
-      if (c->height_ > maxHeight_)
-      {
-        maxHeight_ = c->height_;
-      }
-      
-      for (Json::Value::ArrayIndex j = 0; j < info["Bitmap"].size(); j++)
-      {
-        if (info["Bitmap"][j].type() != Json::intValue)
-        {
-          throw OrthancException(ErrorCode_BadFont);
-        }
-
-        int value = info["Bitmap"][j].asInt();
-        if (value < 0 || value > 255)
-        {
-          throw OrthancException(ErrorCode_BadFont);
-        }
-
-        c->bitmap_[j] = static_cast<uint8_t>(value);
-      }
-
-      int index = boost::lexical_cast<int>(characters[i]);
-      if (index < 0 || index > 255)
-      {
-        throw OrthancException(ErrorCode_BadFont);
-      }
-
-      characters_[static_cast<char>(index)] = c.release();
-    }
-  }
-
-
-#if ORTHANC_SANDBOXED == 0
-  void Font::LoadFromFile(const std::string& path)
-  {
-    std::string font;
-    SystemToolbox::ReadFile(font, path);
-    LoadFromMemory(font);
-  }
-#endif
-
-
-  static unsigned int MyMin(unsigned int a, 
-                            unsigned int b)
-  {
-    return a < b ? a : b;
-  }
-
-
-  void Font::DrawCharacter(ImageAccessor& target,
-                           const Character& character,
-                           int x,
-                           int y,
-                           const uint8_t color[4]) const
-  {
-    // Compute the bounds of the character
-    if (x >= static_cast<int>(target.GetWidth()) ||
-        y >= static_cast<int>(target.GetHeight()))
-    {
-      // The character is out of the image
-      return;
-    }
-
-    unsigned int left = x < 0 ? -x : 0;
-    unsigned int top = y < 0 ? -y : 0;
-    unsigned int width = MyMin(character.width_, target.GetWidth() - x);
-    unsigned int height = MyMin(character.height_, target.GetHeight() - y);
-
-    unsigned int bpp = target.GetBytesPerPixel();
-
-    // Blit the font bitmap OVER the target image
-    // https://en.wikipedia.org/wiki/Alpha_compositing
-
-    for (unsigned int cy = top; cy < height; cy++)
-    {
-      uint8_t* p = reinterpret_cast<uint8_t*>(target.GetRow(y + cy)) + (x + left) * bpp;
-      unsigned int pos = cy * character.width_ + left;
-
-      switch (target.GetFormat())
-      {
-        case PixelFormat_Grayscale8:
-        {
-          assert(bpp == 1);
-          for (unsigned int cx = left; cx < width; cx++, pos++, p++)
-          {
-            uint16_t alpha = character.bitmap_[pos];
-            uint16_t value = alpha * static_cast<uint16_t>(color[0]) + (255 - alpha) * static_cast<uint16_t>(*p);
-            *p = static_cast<uint8_t>(value >> 8);
-          }
-
-          break;
-        }
-
-        case PixelFormat_RGB24:
-        {
-          assert(bpp == 3);
-          for (unsigned int cx = left; cx < width; cx++, pos++, p += 3)
-          {
-            uint16_t alpha = character.bitmap_[pos];
-            for (uint8_t i = 0; i < 3; i++)
-            {
-              uint16_t value = alpha * static_cast<uint16_t>(color[i]) + (255 - alpha) * static_cast<uint16_t>(p[i]);
-              p[i] = static_cast<uint8_t>(value >> 8);
-            }
-          }
-
-          break;
-        }
-
-        case PixelFormat_RGBA32:
-        case PixelFormat_BGRA32:
-        {
-          assert(bpp == 4);
-
-          for (unsigned int cx = left; cx < width; cx++, pos++, p += 4)
-          {
-            float alpha = static_cast<float>(character.bitmap_[pos]) / 255.0f;
-            float beta = (1.0f - alpha) * static_cast<float>(p[3]) / 255.0f;
-            float denom = 1.0f / (alpha + beta);
-
-            for (uint8_t i = 0; i < 3; i++)
-            {
-              p[i] = static_cast<uint8_t>((alpha * static_cast<float>(color[i]) +
-                                           beta * static_cast<float>(p[i])) * denom);
-            }
-
-            p[3] = static_cast<uint8_t>(255.0f * (alpha + beta));
-          }
-
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
-      }
-    }
-
-  }
-
-
-  void Font::DrawInternal(ImageAccessor& target,
-                          const std::string& utf8,
-                          int x,
-                          int y,
-                          const uint8_t color[4]) const
-  {
-    if (target.GetFormat() != PixelFormat_Grayscale8 &&
-        target.GetFormat() != PixelFormat_RGB24 &&
-        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++)
-    {
-      if (s[i] == '\n')
-      {
-        // Go to the next line
-        a = x;
-        y += maxHeight_ + 1;
-      }
-      else
-      {
-        Characters::const_iterator c = characters_.find(s[i]);
-        if (c != characters_.end())
-        {
-          DrawCharacter(target, *c->second, a, y + static_cast<int>(c->second->top_), color);
-          a += c->second->advance_;
-        }
-      }
-    }
-  }
-
-
-  void Font::Draw(ImageAccessor& target,
-                  const std::string& utf8,
-                  int x,
-                  int y,
-                  uint8_t grayscale) const
-  {
-    uint8_t color[4] = { grayscale, grayscale, grayscale, 255 };
-    DrawInternal(target, utf8, x, y, color);
-  }
-
-
-  void Font::Draw(ImageAccessor& target,
-                  const std::string& utf8,
-                  int x,
-                  int y,
-                  uint8_t r,
-                  uint8_t g,
-                  uint8_t b) const
-  {
-    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);
-  }
-
-
-  void Font::ComputeTextExtent(unsigned int& width,
-                               unsigned int& height,
-                               const std::string& utf8) const
-  {
-    width = 0;
-    height = 0;
-    
-#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
-
-    // Compute the text extent
-    unsigned int x = 0;
-    unsigned int y = 0;
-    
-    for (size_t i = 0; i < s.size(); i++)
-    {
-      if (s[i] == '\n')
-      {
-        // Go to the next line
-        x = 0;
-        y += (maxHeight_ + 1);
-      }
-      else
-      {
-        Characters::const_iterator c = characters_.find(s[i]);
-        if (c != characters_.end())
-        {
-          x += c->second->advance_;
-
-          unsigned int bottom = y + c->second->top_ + c->second->height_;
-          if (bottom > height)
-          {
-            height = bottom;
-          }
-          
-          if (x > width)
-          {
-            width = x;
-          }
-        }
-      }
-    }
-  }
-
-
-  ImageAccessor* Font::Render(const std::string& utf8,
-                              PixelFormat format,
-                              uint8_t r,
-                              uint8_t g,
-                              uint8_t b) const
-  {
-    unsigned int width, height;
-    ComputeTextExtent(width, height, utf8);
-    
-    std::unique_ptr<ImageAccessor>  target(new Image(format, width, height, false));
-    ImageProcessing::Set(*target, 0, 0, 0, 255);
-    Draw(*target, utf8, 0, 0, r, g, b);
-
-    return target.release();
-  }
-
-
-  ImageAccessor* Font::RenderAlpha(const std::string& utf8) const
-  {
-    unsigned int width, height;
-    ComputeTextExtent(width, height, utf8);
-
-    std::unique_ptr<ImageAccessor>  target(new Image(PixelFormat_Grayscale8, width, height, false));
-    ImageProcessing::Set(*target, 0);
-    Draw(*target, utf8, 0, 0, 255);
-
-    return target.release();
-  }
-}
--- a/Core/Images/Font.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-
-#include "ImageAccessor.h"
-
-#include <stdint.h>
-#include <vector>
-#include <map>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC Font : public boost::noncopyable
-  {
-  private:
-    struct Character
-    {
-      unsigned int  width_;
-      unsigned int  height_;
-      unsigned int  top_;
-      unsigned int  advance_;
-      std::vector<uint8_t>  bitmap_;
-    };
-
-    typedef std::map<char, Character*>  Characters;
-
-    std::string   name_;
-    unsigned int  size_;
-    Characters    characters_;
-    unsigned int  maxHeight_;
-
-    void DrawCharacter(ImageAccessor& target,
-                       const Character& character,
-                       int x,
-                       int y,
-                       const uint8_t color[4]) const;
-
-    void DrawInternal(ImageAccessor& target,
-                      const std::string& utf8,
-                      int x,
-                      int y,
-                      const uint8_t color[4]) const;
-
-  public:
-    Font() : 
-      size_(0), 
-      maxHeight_(0)
-    {
-    }
-
-    ~Font();
-
-    void LoadFromMemory(const std::string& font);
-
-#if ORTHANC_SANDBOXED == 0
-    void LoadFromFile(const std::string& path);
-#endif
-
-    const std::string& GetName() const
-    {
-      return name_;
-    }
-
-    unsigned int GetSize() const
-    {
-      return size_;
-    }
-
-    void Draw(ImageAccessor& target,
-              const std::string& utf8,
-              int x,
-              int y,
-              uint8_t grayscale) const;
-
-    void Draw(ImageAccessor& target,
-              const std::string& utf8,
-              int x,
-              int y,
-              uint8_t r,
-              uint8_t g,
-              uint8_t b) const;
-
-    void ComputeTextExtent(unsigned int& width,
-                           unsigned int& height,
-                           const std::string& utf8) const;
-
-    ImageAccessor* Render(const std::string& utf8,
-                          PixelFormat format,
-                          uint8_t r,
-                          uint8_t g,
-                          uint8_t b) const;
-
-    ImageAccessor* RenderAlpha(const std::string& utf8) const;
-  };
-}
--- a/Core/Images/FontRegistry.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "FontRegistry.h"
-
-#include "../OrthancException.h"
-
-#include <memory>
-
-namespace Orthanc
-{
-  FontRegistry::~FontRegistry()
-  {
-    for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  void FontRegistry::AddFromMemory(const std::string& font)
-  {
-    std::unique_ptr<Font> f(new Font);
-    f->LoadFromMemory(font);
-    fonts_.push_back(f.release());
-  }
-
-
-#if ORTHANC_SANDBOXED == 0
-  void FontRegistry::AddFromFile(const std::string& path)
-  {
-    std::unique_ptr<Font> f(new Font);
-    f->LoadFromFile(path);
-    fonts_.push_back(f.release());
-  }
-#endif
-
-
-  const Font& FontRegistry::GetFont(size_t i) const
-  {
-    if (i >= fonts_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return *fonts_[i];
-    }
-  }
-
-  const Font* FontRegistry::FindFont(const std::string& fontName) const
-  {
-    for (Fonts::const_iterator it = fonts_.begin(); it != fonts_.end(); it++)
-    {
-      if ((*it)->GetName() == fontName)
-      {
-        return *it;
-      }
-    }
-
-    return NULL;
-  }
-
-}
--- a/Core/Images/FontRegistry.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Font.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC FontRegistry : public boost::noncopyable
-  {
-  private:
-    typedef std::vector<Font*>  Fonts;
-
-    Fonts  fonts_;
-
-  public:
-    ~FontRegistry();
-
-    void AddFromMemory(const std::string& font);
-
-#if ORTHANC_SANDBOXED == 0
-    void AddFromFile(const std::string& path);
-#endif
-
-    size_t GetSize() const
-    {
-      return fonts_.size();
-    }
-
-    const Font& GetFont(size_t i) const;
-
-    const Font* FindFont(const std::string& fontName) const;
-  };
-}
--- a/Core/Images/IImageWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-}
--- a/Core/Images/IImageWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-  };
-}
--- a/Core/Images/Image.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.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::unique_ptr<Image> target(new Image(source.GetFormat(), source.GetWidth(), source.GetHeight(), false));
-    ImageProcessing::Copy(*target, source);
-    return target.release();
-  }
-}
--- a/Core/Images/Image.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ImageBuffer.h"
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC Image : public ImageAccessor
-  {
-  private:
-    ImageBuffer  image_;
-
-  public:
-    Image(PixelFormat format,
-          unsigned int width,
-          unsigned int height,
-          bool forceMinimalPitch);
-
-    static Image* Clone(const ImageAccessor& source);
-  };
-}
--- a/Core/Images/ImageAccessor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,307 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ImageAccessor.h"
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../ChunkedBuffer.h"
-
-#include <stdint.h>
-#include <cassert>
-#include <boost/lexical_cast.hpp>
-
-
-
-namespace Orthanc
-{
-  template <typename PixelType>
-  static void ToMatlabStringInternal(ChunkedBuffer& target,
-                                     const ImageAccessor& source)
-  {
-    target.AddChunk("double([ ");
-
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
-    {
-      const PixelType* p = reinterpret_cast<const PixelType*>(source.GetConstRow(y));
-
-      std::string s;
-      if (y > 0)
-      {
-        s = "; ";
-      }
-
-      s.reserve(source.GetWidth() * 8);
-
-      for (unsigned int x = 0; x < source.GetWidth(); x++, p++)
-      {
-        s += boost::lexical_cast<std::string>(static_cast<double>(*p)) + " ";
-      }
-
-      target.AddChunk(s);
-    }
-
-    target.AddChunk("])");
-  }
-
-
-  static void RGB24ToMatlabString(ChunkedBuffer& target,
-                                  const ImageAccessor& source)
-  {
-    assert(source.GetFormat() == PixelFormat_RGB24);
-
-    target.AddChunk("double(permute(reshape([ ");
-
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
-    {
-      const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-      
-      std::string s;
-      s.reserve(source.GetWidth() * 3 * 8);
-      
-      for (unsigned int x = 0; x < 3 * source.GetWidth(); x++, p++)
-      {
-        s += boost::lexical_cast<std::string>(static_cast<int>(*p)) + " ";
-      }
-      
-      target.AddChunk(s);
-    }
-
-    target.AddChunk("], [ 3 " + boost::lexical_cast<std::string>(source.GetHeight()) +
-                    " " + boost::lexical_cast<std::string>(source.GetWidth()) + " ]), [ 3 2 1 ]))");
-  }
-
-
-  void* ImageAccessor::GetBuffer() const
-  {
-    if (readOnly_)
-    {
-      throw OrthancException(ErrorCode_ReadOnly,
-                             "Trying to write to a read-only image");
-    }
-
-    return buffer_;
-  }
-
-
-  const void* ImageAccessor::GetConstRow(unsigned int y) const
-  {
-    if (buffer_ != NULL)
-    {
-      return buffer_ + y * pitch_;
-    }
-    else
-    {
-      return NULL;
-    }
-  }
-
-
-  void* ImageAccessor::GetRow(unsigned int y) const
-  {
-    if (readOnly_)
-    {
-      throw OrthancException(ErrorCode_ReadOnly,
-                             "Trying to write to a read-only image");
-    }
-
-    if (buffer_ != NULL)
-    {
-      return buffer_ + y * pitch_;
-    }
-    else
-    {
-      return NULL;
-    }
-  }
-
-
-  void ImageAccessor::AssignEmpty(PixelFormat format)
-  {
-    readOnly_ = false;
-    format_ = format;
-    width_ = 0;
-    height_ = 0;
-    pitch_ = 0;
-    buffer_ = NULL;
-  }
-
-
-  void ImageAccessor::AssignReadOnly(PixelFormat format,
-                                     unsigned int width,
-                                     unsigned int height,
-                                     unsigned int pitch,
-                                     const void *buffer)
-  {
-    readOnly_ = true;
-    format_ = format;
-    width_ = width;
-    height_ = height;
-    pitch_ = pitch;
-    buffer_ = reinterpret_cast<uint8_t*>(const_cast<void*>(buffer));
-
-    if (GetBytesPerPixel() * width_ > pitch_)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  void ImageAccessor::AssignWritable(PixelFormat format,
-                                     unsigned int width,
-                                     unsigned int height,
-                                     unsigned int pitch,
-                                     void *buffer)
-  {
-    readOnly_ = false;
-    format_ = format;
-    width_ = width;
-    height_ = height;
-    pitch_ = pitch;
-    buffer_ = reinterpret_cast<uint8_t*>(buffer);
-
-    if (GetBytesPerPixel() * width_ > pitch_)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  void ImageAccessor::GetWriteableAccessor(ImageAccessor& target) const
-  {
-    if (readOnly_)
-    {
-      throw OrthancException(ErrorCode_ReadOnly);
-    }
-    else
-    {
-      target.AssignWritable(format_, width_, height_, pitch_, buffer_);
-    }
-  }
-
-
-  void ImageAccessor::ToMatlabString(std::string& target) const
-  {
-    ChunkedBuffer buffer;
-
-    switch (GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        ToMatlabStringInternal<uint8_t>(buffer, *this);
-        break;
-
-      case PixelFormat_Grayscale16:
-        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;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }   
-
-    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_)
-    {
-      throw OrthancException(ErrorCode_ReadOnly,
-                             "Trying to modify the format of a read-only image");
-    }
-
-    if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_))
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat);
-    }
-
-    format_ = format;
-  }
-}
--- a/Core/Images/ImageAccessor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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>
-#include <stdint.h>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC 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_;
-    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()
-    {
-      AssignEmpty(PixelFormat_Grayscale8);
-    }
-
-    virtual ~ImageAccessor()
-    {
-    }
-
-    bool IsReadOnly() const
-    {
-      return readOnly_;
-    }
-
-    PixelFormat GetFormat() const
-    {
-      return format_;
-    }
-
-    unsigned int GetBytesPerPixel() const
-    {
-      return ::Orthanc::GetBytesPerPixel(format_);
-    }
-
-    unsigned int GetWidth() const
-    {
-      return width_;
-    }
-
-    unsigned int GetHeight() const
-    {
-      return height_;
-    }
-
-    unsigned int GetPitch() const
-    {
-      return pitch_;
-    }
-
-    unsigned int GetSize() const
-    {
-      return GetHeight() * GetPitch();
-    }
-
-    const void* GetConstBuffer() const
-    {
-      return buffer_;
-    }
-
-    void* GetBuffer() const;
-
-    const void* GetConstRow(unsigned int y) const;
-
-    void* GetRow(unsigned int y) const;
-
-    void AssignEmpty(PixelFormat format);
-
-    void AssignReadOnly(PixelFormat format,
-                        unsigned int width,
-                        unsigned int height,
-                        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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,180 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ImageBuffer.h"
-
-#include "../OrthancException.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-
-namespace Orthanc
-{
-  void ImageBuffer::Allocate()
-  {
-    if (changed_)
-    {
-      Deallocate();
-
-      /*
-        if (forceMinimalPitch_)
-        {
-        TODO: Align pitch and memory buffer to optimal size for SIMD.
-        }
-      */
-
-      pitch_ = GetBytesPerPixel() * width_;
-      size_t size = pitch_ * height_;
-
-      if (size == 0)
-      {
-        buffer_ = NULL;
-      }
-      else
-      {
-        buffer_ = malloc(size);
-        if (buffer_ == NULL)
-        {
-          throw OrthancException(ErrorCode_NotEnoughMemory,
-                                 "Failed to allocate an image buffer of size " + boost::lexical_cast<std::string>(width_) + "x" + boost::lexical_cast<std::string>(height_));
-        }
-      }
-
-      changed_ = false;
-    }
-  }
-
-
-  void ImageBuffer::Deallocate()
-  {
-    if (buffer_ != NULL)
-    {
-      free(buffer_);
-      buffer_ = NULL;
-      changed_ = true;
-    }
-  }
-
-
-  ImageBuffer::ImageBuffer(PixelFormat format,
-                           unsigned int width,
-                           unsigned int height,
-                           bool forceMinimalPitch) :
-    forceMinimalPitch_(forceMinimalPitch)
-  {
-    Initialize();
-    SetWidth(width);
-    SetHeight(height);
-    SetFormat(format);
-  }
-
-
-  void ImageBuffer::Initialize()
-  {
-    changed_ = false;
-    forceMinimalPitch_ = true;
-    format_ = PixelFormat_Grayscale8;
-    width_ = 0;
-    height_ = 0;
-    pitch_ = 0;
-    buffer_ = NULL;
-  }
-
-
-  void ImageBuffer::SetFormat(PixelFormat format)
-  {
-    if (format != format_)
-    {
-      changed_ = true;
-      format_ = format;
-    }
-  }
-
-
-  void ImageBuffer::SetWidth(unsigned int width)
-  {
-    if (width != width_)
-    {
-      changed_ = true;
-      width_ = width;     
-    }
-  }
-
-
-  void ImageBuffer::SetHeight(unsigned int height)
-  {
-    if (height != height_)
-    {
-      changed_ = true;
-      height_ = height;     
-    }
-  }
-
-  
-  void ImageBuffer::GetReadOnlyAccessor(ImageAccessor& accessor)
-  {
-    Allocate();
-    accessor.AssignReadOnly(format_, width_, height_, pitch_, buffer_);
-  }
-  
-
-  void ImageBuffer::GetWriteableAccessor(ImageAccessor& accessor)
-  {
-    Allocate();
-    accessor.AssignWritable(format_, width_, height_, pitch_, buffer_);
-  }
-
-
-  void ImageBuffer::AcquireOwnership(ImageBuffer& other)
-  {
-    // Remove the content of the current image
-    Deallocate();
-
-    // Force the allocation of the other image (if not already
-    // allocated)
-    other.Allocate();
-
-    // Transfer the content of the other image
-    changed_ = false;
-    forceMinimalPitch_ = other.forceMinimalPitch_;
-    format_ = other.format_;
-    width_ = other.width_;
-    height_ = other.height_;
-    pitch_ = other.pitch_;
-    buffer_ = other.buffer_;
-
-    // Force the reinitialization of the other image
-    other.Initialize();
-  }
-}
--- a/Core/Images/ImageBuffer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <vector>
-#include <stdint.h>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC ImageBuffer : public boost::noncopyable
-  {
-  private:
-    bool changed_;
-
-    bool forceMinimalPitch_;  // Currently unused
-    PixelFormat format_;
-    unsigned int width_;
-    unsigned int height_;
-    unsigned int pitch_;
-    void *buffer_;
-
-    void Initialize();
-    
-    void Allocate();
-
-    void Deallocate();
-
-  public:
-    ImageBuffer(PixelFormat format,
-                unsigned int width,
-                unsigned int height,
-                bool forceMinimalPitch);
-
-    ImageBuffer()
-    {
-      Initialize();
-    }
-
-    ~ImageBuffer()
-    {
-      Deallocate();
-    }
-
-    PixelFormat GetFormat() const
-    {
-      return format_;
-    }
-
-    void SetFormat(PixelFormat format);
-
-    unsigned int GetWidth() const
-    {
-      return width_;
-    }
-
-    void SetWidth(unsigned int width);
-
-    unsigned int GetHeight() const
-    {
-      return height_;
-    }
-
-    void SetHeight(unsigned int height);
-
-    unsigned int GetBytesPerPixel() const
-    {
-      return ::Orthanc::GetBytesPerPixel(format_);
-    }
-
-    void GetReadOnlyAccessor(ImageAccessor& accessor);
-
-    void GetWriteableAccessor(ImageAccessor& accessor);
-
-    bool IsMinimalPitchForced() const
-    {
-      return forceMinimalPitch_;
-    }
-
-    void AcquireOwnership(ImageBuffer& other);
-  };
-}
--- a/Core/Images/ImageProcessing.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2466 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ImageProcessing.h"
-
-#include "Image.h"
-#include "ImageTraits.h"
-#include "PixelTraits.h"
-#include "../OrthancException.h"
-
-#ifdef __EMSCRIPTEN__
-/* 
-   Avoid this error:
-   -----------------
-   .../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion]
-   .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here
-   .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal<unsigned char, true>' requested here
-*/
-#pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion"
-#endif 
-
-#include <boost/math/special_functions/round.hpp>
-
-#include <cassert>
-#include <string.h>
-#include <limits>
-#include <stdint.h>
-#include <algorithm>
-
-namespace Orthanc
-{
-  double ImageProcessing::ImagePoint::GetDistanceTo(const ImagePoint& other) const
-  {
-    double dx = (double)(other.GetX() - GetX());
-    double dy = (double)(other.GetY() - GetY());
-    return sqrt(dx * dx + dy * dy);
-  }
-
-  double ImageProcessing::ImagePoint::GetDistanceToLine(double a, double b, double c) const // where ax + by + c = 0 is the equation of the line
-  {
-    return std::abs(a * static_cast<double>(GetX()) + b * static_cast<double>(GetY()) + c) / pow(a * a + b * b, 0.5);
-  }
-
-  template <typename TargetType, typename SourceType>
-  static void ConvertInternal(ImageAccessor& target,
-                              const ImageAccessor& source)
-  {
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
-    const TargetType minValue = std::numeric_limits<TargetType>::min();
-    const TargetType maxValue = std::numeric_limits<TargetType>::max();
-
-    const unsigned int width = source.GetWidth();
-    const unsigned int height = source.GetHeight();
-    
-    for (unsigned int y = 0; y < height; y++)
-    {
-      TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
-      const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y));
-
-      for (unsigned int x = 0; x < width; x++, t++, s++)
-      {
-        if (static_cast<int32_t>(*s) < static_cast<int32_t>(minValue))
-        {
-          *t = minValue;
-        }
-        else if (static_cast<int32_t>(*s) > static_cast<int32_t>(maxValue))
-        {
-          *t = maxValue;
-        }
-        else
-        {
-          *t = static_cast<TargetType>(*s);
-        }
-      }
-    }
-  }
-
-
-  template <typename SourceType>
-  static void ConvertGrayscaleToFloat(ImageAccessor& target,
-                                      const ImageAccessor& source)
-  {
-    assert(sizeof(float) == 4);
-
-    const unsigned int width = source.GetWidth();
-    const unsigned int height = source.GetHeight();
-    
-    for (unsigned int y = 0; y < height; 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 < width; x++, t++, s++)
-      {
-        *t = static_cast<float>(*s);
-      }
-    }
-  }
-
-
-  template <PixelFormat TargetFormat>
-  static void ConvertFloatToGrayscale(ImageAccessor& target,
-                                      const ImageAccessor& source)
-  {
-    typedef typename PixelTraits<TargetFormat>::PixelType  TargetType;
-    
-    assert(sizeof(float) == 4);
-
-    const unsigned int width = source.GetWidth();
-    const unsigned int height = source.GetHeight();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      TargetType* q = reinterpret_cast<TargetType*>(target.GetRow(y));
-      const float* p = reinterpret_cast<const float*>(source.GetConstRow(y));
-
-      for (unsigned int x = 0; x < width; x++, p++, q++)
-      {
-        PixelTraits<TargetFormat>::FloatToPixel(*q, *p);
-      }
-    }
-  }
-
-
-  template <typename TargetType>
-  static void ConvertColorToGrayscale(ImageAccessor& target,
-                                      const ImageAccessor& source)
-  {
-    assert(source.GetFormat() == PixelFormat_RGB24);
-
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
-    const TargetType minValue = std::numeric_limits<TargetType>::min();
-    const TargetType maxValue = std::numeric_limits<TargetType>::max();
-
-    const unsigned int width = source.GetWidth();
-    const unsigned int height = source.GetHeight();
-    
-    for (unsigned int y = 0; y < height; y++)
-    {
-      TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
-      const uint8_t* s = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-
-      for (unsigned int x = 0; x < width; x++, t++, s += 3)
-      {
-        // 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])) / 10000;
-        
-        if (static_cast<int32_t>(v) < static_cast<int32_t>(minValue))
-        {
-          *t = minValue;
-        }
-        else if (static_cast<int32_t>(v) > static_cast<int32_t>(maxValue))
-        {
-          *t = maxValue;
-        }
-        else
-        {
-          *t = static_cast<TargetType>(v);
-        }
-      }
-    }
-  }
-
-
-  static void MemsetZeroInternal(ImageAccessor& image)
-  {
-    const unsigned int height = image.GetHeight();
-    const size_t lineSize = image.GetBytesPerPixel() * image.GetWidth();
-    const size_t pitch = image.GetPitch();
-
-    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetBuffer());
-    
-    for (unsigned int y = 0; y < height; y++)
-    {
-      memset(p, 0, lineSize);
-      p += pitch;
-    }
-  }
-
-
-  template <typename PixelType>
-  static void SetInternal(ImageAccessor& image,
-                          int64_t constant)
-  {
-    if (constant == 0 &&
-        (image.GetFormat() == PixelFormat_Grayscale8 ||
-         image.GetFormat() == PixelFormat_Grayscale16 ||
-         image.GetFormat() == PixelFormat_Grayscale32 ||
-         image.GetFormat() == PixelFormat_Grayscale64 ||
-         image.GetFormat() == PixelFormat_SignedGrayscale16))
-    {
-      MemsetZeroInternal(image);
-    }
-    else
-    {
-      const unsigned int width = image.GetWidth();
-      const unsigned int height = image.GetHeight();
-
-      for (unsigned int y = 0; y < height; y++)
-      {
-        PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
-
-        for (unsigned int x = 0; x < width; x++, p++)
-        {
-          *p = static_cast<PixelType>(constant);
-        }
-      }
-    }
-  }
-
-
-  template <typename PixelType>
-  static void GetMinMaxValueInternal(PixelType& minValue,
-                                     PixelType& maxValue,
-                                     const ImageAccessor& source,
-                                     const PixelType LowestValue = std::numeric_limits<PixelType>::min())
-  {
-    // Deal with the special case of empty image
-    if (source.GetWidth() == 0 ||
-        source.GetHeight() == 0)
-    {
-      minValue = 0;
-      maxValue = 0;
-      return;
-    }
-
-    minValue = std::numeric_limits<PixelType>::max();
-    maxValue = LowestValue;
-
-    const unsigned int height = source.GetHeight();
-    const unsigned int width = source.GetWidth();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      const PixelType* p = reinterpret_cast<const PixelType*>(source.GetConstRow(y));
-
-      for (unsigned int x = 0; x < width; x++, p++)
-      {
-        if (*p < minValue)
-        {
-          minValue = *p;
-        }
-
-        if (*p > maxValue)
-        {
-          maxValue = *p;
-        }
-      }
-    }
-  }
-
-
-
-  template <typename PixelType>
-  static void AddConstantInternal(ImageAccessor& image,
-                                  int64_t constant)
-  {
-    if (constant == 0)
-    {
-      return;
-    }
-
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    assert(sizeof(PixelType) <= 2);  // Safeguard to remember about "float/double"
-    const int64_t minValue = std::numeric_limits<PixelType>::min();
-    const int64_t maxValue = std::numeric_limits<PixelType>::max();
-
-    const unsigned int width = image.GetWidth();
-    const unsigned int height = image.GetHeight();
-    
-    for (unsigned int y = 0; y < height; y++)
-    {
-      PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
-
-      for (unsigned int x = 0; x < width; x++, p++)
-      {
-        int64_t v = static_cast<int64_t>(*p) + constant;
-
-        if (v > maxValue)
-        {
-          *p = std::numeric_limits<PixelType>::max();
-        }
-        else if (v < minValue)
-        {
-          *p = std::numeric_limits<PixelType>::min();
-        }
-        else
-        {
-          *p = static_cast<PixelType>(v);
-        }
-      }
-    }
-  }
-
-
-
-  template <typename PixelType,
-            bool UseRound>
-  static void MultiplyConstantInternal(ImageAccessor& image,
-                                       float factor)
-  {
-    if (std::abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon())
-    {
-      return;
-    }
-
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    assert(sizeof(PixelType) <= 2);  // Safeguard to remember about "float/double"
-    const int64_t minValue = std::numeric_limits<PixelType>::min();
-    const int64_t maxValue = std::numeric_limits<PixelType>::max();
-
-    const unsigned int width = image.GetWidth();
-    const unsigned int height = image.GetHeight();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
-
-      for (unsigned int x = 0; x < width; x++, p++)
-      {
-        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)
-        {
-          *p = std::numeric_limits<PixelType>::max();
-        }
-        else if (v < minValue)
-        {
-          *p = std::numeric_limits<PixelType>::min();
-        }
-        else
-        {
-          *p = static_cast<PixelType>(v);
-        }
-      }
-    }
-  }
-
-
-  // Computes "a * x + b" at each pixel => Note that this is not the
-  // same convention as in "ShiftScale()"
-  template <typename TargetType,
-            typename SourceType,
-            bool UseRound,
-            bool Invert>
-  static void ShiftScaleInternal(ImageAccessor& target,
-                                 const ImageAccessor& source,
-                                 float a,
-                                 float b,
-                                 const TargetType LowestValue)
-  // This function can be applied inplace (source == target)
-  {
-    if (source.GetWidth() != target.GetWidth() ||
-        source.GetHeight() != target.GetHeight())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageSize);
-    }
-
-    if (&source == &target &&
-        source.GetFormat() != target.GetFormat())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat);
-    }
-    
-    const TargetType minPixelValue = LowestValue;
-    const TargetType maxPixelValue = std::numeric_limits<TargetType>::max();
-    const float minFloatValue = static_cast<float>(LowestValue);
-    const float maxFloatValue = static_cast<float>(maxPixelValue);
-
-    const unsigned int height = target.GetHeight();
-    const unsigned int width = target.GetWidth();
-    
-    for (unsigned int y = 0; y < height; y++)
-    {
-      TargetType* p = reinterpret_cast<TargetType*>(target.GetRow(y));
-      const SourceType* q = reinterpret_cast<const SourceType*>(source.GetRow(y));
-
-      for (unsigned int x = 0; x < width; x++, p++, q++)
-      {
-        float v = a * static_cast<float>(*q) + b;
-
-        if (v >= maxFloatValue)
-        {
-          *p = maxPixelValue;
-        }
-        else if (v <= minFloatValue)
-        {
-          *p = minPixelValue;
-        }
-        else if (UseRound)
-        {
-          // The "round" operation is very costly
-          *p = static_cast<TargetType>(boost::math::iround(v));
-        }
-        else
-        {
-          *p = static_cast<TargetType>(std::floor(v));
-        }
-
-        if (Invert)
-        {
-          *p = maxPixelValue - *p;
-        }
-      }
-    }
-  }
-
-  template <typename PixelType>
-  static void ShiftRightInternal(ImageAccessor& image,
-                                 unsigned int shift)
-  {
-    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 < width; x++, p++)
-      {
-        *p = *p >> shift;
-      }
-    }
-  }
-
-  template <typename PixelType>
-  static void ShiftLeftInternal(ImageAccessor& image,
-                                unsigned int shift)
-  {
-    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 < width; x++, p++)
-      {
-        *p = *p << shift;
-      }
-    }
-  }
-
-  void ImageProcessing::Copy(ImageAccessor& target,
-                             const ImageAccessor& source)
-  {
-    if (target.GetWidth() != source.GetWidth() ||
-        target.GetHeight() != source.GetHeight())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageSize);
-    }
-
-    if (target.GetFormat() != source.GetFormat())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat);
-    }
-
-    unsigned int lineSize = source.GetBytesPerPixel() * source.GetWidth();
-
-    assert(source.GetPitch() >= lineSize && target.GetPitch() >= lineSize);
-
-    for (unsigned int y = 0; y < source.GetHeight(); y++)
-    {
-      memcpy(target.GetRow(y), source.GetConstRow(y), lineSize);
-    }
-  }
-
-  template <typename TargetType, typename SourceType>
-  static void ApplyWindowingInternal(ImageAccessor& target,
-                                     const ImageAccessor& source,
-                                     float windowCenter,
-                                     float windowWidth,
-                                     float rescaleSlope,
-                                     float rescaleIntercept,
-                                     bool invert)
-  {
-    assert(sizeof(SourceType) == source.GetBytesPerPixel() &&
-           sizeof(TargetType) == target.GetBytesPerPixel());
-    
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
-    const TargetType minTargetValue = std::numeric_limits<TargetType>::min();
-    const TargetType maxTargetValue = std::numeric_limits<TargetType>::max();
-    const float maxFloatValue = static_cast<float>(maxTargetValue);
-    
-    const float windowIntercept = windowCenter - windowWidth / 2.0f;
-    const float windowSlope = (maxFloatValue + 1.0f) / windowWidth;
-
-    const float a = rescaleSlope * windowSlope;
-    const float b = (rescaleIntercept - windowIntercept) * windowSlope;
-
-    if (invert)
-    {
-      ShiftScaleInternal<TargetType, SourceType, false, true>(target, source, a, b, minTargetValue);
-    }
-    else
-    {
-      ShiftScaleInternal<TargetType, SourceType, false, false>(target, source, a, b, minTargetValue);
-    }
-  }
-
-  void ImageProcessing::ApplyWindowing_Deprecated(ImageAccessor& target,
-                                                  const ImageAccessor& source,
-                                                  float windowCenter,
-                                                  float windowWidth,
-                                                  float rescaleSlope,
-                                                  float rescaleIntercept,
-                                                  bool invert)
-  {
-    if (target.GetWidth() != source.GetWidth() ||
-        target.GetHeight() != source.GetHeight())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageSize);
-    }
-
-    switch (source.GetFormat())
-    {
-      case Orthanc::PixelFormat_Float32:
-      {
-        switch (target.GetFormat())
-        {
-          case Orthanc::PixelFormat_Grayscale8:
-            ApplyWindowingInternal<uint8_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-            break;
-          case Orthanc::PixelFormat_Grayscale16:
-            ApplyWindowingInternal<uint16_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-            break;
-          default:
-            throw OrthancException(ErrorCode_NotImplemented);
-        }
-      };break;
-      case Orthanc::PixelFormat_Grayscale8:
-      {
-        switch (target.GetFormat())
-        {
-          case Orthanc::PixelFormat_Grayscale8:
-            ApplyWindowingInternal<uint8_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-            break;
-          case Orthanc::PixelFormat_Grayscale16:
-            ApplyWindowingInternal<uint16_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-            break;
-          default:
-            throw OrthancException(ErrorCode_NotImplemented);
-        }
-      };break;
-      case Orthanc::PixelFormat_Grayscale16:
-      {
-        switch (target.GetFormat())
-        {
-          case Orthanc::PixelFormat_Grayscale8:
-            ApplyWindowingInternal<uint8_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-            break;
-          case Orthanc::PixelFormat_Grayscale16:
-            ApplyWindowingInternal<uint16_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
-            break;
-          default:
-            throw OrthancException(ErrorCode_NotImplemented);
-        }
-      };break;
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::Convert(ImageAccessor& target,
-                                const ImageAccessor& source)
-  {
-    if (target.GetWidth() != source.GetWidth() ||
-        target.GetHeight() != source.GetHeight())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageSize);
-    }
-
-    const unsigned int width = source.GetWidth();
-    const unsigned int height = source.GetHeight();
-
-    if (source.GetFormat() == target.GetFormat())
-    {
-      Copy(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale16 &&
-        source.GetFormat() == PixelFormat_Grayscale8)
-    {
-      ConvertInternal<uint16_t, uint8_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
-        source.GetFormat() == PixelFormat_Grayscale8)
-    {
-      ConvertInternal<int16_t, uint8_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale8 &&
-        source.GetFormat() == PixelFormat_Grayscale16)
-    {
-      ConvertInternal<uint8_t, uint16_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
-        source.GetFormat() == PixelFormat_Grayscale16)
-    {
-      ConvertInternal<int16_t, uint16_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale8 &&
-        source.GetFormat() == PixelFormat_SignedGrayscale16)
-    {
-      ConvertInternal<uint8_t, int16_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale16 &&
-        source.GetFormat() == PixelFormat_SignedGrayscale16)
-    {
-      ConvertInternal<uint16_t, int16_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale8 &&
-        source.GetFormat() == PixelFormat_RGB24)
-    {
-      ConvertColorToGrayscale<uint8_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale16 &&
-        source.GetFormat() == PixelFormat_RGB24)
-    {
-      ConvertColorToGrayscale<uint16_t>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
-        source.GetFormat() == PixelFormat_RGB24)
-    {
-      ConvertColorToGrayscale<int16_t>(target, source);
-      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)
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++, q++)
-        {
-          *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[0]) +
-                                     7152 * static_cast<uint32_t>(p[1]) +
-                                     0722 * static_cast<uint32_t>(p[2])) / 10000);
-          p += 4;
-        }
-      }
-
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale8 &&
-        source.GetFormat() == PixelFormat_BGRA32)
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++, 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)
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          q[0] = p[0];
-          q[1] = p[1];
-          q[2] = p[2];
-          p += 4;
-          q += 3;
-        }
-      }
-
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_RGB24 &&
-        source.GetFormat() == PixelFormat_BGRA32)
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          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)
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          q[0] = p[0];
-          q[1] = p[1];
-          q[2] = p[2];
-          q[3] = 255;   // Set the alpha channel to full opacity
-          p += 3;
-          q += 4;
-        }
-      }
-
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_RGB24 &&
-        source.GetFormat() == PixelFormat_Grayscale8)
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          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 < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          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 < height; 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 < width; 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 < height; 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 < width; 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 < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          q[0] = p[2];
-          q[1] = p[1];
-          q[2] = p[0];
-          q[3] = 255;
-          p += 3;
-          q += 4;
-        }
-      }
-
-      return;
-    }
-
-    if ((target.GetFormat() == PixelFormat_BGRA32 &&
-         source.GetFormat() == PixelFormat_RGBA32)
-        || (target.GetFormat() == PixelFormat_RGBA32 &&
-            source.GetFormat() == PixelFormat_BGRA32))
-    {
-      for (unsigned int y = 0; y < height; y++)
-      {
-        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
-        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
-        for (unsigned int x = 0; x < width; x++)
-        {
-          q[0] = p[2];
-          q[1] = p[1];
-          q[2] = p[0];
-          q[3] = p[3];
-          p += 4;
-          q += 4;
-        }
-      }
-
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_RGB24 &&
-        source.GetFormat() == PixelFormat_RGB48)
-    {
-      for (unsigned int y = 0; y < height; 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 < width; x++)
-        {
-          q[0] = p[0] >> 8;
-          q[1] = p[1] >> 8;
-          q[2] = p[2] >> 8;
-          p += 3;
-          q += 3;
-        }
-      }
-
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale16 &&
-        source.GetFormat() == PixelFormat_Float32)
-    {
-      ConvertFloatToGrayscale<PixelFormat_Grayscale16>(target, source);
-      return;
-    }
-
-    if (target.GetFormat() == PixelFormat_Grayscale8 &&
-        source.GetFormat() == PixelFormat_Float32)
-    {
-      ConvertFloatToGrayscale<PixelFormat_Grayscale8>(target, source);
-      return;
-    }
-
-    throw OrthancException(ErrorCode_NotImplemented);
-  }
-
-
-
-  void ImageProcessing::Set(ImageAccessor& image,
-                            int64_t value)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        SetInternal<uint8_t>(image, value);
-        return;
-
-      case PixelFormat_Grayscale16:
-        SetInternal<uint16_t>(image, value);
-        return;
-
-      case PixelFormat_Grayscale32:
-        SetInternal<uint32_t>(image, value);
-        return;
-
-      case PixelFormat_Grayscale64:
-        SetInternal<uint64_t>(image, value);
-        return;
-
-      case PixelFormat_SignedGrayscale16:
-        SetInternal<int16_t>(image, value);
-        return;
-
-      case PixelFormat_Float32:
-        assert(sizeof(float) == 4);
-        SetInternal<float>(image, value);
-        return;
-
-      case PixelFormat_RGBA32:
-      case PixelFormat_BGRA32:
-      case PixelFormat_RGB24:
-      {
-        uint8_t v = static_cast<uint8_t>(value);
-        Set(image, v, v, v, v);  // Use the color version
-        return;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  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);
-    }
-
-    const unsigned int width = image.GetWidth();
-    const unsigned int height = image.GetHeight();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      uint8_t* q = reinterpret_cast<uint8_t*>(image.GetRow(y));
-
-      for (unsigned int x = 0; x < width; x++)
-      {
-        for (unsigned int i = 0; i < size; i++)
-        {
-          q[i] = p[i];
-        }
-
-        q += size;
-      }
-    }
-  }
-
-  void ImageProcessing::Set(ImageAccessor& image,
-                            uint8_t red,
-                            uint8_t green,
-                            uint8_t blue,
-                            ImageAccessor& alpha)
-  {
-    uint8_t p[4];
-
-    if (alpha.GetWidth() != image.GetWidth() || alpha.GetHeight() != image.GetHeight())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageSize);
-    }
-
-    if (alpha.GetFormat() != PixelFormat_Grayscale8)
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    switch (image.GetFormat())
-    {
-      case PixelFormat_RGBA32:
-        p[0] = red;
-        p[1] = green;
-        p[2] = blue;
-        break;
-
-      case PixelFormat_BGRA32:
-        p[0] = blue;
-        p[1] = green;
-        p[2] = red;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    const unsigned int width = image.GetWidth();
-    const unsigned int height = image.GetHeight();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      uint8_t* q = reinterpret_cast<uint8_t*>(image.GetRow(y));
-      uint8_t* a = reinterpret_cast<uint8_t*>(alpha.GetRow(y));
-
-      for (unsigned int x = 0; x < width; x++)
-      {
-        for (unsigned int i = 0; i < 3; i++)
-        {
-          q[i] = p[i];
-        }
-        q[3] = *a;
-        q += 4;
-        ++a;
-      }
-    }
-  }
-
-
-  void ImageProcessing::ShiftRight(ImageAccessor& image,
-                                   unsigned int shift)
-  {
-    if (image.GetWidth() == 0 ||
-        image.GetHeight() == 0 ||
-        shift == 0)
-    {
-      // Nothing to do
-      return;
-    }
-
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-      {
-        ShiftRightInternal<uint8_t>(image, shift);
-        break;
-      }
-
-      case PixelFormat_Grayscale16:
-      {
-        ShiftRightInternal<uint16_t>(image, shift);
-        break;
-      }
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-  void ImageProcessing::ShiftLeft(ImageAccessor& image,
-                                  unsigned int shift)
-  {
-    if (image.GetWidth() == 0 ||
-        image.GetHeight() == 0 ||
-        shift == 0)
-    {
-      // Nothing to do
-      return;
-    }
-
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-      {
-        ShiftLeftInternal<uint8_t>(image, shift);
-        break;
-      }
-
-      case PixelFormat_Grayscale16:
-      {
-        ShiftLeftInternal<uint16_t>(image, shift);
-        break;
-      }
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-  void ImageProcessing::GetMinMaxIntegerValue(int64_t& minValue,
-                                              int64_t& maxValue,
-                                              const ImageAccessor& image)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-      {
-        uint8_t a, b;
-        GetMinMaxValueInternal<uint8_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
-
-      case PixelFormat_Grayscale16:
-      {
-        uint16_t a, b;
-        GetMinMaxValueInternal<uint16_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        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;
-        GetMinMaxValueInternal<int16_t>(a, b, image);
-        minValue = a;
-        maxValue = b;
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::GetMinMaxFloatValue(float& minValue,
-                                            float& maxValue,
-                                            const ImageAccessor& image)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Float32:
-      {
-        assert(sizeof(float) == 4);
-        float a, b;
-
-        /**
-         * WARNING - On floating-point types, the minimal value is
-         * "-FLT_MAX" (as implemented by "::lowest()"), not "FLT_MIN"
-         * (as implemented by "::min()")
-         * https://en.cppreference.com/w/cpp/types/numeric_limits
-         **/
-        GetMinMaxValueInternal<float>(a, b, image, -std::numeric_limits<float>::max());
-        minValue = a;
-        maxValue = b;
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-
-  void ImageProcessing::AddConstant(ImageAccessor& image,
-                                    int64_t value)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        AddConstantInternal<uint8_t>(image, value);
-        return;
-
-      case PixelFormat_Grayscale16:
-        AddConstantInternal<uint16_t>(image, value);
-        return;
-
-      case PixelFormat_SignedGrayscale16:
-        AddConstantInternal<int16_t>(image, value);
-        return;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::MultiplyConstant(ImageAccessor& image,
-                                         float factor,
-                                         bool useRound)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        if (useRound)
-        {
-          MultiplyConstantInternal<uint8_t, true>(image, factor);
-        }
-        else
-        {
-          MultiplyConstantInternal<uint8_t, false>(image, factor);
-        }
-        return;
-
-      case PixelFormat_Grayscale16:
-        if (useRound)
-        {
-          MultiplyConstantInternal<uint16_t, true>(image, factor);
-        }
-        else
-        {
-          MultiplyConstantInternal<uint16_t, false>(image, factor);
-        }
-        return;
-
-      case PixelFormat_SignedGrayscale16:
-        if (useRound)
-        {
-          MultiplyConstantInternal<int16_t, true>(image, factor);
-        }
-        else
-        {
-          MultiplyConstantInternal<int16_t, false>(image, factor);
-        }
-        return;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::ShiftScale(ImageAccessor& image,
-                                   float offset,
-                                   float scaling,
-                                   bool useRound)
-  {
-    // Rewrite "(x + offset) * scaling" as "a * x + b"
-
-    const float a = scaling;
-    const float b = offset * scaling;
-    
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        if (useRound)
-        {
-          ShiftScaleInternal<uint8_t, uint8_t, true, false>(image, image, a, b, std::numeric_limits<uint8_t>::min());
-        }
-        else
-        {
-          ShiftScaleInternal<uint8_t, uint8_t, false, false>(image, image, a, b, std::numeric_limits<uint8_t>::min());
-        }
-        return;
-
-      case PixelFormat_Grayscale16:
-        if (useRound)
-        {
-          ShiftScaleInternal<uint16_t, uint16_t, true, false>(image, image, a, b, std::numeric_limits<uint16_t>::min());
-        }
-        else
-        {
-          ShiftScaleInternal<uint16_t, uint16_t, false, false>(image, image, a, b, std::numeric_limits<uint16_t>::min());
-        }
-        return;
-
-      case PixelFormat_SignedGrayscale16:
-        if (useRound)
-        {
-          ShiftScaleInternal<int16_t, int16_t, true, false>(image, image, a, b, std::numeric_limits<int16_t>::min());
-        }
-        else
-        {
-          ShiftScaleInternal<int16_t, int16_t, false, false>(image, image, a, b, std::numeric_limits<int16_t>::min());
-        }
-        return;
-
-      case PixelFormat_Float32:
-        // "::min()" must be replaced by "::lowest()" or "-::max()" if dealing with float or double.
-        if (useRound)
-        {
-          ShiftScaleInternal<float, float, true, false>(image, image, a, b, -std::numeric_limits<float>::max());
-        }
-        else
-        {
-          ShiftScaleInternal<float, float, false, false>(image, image, a, b, -std::numeric_limits<float>::max());
-        }
-        return;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::ShiftScale(ImageAccessor& target,
-                                   const ImageAccessor& source,
-                                   float offset,
-                                   float scaling,
-                                   bool useRound)
-  {
-    // Rewrite "(x + offset) * scaling" as "a * x + b"
-
-    const float a = scaling;
-    const float b = offset * scaling;
-    
-    switch (target.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-
-        switch (source.GetFormat())
-        {
-          case PixelFormat_Float32:
-            if (useRound)
-            {
-              ShiftScaleInternal<uint8_t, float, true, false>(
-                target, source, a, b, std::numeric_limits<uint8_t>::min());
-            }
-            else
-            {
-              ShiftScaleInternal<uint8_t, float, false, false>(
-                target, source, a, b, std::numeric_limits<uint8_t>::min());
-            }
-            return;
-
-          default:
-            throw OrthancException(ErrorCode_NotImplemented);
-        }
-        
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue)
-  {
-    const unsigned int width = image.GetWidth();
-    const unsigned int height = image.GetHeight();
-
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale16:
-      {
-        uint16_t maxValueUint16 = (uint16_t)(std::min(maxValue, static_cast<int64_t>(std::numeric_limits<uint16_t>::max())));
-
-        for (unsigned int y = 0; y < height; y++)
-        {
-          uint16_t* p = reinterpret_cast<uint16_t*>(image.GetRow(y));
-
-          for (unsigned int x = 0; x < width; x++, p++)
-          {
-            *p = maxValueUint16 - (*p);
-          }
-        }
-
-        return;
-      }
-      case PixelFormat_Grayscale8:
-      {
-        uint8_t maxValueUint8 = (uint8_t)(std::min(maxValue, static_cast<int64_t>(std::numeric_limits<uint8_t>::max())));
-
-        for (unsigned int y = 0; y < height; y++)
-        {
-          uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
-
-          for (unsigned int x = 0; x < width; x++, p++)
-          {
-            *p = maxValueUint8 - (*p);
-          }
-        }
-
-        return;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-  }
-
-  void ImageProcessing::Invert(ImageAccessor& image)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        return Invert(image, 255);
-      default:
-        throw OrthancException(ErrorCode_NotImplemented); // you should use the Invert(image, maxValue) overload
-    }
-  }
-
-
-
-  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);
-    }
-  }
-
-  void ComputePolygonExtent(int32_t& left, int32_t& right, int32_t& top, int32_t& bottom, const std::vector<ImageProcessing::ImagePoint>& points)
-  {
-    left = std::numeric_limits<int32_t>::max();
-    right = std::numeric_limits<int32_t>::min();
-    top = std::numeric_limits<int32_t>::max();
-    bottom = std::numeric_limits<int32_t>::min();
-
-    for (size_t i = 0; i < points.size(); i++)
-    {
-      const ImageProcessing::ImagePoint& p = points[i];
-      left = std::min(p.GetX(), left);
-      right = std::max(p.GetX(), right);
-      bottom = std::max(p.GetY(), bottom);
-      top = std::min(p.GetY(), top);
-    }
-  }
-
-  template <PixelFormat TargetFormat>
-  void FillPolygon_(ImageAccessor& image,
-                    const std::vector<ImageProcessing::ImagePoint>& points,
-                    int64_t value_)
-  {
-    typedef typename PixelTraits<TargetFormat>::PixelType  TargetType;
-
-    TargetType value = PixelTraits<TargetFormat>::IntegerToPixel(value_);
-    int imageWidth = static_cast<int>(image.GetWidth());
-    int imageHeight = static_cast<int>(image.GetHeight());
-    int32_t left;
-    int32_t right;
-    int32_t top;
-    int32_t bottom;
-
-    // TODO: test clipping in UT (in Trello board)
-    ComputePolygonExtent(left, right, top, bottom, points);
-
-    // clip the computed extent with the target image
-    // L and R
-    left = std::max(0, left);
-    left = std::min(imageWidth, left);
-    right = std::max(0, right);
-    right = std::min(imageWidth, right);
-    if (left > right)
-      std::swap(left, right);
-
-    // T and B
-    top = std::max(0, top);
-    top = std::min(imageHeight, top);
-    bottom = std::max(0, bottom);
-    bottom = std::min(imageHeight, bottom);
-    if (top > bottom)
-      std::swap(top, bottom);
-
-    // from http://alienryderflex.com/polygon_fill/
-
-    // convert all "corner"  points to double only once
-    std::vector<double> cpx;
-    std::vector<double> cpy;
-    size_t cpSize = points.size();
-    for (size_t i = 0; i < points.size(); i++)
-    {
-      if (points[i].GetX() < 0 || points[i].GetX() >= imageWidth
-          || points[i].GetY() < 0 || points[i].GetY() >= imageHeight)
-      {
-        throw Orthanc::OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      cpx.push_back((double)points[i].GetX());
-      cpy.push_back((double)points[i].GetY());
-    }
-
-    // Draw the lines segments
-    for (size_t i = 0; i < (points.size() -1); i++)
-    {
-      ImageProcessing::DrawLineSegment(image, points[i].GetX(), points[i].GetY(), points[i+1].GetX(), points[i+1].GetY(), value_);
-    }
-    ImageProcessing::DrawLineSegment(image, points[points.size() -1].GetX(), points[points.size() -1].GetY(), points[0].GetX(), points[0].GetY(), value_);
-
-    std::vector<int32_t> nodeX;
-    nodeX.resize(cpSize);
-    int  nodes, pixelX, pixelY, i, j, swap ;
-
-    //  Loop through the rows of the image.
-    for (pixelY = top; pixelY < bottom; pixelY++)
-    {
-      double y = (double)pixelY;
-      //  Build a list of nodes.
-      nodes = 0;
-      j = static_cast<int>(cpSize) - 1;
-
-      for (i = 0; i < static_cast<int>(cpSize); i++)
-      {
-        if ((cpy[i] < y && cpy[j] >=  y) || (cpy[j] < y && cpy[i] >= y))
-        {
-          nodeX[nodes++] = (int32_t)(cpx[i] + (y - cpy[i])/(cpy[j] - cpy[i]) * (cpx[j] - cpx[i]));
-        }
-        j=i;
-      }
-
-      //  Sort the nodes, via a simple “Bubble” sort.
-      i=0;
-      while (i < nodes-1)
-      {
-        if (nodeX[i] > nodeX[i+1])
-        {
-          swap = nodeX[i];
-          nodeX[i] = nodeX[i+1];
-          nodeX[i+1] = swap;
-          if (i > 0)
-          {
-            i--;
-          }
-        }
-        else
-        {
-          i++;
-        }
-      }
-
-      TargetType* row = reinterpret_cast<TargetType*>(image.GetRow(pixelY));
-      //  Fill the pixels between node pairs.
-      for (i = 0; i < nodes; i += 2)
-      {
-        if (nodeX[i] >= right)
-          break;
-
-        if (nodeX[i + 1] >= left)
-        {
-          if (nodeX[i] < left)
-          {
-            nodeX[i] = left;
-          }
-
-          if (nodeX[i + 1] > right)
-          {
-            nodeX[i + 1] = right;
-          }
-
-          for (pixelX = nodeX[i]; pixelX <= nodeX[i + 1]; pixelX++)
-          {
-            *(row + pixelX) = value;
-          }
-        }
-      }
-    }
-  }
-
-  void ImageProcessing::FillPolygon(ImageAccessor& image,
-                                    const std::vector<ImagePoint>& points,
-                                    int64_t value)
-  {
-    switch (image.GetFormat())
-    {
-      case Orthanc::PixelFormat_Grayscale8:
-      {
-        FillPolygon_<Orthanc::PixelFormat_Grayscale8>(image, points, value);
-        break;
-      }
-      case Orthanc::PixelFormat_Grayscale16:
-      {
-        FillPolygon_<Orthanc::PixelFormat_Grayscale16>(image, points, value);
-        break;
-      }
-      case Orthanc::PixelFormat_SignedGrayscale16:
-      {
-        FillPolygon_<Orthanc::PixelFormat_SignedGrayscale16>(image, points, value);
-        break;
-      }
-      default:
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-    }
-  }
-
-
-  template <PixelFormat Format>
-  static void ResizeInternal(ImageAccessor& target,
-                             const ImageAccessor& source)
-  {
-    assert(target.GetFormat() == source.GetFormat() &&
-           target.GetFormat() == Format);
-      
-    const unsigned int sourceWidth = source.GetWidth();
-    const unsigned int sourceHeight = source.GetHeight();
-    const unsigned int targetWidth = target.GetWidth();
-    const unsigned int targetHeight = target.GetHeight();
-
-    if (targetWidth == 0 || targetHeight == 0)
-    {
-      return;
-    }
-
-    if (sourceWidth == 0 || sourceHeight == 0)
-    {
-      // Avoids division by zero below
-      ImageProcessing::Set(target, 0);
-      return;
-    }
-      
-    const float scaleX = static_cast<float>(sourceWidth) / static_cast<float>(targetWidth);
-    const float scaleY = static_cast<float>(sourceHeight) / static_cast<float>(targetHeight);
-
-
-    /**
-     * Create two lookup tables to quickly know the (x,y) position
-     * in the source image, given the (x,y) position in the target
-     * image.
-     **/
-      
-    std::vector<unsigned int>  lookupX(targetWidth);
-      
-    for (unsigned int x = 0; x < targetWidth; x++)
-    {
-      int sourceX = static_cast<int>(std::floor((static_cast<float>(x) + 0.5f) * scaleX));
-      if (sourceX < 0)
-      {
-        sourceX = 0;  // Should never happen
-      }
-      else if (sourceX >= static_cast<int>(sourceWidth))
-      {
-        sourceX = sourceWidth - 1;
-      }
-
-      lookupX[x] = static_cast<unsigned int>(sourceX);
-    }
-      
-    std::vector<unsigned int>  lookupY(targetHeight);
-      
-    for (unsigned int y = 0; y < targetHeight; y++)
-    {
-      int sourceY = static_cast<int>(std::floor((static_cast<float>(y) + 0.5f) * scaleY));
-      if (sourceY < 0)
-      {
-        sourceY = 0;  // Should never happen
-      }
-      else if (sourceY >= static_cast<int>(sourceHeight))
-      {
-        sourceY = sourceHeight - 1;
-      }
-
-      lookupY[y] = static_cast<unsigned int>(sourceY);
-    }
-
-
-    /**
-     * Actual resizing
-     **/
-      
-    for (unsigned int targetY = 0; targetY < targetHeight; targetY++)
-    {
-      unsigned int sourceY = lookupY[targetY];
-
-      for (unsigned int targetX = 0; targetX < targetWidth; targetX++)
-      {
-        unsigned int sourceX = lookupX[targetX];
-
-        typename ImageTraits<Format>::PixelType pixel;
-        ImageTraits<Format>::GetPixel(pixel, source, sourceX, sourceY);
-        ImageTraits<Format>::SetPixel(target, pixel, targetX, targetY);
-      }
-    }            
-  }
-
-
-
-  void ImageProcessing::Resize(ImageAccessor& target,
-                               const ImageAccessor& source)
-  {
-    if (source.GetFormat() != source.GetFormat())
-    {
-      throw OrthancException(ErrorCode_IncompatibleImageFormat);
-    }
-
-    if (source.GetWidth() == target.GetWidth() &&
-        source.GetHeight() == target.GetHeight())
-    {
-      Copy(target, source);
-      return;
-    }
-
-    switch (source.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        ResizeInternal<PixelFormat_Grayscale8>(target, source);
-        break;
-
-      case PixelFormat_Float32:
-        ResizeInternal<PixelFormat_Float32>(target, source);
-        break;
-
-      case PixelFormat_RGB24:
-        ResizeInternal<PixelFormat_RGB24>(target, source);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  ImageAccessor* ImageProcessing::Halve(const ImageAccessor& source,
-                                        bool forceMinimalPitch)
-  {
-    std::unique_ptr<Image> target(new Image(source.GetFormat(), source.GetWidth() / 2,
-                                            source.GetHeight() / 2, forceMinimalPitch));
-    Resize(*target, source);
-    return target.release();
-  }
-
-    
-  template <PixelFormat Format>
-  static void FlipXInternal(ImageAccessor& image)
-  {     
-    const unsigned int height = image.GetHeight();
-    const unsigned int width = image.GetWidth();
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      for (unsigned int x1 = 0; x1 < width / 2; x1++)
-      {
-        unsigned int x2 = width - 1 - x1;
-          
-        typename ImageTraits<Format>::PixelType a, b;
-        ImageTraits<Format>::GetPixel(a, image, x1, y);
-        ImageTraits<Format>::GetPixel(b, image, x2, y);
-        ImageTraits<Format>::SetPixel(image, a, x2, y);
-        ImageTraits<Format>::SetPixel(image, b, x1, y);
-      }
-    }        
-  }
-
-    
-  void ImageProcessing::FlipX(ImageAccessor& image)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        FlipXInternal<PixelFormat_Grayscale8>(image);
-        break;
-
-      case PixelFormat_RGB24:
-        FlipXInternal<PixelFormat_RGB24>(image);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-    
-  template <PixelFormat Format>
-  static void FlipYInternal(ImageAccessor& image)
-  {     
-    const unsigned int height = image.GetHeight();
-    const unsigned int width = image.GetWidth();
-
-    for (unsigned int y1 = 0; y1 < height / 2; y1++)
-    {
-      unsigned int y2 = height - 1 - y1;
-        
-      for (unsigned int x = 0; x < width; x++)
-      {
-        typename ImageTraits<Format>::PixelType a, b;
-        ImageTraits<Format>::GetPixel(a, image, x, y1);
-        ImageTraits<Format>::GetPixel(b, image, x, y2);
-        ImageTraits<Format>::SetPixel(image, a, x, y2);
-        ImageTraits<Format>::SetPixel(image, b, x, y1);
-      }
-    }        
-  }
-
-    
-  void ImageProcessing::FlipY(ImageAccessor& image)
-  {
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        FlipYInternal<PixelFormat_Grayscale8>(image);
-        break;
-
-      case PixelFormat_RGB24:
-        FlipYInternal<PixelFormat_RGB24>(image);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  // This is a slow implementation of horizontal convolution on one
-  // individual channel, that checks for out-of-image values
-  template <typename RawPixel, unsigned int ChannelsCount>
-  static float GetHorizontalConvolutionFloatSecure(const Orthanc::ImageAccessor& source,
-                                                   const std::vector<float>& horizontal,
-                                                   size_t horizontalAnchor,
-                                                   unsigned int x,
-                                                   unsigned int y,
-                                                   float leftBorder,
-                                                   float rightBorder,
-                                                   unsigned int channel)
-  {
-    const RawPixel* row = reinterpret_cast<const RawPixel*>(source.GetConstRow(y)) + channel;
-
-    float p = 0;
-
-    for (unsigned int k = 0; k < horizontal.size(); k++)
-    {
-      float value;
-
-      if (x + k < horizontalAnchor)   // Negation of "x - horizontalAnchor + k >= 0"
-      {
-        value = leftBorder;
-      }
-      else if (x + k >= source.GetWidth() + horizontalAnchor)   // Negation of "x - horizontalAnchor + k < width"
-      {
-        value = rightBorder;
-      }
-      else
-      {
-        // The value lies within the image
-        value = row[(x - horizontalAnchor + k) * ChannelsCount];
-      }
-
-      p += value * horizontal[k];
-    }
-
-    return p;
-  }
-  
-
-  
-  // This is an implementation of separable convolution that uses
-  // floating-point arithmetics, and an intermediate Float32
-  // image. The out-of-image values are taken as the border
-  // value. Further optimization is possible.
-  template <typename RawPixel, unsigned int ChannelsCount>
-  static void SeparableConvolutionFloat(ImageAccessor& image /* inplace */,
-                                        const std::vector<float>& horizontal,
-                                        size_t horizontalAnchor,
-                                        const std::vector<float>& vertical,
-                                        size_t verticalAnchor,
-                                        float normalization)
-  {
-    // WARNING - "::min()" should be replaced by "::lowest()" if
-    // dealing with float or double (which is not the case so far)
-    assert(sizeof(RawPixel) <= 2);  // Safeguard to remember about "float/double"
-
-    const unsigned int width = image.GetWidth();
-    const unsigned int height = image.GetHeight();
-    
-
-    /**
-     * Horizontal convolution
-     **/
-
-    Image tmp(PixelFormat_Float32, ChannelsCount * width, height, false);
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      const RawPixel* row = reinterpret_cast<const RawPixel*>(image.GetConstRow(y));
-
-      float leftBorder[ChannelsCount], rightBorder[ChannelsCount];
-      
-      for (unsigned int c = 0; c < ChannelsCount; c++)
-      {
-        leftBorder[c] = row[c];
-        rightBorder[c] = row[ChannelsCount * (width - 1) + c];
-      }
-
-      float* p = static_cast<float*>(tmp.GetRow(y));
-
-      if (width < horizontal.size())
-      {
-        // It is not possible to have the full kernel within the image, use the direct implementation
-        for (unsigned int x = 0; x < width; x++)
-        {
-          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
-          {
-            *p = GetHorizontalConvolutionFloatSecure<RawPixel, ChannelsCount>
-              (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c);
-          }
-        }
-      }
-      else
-      {
-        // Deal with the left border
-        for (unsigned int x = 0; x < horizontalAnchor; x++)
-        {
-          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
-          {
-            *p = GetHorizontalConvolutionFloatSecure<RawPixel, ChannelsCount>
-              (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c);
-          }
-        }
-
-        // Deal with the central portion of the image (all pixel values
-        // scanned by the kernel lie inside the image)
-
-        for (unsigned int x = 0; x < width - horizontal.size() + 1; x++)
-        {
-          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
-          {
-            *p = 0;
-            for (unsigned int k = 0; k < horizontal.size(); k++)
-            {
-              *p += static_cast<float>(row[(x + k) * ChannelsCount + c]) * horizontal[k];
-            }
-          }
-        }
-
-        // Deal with the right border
-        for (unsigned int x = static_cast<unsigned int>(
-               horizontalAnchor + width - horizontal.size() + 1); x < width; x++)
-        {
-          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
-          {
-            *p = GetHorizontalConvolutionFloatSecure<RawPixel, ChannelsCount>
-              (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c);
-          }
-        }
-      }
-    }
-
-
-    /**
-     * Vertical convolution
-     **/
-
-    std::vector<const float*> rows(vertical.size());
-
-    for (unsigned int y = 0; y < height; y++)
-    {
-      for (unsigned int k = 0; k < vertical.size(); k++)
-      {
-        if (y + k < verticalAnchor)
-        {
-          rows[k] = reinterpret_cast<const float*>(tmp.GetConstRow(0));   // Use top border
-        }
-        else if (y + k >= height + verticalAnchor)
-        {
-          rows[k] = reinterpret_cast<const float*>(tmp.GetConstRow(height - 1));  // Use bottom border
-        }
-        else
-        {
-          rows[k] = reinterpret_cast<const float*>(tmp.GetConstRow(static_cast<unsigned int>(y + k - verticalAnchor)));
-        }
-      }
-
-      RawPixel* p = reinterpret_cast<RawPixel*>(image.GetRow(y));
-        
-      for (unsigned int x = 0; x < width; x++)
-      {
-        for (unsigned int c = 0; c < ChannelsCount; c++, p++)
-        {
-          float accumulator = 0;
-        
-          for (unsigned int k = 0; k < vertical.size(); k++)
-          {
-            accumulator += rows[k][ChannelsCount * x + c] * vertical[k];
-          }
-
-          accumulator *= normalization;
-
-          if (accumulator <= static_cast<float>(std::numeric_limits<RawPixel>::min()))
-          {
-            *p = std::numeric_limits<RawPixel>::min();
-          }
-          else if (accumulator >= static_cast<float>(std::numeric_limits<RawPixel>::max()))
-          {
-            *p = std::numeric_limits<RawPixel>::max();
-          }
-          else
-          {
-            *p = static_cast<RawPixel>(accumulator);
-          }
-        }
-      }
-    }
-  }
-
-
-  void ImageProcessing::SeparableConvolution(ImageAccessor& image /* inplace */,
-                                             const std::vector<float>& horizontal,
-                                             size_t horizontalAnchor,
-                                             const std::vector<float>& vertical,
-                                             size_t verticalAnchor)
-  {
-    if (horizontal.size() == 0 ||
-        vertical.size() == 0 ||
-        horizontalAnchor >= horizontal.size() ||
-        verticalAnchor >= vertical.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    
-    if (image.GetWidth() == 0 ||
-        image.GetHeight() == 0)
-    {
-      return;
-    }
-    
-    /**
-     * Compute normalization
-     **/
-    
-    float sumHorizontal = 0;
-    for (size_t i = 0; i < horizontal.size(); i++)
-    {
-      sumHorizontal += horizontal[i];
-    }
-    
-    float sumVertical = 0;
-    for (size_t i = 0; i < vertical.size(); i++)
-    {
-      sumVertical += vertical[i];
-    }
-
-    if (fabsf(sumHorizontal) <= std::numeric_limits<float>::epsilon() ||
-        fabsf(sumVertical) <= std::numeric_limits<float>::epsilon())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange, "Singular convolution kernel");
-    }      
-
-    const float normalization = 1.0f / (sumHorizontal * sumVertical);
-
-    switch (image.GetFormat())
-    {
-      case PixelFormat_Grayscale8:
-        SeparableConvolutionFloat<uint8_t, 1u>
-          (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization);
-        break;
-
-      case PixelFormat_RGB24:
-        SeparableConvolutionFloat<uint8_t, 3u>
-          (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void ImageProcessing::SmoothGaussian5x5(ImageAccessor& image)
-  {
-    std::vector<float> kernel(5);
-    kernel[0] = 1;
-    kernel[1] = 4;
-    kernel[2] = 6;
-    kernel[3] = 4;
-    kernel[4] = 1;
-
-    SeparableConvolution(image, kernel, 2, kernel, 2);
-  }
-
-
-  void ImageProcessing::FitSize(ImageAccessor& target,
-                                const ImageAccessor& source)
-  {
-    if (target.GetWidth() == 0 ||
-        target.GetHeight() == 0)
-    {
-      return;
-    }
-
-    if (source.GetWidth() == target.GetWidth() &&
-        source.GetHeight() == target.GetHeight())
-    {
-      Copy(target, source);
-      return;
-    }
-
-    Set(target, 0);
-
-    // Preserve the aspect ratio
-    float cw = static_cast<float>(source.GetWidth());
-    float ch = static_cast<float>(source.GetHeight());
-    float r = std::min(
-      static_cast<float>(target.GetWidth()) / cw,
-      static_cast<float>(target.GetHeight()) / ch);
-
-    unsigned int sw = std::min(static_cast<unsigned int>(boost::math::iround(cw * r)), target.GetWidth());  
-    unsigned int sh = std::min(static_cast<unsigned int>(boost::math::iround(ch * r)), target.GetHeight());
-    Image resized(target.GetFormat(), sw, sh, false);
-  
-    //ImageProcessing::SmoothGaussian5x5(source);
-    ImageProcessing::Resize(resized, source);
-
-    assert(target.GetWidth() >= resized.GetWidth() &&
-           target.GetHeight() >= resized.GetHeight());
-    unsigned int offsetX = (target.GetWidth() - resized.GetWidth()) / 2;
-    unsigned int offsetY = (target.GetHeight() - resized.GetHeight()) / 2;
-
-    ImageAccessor region;
-    target.GetRegion(region, offsetX, offsetY, resized.GetWidth(), resized.GetHeight());
-    ImageProcessing::Copy(region, resized);
-  }
-
-
-  ImageAccessor* ImageProcessing::FitSize(const ImageAccessor& source,
-                                          unsigned int width,
-                                          unsigned int height)
-  {
-    std::unique_ptr<ImageAccessor> target(new Image(source.GetFormat(), width, height, false));
-    FitSize(*target, source);
-    return target.release();
-  }
-}
--- a/Core/Images/ImageProcessing.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,197 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancFramework.h"
-
-#include "ImageAccessor.h"
-#include <vector>
-
-#include <stdint.h>
-#include <algorithm>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC ImageProcessing : public boost::noncopyable
-  {
-  public:
-    class ImagePoint
-    {
-      int32_t x_;
-      int32_t y_;
-      
-    public:
-      ImagePoint(int32_t x, int32_t y)
-        : x_(x),
-          y_(y)
-      {
-      }
-
-      int32_t GetX() const {return x_;}
-
-      int32_t GetY() const {return y_;}
-
-      void Set(int32_t x, int32_t y)
-      {
-        x_ = x;
-        y_ = y;
-      }
-
-      void ClipTo(int32_t minX, int32_t maxX, int32_t minY, int32_t maxY)
-      {
-        x_ = std::max(minX, std::min(maxX, x_));
-        y_ = std::max(minY, std::min(maxY, y_));
-      }
-
-      double GetDistanceTo(const ImagePoint& other) const;
-
-      double GetDistanceToLine(double a, double b, double c) const; // where ax + by + c = 0 is the equation of the line
-    };
-
-    static void Copy(ImageAccessor& target,
-                     const ImageAccessor& source);
-
-    static void Convert(ImageAccessor& target,
-                        const ImageAccessor& source);
-
-    static void ApplyWindowing_Deprecated(ImageAccessor& target,
-                                          const ImageAccessor& source,
-                                          float windowCenter,
-                                          float windowWidth,
-                                          float rescaleSlope,
-                                          float rescaleIntercept,
-                                          bool invert);
-
-    static void Set(ImageAccessor& image,
-                    int64_t value);
-
-    static void Set(ImageAccessor& image,
-                    uint8_t red,
-                    uint8_t green,
-                    uint8_t blue,
-                    uint8_t alpha);
-
-    static void Set(ImageAccessor& image,
-                    uint8_t red,
-                    uint8_t green,
-                    uint8_t blue,
-                    ImageAccessor& alpha);
-
-    static void ShiftRight(ImageAccessor& target,
-                           unsigned int shift);
-
-    static void ShiftLeft(ImageAccessor& target,
-                          unsigned int shift);
-
-    static void GetMinMaxIntegerValue(int64_t& minValue,
-                                      int64_t& maxValue,
-                                      const ImageAccessor& image);
-
-    static void GetMinMaxFloatValue(float& minValue,
-                                    float& maxValue,
-                                    const ImageAccessor& image);
-
-    static void AddConstant(ImageAccessor& image,
-                            int64_t value);
-
-    // "useRound" is expensive
-    static void MultiplyConstant(ImageAccessor& image,
-                                 float factor,
-                                 bool useRound);
-
-    // Computes "(x + offset) * scaling" inplace. "useRound" is expensive.
-    static void ShiftScale(ImageAccessor& image,
-                           float offset,
-                           float scaling,
-                           bool useRound);
-
-    static void ShiftScale(ImageAccessor& target,
-                           const ImageAccessor& source,
-                           float offset,
-                           float scaling,
-                           bool useRound);
-
-    static void Invert(ImageAccessor& image);
-
-    static void Invert(ImageAccessor& image, int64_t maxValue);
-
-    static void DrawLineSegment(ImageAccessor& image,
-                                int x0,
-                                int y0,
-                                int x1,
-                                int y1,
-                                int64_t value);
-
-    static void DrawLineSegment(ImageAccessor& image,
-                                int x0,
-                                int y0,
-                                int x1,
-                                int y1,
-                                uint8_t red,
-                                uint8_t green,
-                                uint8_t blue,
-                                uint8_t alpha);
-
-    static void FillPolygon(ImageAccessor& image,
-                            const std::vector<ImagePoint>& points,
-                            int64_t value);
-
-    static void Resize(ImageAccessor& target,
-                       const ImageAccessor& source);
-
-    static ImageAccessor* Halve(const ImageAccessor& source,
-                                bool forceMinimalPitch);
-
-    static void FlipX(ImageAccessor& image);
-
-    static void FlipY(ImageAccessor& image);
-
-    static void SeparableConvolution(ImageAccessor& image /* inplace */,
-                                     const std::vector<float>& horizontal,
-                                     size_t horizontalAnchor,
-                                     const std::vector<float>& vertical,
-                                     size_t verticalAnchor);
-
-    static void SmoothGaussian5x5(ImageAccessor& image);
-
-    static void FitSize(ImageAccessor& target,
-                        const ImageAccessor& source);
-    
-    static ImageAccessor* FitSize(const ImageAccessor& source,
-                                  unsigned int width,
-                                  unsigned int height);
-  };
-}
-
--- a/Core/Images/ImageTraits.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "JpegErrorManager.h"
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    void JpegErrorManager::OutputMessage(j_common_ptr cinfo)
-    {
-      char message[JMSG_LENGTH_MAX];
-      (*cinfo->err->format_message) (cinfo, message);
-
-      JpegErrorManager* that = reinterpret_cast<JpegErrorManager*>(cinfo->err);
-      that->message = std::string(message);
-    }
-
-
-    void JpegErrorManager::ErrorExit(j_common_ptr cinfo)
-    {
-      (*cinfo->err->output_message) (cinfo);
-
-      JpegErrorManager* that = reinterpret_cast<JpegErrorManager*>(cinfo->err);
-      longjmp(that->setjmp_buffer, 1);
-    }
-      
-
-    JpegErrorManager::JpegErrorManager()
-    {
-      memset(&pub, 0, sizeof(struct jpeg_error_mgr));
-      memset(&setjmp_buffer, 0, sizeof(jmp_buf));
-
-      jpeg_std_error(&pub);
-      pub.error_exit = ErrorExit;
-      pub.output_message = OutputMessage;
-    }
-  }
-}
--- a/Core/Images/JpegErrorManager.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 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>
-#include <setjmp.h>
-#include <string>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    class JpegErrorManager 
-    {
-    private:
-      struct jpeg_error_mgr pub;  /* "public" fields */
-      jmp_buf setjmp_buffer;      /* for return to caller */
-      std::string message;
-
-      static void OutputMessage(j_common_ptr cinfo);
-
-      static void ErrorExit(j_common_ptr cinfo);
-
-    public:
-      JpegErrorManager();
-
-      struct jpeg_error_mgr* GetPublic()
-      {
-        return &pub;
-      }
-
-      jmp_buf& GetJumpBuffer()
-      {
-        return setjmp_buffer;
-      }
-
-      const std::string& GetMessage() const
-      {
-        return message;
-      }
-    };
-  }
-}
--- a/Core/Images/JpegReader.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,199 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "JpegReader.h"
-
-#include "JpegErrorManager.h"
-#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)
-  {
-    // 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;
-    if (cinfo.output_components == 1 &&
-        cinfo.out_color_space == JCS_GRAYSCALE)
-    {
-      format = PixelFormat_Grayscale8;
-    }
-    else if (cinfo.output_components == 3 &&
-             cinfo.out_color_space == JCS_RGB)
-    {
-      format = PixelFormat_RGB24;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    unsigned int pitch = cinfo.output_width * cinfo.output_components;
-
-    /* Make a one-row-high sample array that will go away when done with image */
-    JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, pitch, 1);
-
-    try
-    {
-      content.resize(pitch * cinfo.output_height);
-    }
-    catch (...)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    accessor.AssignWritable(format, cinfo.output_width, cinfo.output_height, pitch, 
-                            content.empty() ? NULL : &content[0]);
-
-    uint8_t* target = reinterpret_cast<uint8_t*>(&content[0]);
-    while (cinfo.output_scanline < cinfo.output_height) 
-    {
-      jpeg_read_scanlines(&cinfo, buffer, 1);
-      memcpy(target, buffer[0], pitch);
-      target += pitch;
-    }
-
-    // Everything went fine, "setjmp()" didn't get called
-
-    jpeg_finish_decompress(&cinfo);
-  }
-
-
-#if ORTHANC_SANDBOXED == 0
-  void JpegReader::ReadFromFile(const std::string& filename)
-  {
-    FILE* fp = SystemToolbox::OpenFile(filename, FileMode_ReadBinary);
-    if (!fp)
-    {
-      throw OrthancException(ErrorCode_InexistentFile);
-    }
-
-    struct jpeg_decompress_struct cinfo;
-    memset(&cinfo, 0, sizeof(struct jpeg_decompress_struct));
-
-    Internals::JpegErrorManager jerr;
-    cinfo.err = jerr.GetPublic();
-    
-    if (setjmp(jerr.GetJumpBuffer())) 
-    {
-      jpeg_destroy_decompress(&cinfo);
-      fclose(fp);
-
-      throw OrthancException(ErrorCode_InternalError,
-                             "Error during JPEG decoding: " + jerr.GetMessage());
-    }
-
-    // Below this line, we are under the scope of a "setjmp"
-
-    jpeg_create_decompress(&cinfo);
-    jpeg_stdio_src(&cinfo, fp);
-
-    try
-    {
-      Uncompress(cinfo, content_, *this);
-    }
-    catch (OrthancException&)
-    {
-      jpeg_destroy_decompress(&cinfo);
-      fclose(fp);
-      throw;
-    }
-
-    jpeg_destroy_decompress(&cinfo);
-    fclose(fp);
-  }
-#endif
-
-
-  void JpegReader::ReadFromMemory(const void* buffer,
-                                  size_t size)
-  {
-    struct jpeg_decompress_struct cinfo;
-    memset(&cinfo, 0, sizeof(struct jpeg_decompress_struct));
-
-    Internals::JpegErrorManager jerr;
-    cinfo.err = jerr.GetPublic();
-    
-    if (setjmp(jerr.GetJumpBuffer())) 
-    {
-      jpeg_destroy_decompress(&cinfo);
-      throw OrthancException(ErrorCode_InternalError,
-        "Error during JPEG decoding: " + jerr.GetMessage());
-    }
-
-    // Below this line, we are under the scope of a "setjmp"
-    jpeg_create_decompress(&cinfo);
-    jpeg_mem_src(&cinfo, 
-      const_cast<unsigned char*>(
-        reinterpret_cast<const unsigned char*>(buffer)),
-      static_cast<unsigned long>(size));
-
-    try
-    {
-      Uncompress(cinfo, content_, *this);
-    }
-    catch (OrthancException&)
-    {
-      jpeg_destroy_decompress(&cinfo);
-      throw;
-    }
-
-    jpeg_destroy_decompress(&cinfo);
-  }
-
-
-  void JpegReader::ReadFromMemory(const std::string& buffer)
-  {
-    if (buffer.empty())
-    {
-      ReadFromMemory(NULL, 0);
-    }
-    else
-    {
-      ReadFromMemory(buffer.c_str(), buffer.size());
-    }
-  }
-}
--- a/Core/Images/JpegReader.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_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>
-
-namespace Orthanc
-{
-  class JpegReader : public ImageAccessor
-  {
-  private:
-    std::string  content_;
-
-  public:
-#if ORTHANC_SANDBOXED == 0
-    void ReadFromFile(const std::string& filename);
-#endif
-
-    void ReadFromMemory(const void* buffer,
-                        size_t size);
-
-    void ReadFromMemory(const std::string& buffer);
-  };
-}
--- a/Core/Images/JpegWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,213 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "JpegWriter.h"
-
-#include "../OrthancException.h"
-#include "../Logging.h"
-#include "JpegErrorManager.h"
-
-#if ORTHANC_SANDBOXED == 0
-#  include "../SystemToolbox.h"
-#endif
-
-#include <stdlib.h>
-#include <vector>
-
-namespace Orthanc
-{
-  static void GetLines(std::vector<uint8_t*>& lines,
-                       unsigned int height,
-                       unsigned int pitch,
-                       PixelFormat format,
-                       const void* buffer)
-  {
-    if (format != PixelFormat_Grayscale8 &&
-        format != PixelFormat_RGB24)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    lines.resize(height);
-
-    uint8_t* base = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(buffer));
-    for (unsigned int y = 0; y < height; y++)
-    {
-      lines[y] = base + static_cast<intptr_t>(y) * static_cast<intptr_t>(pitch);
-    }
-  }
-
-
-  static void Compress(struct jpeg_compress_struct& cinfo,
-                       std::vector<uint8_t*>& lines,
-                       unsigned int width,
-                       unsigned int height,
-                       PixelFormat format,
-                       uint8_t quality)
-  {
-    cinfo.image_width = width;
-    cinfo.image_height = height;
-
-    switch (format)
-    {
-      case PixelFormat_Grayscale8:
-        cinfo.input_components = 1;
-        cinfo.in_color_space = JCS_GRAYSCALE;
-        break;
-
-      case PixelFormat_RGB24:
-        cinfo.input_components = 3;
-        cinfo.in_color_space = JCS_RGB;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    jpeg_set_defaults(&cinfo);
-
-    // 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);
-  }
-                       
-
-  void JpegWriter::SetQuality(uint8_t quality)
-  {
-    if (quality == 0 || quality > 100)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    quality_ = quality;
-  }
-
-
-#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 = SystemToolbox::OpenFile(filename, FileMode_WriteBinary);
-    if (fp == NULL)
-    {
-      throw OrthancException(ErrorCode_CannotWriteFile);
-    }
-
-    std::vector<uint8_t*> lines;
-    GetLines(lines, height, pitch, format, buffer);
-
-    struct jpeg_compress_struct cinfo;
-    memset(&cinfo, 0, sizeof(struct jpeg_compress_struct));
-
-    Internals::JpegErrorManager jerr;
-    cinfo.err = jerr.GetPublic();
-
-    if (setjmp(jerr.GetJumpBuffer())) 
-    {
-      /* If we get here, the JPEG code has signaled an error.
-       * We need to clean up the JPEG object, close the input file, and return.
-       */
-      jpeg_destroy_compress(&cinfo);
-      fclose(fp);
-      throw OrthancException(ErrorCode_InternalError,
-                             "Error during JPEG encoding: " + jerr.GetMessage());
-    }
-
-    // Do not allocate data on the stack below this line!
-
-    jpeg_create_compress(&cinfo);
-    jpeg_stdio_dest(&cinfo, fp);
-    Compress(cinfo, lines, width, height, format, quality_);
-
-    // Everything went fine, "setjmp()" didn't get called
-
-    fclose(fp);
-  }
-#endif
-
-
-  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);
-
-    struct jpeg_compress_struct cinfo;
-    memset(&cinfo, 0, sizeof(struct jpeg_compress_struct));
-
-    Internals::JpegErrorManager jerr;
-
-    unsigned char* data = NULL;
-    unsigned long size;
-
-    if (setjmp(jerr.GetJumpBuffer())) 
-    {
-      jpeg_destroy_compress(&cinfo);
-
-      if (data != NULL)
-      {
-        free(data);
-      }
-
-      throw OrthancException(ErrorCode_InternalError,
-                             "Error during JPEG encoding: " + jerr.GetMessage());
-    }
-
-    // Do not allocate data on the stack below this line!
-
-    jpeg_create_compress(&cinfo);
-    cinfo.err = jerr.GetPublic();
-    jpeg_mem_dest(&cinfo, &data, &size);
-
-    Compress(cinfo, lines, width, height, format, quality_);
-
-    // Everything went fine, "setjmp()" didn't get called
-
-    jpeg.assign(reinterpret_cast<const char*>(data), size);
-    free(data);
-  }
-}
--- a/Core/Images/JpegWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 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 "IImageWriter.h"
-
-namespace Orthanc
-{
-  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_;
-
-  public:
-    JpegWriter() : quality_(90)
-    {
-    }
-
-    void SetQuality(uint8_t quality);
-
-    uint8_t GetQuality() const
-    {
-      return quality_;
-    }
-  };
-}
--- a/Core/Images/PamReader.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,309 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <stdlib.h>  // For malloc/free
-#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;
-
-    {
-      intptr_t bufferAddr = reinterpret_cast<intptr_t>(&content_[offset]);
-      if((bufferAddr % 8) == 0)
-        LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr;
-      else
-        LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr << " (not a multiple of 8!)";
-    }
-    
-    // if we want to enforce alignment, we need to use a freshly allocated
-    // buffer, since we have no alignment guarantees on the original one
-    if (enforceAligned_)
-    {
-      if (alignedImageBuffer_ != NULL)
-        free(alignedImageBuffer_);
-      alignedImageBuffer_ = malloc(pitch * height);
-      memcpy(alignedImageBuffer_, &content_[offset], pitch* height);
-      content_ = "";
-      AssignWritable(format, width, height, pitch, alignedImageBuffer_);
-    }
-    else
-    {
-      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)
-        {
-#if ORTHANC_ENABLE_WASM == 1
-          /* 
-          
-          crash (2019-08-05):
-
-          Uncaught abort(alignment fault) at Error
-            at jsStackTrace
-            at stackTrace
-            at abort
-            at alignfault
-            at SAFE_HEAP_LOAD_i32_2_2 (wasm-function[251132]:39)
-            at __ZN7Orthanc9PamReader12ParseContentEv (wasm-function[11457]:8088)
-
-          Web Assembly IS LITTLE ENDIAN!
-
-          Perhaps in htobe16 ?
-          */
-          uint8_t* srcdst = reinterpret_cast<uint8_t*>(pixel);
-          uint8_t tmp = srcdst[0];
-          srcdst[0] = srcdst[1];
-          srcdst[1] = tmp;
-#else
-          // 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));
-#endif
-        }
-      }
-    }
-  }
-
-  
-#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();
-  }
-
-  void PamReader::ReadFromMemory(const void* buffer,
-                                 size_t size)
-  {
-    content_.assign(reinterpret_cast<const char*>(buffer), size);
-    ParseContent();
-  }
-
-  PamReader::~PamReader()
-  {
-    if (alignedImageBuffer_ != NULL)
-    {
-      free(alignedImageBuffer_);
-    }
-  }
-}
--- a/Core/Images/PamReader.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,94 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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();
-    
-    /**
-    Whether we want to use the default malloc alignment in the image buffer,
-    at the expense of an extra copy
-    */
-    bool enforceAligned_;
-
-    /**
-    This is actually a copy of wrappedContent_, but properly aligned.
-
-    It is only used if the enforceAligned parameter is set to true in the
-    constructor.
-    */
-    void* alignedImageBuffer_;
-    
-    /**
-    Points somewhere in the content_ buffer.      
-    */
-    ImageAccessor wrappedContent_;
-
-    /**
-    Raw content (file bytes or answer from the server, for instance). 
-    */
-    std::string content_;
-
-  public:
-    /**
-    See doc for field enforceAligned_
-    */
-    PamReader(bool enforceAligned = false) :
-      enforceAligned_(enforceAligned),
-      alignedImageBuffer_(NULL)
-    {
-    }
-
-    virtual ~PamReader();
-
-#if ORTHANC_SANDBOXED == 0
-    void ReadFromFile(const std::string& filename);
-#endif
-
-    void ReadFromMemory(const std::string& buffer);
-
-    void ReadFromMemory(const void* buffer,
-                        size_t size);
-  };
-}
--- a/Core/Images/PamWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_SignedGrayscale16:
-      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);
-      }
-    }
-  }
-}
--- a/Core/Images/PamWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Core/Images/PixelTraits.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,412 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <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()))
-      {
-        return std::numeric_limits<PixelType>::min();
-      }
-      else if (value > static_cast<int64_t>(std::numeric_limits<PixelType>::max()))
-      {
-        return std::numeric_limits<PixelType>::max();        
-      }
-      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)
-    {
-      value += 0.5f;
-      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 SetMinValue(PixelType& target)
-    {
-      // std::numeric_limits<float>::lowest is not supported on
-      // all compilers (for instance, Visual Studio 9.0 2008)
-      target = -std::numeric_limits<float>::max();
-    }
-
-    ORTHANC_FORCE_INLINE
-    static void SetMaxValue(PixelType& target)
-    {
-      target = std::numeric_limits<float>::max();
-    }
-
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,325 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PngReader.h"
-
-#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
-    {
-      FILE* fp_;
-
-      FileRabi(const char* filename)
-      {
-        fp_ = SystemToolbox::OpenFile(filename, FileMode_ReadBinary);
-        if (!fp_)
-        {
-          throw OrthancException(ErrorCode_InexistentFile);
-        }
-      }
-
-      ~FileRabi()
-      {
-        if (fp_)
-        {
-          fclose(fp_);
-        }
-      }
-    };
-  }
-#endif
-
-
-  struct PngReader::PngRabi
-  {
-    png_structp png_;
-    png_infop info_;
-    png_infop endInfo_;
-
-    void Destruct()
-    {
-      if (png_)
-      {
-        png_destroy_read_struct(&png_, &info_, &endInfo_);
-
-        png_ = NULL;
-        info_ = NULL;
-        endInfo_ = NULL;
-      }
-    }
-
-    PngRabi()
-    {
-      png_ = NULL;
-      info_ = NULL;
-      endInfo_ = NULL;
-
-      png_ = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
-      if (!png_)
-      {
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-
-      info_ = png_create_info_struct(png_);
-      if (!info_)
-      {
-        png_destroy_read_struct(&png_, NULL, NULL);
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-
-      endInfo_ = png_create_info_struct(png_);
-      if (!info_)
-      {
-        png_destroy_read_struct(&png_, &info_, NULL);
-        throw OrthancException(ErrorCode_NotEnoughMemory);
-      }
-    }
-
-    ~PngRabi()
-    {
-      Destruct();
-    }
-
-    static void MemoryCallback(png_structp png_ptr, 
-                               png_bytep data, 
-                               png_size_t size);
-  };
-
-
-  void PngReader::CheckHeader(const void* header)
-  {
-    int is_png = !png_sig_cmp((png_bytep) header, 0, 8);
-    if (!is_png)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-  PngReader::PngReader()
-  {
-  }
-
-  void PngReader::Read(PngRabi& rabi)
-  {
-    png_set_sig_bytes(rabi.png_, 8);
-
-    png_read_info(rabi.png_, rabi.info_);
-
-    png_uint_32 width, height;
-    int bit_depth, color_type, interlace_type;
-    int compression_type, filter_method;
-    // get size and bit-depth of the PNG-image
-    png_get_IHDR(rabi.png_, rabi.info_,
-                 &width, &height,
-                 &bit_depth, &color_type, &interlace_type,
-                 &compression_type, &filter_method);
-
-    PixelFormat format;
-    unsigned int pitch;
-
-    if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 8)
-    {
-      format = PixelFormat_Grayscale8;
-      pitch = width;
-    }
-    else if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 16)
-    {
-      format = PixelFormat_Grayscale16;
-      pitch = 2 * width;
-
-      if (Toolbox::DetectEndianness() == Endianness_Little)
-      {
-        png_set_swap(rabi.png_);
-      }
-    }
-    else if (color_type == PNG_COLOR_TYPE_RGB && bit_depth == 8)
-    {
-      format = PixelFormat_RGB24;
-      pitch = 3 * width;
-    }
-    else if (color_type == PNG_COLOR_TYPE_RGBA && bit_depth == 8)
-    {
-      format = PixelFormat_RGBA32;
-      pitch = 4 * width;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    data_.resize(height * pitch);
-
-    if (height == 0 || width == 0)
-    {
-      // Empty image, we are done
-      AssignEmpty(format);
-      return;
-    }
-    
-    png_read_update_info(rabi.png_, rabi.info_);
-
-    std::vector<png_bytep> rows(height);
-    for (size_t i = 0; i < height; i++)
-    {
-      rows[i] = &data_[0] + i * pitch;
-    }
-
-    png_read_image(rabi.png_, &rows[0]);
-
-    AssignWritable(format, width, height, pitch, &data_[0]);
-  }
-
-
-#if ORTHANC_SANDBOXED == 0
-  void PngReader::ReadFromFile(const std::string& filename)
-  {
-    FileRabi f(filename.c_str());
-
-    char header[8];
-    if (fread(header, 1, 8, f.fp_) != 8)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    CheckHeader(header);
-
-    PngRabi rabi;
-
-    if (setjmp(png_jmpbuf(rabi.png_)))
-    {
-      rabi.Destruct();
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    png_init_io(rabi.png_, f.fp_);
-
-    Read(rabi);
-  }
-#endif
-
-
-  namespace
-  {
-    struct MemoryBuffer
-    {
-      const uint8_t* buffer_;
-      size_t size_;
-      size_t pos_;
-      bool ok_;
-    };
-  }
-
-
-  void PngReader::PngRabi::MemoryCallback(png_structp png_ptr, 
-                                          png_bytep outBytes, 
-                                          png_size_t byteCountToRead)
-  {
-    MemoryBuffer* from = reinterpret_cast<MemoryBuffer*>(png_get_io_ptr(png_ptr));
-
-    if (!from->ok_)
-    {
-      return;
-    }
-
-    if (from->pos_ + byteCountToRead > from->size_)
-    {
-      from->ok_ = false;
-      return;
-    }
-
-    memcpy(outBytes, from->buffer_ + from->pos_, byteCountToRead);
-
-    from->pos_ += byteCountToRead;
-  }
-
-
-  void PngReader::ReadFromMemory(const void* buffer,
-                                 size_t size)
-  {
-    if (size < 8)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    CheckHeader(buffer);
-
-    PngRabi rabi;
-
-    if (setjmp(png_jmpbuf(rabi.png_)))
-    {
-      rabi.Destruct();
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    MemoryBuffer tmp;
-    tmp.buffer_ = reinterpret_cast<const uint8_t*>(buffer) + 8;  // We skip the header
-    tmp.size_ = size - 8;
-    tmp.pos_ = 0;
-    tmp.ok_ = true;
-
-    png_set_read_fn(rabi.png_, &tmp, PngRabi::MemoryCallback);
-
-    Read(rabi);
-
-    if (!tmp.ok_)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-  void PngReader::ReadFromMemory(const std::string& buffer)
-  {
-    if (buffer.size() != 0)
-    {
-      ReadFromMemory(&buffer[0], buffer.size());
-    }
-    else
-    {
-      ReadFromMemory(NULL, 0);
-    }
-  }
-}
--- a/Core/Images/PngReader.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_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"
-
-#include <vector>
-#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
-  {
-  private:
-    struct PngRabi;
-
-    std::vector<uint8_t> data_;
-
-    void CheckHeader(const void* header);
-
-    void Read(PngRabi& rabi);
-
-  public:
-    PngReader();
-
-#if ORTHANC_SANDBOXED == 0
-    void ReadFromFile(const std::string& filename);
-#endif
-
-    void ReadFromMemory(const void* buffer,
-                        size_t size);
-
-    void ReadFromMemory(const std::string& buffer);
-  };
-}
--- a/Core/Images/PngWriter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,274 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PngWriter.h"
-
-#include <vector>
-#include <stdint.h>
-#include <png.h>
-#include "../OrthancException.h"
-#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
-/*
-  void write_row_callback(png_ptr, png_uint_32 row, int pass)
-  {
-  }*/
-
-
-
-
-/*  bool isError_;
-
-// http://www.libpng.org/pub/png/book/chapter14.html#png.ch14.div.2
-
-static void ErrorHandler(png_structp png, png_const_charp message)
-{
-printf("** [%s]\n", message);
-
-PngWriter* that = (PngWriter*) png_get_error_ptr(png);
-that->isError_ = true;
-printf("** %d\n", (int)that);
-
-//((PngWriter*) payload)->isError_ = true;
-}
-
-static void WarningHandler(png_structp png, png_const_charp message)
-{
-  printf("++ %d\n", (int)message);
-}*/
-
-
-namespace Orthanc
-{
-  struct PngWriter::PImpl
-  {
-    png_structp png_;
-    png_infop info_;
-
-    // Filled by Prepare()
-    std::vector<uint8_t*> rows_;
-    int bitDepth_;
-    int colorType_;
-  };
-
-
-
-  PngWriter::PngWriter() : pimpl_(new PImpl)
-  {
-    pimpl_->png_ = NULL;
-    pimpl_->info_ = NULL;
-
-    pimpl_->png_ = png_create_write_struct
-      (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); //this, ErrorHandler, WarningHandler);
-    if (!pimpl_->png_)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-
-    pimpl_->info_ = png_create_info_struct(pimpl_->png_);
-    if (!pimpl_->info_)
-    {
-      png_destroy_write_struct(&pimpl_->png_, NULL);
-      throw OrthancException(ErrorCode_NotEnoughMemory);
-    }
-  }
-
-  PngWriter::~PngWriter()
-  {
-    if (pimpl_->info_)
-    {
-      png_destroy_info_struct(pimpl_->png_, &pimpl_->info_);
-    }
-
-    if (pimpl_->png_)
-    {
-      png_destroy_write_struct(&pimpl_->png_, NULL);
-    }
-  }
-
-
-
-  void PngWriter::Prepare(unsigned int width,
-                          unsigned int height,
-                          unsigned int pitch,
-                          PixelFormat format,
-                          const void* buffer)
-  {
-    pimpl_->rows_.resize(height);
-    for (unsigned int y = 0; y < height; y++)
-    {
-      pimpl_->rows_[y] = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(buffer)) + y * pitch;
-    }
-
-    switch (format)
-    {
-    case PixelFormat_RGB24:
-      pimpl_->bitDepth_ = 8;
-      pimpl_->colorType_ = PNG_COLOR_TYPE_RGB;
-      break;
-
-    case PixelFormat_RGBA32:
-      pimpl_->bitDepth_ = 8;
-      pimpl_->colorType_ = PNG_COLOR_TYPE_RGBA;
-      break;
-
-    case PixelFormat_Grayscale8:
-      pimpl_->bitDepth_ = 8;
-      pimpl_->colorType_ = PNG_COLOR_TYPE_GRAY;
-      break;
-
-    case PixelFormat_Grayscale16:
-    case PixelFormat_SignedGrayscale16:
-      pimpl_->bitDepth_ = 16;
-      pimpl_->colorType_ = PNG_COLOR_TYPE_GRAY;
-      break;
-
-    default:
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void PngWriter::Compress(unsigned int width,
-                           unsigned int height,
-                           unsigned int pitch,
-                           PixelFormat format)
-  {
-    png_set_IHDR(pimpl_->png_, pimpl_->info_, width, height,
-                 pimpl_->bitDepth_, pimpl_->colorType_, PNG_INTERLACE_NONE,
-                 PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
-
-    png_write_info(pimpl_->png_, pimpl_->info_);
-
-    if (height > 0)
-    {
-      switch (format)
-      {
-      case PixelFormat_Grayscale16:
-      case PixelFormat_SignedGrayscale16:
-      {
-        int transforms = 0;
-        if (Toolbox::DetectEndianness() == Endianness_Little)
-        {
-          transforms = PNG_TRANSFORM_SWAP_ENDIAN;
-        }
-
-        png_set_rows(pimpl_->png_, pimpl_->info_, &pimpl_->rows_[0]);
-        png_write_png(pimpl_->png_, pimpl_->info_, transforms, NULL);
-
-        break;
-      }
-
-      default:
-        png_write_image(pimpl_->png_, &pimpl_->rows_[0]);
-      }
-    }
-
-    png_write_end(pimpl_->png_, NULL);
-  }
-
-
-#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 = SystemToolbox::OpenFile(filename, FileMode_WriteBinary);
-    if (!fp)
-    {
-      throw OrthancException(ErrorCode_CannotWriteFile);
-    }    
-
-    png_init_io(pimpl_->png_, fp);
-
-    if (setjmp(png_jmpbuf(pimpl_->png_)))
-    {
-      // Error during writing PNG
-      throw OrthancException(ErrorCode_CannotWriteFile);      
-    }
-
-    Compress(width, height, pitch, format);
-
-    fclose(fp);
-  }
-#endif
-
-
-  static void MemoryCallback(png_structp png_ptr, 
-                             png_bytep data, 
-                             png_size_t size)
-  {
-    ChunkedBuffer* buffer = reinterpret_cast<ChunkedBuffer*>(png_get_io_ptr(png_ptr));
-    buffer->AddChunk(data, size);
-  }
-
-
-
-  void PngWriter::WriteToMemoryInternal(std::string& png,
-                                        unsigned int width,
-                                        unsigned int height,
-                                        unsigned int pitch,
-                                        PixelFormat format,
-                                        const void* buffer)
-  {
-    ChunkedBuffer chunks;
-
-    Prepare(width, height, pitch, format, buffer);
-
-    if (setjmp(png_jmpbuf(pimpl_->png_)))
-    {
-      // Error during writing PNG
-      throw OrthancException(ErrorCode_InternalError);      
-    }
-
-    png_set_write_fn(pimpl_->png_, &chunks, MemoryCallback, NULL);
-
-    Compress(width, height, pitch, format);
-
-    chunks.Flatten(png);
-  }
-}
--- a/Core/Images/PngWriter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_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>
-
-namespace Orthanc
-{
-  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_;
-
-    void Compress(unsigned int width,
-                  unsigned int height,
-                  unsigned int pitch,
-                  PixelFormat format);
-
-    void Prepare(unsigned int width,
-                 unsigned int height,
-                 unsigned int pitch,
-                 PixelFormat format,
-                 const void* buffer);
-
-  public:
-    PngWriter();
-
-    ~PngWriter();
-  };
-}
--- a/Core/JobsEngine/GenericJobUnserializer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Cannot unserialize job of type: " + type);
-    }
-  }
-
-
-  IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source)
-  {
-    const std::string type = SerializationToolbox::ReadString(source, "Type");
-
-    if (type == "Log")
-    {
-      return new LogJobOperation;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Cannot unserialize operation of type: " + type);
-    }
-  }
-
-
-  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
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Cannot unserialize value of type: " + type);
-    }
-  }
-}
-
--- a/Core/JobsEngine/GenericJobUnserializer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Core/JobsEngine/IJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(const std::string& jobId) = 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;
-
-    // This function can only be called if the job has reached its
-    // "success" state
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) = 0;
-  };
-}
--- a/Core/JobsEngine/IJobUnserializer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/JobsEngine/JobInfo.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,164 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#ifdef __EMSCRIPTEN__
-/* 
-Avoid this error:
-
-.../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion]
-.../boost/math/special_functions/round.hpp:125:11: note: in instantiation of function template specialization 'boost::math::llround<float, boost::math::policies::policy<boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy> >' requested here
-.../orthanc/Core/JobsEngine/JobInfo.cpp:69:44: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here
-
-.../boost/math/special_functions/round.hpp:86:12: warning: implicit conversion from 'std::__2::numeric_limits<int>::type' (aka 'int') to 'float' changes value from 2147483647 to 2147483648 [-Wimplicit-int-float-conversion]
-.../boost/math/special_functions/round.hpp:93:11: note: in instantiation of function template specialization 'boost::math::iround<float, boost::math::policies::policy<boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy> >' requested here
-.../orthanc/Core/JobsEngine/JobInfo.cpp:133:39: note: in instantiation of function template specialization 'boost::math::iround<float>' requested here
-*/
-#pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion"
-#endif 
-
-#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());
-    }
-  }
-}
--- a/Core/JobsEngine/JobInfo.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/JobsEngine/JobStatus.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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,
-                       const std::string& details,
-                       IJob& job) :
-    errorCode_(code),
-    progress_(job.GetProgress()),
-    publicContent_(Json::objectValue),
-    details_(details)
-  {
-    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_;
-    }
-  }
-}
--- a/Core/JobsEngine/JobStatus.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-    std::string    details_;
-
-  public:
-    JobStatus();
-
-    JobStatus(ErrorCode code,
-              const std::string& details,
-              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_;
-    }
-
-    const std::string& GetDetails() const
-    {
-      return details_;
-    }
-  };
-}
--- a/Core/JobsEngine/JobStepResult.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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,
-                                       const char* details)
-  {
-    JobStepResult result(JobStepCode_Failure);
-    result.error_ = error;
-
-    if (details != NULL)
-    {
-      result.failureDetails_ = details;
-    }
-    
-    return result;
-  }
-
-
-  JobStepResult JobStepResult::Failure(const OrthancException& exception)
-  {
-    return Failure(exception.GetErrorCode(),
-                   exception.HasDetails() ? exception.GetDetails() : NULL);
-  }
-  
-
-  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);
-    }
-  }
-
-
-  const std::string& JobStepResult::GetFailureDetails() const
-  {
-    if (code_ == JobStepCode_Failure)
-    {
-      return failureDetails_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-}
--- a/Core/JobsEngine/JobStepResult.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 OrthancException;
-  
-  class JobStepResult
-  {
-  private:
-    JobStepCode   code_;
-    unsigned int  timeout_;
-    ErrorCode     error_;
-    std::string   failureDetails_;
-    
-    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,
-                                 const char* details);
-
-    static JobStepResult Failure(const OrthancException& exception);
-
-    JobStepCode GetCode() const
-    {
-      return code_;
-    }
-
-    unsigned int GetRetryTimeout() const;
-
-    ErrorCode GetFailureCode() const;
-
-    const std::string& GetFailureDetails() const;
-  };
-}
--- a/Core/JobsEngine/JobsEngine.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,326 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(running.GetId());
-    }
-    catch (OrthancException& e)
-    {
-      result = JobStepResult::Failure(e);
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      result = JobStepResult::Failure(ErrorCode_BadFileFormat, NULL);
-    }
-    catch (...)
-    {
-      result = JobStepResult::Failure(ErrorCode_InternalError, NULL);
-    }
-
-    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(), result.GetFailureDetails());
-        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(size_t maxCompletedJobs) :
-    state_(State_Setup),
-    registry_(new JobsRegistry(maxCompletedJobs)),
-    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);
-    }
-
-    assert(registry_.get() != NULL);
-    const size_t maxCompletedJobs = registry_->GetMaxCompletedJobs();
-    registry_.reset(new JobsRegistry(unserializer, serialized, maxCompletedJobs));
-  }
-
-
-  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";
-  }
-}
--- a/Core/JobsEngine/JobsEngine.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Compatibility.h"
-
-#include <boost/thread.hpp>
-
-namespace Orthanc
-{
-  class JobsEngine : public boost::noncopyable
-  {
-  private:
-    enum State
-    {
-      State_Setup,
-      State_Running,
-      State_Stopping,
-      State_Done
-    };
-
-    boost::mutex                 stateMutex_;
-    State                        state_;
-    std::unique_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(size_t maxCompletedJobs);
-
-    ~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();
-  };
-}
--- a/Core/JobsEngine/JobsRegistry.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1474 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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* 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::unique_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
-      {
-        VLOG(1) << "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()
-  {
-    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();
-    }
-
-    CheckInvariants();
-  }
-
-
-  void JobsRegistry::SetCompletedJob(JobHandler& job,
-                                     bool success)
-  {
-    job.SetState(success ? JobState_Success : JobState_Failure);
-
-    completedJobs_.push_back(&job);
-    someJobComplete_.notify_all();
-  }
-
-
-  void JobsRegistry::MarkRunningAsCompleted(JobHandler& job,
-                                            CompletedReason reason)
-  {
-    const char* tmp;
-
-    switch (reason)
-    {
-      case CompletedReason_Success:
-        tmp = "success";
-        break;
-
-      case CompletedReason_Failure:
-        tmp = "failure";
-        break;
-
-      case CompletedReason_Canceled:
-        tmp = "cancel";
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    LOG(INFO) << "Job has completed with " << tmp << ": " << job.GetId();
-
-    CheckInvariants();
-
-    assert(job.GetState() == JobState_Running);
-    SetCompletedJob(job, reason == CompletedReason_Success);
-
-    if (reason == CompletedReason_Canceled)
-    {
-      job.SetLastErrorCode(ErrorCode_CanceledJob);
-    }
-
-    if (observer_ != NULL)
-    {
-      if (reason == CompletedReason_Success)
-      {
-        observer_->SignalJobSuccess(job.GetId());
-      }
-      else
-      {
-        observer_->SignalJobFailure(job.GetId());
-      }
-    }
-
-    // WARNING: The following call might make "job" invalid if the job
-    // history size is empty
-    ForgetOldCompletedJobs();
-  }
-
-
-  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();
-  }
-
-
-  size_t JobsRegistry::GetMaxCompletedJobs()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    CheckInvariants();
-    return maxCompletedJobs_;
-  }
-
-
-  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;
-    }
-  }
-
-
-  bool JobsRegistry::GetJobOutput(std::string& output,
-                                  MimeType& mime,
-                                  const std::string& job,
-                                  const std::string& key)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    CheckInvariants();
-
-    JobsIndex::const_iterator found = jobsIndex_.find(job);
-
-    if (found == jobsIndex_.end())
-    {
-      return false;
-    }
-    else
-    {
-      const JobHandler& handler = *found->second;
-
-      if (handler.GetState() == JobState_Success)
-      {
-        return handler.GetJob().GetOutput(output, mime, key);
-      }
-      else
-      {
-        return false;
-      }
-    }
-  }
-
-
-  void JobsRegistry::SubmitInternal(std::string& id,
-                                    JobHandler* handler)
-  {
-    if (handler == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    std::unique_ptr<JobHandler>  protection(handler);
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      CheckInvariants();
-
-      id = handler->GetId();
-      int priority = handler->GetPriority();
-
-      jobsIndex_.insert(std::make_pair(id, protection.release()));
-
-      switch (handler->GetState())
-      {
-        case JobState_Pending:
-        case JobState_Retry:
-        case JobState_Running:
-          handler->SetState(JobState_Pending);
-          pendingJobs_.push(handler);
-          pendingJobAvailable_.notify_one();
-          break;
-
-        case JobState_Success:
-          SetCompletedJob(*handler, true);
-          break;
-
-        case JobState_Failure:
-          SetCompletedJob(*handler, false);
-          break;
-
-        case JobState_Paused:
-          break;
-
-        default:
-        {
-          std::string details = ("A job should not be loaded from state: " +
-                                 std::string(EnumerationToString(handler->GetState())));
-          throw OrthancException(ErrorCode_InternalError, details);
-        }
-      }
-
-      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
-
-      if (observer_ != NULL)
-      {
-        observer_->SignalJobSubmitted(id);
-      }
-
-      // WARNING: The following call might make "handler" invalid if
-      // the job history size is empty
-      ForgetOldCompletedJobs();
-    }
-  }
-
-
-  void JobsRegistry::Submit(std::string& id,
-                            IJob* job,        // Takes ownership
-                            int priority)
-  {
-    SubmitInternal(id, new JobHandler(job, priority));
-  }
-
-
-  void JobsRegistry::Submit(IJob* job,        // Takes ownership
-                            int priority)
-  {
-    std::string id;
-    SubmitInternal(id, new JobHandler(job, priority));
-  }
-
-
-  void 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 (typically happens if
-          // "JobsHistorySize" is 0)
-          throw OrthancException(ErrorCode_InexistentItem,
-                                 "Cannot retrieve the status of the job, "
-                                 "make sure that \"JobsHistorySize\" is not 0");
-        }
-        else if (state == JobState_Failure)
-        {
-          // Failure
-          JobsIndex::const_iterator it = jobsIndex_.find(id);
-          if (it != jobsIndex_.end())  // Should always be true, already tested in GetStateInternal()
-          {
-            ErrorCode code = it->second->GetLastStatus().GetErrorCode();
-            const std::string& details = it->second->GetLastStatus().GetDetails();
-
-            if (details.empty())
-            {
-              throw OrthancException(code);
-            }
-            else
-            {
-              throw OrthancException(code, details);
-            }
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        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();
-          }
-
-          return;
-        }
-        else
-        {
-          // This job has not finished yet, wait for new completion
-          someJobComplete_.wait(lock);
-        }
-      }
-    }
-  }
-
-
-  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);
-      }
-
-      // WARNING: The following call might make "handler" invalid if
-      // the job history size is empty
-      ForgetOldCompletedJobs();
-
-      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_, canceled_ ? CompletedReason_Canceled : CompletedReason_Failure);
-          break;
-
-        case JobState_Success:
-          registry_.MarkRunningAsCompleted(*handler_, CompletedReason_Success);
-          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,
-                                              const std::string& details)
-  {
-    if (!IsValid())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      JobStatus status(code, details, *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[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,
-                             size_t maxCompletedJobs) :
-    maxCompletedJobs_(maxCompletedJobs),
-    observer_(NULL)
-  {
-    if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY ||
-        !s.isMember(JOBS) ||
-        s[JOBS].type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    Json::Value::Members members = s[JOBS].getMemberNames();
-
-    for (Json::Value::Members::const_iterator it = members.begin();
-         it != members.end(); ++it)
-    {
-      std::unique_ptr<JobHandler> job;
-
-      try
-      {
-        job.reset(new JobHandler(unserializer, s[JOBS][*it], *it));
-      }
-      catch (OrthancException& e)
-      {
-        LOG(WARNING) << "Cannot unserialize one job from previous execution, "
-                     << "skipping it: " << e.What();
-        continue;
-      }
-
-      const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime();
-
-      std::string id;
-      SubmitInternal(id, job.release());
-
-      // Check whether the job has not been removed (which could be
-      // the case if the "maxCompletedJobs_" value gets smaller)
-      JobsIndex::iterator found = jobsIndex_.find(id);
-      if (found != jobsIndex_.end())
-      {
-        // The job still lies in the history: Update the time of its
-        // last change to the time that was serialized
-        assert(found->second != NULL);
-        found->second->SetLastStateChangeTime(lastChangeTime);
-      }
-    }
-  }
-
-
-  void JobsRegistry::GetStatistics(unsigned int& pending,
-                                   unsigned int& running,
-                                   unsigned int& success,
-                                   unsigned int& failed)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    CheckInvariants();
-
-    pending = 0;
-    running = 0;
-    success = 0;
-    failed = 0;
-
-    for (JobsIndex::const_iterator it = jobsIndex_.begin();
-         it != jobsIndex_.end(); ++it)
-    {
-      JobHandler& job = *it->second;
-
-      switch (job.GetState())
-      {
-        case JobState_Retry:
-        case JobState_Pending:
-          pending ++;
-          break;
-
-        case JobState_Paused:
-        case JobState_Running:
-          running ++;
-          break;
-
-        case JobState_Success:
-          success ++;
-          break;
-
-        case JobState_Failure:
-          failed ++;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  }
-}
--- a/Core/JobsEngine/JobsRegistry.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,255 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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:
-    enum CompletedReason
-    {
-      CompletedReason_Success,
-      CompletedReason_Failure,
-      CompletedReason_Canceled
-    };
-
-    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,
-                                CompletedReason reason);
-
-    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);
-
-  public:
-    JobsRegistry(size_t maxCompletedJobs) :
-      maxCompletedJobs_(maxCompletedJobs),
-      observer_(NULL)
-    {
-    }
-
-    JobsRegistry(IJobUnserializer& unserializer,
-                 const Json::Value& s,
-                 size_t maxCompletedJobs);
-
-    ~JobsRegistry();
-
-    void SetMaxCompletedJobs(size_t i);
-
-    size_t GetMaxCompletedJobs();
-
-    void ListJobs(std::set<std::string>& target);
-
-    bool GetJobInfo(JobInfo& target,
-                    const std::string& id);
-
-    bool GetJobOutput(std::string& output,
-                      MimeType& mime,
-                      const std::string& job,
-                      const std::string& key);
-
-    void Serialize(Json::Value& target);
-
-    void Submit(std::string& id,
-                IJob* job,        // Takes ownership
-                int priority);
-
-    void Submit(IJob* job,        // Takes ownership
-                int priority);
-
-    void 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();
-
-    void GetStatistics(unsigned int& pending,
-                       unsigned int& running,
-                       unsigned int& success,
-                       unsigned int& errors);
-
-    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,
-                        const std::string& details);
-    };
-  };
-}
--- a/Core/JobsEngine/Operations/IJobOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-namespace Orthanc
-{
-  class IJobOperation : public boost::noncopyable
-  {
-  public:
-    virtual ~IJobOperation()
-    {
-    }
-
-    virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input) = 0;
-
-    virtual void Serialize(Json::Value& result) const = 0;
-  };
-}
--- a/Core/JobsEngine/Operations/JobOperationValue.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Core/JobsEngine/Operations/JobOperationValues.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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::unique_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();
-  }
-}
--- a/Core/JobsEngine/Operations/JobOperationValues.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Core/JobsEngine/Operations/LogJobOperation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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)
-  {
-    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());
-  }
-}
--- a/Core/JobsEngine/Operations/LogJobOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-
-    virtual void Serialize(Json::Value& result) const
-    {
-      result = Json::objectValue;
-      result["Type"] = "Log";
-    }
-  };
-}
--- a/Core/JobsEngine/Operations/NullOperationValue.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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";
-    }
-  };
-}
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,475 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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* 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::unique_ptr<IJobOperation>       operation_;
-    std::unique_ptr<JobOperationValues>  originalInputs_;
-    std::unique_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()
-    {
-      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);
-
-      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);
-  }
-
-  
-  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(const std::string& jobId)
-  {
-    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);
-        }
-
-        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();
-    }
-
-    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();
-    }
-  }
-
-
-  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[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));
-    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);
-        }
-      }
-    }  
-  }
-}
--- a/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <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_;
-
-    void NotifyDone() const;
-
-  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);
-      
-      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() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
-
-    virtual void Reset() ORTHANC_OVERRIDE;
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual float GetProgress() ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
-    {
-      target = "SequenceOfOperations";
-    }
-
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
-    void AwakeTrailingSleep()
-    {
-      operationAdded_.notify_one();
-    }
-  };
-}
--- a/Core/JobsEngine/Operations/StringOperationValue.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-    }
-  };
-}
--- a/Core/JobsEngine/SetOfCommandsJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,303 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(const std::string& jobId)
-  {
-    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(jobId))
-      {
-        // Error
-        if (!permissive_)
-        {
-          return JobStepResult::Failure(ErrorCode_InternalError, NULL);
-        }
-      }
-    }
-    catch (OrthancException& e)
-    {
-      if (permissive_)
-      {
-        LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What();
-      }
-      else
-      {
-        return JobStepResult::Failure(e);
-      }
-    }
-
-    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::unique_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);
-    }
-  }
-}
--- a/Core/JobsEngine/SetOfCommandsJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,142 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(const std::string& jobId) = 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() ORTHANC_OVERRIDE;
-    
-    virtual void Start() ORTHANC_OVERRIDE
-    {
-      started_ = true;
-    }
-    
-    virtual float GetProgress() ORTHANC_OVERRIDE;
-
-    bool IsStarted() const
-    {
-      return started_;
-    }
-
-    const ICommand& GetCommand(size_t index) const;
-      
-    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
-    
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
-    
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
-
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-  };
-}
--- a/Core/JobsEngine/SetOfInstancesJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,252 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      if (!that_.HandleInstance(instance_))
-      {
-        that_.failedInstances_.insert(instance_);
-        return false;
-      }
-      else
-      {
-        return true;
-      }
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      target = instance_;
-    }
-  };
-
-
-  class SetOfInstancesJob::TrailingStepCommand : public SetOfInstancesJob::ICommand
-  {
-  private:
-    SetOfInstancesJob& that_;
-
-  public:
-    TrailingStepCommand(SetOfInstancesJob& that) :
-      that_(that)
-    {
-    }       
-      
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      return that_.HandleTrailingStep();
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      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();
-  }
-
-
-  static const char* KEY_TRAILING_STEP = "TrailingStep";
-  static const char* KEY_FAILED_INSTANCES = "FailedInstances";
-  static const char* KEY_PARENT_RESOURCES = "ParentResources";
-
-  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
-  {
-    SetOfCommandsJob::GetPublicContent(target);
-    target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
-    target["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
-
-    if (!parentResources_.empty())
-    {
-      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
-    }
-  }
-
-
-  bool SetOfInstancesJob::Serialize(Json::Value& target) 
-  {
-    if (SetOfCommandsJob::Serialize(target))
-    {
-      target[KEY_TRAILING_STEP] = hasTrailingStep_;
-      SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
-      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
-      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_PARENT_RESOURCES))
-    {
-      // Backward compatibility with Orthanc <= 1.5.6
-      SerializationToolbox::ReadSetOfStrings(parentResources_, source, KEY_PARENT_RESOURCES);
-    }
-    
-    if (source.isMember(KEY_TRAILING_STEP))
-    {
-      hasTrailingStep_ = SerializationToolbox::ReadBoolean(source, KEY_TRAILING_STEP);
-    }
-    else
-    {
-      // Backward compatibility with Orthanc <= 1.4.2
-      hasTrailingStep_ = false;
-    }
-  }
-  
-
-}
--- a/Core/JobsEngine/SetOfInstancesJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-    std::set<std::string>  parentResources_;
-
-  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
-
-    // Only used for reporting in the public content
-    // https://groups.google.com/d/msg/orthanc-users/9GCV88GLEzw/6wAgP_PRAgAJ
-    void AddParentResource(const std::string& resource)
-    {
-      parentResources_.insert(resource);
-    }
-    
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,817 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Logging.h"
-
-#include "OrthancException.h"
-
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    const char* EnumerationToString(LogLevel level)
-    {
-      switch (level)
-      {
-        case LogLevel_ERROR:
-          return "ERROR";
-
-        case LogLevel_WARNING:
-          return "WARNING";
-
-        case LogLevel_INFO:
-          return "INFO";
-
-        case LogLevel_TRACE:
-          return "TRACE";
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    LogLevel StringToLogLevel(const char *level)
-    {
-      if (strcmp(level, "ERROR") == 0)
-      {
-        return LogLevel_ERROR;
-      }
-      else if (strcmp(level, "WARNING") == 0)
-      {
-        return LogLevel_WARNING;
-      }
-      else if (strcmp(level, "INFO") == 0)
-      {
-        return LogLevel_INFO;
-      }
-      else if (strcmp(level, "TRACE") == 0)
-      {
-        return LogLevel_TRACE;
-      }
-      else 
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  }
-}
-
-
-#if ORTHANC_ENABLE_LOGGING != 1
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    void InitializePluginContext(void* pluginContext)
-    {
-    }
-
-    void Initialize()
-    {
-    }
-
-    void Finalize()
-    {
-    }
-
-    void Reset()
-    {
-    }
-
-    void Flush()
-    {
-    }
-
-    void EnableInfoLevel(bool enabled)
-    {
-    }
-
-    void EnableTraceLevel(bool enabled)
-    {
-    }
-    
-    bool IsTraceLevelEnabled()
-    {
-      return false;
-    }
-
-    bool IsInfoLevelEnabled()
-    {
-      return false;
-    }
-    
-    void SetTargetFile(const std::string& path)
-    {
-    }
-
-    void SetTargetFolder(const std::string& path)
-    {
-    }
-  }
-}
-
-
-#elif ORTHANC_ENABLE_LOGGING_STDIO == 1
-
-/*********************************************************
- * Logger compatible with <stdio.h> OR logger that sends its
- * output to the emscripten html5 api (depending on the 
- * definition of __EMSCRIPTEN__)
- *********************************************************/
-
-#include <stdio.h>
-
-#ifdef __EMSCRIPTEN__
-#  include <emscripten/html5.h>
-#endif
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    static bool infoEnabled_ = false;
-    static bool traceEnabled_ = false;
-    
-#ifdef __EMSCRIPTEN__
-    static void ErrorLogFunc(const char* msg)
-    {
-      emscripten_console_error(msg);
-    }
-
-    static void WarningLogFunc(const char* msg)
-    {
-      emscripten_console_warn(msg);
-    }
-
-    static void InfoLogFunc(const char* msg)
-    {
-      emscripten_console_log(msg);
-    }
-
-    static void TraceLogFunc(const char* msg)
-    {
-      emscripten_console_log(msg);
-    }
-#else  /* __EMSCRIPTEN__ not #defined */
-    static void ErrorLogFunc(const char* msg)
-    {
-      fprintf(stderr, "E: %s\n", msg);
-    }
-
-    static void WarningLogFunc(const char*)
-    {
-      fprintf(stdout, "W: %s\n", msg);
-    }
-
-    static void InfoLogFunc(const char*)
-    {
-      fprintf(stdout, "I: %s\n", msg);
-    }
-
-    static void TraceLogFunc(const char*)
-    {
-      fprintf(stdout, "T: %s\n", msg);
-    }
-#endif  /* __EMSCRIPTEN__ */
-
-
-    InternalLogger::~InternalLogger()
-    {
-      std::string message = messageStream_.str();
-
-      switch (level_)
-      {
-        case LogLevel_ERROR:
-          ErrorLogFunc(message.c_str());
-          break;
-
-        case LogLevel_WARNING:
-          WarningLogFunc(message.c_str());
-          break;
-
-        case LogLevel_INFO:
-          if (infoEnabled_)
-          {
-            InfoLogFunc(message.c_str());
-            // TODO: stone_console_info(message_.c_str());
-          }
-          break;
-
-        case LogLevel_TRACE:
-          if (traceEnabled_)
-          {
-            TraceLogFunc(message.c_str());
-          }
-          break;
-
-        default:
-        {
-          std::stringstream ss;
-          ss << "Unknown log level (" << level_ << ") for message: " << message;
-          std::string s = ss.str();
-          ErrorLogFunc(s.c_str());
-        }
-      }
-    }
-
-    void InitializePluginContext(void* pluginContext)
-    {
-    }
-
-    void Initialize()
-    {
-    }
-
-    void Finalize()
-    {
-    }
-
-    void Reset()
-    {
-    }
-
-    void Flush()
-    {
-    }
-
-    void EnableInfoLevel(bool enabled)
-    {
-      infoEnabled_ = enabled;
-
-      if (!enabled)
-      {
-        // Also disable the "TRACE" level when info-level debugging is disabled
-        traceEnabled_ = false;
-      }
-    }
-
-    bool IsInfoLevelEnabled()
-    {
-      return infoEnabled_;
-    }
-
-    void EnableTraceLevel(bool enabled)
-    {
-      traceEnabled_ = enabled;
-    }
-
-    bool IsTraceLevelEnabled()
-    {
-      return traceEnabled_;
-    }
-
-    void SetTargetFile(const std::string& path)
-    {
-    }
-
-    void SetTargetFolder(const std::string& path)
-    {
-    }
-  }
-}
-
-
-#else
-
-/*********************************************************
- * Logger compatible with the Orthanc plugin SDK, or that
- * mimics behavior from Google Log.
- *********************************************************/
-
-#include <cassert>
-
-namespace
-{
-  /**
-   * This is minimal implementation of the context for an Orthanc
-   * plugin, limited to the logging facilities, and that is binary
-   * compatible with the definitions of "OrthancCPlugin.h"
-   **/
-  typedef enum 
-  {
-    _OrthancPluginService_LogInfo = 1,
-    _OrthancPluginService_LogWarning = 2,
-    _OrthancPluginService_LogError = 3,
-    _OrthancPluginService_INTERNAL = 0x7fffffff
-  } _OrthancPluginService;
-
-  typedef struct _OrthancPluginContext_t
-  {
-    void*          pluginsManager;
-    const char*    orthancVersion;
-    void         (*Free) (void* buffer);
-    int32_t      (*InvokeService) (struct _OrthancPluginContext_t* context,
-                                   _OrthancPluginService service,
-                                   const void* params);
-  } OrthancPluginContext;
-}
-  
-
-#include "Enumerations.h"
-#include "SystemToolbox.h"
-
-#include <fstream>
-#include <boost/filesystem.hpp>
-#include <boost/thread.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-
-namespace
-{
-  struct LoggingStreamsContext
-  {
-    std::string  targetFile_;
-    std::string  targetFolder_;
-
-    std::ostream* error_;
-    std::ostream* warning_;
-    std::ostream* info_;
-
-    std::unique_ptr<std::ofstream> file_;
-
-    LoggingStreamsContext() : 
-      error_(&std::cerr),
-      warning_(&std::cerr),
-      info_(&std::cerr)
-    {
-    }
-  };
-}
-
-
-
-static std::unique_ptr<LoggingStreamsContext> loggingStreamsContext_;
-static boost::mutex                           loggingStreamsMutex_;
-static Orthanc::Logging::NullStream           nullStream_;
-static OrthancPluginContext*                  pluginContext_ = NULL;
-static bool                                   infoEnabled_ = false;
-static bool                                   traceEnabled_ = false;
-
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    static void GetLogPath(boost::filesystem::path& log,
-                           boost::filesystem::path& link,
-                           const std::string& suffix,
-                           const std::string& directory)
-    {
-      /**
-         From Google Log documentation:
-
-         Unless otherwise specified, logs will be written to the filename
-         "<program name>.<hostname>.<user name>.log<suffix>.",
-         followed by the date, time, and pid (you can't prevent the date,
-         time, and pid from being in the filename).
-
-         In this implementation : "hostname" and "username" are not used
-      **/
-
-      boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
-      boost::filesystem::path root(directory);
-      boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
-      
-      if (!boost::filesystem::exists(root) ||
-          !boost::filesystem::is_directory(root))
-      {
-        throw OrthancException(ErrorCode_CannotWriteFile);
-      }
-
-      char date[64];
-      sprintf(date, "%04d%02d%02d-%02d%02d%02d.%d",
-              static_cast<int>(now.date().year()),
-              now.date().month().as_number(),
-              now.date().day().as_number(),
-              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();
-
-      log = (root / (programName + ".log" + suffix + "." + std::string(date)));
-      link = (root / (programName + ".log" + suffix));
-    }
-
-
-    static void PrepareLogFolder(std::unique_ptr<std::ofstream>& file,
-                                 const std::string& suffix,
-                                 const std::string& directory)
-    {
-      boost::filesystem::path log, link;
-      GetLogPath(log, link, suffix, directory);
-
-#if !defined(_WIN32) && (defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__)))
-      boost::filesystem::remove(link);
-      boost::filesystem::create_symlink(log.filename(), link);
-#endif
-
-      file.reset(new std::ofstream(log.string().c_str()));
-    }
-
-
-    // "loggingStreamsMutex_" must be locked
-    static void CheckFile(std::unique_ptr<std::ofstream>& f)
-    {
-      if (loggingStreamsContext_->file_.get() == NULL ||
-          !loggingStreamsContext_->file_->is_open())
-      {
-        throw OrthancException(ErrorCode_CannotWriteFile);
-      }
-    }
-    
-
-    static void GetLinePrefix(std::string& prefix,
-                              LogLevel level,
-                              const char* file,
-                              int line)
-    {
-      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:
-
-         "Log lines have this form:
-
-         Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
-
-         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"
-
-         In this implementation, "threadid" is not printed.
-      **/
-
-      char c;
-      switch (level)
-      {
-        case LogLevel_ERROR:
-          c = 'E';
-          break;
-
-        case LogLevel_WARNING:
-          c = 'W';
-          break;
-
-        case LogLevel_INFO:
-          c = 'I';
-          break;
-
-        case LogLevel_TRACE:
-          c = 'T';
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);            
-      }
-
-      char date[64];
-      sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ",
-              c,
-              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()));
-
-      prefix = (std::string(date) + path.filename().string() + ":" +
-                boost::lexical_cast<std::string>(line) + "] ");
-    }
-    
-
-    void InitializePluginContext(void* pluginContext)
-    {
-      assert(sizeof(_OrthancPluginService) == sizeof(int32_t));
-
-      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-      loggingStreamsContext_.reset(NULL);
-      pluginContext_ = reinterpret_cast<OrthancPluginContext*>(pluginContext);
-    }
-
-
-    void Initialize()
-    {
-      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-
-      if (loggingStreamsContext_.get() == NULL)
-      {
-        loggingStreamsContext_.reset(new LoggingStreamsContext);
-      }
-    }
-
-    void Finalize()
-    {
-      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-      loggingStreamsContext_.reset(NULL);
-    }
-
-    void Reset()
-    {
-      // Recover the old logging context
-      std::unique_ptr<LoggingStreamsContext> old;
-
-      {
-        boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-        if (loggingStreamsContext_.get() == NULL)
-        {
-          return;
-        }
-        else
-        {
-#if __cplusplus < 201103L
-          old.reset(loggingStreamsContext_.release());
-#else
-          old = std::move(loggingStreamsContext_);
-#endif
-
-          // Create a new logging context, 
-          loggingStreamsContext_.reset(new LoggingStreamsContext);
-        }
-      }
-      
-      if (!old->targetFolder_.empty())
-      {
-        SetTargetFolder(old->targetFolder_);
-      }
-      else if (!old->targetFile_.empty())
-      {
-        SetTargetFile(old->targetFile_);
-      }
-    }
-
-
-    void EnableInfoLevel(bool enabled)
-    {
-      infoEnabled_ = enabled;
-      
-      if (!enabled)
-      {
-        // Also disable the "TRACE" level when info-level debugging is disabled
-        traceEnabled_ = false;
-      }
-    }
-
-    bool IsInfoLevelEnabled()
-    {
-      return infoEnabled_;
-    }
-
-    void EnableTraceLevel(bool enabled)
-    {
-      traceEnabled_ = enabled;
-      
-      if (enabled)
-      {
-        // Also enable the "INFO" level when trace-level debugging is enabled
-        infoEnabled_ = true;
-      }
-    }
-
-    bool IsTraceLevelEnabled()
-    {
-      return traceEnabled_;
-    }
-
-
-    void SetTargetFolder(const std::string& path)
-    {
-      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-      if (loggingStreamsContext_.get() != NULL)
-      {
-        PrepareLogFolder(loggingStreamsContext_->file_, "" /* no suffix */, path);
-        CheckFile(loggingStreamsContext_->file_);
-
-        loggingStreamsContext_->targetFile_.clear();
-        loggingStreamsContext_->targetFolder_ = path;
-        loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get();
-        loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get();
-        loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get();
-      }
-    }
-
-
-    void SetTargetFile(const std::string& path)
-    {
-      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-
-      if (loggingStreamsContext_.get() != NULL)
-      {
-        loggingStreamsContext_->file_.reset(new std::ofstream(path.c_str(), std::fstream::app));
-        CheckFile(loggingStreamsContext_->file_);
-
-        loggingStreamsContext_->targetFile_ = path;
-        loggingStreamsContext_->targetFolder_.clear();
-        loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get();
-        loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get();
-        loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get();
-      }
-    }
-
-
-    InternalLogger::InternalLogger(LogLevel level,
-                                   const char* file,
-                                   int line) : 
-      lock_(loggingStreamsMutex_, boost::defer_lock_t()),
-      level_(level),
-      stream_(&nullStream_)  // By default, logging to "/dev/null" is simulated
-    {
-      if (pluginContext_ != NULL)
-      {
-        // We are logging using the Orthanc plugin SDK
-
-        if (level == LogLevel_TRACE)
-        {
-          // No trace level in plugins, directly exit as the stream is
-          // set to "/dev/null"
-          return;
-        }
-        else
-        {
-          pluginStream_.reset(new std::stringstream);
-          stream_ = pluginStream_.get();
-        }
-      }
-      else
-      {
-        // We are logging in a standalone application, not inside an Orthanc plugin
-
-        if ((level == LogLevel_INFO  && !infoEnabled_) ||
-            (level == LogLevel_TRACE && !traceEnabled_))
-        {
-          // This logging level is disabled, directly exit as the
-          // stream is set to "/dev/null"
-          return;
-        }
-
-        std::string prefix;
-        GetLinePrefix(prefix, level, file, line);
-
-        {
-          // We lock the global mutex. The mutex is locked until the
-          // destructor is called: No change in the output can be done.
-          lock_.lock();
-      
-          if (loggingStreamsContext_.get() == NULL)
-          {
-            fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
-            lock_.unlock();
-            return;
-          }
-
-          switch (level)
-          {
-            case LogLevel_ERROR:
-              stream_ = loggingStreamsContext_->error_;
-              break;
-              
-            case LogLevel_WARNING:
-              stream_ = loggingStreamsContext_->warning_;
-              break;
-              
-            case LogLevel_INFO:
-            case LogLevel_TRACE:
-              stream_ = loggingStreamsContext_->info_;
-              break;
-              
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-
-          if (stream_ == &nullStream_)
-          {
-            // The logging is disabled for this level, we can release
-            // the global mutex.
-            lock_.unlock();
-          }
-          else
-          {
-            try
-            {
-              (*stream_) << prefix;
-            }
-            catch (...)
-            { 
-              // Something is going really wrong, probably running out of
-              // memory. Fallback to a degraded mode.
-              stream_ = loggingStreamsContext_->error_;
-              (*stream_) << "E???? ??:??:??.?????? ] ";
-            }
-          }
-        }
-      }
-    }
-
-
-    InternalLogger::~InternalLogger()
-    {
-      if (pluginStream_.get() != NULL)
-      {
-        // We are logging through the Orthanc SDK
-        
-        std::string message = pluginStream_->str();
-
-        if (pluginContext_ != NULL)
-        {
-          switch (level_)
-          {
-            case LogLevel_ERROR:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str());
-              break;
-
-            case LogLevel_WARNING:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str());
-              break;
-
-            case LogLevel_INFO:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
-              break;
-
-            default:
-              break;
-          }
-        }
-      }
-      else if (stream_ != &nullStream_)
-      {
-        *stream_ << "\n";
-        stream_->flush();
-      }
-    }
-      
-
-    void Flush()
-    {
-      if (pluginContext_ != NULL)
-      {
-        boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-
-        if (loggingStreamsContext_.get() != NULL &&
-            loggingStreamsContext_->file_.get() != NULL)
-        {
-          loggingStreamsContext_->file_->flush();
-        }
-      }
-    }
-    
-
-    void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream,
-                                        std::ostream& warningStream,
-                                        std::ostream& infoStream)
-    {
-      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
-
-      loggingStreamsContext_.reset(new LoggingStreamsContext);
-      loggingStreamsContext_->error_ = &errorStream;
-      loggingStreamsContext_->warning_ = &warningStream;
-      loggingStreamsContext_->info_ = &infoStream;
-    }
-  }
-}
-
-
-#endif   // ORTHANC_ENABLE_LOGGING
--- a/Core/Logging.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,221 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-// To have ORTHANC_ENABLE_LOGGING defined if using the shared library
-#include "OrthancFramework.h"
-
-#include <iostream>
-
-#if !defined(ORTHANC_ENABLE_LOGGING)
-#  error The macro ORTHANC_ENABLE_LOGGING must be defined
-#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
-
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    enum LogLevel
-    {
-      LogLevel_ERROR,
-      LogLevel_WARNING,
-      LogLevel_INFO,
-      LogLevel_TRACE
-    };
-    
-    ORTHANC_PUBLIC const char* EnumerationToString(LogLevel level);
-
-    ORTHANC_PUBLIC LogLevel StringToLogLevel(const char* level);
-
-    // "pluginContext" must be of type "OrthancPluginContext"
-    ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext);
-
-    ORTHANC_PUBLIC void Initialize();
-
-    ORTHANC_PUBLIC void Finalize();
-
-    ORTHANC_PUBLIC void Reset();
-
-    ORTHANC_PUBLIC void Flush();
-
-    ORTHANC_PUBLIC void EnableInfoLevel(bool enabled);
-
-    ORTHANC_PUBLIC void EnableTraceLevel(bool enabled);
-
-    ORTHANC_PUBLIC bool IsTraceLevelEnabled();
-
-    ORTHANC_PUBLIC bool IsInfoLevelEnabled();
-
-    ORTHANC_PUBLIC void SetTargetFile(const std::string& path);
-
-    ORTHANC_PUBLIC void SetTargetFolder(const std::string& path);
-
-    struct NullStream : public std::ostream 
-    {
-      NullStream() : 
-        std::ios(0), 
-        std::ostream(0)
-      {
-      }
-      
-      template <typename T>
-      std::ostream& operator<< (const T& message)
-      {
-        return *this;
-      }
-    };
-  }
-}
-
-
-#if ORTHANC_ENABLE_LOGGING != 1
-#  define LOG(level)   ::Orthanc::Logging::NullStream()
-#  define VLOG(level)  ::Orthanc::Logging::NullStream()
-#else /* ORTHANC_ENABLE_LOGGING == 1 */
-#  define LOG(level)  ::Orthanc::Logging::InternalLogger        \
-  (::Orthanc::Logging::LogLevel_ ## level, __FILE__, __LINE__)
-#  define VLOG(level) ::Orthanc::Logging::InternalLogger        \
-  (::Orthanc::Logging::LogLevel_TRACE, __FILE__, __LINE__)
-#endif
-
-
-
-#if (ORTHANC_ENABLE_LOGGING == 1 &&             \
-     ORTHANC_ENABLE_LOGGING_STDIO == 1)
-// This is notably for WebAssembly
-
-#include <boost/lexical_cast.hpp>
-#include <boost/noncopyable.hpp>
-#include <sstream>
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    class ORTHANC_PUBLIC InternalLogger : public boost::noncopyable
-    {
-    private:
-      LogLevel           level_;
-      std::stringstream  messageStream_;
-
-    public:
-      InternalLogger(LogLevel level,
-                     const char* file  /* ignored */,
-                     int line  /* ignored */) :
-        level_(level)
-      {
-      }
-
-      ~InternalLogger();
-      
-      template <typename T>
-        std::ostream& operator<< (const T& message)
-      {
-        return messageStream_ << boost::lexical_cast<std::string>(message);
-      }
-    };
-  }
-}
-
-#endif
-
-
-
-#if (ORTHANC_ENABLE_LOGGING == 1 &&             \
-     ORTHANC_ENABLE_LOGGING_STDIO == 0)
-
-#include "Compatibility.h"  // For std::unique_ptr<>
-
-#include <boost/lexical_cast.hpp>
-#include <boost/noncopyable.hpp>
-#include <boost/thread/mutex.hpp>
-#include <sstream>
-
-namespace Orthanc
-{
-  namespace Logging
-  {
-    class ORTHANC_PUBLIC InternalLogger : public boost::noncopyable
-    {
-    private:
-      boost::mutex::scoped_lock           lock_;
-      LogLevel                            level_;
-      std::unique_ptr<std::stringstream>  pluginStream_;
-      std::ostream*                       stream_;
-
-    public:
-      InternalLogger(LogLevel level,
-                     const char* file,
-                     int line);
-
-      ~InternalLogger();
-      
-      template <typename T>
-        std::ostream& operator<< (const T& message)
-      {
-        return (*stream_) << boost::lexical_cast<std::string>(message);
-      }
-    };
-
-
-    /**
-     * Set custom logging streams for the error, warning and info
-     * logs. This function may not be called if a log file or folder
-     * has been set beforehand. All three references must be valid.
-     *
-     * Please ensure the supplied streams remain alive and valid as
-     * long as logging calls are performed. In order to prevent
-     * dangling pointer usage, it is mandatory to call
-     * Orthanc::Logging::Reset() before the stream objects are
-     * destroyed and the references become invalid.
-     *
-     * This function must only be used by unit tests. It is ignored if
-     * InitializePluginContext() was called.
-     **/
-    ORTHANC_PUBLIC void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream,
-                                                       std::ostream& warningStream, 
-                                                       std::ostream& infoStream);
-  }
-}
-
-#endif
--- a/Core/Lua/LuaContext.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,690 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "LuaContext.h"
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-#include <set>
-#include <cassert>
-#include <boost/lexical_cast.hpp>
-
-extern "C" 
-{
-#include <lualib.h>
-#include <lauxlib.h>
-}
-
-namespace Orthanc
-{
-  static bool OnlyContainsDigits(const std::string& s)
-  {
-    for (size_t i = 0; i < s.size(); i++)
-    {
-      if (!isdigit(s[i]))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-  
-  LuaContext& LuaContext::GetLuaContext(lua_State *state)
-  {
-    const void* value = GetGlobalVariable(state, "_LuaContext");
-    assert(value != NULL);
-
-    return *const_cast<LuaContext*>(reinterpret_cast<const LuaContext*>(value));
-  }
-
-  int LuaContext::PrintToLog(lua_State *state)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    // http://medek.wordpress.com/2009/02/03/wrapping-lua-errors-and-print-function/
-    int nArgs = lua_gettop(state);
-    lua_getglobal(state, "tostring");
-
-    // Make sure you start at 1 *NOT* 0 for arrays in Lua.
-    std::string result;
-
-    for (int i = 1; i <= nArgs; i++)
-    {
-      const char *s;
-      lua_pushvalue(state, -1);
-      lua_pushvalue(state, i);
-      lua_call(state, 1, 1);
-      s = lua_tostring(state, -1);
-
-      if (result.size() > 0)
-        result.append(", ");
-
-      if (s == NULL)
-        result.append("<No conversion to string>");
-      else
-        result.append(s);
- 
-      lua_pop(state, 1);
-    }
-
-    LOG(WARNING) << "Lua says: " << result;         
-    that.log_.append(result);
-    that.log_.append("\n");
-
-    return 0;
-  }
-
-
-  int LuaContext::ParseJson(lua_State *state)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    int nArgs = lua_gettop(state);
-    if (nArgs != 1 ||
-        !lua_isstring(state, 1))    // Password
-    {
-      lua_pushnil(state);
-      return 1;
-    }
-
-    const char* str = lua_tostring(state, 1);
-
-    Json::Value value;
-    Json::Reader reader;
-    if (reader.parse(str, str + strlen(str), value))
-    {
-      that.PushJson(value);
-    }
-    else
-    {
-      lua_pushnil(state);
-    }
-
-    return 1;
-  }
-
-
-  int LuaContext::DumpJson(lua_State *state)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    int nArgs = lua_gettop(state);
-    if ((nArgs != 1 && nArgs != 2) ||
-        (nArgs == 2 && !lua_isboolean(state, 2)))
-    {
-      lua_pushnil(state);
-      return 1;
-    }
-
-    bool keepStrings = false;
-    if (nArgs == 2)
-    {
-      keepStrings = lua_toboolean(state, 2) ? true : false;
-    }
-
-    Json::Value json;
-    that.GetJson(json, state, 1, keepStrings);
-
-    Json::FastWriter writer;
-    std::string s = writer.write(json);
-    lua_pushlstring(state, s.c_str(), s.size());
-
-    return 1;
-  }
-
-
-#if ORTHANC_ENABLE_CURL == 1
-  int LuaContext::SetHttpCredentials(lua_State *state)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    if (nArgs != 2 ||
-        !lua_isstring(state, 1) ||  // Username
-        !lua_isstring(state, 2))    // Password
-    {
-      LOG(ERROR) << "Lua: Bad parameters to SetHttpCredentials()";
-    }
-    else
-    {
-      // Configure the HTTP client
-      const char* username = lua_tostring(state, 1);
-      const char* password = lua_tostring(state, 2);
-      that.httpClient_.SetCredentials(username, password);
-    }
-
-    return 0;
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_CURL == 1
-  bool LuaContext::AnswerHttpQuery(lua_State* state)
-  {
-    std::string str;
-
-    try
-    {
-      httpClient_.Apply(str);
-    }
-    catch (OrthancException&)
-    {
-      return false;
-    }
-
-    // Return the result of the HTTP request
-    lua_pushlstring(state, str.c_str(), str.size());
-
-    return true;
-  }
-#endif
-  
-
-#if ORTHANC_ENABLE_CURL == 1
-  void LuaContext::SetHttpHeaders(int top)
-  {
-    std::map<std::string, std::string> headers;
-    GetDictionaryArgument(headers, lua_, top, false /* keep key case as provided by Lua script */);
-      
-    httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request
-
-    for (std::map<std::string, std::string>::const_iterator
-           it = headers.begin(); it != headers.end(); ++it)
-    {
-      httpClient_.AddHeader(it->first, it->second);
-    }
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_CURL == 1
-  int LuaContext::CallHttpGet(lua_State *state)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    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);
-      return 1;
-    }
-
-    // Configure the HTTP client class
-    const char* url = lua_tostring(state, 1);
-    that.httpClient_.SetMethod(HttpMethod_Get);
-    that.httpClient_.SetUrl(url);
-    that.httpClient_.GetBody().clear();
-    that.SetHttpHeaders(2);
-
-    // Do the HTTP GET request
-    if (!that.AnswerHttpQuery(state))
-    {
-      LOG(ERROR) << "Lua: Error in HttpGet() for URL " << url;
-      lua_pushnil(state);
-    }
-
-    return 1;
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_CURL == 1
-  int LuaContext::CallHttpPostOrPut(lua_State *state,
-                                    HttpMethod method)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    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);
-      return 1;
-    }
-
-    // Configure the HTTP client class
-    const char* url = lua_tostring(state, 1);
-    that.httpClient_.SetMethod(method);
-    that.httpClient_.SetUrl(url);
-    that.SetHttpHeaders(3);
-
-    if (nArgs >= 2 && !lua_isnil(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
-    {
-      that.httpClient_.GetBody().clear();
-    }
-
-    // Do the HTTP POST/PUT request
-    if (!that.AnswerHttpQuery(state))
-    {
-      LOG(ERROR) << "Lua: Error in HttpPost() or HttpPut() for URL " << url;
-      lua_pushnil(state);
-    }
-
-    return 1;
-  }
-#endif
-  
-
-#if ORTHANC_ENABLE_CURL == 1
-  int LuaContext::CallHttpPost(lua_State *state)
-  {
-    return CallHttpPostOrPut(state, HttpMethod_Post);
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_CURL == 1
-  int LuaContext::CallHttpPut(lua_State *state)
-  {
-    return CallHttpPostOrPut(state, HttpMethod_Put);
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_CURL == 1
-  int LuaContext::CallHttpDelete(lua_State *state)
-  {
-    LuaContext& that = GetLuaContext(state);
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    if (nArgs < 1 || nArgs > 2 || !lua_isstring(state, 1))  // URL
-    {
-      LOG(ERROR) << "Lua: Bad parameters to HttpDelete()";
-      lua_pushnil(state);
-      return 1;
-    }
-
-    // Configure the HTTP client class
-    const char* url = lua_tostring(state, 1);
-    that.httpClient_.SetMethod(HttpMethod_Delete);
-    that.httpClient_.SetUrl(url);
-    that.httpClient_.GetBody().clear();
-    that.SetHttpHeaders(2);
-
-    // Do the HTTP DELETE request
-    std::string s;
-    if (!that.httpClient_.Apply(s))
-    {
-      LOG(ERROR) << "Lua: Error in HttpDelete() for URL " << url;
-      lua_pushnil(state);
-    }
-    else
-    {
-      lua_pushstring(state, "SUCCESS");
-    }
-
-    return 1;
-  }
-#endif
-
-
-  void LuaContext::PushJson(const Json::Value& value)
-  {
-    if (value.isString())
-    {
-      const std::string s = value.asString();
-      lua_pushlstring(lua_, s.c_str(), s.size());
-    }
-    else if (value.isDouble())
-    {
-      lua_pushnumber(lua_, value.asDouble());
-    }
-    else if (value.isInt())
-    {
-      lua_pushinteger(lua_, value.asInt());
-    }
-    else if (value.isUInt())
-    {
-      lua_pushinteger(lua_, value.asUInt());
-    }
-    else if (value.isBool())
-    {
-      lua_pushboolean(lua_, value.asBool());
-    }
-    else if (value.isNull())
-    {
-      lua_pushnil(lua_);
-    }
-    else if (value.isArray())
-    {
-      lua_newtable(lua_);
-
-      // http://lua-users.org/wiki/SimpleLuaApiExample
-      for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
-      {
-        // Push the table index (note the "+1" because of Lua conventions)
-        lua_pushnumber(lua_, i + 1);
-
-        // Push the value of the cell
-        PushJson(value[i]);
-
-        // Stores the pair in the table
-        lua_rawset(lua_, -3);
-      }
-    }
-    else if (value.isObject())
-    {
-      lua_newtable(lua_);
-
-      Json::Value::Members members = value.getMemberNames();
-
-      for (Json::Value::Members::const_iterator 
-             it = members.begin(); it != members.end(); ++it)
-      {
-        // Push the index of the cell
-        lua_pushlstring(lua_, it->c_str(), it->size());
-
-        // Push the value of the cell
-        PushJson(value[*it]);
-
-        // Stores the pair in the table
-        lua_rawset(lua_, -3);
-      }
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_JsonToLuaTable);
-    }
-  }
-
-
-  void LuaContext::GetJson(Json::Value& result,
-                           lua_State* state,
-                           int top,
-                           bool keepStrings)
-  {
-    if (lua_istable(state, top))
-    {
-      Json::Value tmp = Json::objectValue;
-      bool isArray = true;
-      size_t size = 0;
-
-      // Code adapted from: http://stackoverflow.com/a/6142700/881731
-      
-      // Push another reference to the table on top of the stack (so we know
-      // where it is, and this function can work for negative, positive and
-      // pseudo indices
-      lua_pushvalue(state, top);
-      // stack now contains: -1 => table
-      lua_pushnil(state);
-      // stack now contains: -1 => nil; -2 => table
-      while (lua_next(state, -2))
-      {
-        // stack now contains: -1 => value; -2 => key; -3 => table
-        // copy the key so that lua_tostring does not modify the original
-        lua_pushvalue(state, -2);
-        // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table
-        std::string key(lua_tostring(state, -1));
-        Json::Value v;
-        GetJson(v, state, -2, keepStrings);
-
-        tmp[key] = v;
-
-        size += 1;
-        try
-        {
-          if (!OnlyContainsDigits(key) ||
-              boost::lexical_cast<size_t>(key) != size)
-          {
-            isArray = false;
-          }
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-          isArray = false;
-        }
-        
-        // pop value + copy of key, leaving original key
-        lua_pop(state, 2);
-        // stack now contains: -1 => key; -2 => table
-      }
-      // stack now contains: -1 => table (when lua_next returns 0 it pops the key
-      // but does not push anything.)
-      // Pop table
-      lua_pop(state, 1);
-
-      // Stack is now the same as it was on entry to this function
-
-      if (isArray)
-      {
-        result = Json::arrayValue;
-        for (size_t i = 0; i < size; i++)
-        {
-          result.append(tmp[boost::lexical_cast<std::string>(i + 1)]);
-        }
-      }
-      else
-      {
-        result = tmp;
-      }
-    }
-    else if (lua_isnil(state, top))
-    {
-      result = Json::nullValue;
-    }
-    else if (!keepStrings &&
-             lua_isboolean(state, top))
-    {
-      result = lua_toboolean(state, top) ? true : false;
-    }
-    else if (!keepStrings &&
-             lua_isnumber(state, top))
-    {
-      // Convert to "int" if truncation does not loose precision
-      double value = static_cast<double>(lua_tonumber(state, top));
-      int truncated = static_cast<int>(value);
-
-      if (std::abs(value - static_cast<double>(truncated)) <= 
-          std::numeric_limits<double>::epsilon())
-      {
-        result = truncated;
-      }
-      else
-      {
-        result = value;
-      }
-    }
-    else if (lua_isstring(state, top))
-    {
-      // Caution: The "lua_isstring()" case must be the last, since
-      // Lua can convert most types to strings by default.
-      result = std::string(lua_tostring(state, top));
-    }
-    else if (lua_isboolean(state, top))
-    {
-      result = lua_toboolean(state, top) ? true : false;
-    }
-    else
-    {
-      LOG(WARNING) << "Unsupported Lua type when returning Json";
-      result = Json::nullValue;
-    }
-  }
-
-
-  LuaContext::LuaContext()
-  {
-    lua_ = luaL_newstate();
-    if (!lua_)
-    {
-      throw OrthancException(ErrorCode_CannotCreateLua);
-    }
-
-    luaL_openlibs(lua_);
-    lua_register(lua_, "print", PrintToLog);
-    lua_register(lua_, "ParseJson", ParseJson);
-    lua_register(lua_, "DumpJson", DumpJson);
-    
-#if ORTHANC_ENABLE_CURL == 1
-    lua_register(lua_, "HttpGet", CallHttpGet);
-    lua_register(lua_, "HttpPost", CallHttpPost);
-    lua_register(lua_, "HttpPut", CallHttpPut);
-    lua_register(lua_, "HttpDelete", CallHttpDelete);
-    lua_register(lua_, "SetHttpCredentials", SetHttpCredentials);
-#endif
-
-    SetGlobalVariable("_LuaContext", this);
-  }
-
-
-  LuaContext::~LuaContext()
-  {
-    lua_close(lua_);
-  }
-
-
-  void LuaContext::ExecuteInternal(std::string* output,
-                                   const std::string& command)
-  {
-    log_.clear();
-    int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") ||
-                 lua_pcall(lua_, 0, 0, 0));
-
-    if (error) 
-    {
-      assert(lua_gettop(lua_) >= 1);
-
-      std::string description(lua_tostring(lua_, -1));
-      lua_pop(lua_, 1); /* pop error message from the stack */
-      throw OrthancException(ErrorCode_CannotExecuteLua, description);
-    }
-
-    if (output != NULL)
-    {
-      *output = log_;
-    }
-  }
-
-
-  bool LuaContext::IsExistingFunction(const char* name)
-  {
-    lua_settop(lua_, 0);
-    lua_getglobal(lua_, name);
-    return lua_type(lua_, -1) == LUA_TFUNCTION;
-  }
-
-
-  void LuaContext::Execute(Json::Value& output,
-                           const std::string& command)
-  {
-    std::string s;
-    ExecuteInternal(&s, command);
-
-    Json::Reader reader;
-    if (!reader.parse(s, output))
-    {
-      throw OrthancException(ErrorCode_BadJson);
-    }
-  }
-
-
-  void LuaContext::RegisterFunction(const char* name,
-                                    lua_CFunction func)
-  {
-    lua_register(lua_, name, func);
-  }
-
-
-  void LuaContext::SetGlobalVariable(const char* name,
-                                     void* value)
-  {
-    lua_pushlightuserdata(lua_, value);
-    lua_setglobal(lua_, name);
-  }
-
-  
-  const void* LuaContext::GetGlobalVariable(lua_State* state,
-                                            const char* name)
-  {
-    lua_getglobal(state, name);
-    assert(lua_type(state, -1) == LUA_TLIGHTUSERDATA);
-    const void* value = lua_topointer(state, -1);
-    lua_pop(state, 1);
-    return value;
-  }
-
-
-  void LuaContext::GetDictionaryArgument(std::map<std::string, std::string>& target,
-                                         lua_State* state,
-                                         int top,
-                                         bool keyToLowerCase)
-  {
-    target.clear();
-
-    if (lua_gettop(state) >= top)
-    {
-      Json::Value headers;
-      GetJson(headers, state, top, true);
-
-      Json::Value::Members members = headers.getMemberNames();
-
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        std::string key = members[i];
-
-        if (keyToLowerCase)
-        {
-          Toolbox::ToLowerCase(key);
-        }
-        
-        target[key] = headers[members[i]].asString();
-      }
-    }
-  }
-}
--- a/Core/Lua/LuaContext.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-// To have ORTHANC_ENABLE_LUA defined if using the shared library
-#include "../OrthancFramework.h"
-
-#if !defined(ORTHANC_ENABLE_LUA)
-#  error The macro ORTHANC_ENABLE_LUA must be defined
-#endif
-
-#if !defined(ORTHANC_ENABLE_CURL)
-#  error Macro ORTHANC_ENABLE_CURL must be defined
-#endif
-
-#if ORTHANC_ENABLE_LUA == 0
-#  error The Lua support is disabled, cannot include this file
-#endif
-
-#if ORTHANC_ENABLE_CURL == 1
-#  include "../HttpClient.h"
-#endif
-
-extern "C" 
-{
-#include <lua.h>
-}
-
-#include <boost/noncopyable.hpp>
-#include <json/value.h>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC LuaContext : public boost::noncopyable
-  {
-  private:
-    friend class LuaFunctionCall;
-
-    lua_State *lua_;
-    std::string log_;
-
-#if ORTHANC_ENABLE_CURL == 1
-    HttpClient httpClient_;
-#endif
-
-    static int PrintToLog(lua_State *state);
-    static int ParseJson(lua_State *state);
-    static int DumpJson(lua_State *state);
-
-#if ORTHANC_ENABLE_CURL == 1
-    static int SetHttpCredentials(lua_State *state);
-    static int CallHttpPostOrPut(lua_State *state,
-                                 HttpMethod method);
-    static int CallHttpGet(lua_State *state);
-    static int CallHttpPost(lua_State *state);
-    static int CallHttpPut(lua_State *state);
-    static int CallHttpDelete(lua_State *state);
-#endif
-
-    bool AnswerHttpQuery(lua_State* state);
-
-    void ExecuteInternal(std::string* output,
-                         const std::string& command);
-
-    static void GetJson(Json::Value& result,
-                        lua_State* state,
-                        int top,
-                        bool keepStrings);
-
-    void SetHttpHeaders(int top);
-    
-  public:
-    LuaContext();
-
-    ~LuaContext();
-
-    void Execute(const std::string& command)
-    {
-      ExecuteInternal(NULL, command);
-    }
-
-    void Execute(std::string& output,
-                 const std::string& command)
-    {
-      ExecuteInternal(&output, command);
-    }
-
-    void Execute(Json::Value& output,
-                 const std::string& command);
-
-    bool IsExistingFunction(const char* name);
-
-#if ORTHANC_ENABLE_CURL == 1
-    void SetHttpCredentials(const char* username,
-                            const char* password)
-    {
-      httpClient_.SetCredentials(username, password);
-    }
-#endif
-
-    void RegisterFunction(const char* name,
-                          lua_CFunction func);
-
-    void SetGlobalVariable(const char* name,
-                           void* value);
-
-    static LuaContext& GetLuaContext(lua_State *state);
-
-    static const void* GetGlobalVariable(lua_State* state,
-                                         const char* name);
-
-    void PushJson(const Json::Value& value);
-
-    static void GetDictionaryArgument(std::map<std::string, std::string>& target,
-                                      lua_State* state,
-                                      int top,
-                                      bool keyToLowerCase);
-  };
-}
--- a/Core/Lua/LuaFunctionCall.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "LuaFunctionCall.h"
-
-#include "../OrthancException.h"
-#include "../Logging.h"
-
-#include <cassert>
-#include <stdio.h>
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  void LuaFunctionCall::CheckAlreadyExecuted()
-  {
-    if (isExecuted_)
-    {
-      throw OrthancException(ErrorCode_LuaAlreadyExecuted);
-    }
-  }
-
-  LuaFunctionCall::LuaFunctionCall(LuaContext& context,
-                                   const char* functionName) : 
-    context_(context),
-    isExecuted_(false)
-  {
-    // Clear the stack to fulfill the invariant
-    lua_settop(context_.lua_, 0);
-    lua_getglobal(context_.lua_, functionName);
-  }
-
-  void LuaFunctionCall::PushString(const std::string& value)
-  {
-    CheckAlreadyExecuted();
-    lua_pushlstring(context_.lua_, value.c_str(), value.size());
-  }
-
-  void LuaFunctionCall::PushBoolean(bool value)
-  {
-    CheckAlreadyExecuted();
-    lua_pushboolean(context_.lua_, value);
-  }
-
-  void LuaFunctionCall::PushInteger(int value)
-  {
-    CheckAlreadyExecuted();
-    lua_pushinteger(context_.lua_, value);
-  }
-
-  void LuaFunctionCall::PushDouble(double value)
-  {
-    CheckAlreadyExecuted();
-    lua_pushnumber(context_.lua_, value);
-  }
-
-  void LuaFunctionCall::PushJson(const Json::Value& value)
-  {
-    CheckAlreadyExecuted();
-    context_.PushJson(value);
-  }
-
-  void LuaFunctionCall::ExecuteInternal(int numOutputs)
-  {
-    CheckAlreadyExecuted();
-
-    assert(lua_gettop(context_.lua_) >= 1);
-    int nargs = lua_gettop(context_.lua_) - 1;
-    int error = lua_pcall(context_.lua_, nargs, numOutputs, 0);
-
-    if (error) 
-    {
-      assert(lua_gettop(context_.lua_) >= 1);
-          
-      std::string description(lua_tostring(context_.lua_, -1));
-      lua_pop(context_.lua_, 1); /* pop error message from the stack */
-
-      throw OrthancException(ErrorCode_CannotExecuteLua, description);
-    }
-
-    if (lua_gettop(context_.lua_) < numOutputs)
-    {
-      throw OrthancException(ErrorCode_LuaBadOutput);
-    }
-
-    isExecuted_ = true;
-  }
-
-  bool LuaFunctionCall::ExecutePredicate()
-  {
-    ExecuteInternal(1);
-    
-    if (!lua_isboolean(context_.lua_, 1))
-    {
-      throw OrthancException(ErrorCode_NotLuaPredicate);
-    }
-
-    return lua_toboolean(context_.lua_, 1) != 0;
-  }
-
-
-  void LuaFunctionCall::ExecuteToJson(Json::Value& result,
-                                      bool keepStrings)
-  {
-    ExecuteInternal(1);
-    context_.GetJson(result, context_.lua_, lua_gettop(context_.lua_), keepStrings);
-  }
-
-
-  void LuaFunctionCall::ExecuteToString(std::string& result)
-  {
-    ExecuteInternal(1);
-    
-    int top = lua_gettop(context_.lua_);
-    if (lua_isstring(context_.lua_, top))
-    {
-      result = lua_tostring(context_.lua_, top);
-    }
-    else
-    {
-      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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "LuaContext.h"
-
-#include "../DicomFormat/DicomArray.h"
-#include "../DicomFormat/DicomMap.h"
-
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC LuaFunctionCall : public boost::noncopyable
-  {
-  private:
-    LuaContext& context_;
-    bool isExecuted_;
-
-    void CheckAlreadyExecuted();
-
-  protected:
-    void ExecuteInternal(int numOutputs);
-
-    lua_State* GetState()
-    {
-      return context_.lua_;
-    }
-
-  public:
-    LuaFunctionCall(LuaContext& context,
-                    const char* functionName);
-
-    void PushString(const std::string& value);
-
-    void PushBoolean(bool value);
-
-    void PushInteger(int value);
-
-    void PushDouble(double value);
-
-    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);
-    }
-
-    bool ExecutePredicate();
-
-    void ExecuteToJson(Json::Value& result,
-                       bool keepStrings);
-
-    void ExecuteToString(std::string& result);
-  };
-}
--- a/Core/MetricsRegistry.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,330 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "MetricsRegistry.h"
-
-#include "ChunkedBuffer.h"
-#include "Compatibility.h"
-#include "OrthancException.h"
-
-namespace Orthanc
-{
-  static const boost::posix_time::ptime GetNow()
-  {
-    return boost::posix_time::microsec_clock::universal_time();
-  }
-
-  class MetricsRegistry::Item
-  {
-  private:
-    MetricsType               type_;
-    boost::posix_time::ptime  time_;
-    bool                      hasValue_;
-    float                     value_;
-    
-    void Touch(float value,
-               const boost::posix_time::ptime& now)
-    {
-      hasValue_ = true;
-      value_ = value;
-      time_ = now;
-    }
-
-    void Touch(float value)
-    {
-      Touch(value, GetNow());
-    }
-
-    void UpdateMax(float value,
-                   int duration)
-    {
-      if (hasValue_)
-      {
-        const boost::posix_time::ptime now = GetNow();
-
-        if (value > value_ ||
-            (now - time_).total_seconds() > duration)
-        {
-          Touch(value, now);
-        }
-      }
-      else
-      {
-        Touch(value);
-      }
-    }
-    
-    void UpdateMin(float value,
-                   int duration)
-    {
-      if (hasValue_)
-      {
-        const boost::posix_time::ptime now = GetNow();
-        
-        if (value < value_ ||
-            (now - time_).total_seconds() > duration)
-        {
-          Touch(value, now);
-        }
-      }
-      else
-      {
-        Touch(value);
-      }
-    }
-
-  public:
-    Item(MetricsType type) :
-    type_(type),
-    hasValue_(false)
-    {
-    }
-
-    MetricsType GetType() const
-    {
-      return type_;
-    }
-
-    void Update(float value)
-    {
-      switch (type_)
-      {
-        case MetricsType_Default:
-          Touch(value);
-          break;
-          
-        case MetricsType_MaxOver10Seconds:
-          UpdateMax(value, 10);
-          break;
-
-        case MetricsType_MaxOver1Minute:
-          UpdateMax(value, 60);
-          break;
-
-        case MetricsType_MinOver10Seconds:
-          UpdateMin(value, 10);
-          break;
-
-        case MetricsType_MinOver1Minute:
-          UpdateMin(value, 60);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_NotImplemented);
-      }
-    }
-
-    bool HasValue() const
-    {
-      return hasValue_;
-    }
-
-    const boost::posix_time::ptime& GetTime() const
-    {
-      if (hasValue_)
-      {
-        return time_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    float GetValue() const
-    {
-      if (hasValue_)
-      {
-        return value_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-    }
-  };
-
-
-  MetricsRegistry::~MetricsRegistry()
-  {
-    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
-    {
-      assert(it->second != NULL);
-      delete it->second;
-    }
-  }
-
-
-  void MetricsRegistry::SetEnabled(bool enabled)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    enabled_ = enabled;
-  }
-
-
-  void MetricsRegistry::Register(const std::string& name,
-                                 MetricsType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Content::iterator found = content_.find(name);
-
-    if (found == content_.end())
-    {
-      content_[name] = new Item(type);
-    }
-    else
-    {
-      assert(found->second != NULL);
-
-      // This metrics already exists: Only recreate it if there is a
-      // mismatch in the type of metrics
-      if (found->second->GetType() != type)
-      {
-        delete found->second;
-        found->second = new Item(type);
-      }
-    }    
-  }
-
-
-  void MetricsRegistry::SetValueInternal(const std::string& name,
-                                         float value,
-                                         MetricsType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Content::iterator found = content_.find(name);
-
-    if (found == content_.end())
-    {
-      std::unique_ptr<Item> item(new Item(type));
-      item->Update(value);
-      content_[name] = item.release();
-    }
-    else
-    {
-      assert(found->second != NULL);
-      found->second->Update(value);
-    }
-  }
-
-
-  MetricsType MetricsRegistry::GetMetricsType(const std::string& name)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Content::const_iterator found = content_.find(name);
-
-    if (found == content_.end())
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-    else
-    {
-      assert(found->second != NULL);
-      return found->second->GetType();
-    }
-  }
-
-
-  void MetricsRegistry::ExportPrometheusText(std::string& s)
-  {
-    // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch
-    static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1));
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    s.clear();
-
-    if (!enabled_)
-    {
-      return;
-    }
-
-    ChunkedBuffer buffer;
-
-    for (Content::const_iterator it = content_.begin();
-         it != content_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (it->second->HasValue())
-      {
-        boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH;
-
-        std::string line = (it->first + " " +
-                            boost::lexical_cast<std::string>(it->second->GetValue()) + " " + 
-                            boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n");
-
-        buffer.AddChunk(line);
-      }
-    }
-
-    buffer.Flatten(s);
-  }
-
-
-  void MetricsRegistry::SharedMetrics::Add(float delta)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    value_ += delta;
-    registry_.SetValue(name_, value_);
-  }
-
-
-  void  MetricsRegistry::Timer::Start()
-  {
-    if (registry_.IsEnabled())
-    {
-      active_ = true;
-      start_ = GetNow();
-    }
-    else
-    {
-      active_ = false;
-    }
-  }
-
-
-  MetricsRegistry::Timer::~Timer()
-  {
-    if (active_)
-    {   
-      boost::posix_time::time_duration diff = GetNow() - start_;
-      registry_.SetValue(
-        name_, static_cast<float>(diff.total_milliseconds()), type_);
-    }
-  }
-}
--- a/Core/MetricsRegistry.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,191 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#if !defined(ORTHANC_SANDBOXED)
-#  error The macro ORTHANC_SANDBOXED must be defined
-#endif
-
-#if ORTHANC_SANDBOXED == 1
-#  error The class MetricsRegistry cannot be used in sandboxed environments
-#endif
-
-#include <boost/thread/mutex.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-namespace Orthanc
-{
-  enum MetricsType
-  {
-    MetricsType_Default,
-    MetricsType_MaxOver10Seconds,
-    MetricsType_MaxOver1Minute,
-    MetricsType_MinOver10Seconds,
-    MetricsType_MinOver1Minute
-  };
-  
-  class ORTHANC_PUBLIC MetricsRegistry : public boost::noncopyable
-  {
-  private:
-    class Item;
-
-    typedef std::map<std::string, Item*>   Content;
-
-    bool          enabled_;
-    boost::mutex  mutex_;
-    Content       content_;
-
-    void SetValueInternal(const std::string& name,
-                          float value,
-                          MetricsType type);
-
-  public:
-    MetricsRegistry() :
-      enabled_(true)
-    {
-    }
-
-    ~MetricsRegistry();
-
-    bool IsEnabled() const
-    {
-      return enabled_;
-    }
-
-    void SetEnabled(bool enabled);
-
-    void Register(const std::string& name,
-                  MetricsType type);
-
-    void SetValue(const std::string& name,
-                  float value,
-                  MetricsType type)
-    {
-      // Inlining to avoid loosing time if metrics are disabled
-      if (enabled_)
-      {
-        SetValueInternal(name, value, type);
-      }
-    }
-    
-    void SetValue(const std::string& name,
-                  float value)
-    {
-      SetValue(name, value, MetricsType_Default);
-    }
-
-    MetricsType GetMetricsType(const std::string& name);
-
-    // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
-    void ExportPrometheusText(std::string& s);
-
-
-    class ORTHANC_PUBLIC SharedMetrics : public boost::noncopyable
-    {
-    private:
-      boost::mutex      mutex_;
-      MetricsRegistry&  registry_;
-      std::string       name_;
-      float             value_;
-
-    public:
-      SharedMetrics(MetricsRegistry& registry,
-                    const std::string& name,
-                    MetricsType type) :
-        registry_(registry),
-        name_(name),
-        value_(0)
-      {
-      }
-
-      void Add(float delta);
-    };
-
-
-    class ORTHANC_PUBLIC ActiveCounter : public boost::noncopyable
-    {
-    private:
-      SharedMetrics&   metrics_;
-
-    public:
-      ActiveCounter(SharedMetrics& metrics) :
-        metrics_(metrics)
-      {
-        metrics_.Add(1);
-      }
-
-      ~ActiveCounter()
-      {
-        metrics_.Add(-1);
-      }
-    };
-
-
-    class ORTHANC_PUBLIC Timer : public boost::noncopyable
-    {
-    private:
-      MetricsRegistry&          registry_;
-      std::string               name_;
-      MetricsType               type_;
-      bool                      active_;
-      boost::posix_time::ptime  start_;
-
-      void Start();
-
-    public:
-      Timer(MetricsRegistry& registry,
-            const std::string& name) :
-        registry_(registry),
-        name_(name),
-        type_(MetricsType_MaxOver10Seconds)
-      {
-        Start();
-      }
-
-      Timer(MetricsRegistry& registry,
-            const std::string& name,
-            MetricsType type) :
-        registry_(registry),
-        name_(name),
-        type_(type)
-      {
-        Start();
-      }
-
-      ~Timer();
-    };
-  };
-}
--- a/Core/MultiThreading/IRunnableBySteps.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-{
-  class IRunnableBySteps : public IDynamicObject
-  {
-  public:
-    virtual ~IRunnableBySteps()
-    {
-    }
-
-    // Must return "true" if the runnable wishes to continue. Must
-    // return "false" if the runnable has not finished its job.
-    virtual bool Step() = 0;
-
-    static void RunUntilDone(IRunnableBySteps& runnable)
-    {
-      while (runnable.Step());
-    }
-  };
-}
--- a/Core/MultiThreading/RunnableWorkersPool.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,171 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RunnableWorkersPool.h"
-
-#include "SharedMessageQueue.h"
-#include "../Compatibility.h"
-#include "../OrthancException.h"
-#include "../Logging.h"
-
-namespace Orthanc
-{
-  struct RunnableWorkersPool::PImpl
-  {
-    class Worker
-    {
-    private:
-      const bool&           continue_;
-      SharedMessageQueue&   queue_;
-      boost::thread         thread_;
- 
-      static void WorkerThread(Worker* that)
-      {
-        while (that->continue_)
-        {
-          try
-          {
-            std::unique_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 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";
-          }
-        }
-      }
-
-    public:
-      Worker(const bool& globalContinue,
-             SharedMessageQueue& queue) : 
-        continue_(globalContinue),
-        queue_(queue)
-      {
-        thread_ = boost::thread(WorkerThread, this);
-      }
-
-      void Join()
-      {
-        if (thread_.joinable())
-        {
-          thread_.join();
-        }
-      }
-    };
-
-
-    bool                  continue_;
-    std::vector<Worker*>  workers_;
-    SharedMessageQueue    queue_;
-  };
-
-
-
-  RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers) : pimpl_(new PImpl)
-  {
-    pimpl_->continue_ = true;
-
-    if (countWorkers == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    pimpl_->workers_.resize(countWorkers);
-
-    for (size_t i = 0; i < countWorkers; i++)
-    {
-      pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_);
-    }
-  }
-
-
-  void RunnableWorkersPool::Stop()
-  {
-    if (pimpl_->continue_)
-    {
-      pimpl_->continue_ = false;
-
-      for (size_t i = 0; i < pimpl_->workers_.size(); i++)
-      {
-        PImpl::Worker* worker = pimpl_->workers_[i];
-
-        if (worker != NULL)
-        {
-          worker->Join();
-          delete worker;
-        }
-      }
-    }
-  }
-
-
-  RunnableWorkersPool::~RunnableWorkersPool()
-  {
-    Stop();
-  }
-
-
-  void RunnableWorkersPool::Add(IRunnableBySteps* runnable)
-  {
-    if (!pimpl_->continue_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    pimpl_->queue_.Enqueue(runnable);
-  }
-}
--- a/Core/MultiThreading/RunnableWorkersPool.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IRunnableBySteps.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class RunnableWorkersPool : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    void Stop();
-
-  public:
-    explicit RunnableWorkersPool(size_t countWorkers);
-
-    ~RunnableWorkersPool();
-
-    void Add(IRunnableBySteps* runnable);  // Takes the ownership
-  };
-}
--- a/Core/MultiThreading/Semaphore.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Semaphore.h"
-
-#include "../OrthancException.h"
-
-
-namespace Orthanc
-{
-  Semaphore::Semaphore(unsigned int availableResources) :
-    availableResources_(availableResources)
-  {
-    if (availableResources_ == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-  void Semaphore::Release()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    availableResources_++;
-    condition_.notify_one(); 
-  }
-
-  void Semaphore::Acquire()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    while (availableResources_ == 0)
-    {
-      condition_.wait(lock);
-    }
-
-    availableResources_--;
-  }
-
-  bool Semaphore::TryAcquire()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (availableResources_ == 0)
-    {
-      return false;
-    }
-
-    availableResources_--;
-    return true;
-  }
-}
--- a/Core/MultiThreading/Semaphore.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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>
-#include <boost/thread.hpp>
-
-namespace Orthanc
-{
-  class Semaphore : public boost::noncopyable
-  {
-  private:
-    unsigned int availableResources_;
-    boost::mutex mutex_;
-    boost::condition_variable condition_;
-    
-    void Release();
-
-    void Acquire();
-
-    bool TryAcquire();
-  public:
-    explicit Semaphore(unsigned int availableResources);
-
-    unsigned int GetAvailableResourcesCount() const
-    {
-      return availableResources_;
-    }
-
-
-    class Locker : public boost::noncopyable
-    {
-    private:
-      Semaphore&  that_;
-
-    public:
-      explicit Locker(Semaphore& that) :
-        that_(that)
-      {
-        that_.Acquire();
-      }
-
-      ~Locker()
-      {
-        that_.Release();
-      }
-    };
-
-    class TryLocker : public boost::noncopyable
-    {
-    private:
-      Semaphore&  that_;
-      bool        isAcquired_;
-
-    public:
-      explicit TryLocker(Semaphore& that) :
-        that_(that)
-      {
-        isAcquired_ = that_.TryAcquire();
-      }
-
-      ~TryLocker()
-      {
-        if (isAcquired_)
-        {
-          that_.Release();
-        }
-      }
-
-      bool IsAcquired() const
-      {
-        return isAcquired_;
-      }
-    };
-
-  };
-}
--- a/Core/MultiThreading/SharedMessageQueue.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,211 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "SharedMessageQueue.h"
-
-
-#include "../Compatibility.h"
-
-
-/**
- * FIFO (queue):
- * 
- *            back                         front
- *            +--+--+--+--+--+--+--+--+--+--+--+
- * Enqueue -> |  |  |  |  |  |  |  |  |  |  |  |
- *            |  |  |  |  |  |  |  |  |  |  |  | -> Dequeue
- *            +--+--+--+--+--+--+--+--+--+--+--+
- *                                            ^
- *                                            |
- *                                      Make room here
- *
- *
- * LIFO (stack):
- * 
- *            back                         front
- *            +--+--+--+--+--+--+--+--+--+--+--+
- *            |  |  |  |  |  |  |  |  |  |  |  | <- Enqueue
- *            |  |  |  |  |  |  |  |  |  |  |  | -> Dequeue
- *            +--+--+--+--+--+--+--+--+--+--+--+
- *              ^
- *              |
- *        Make room here
- **/
-
-
-namespace Orthanc
-{
-  SharedMessageQueue::SharedMessageQueue(unsigned int maxSize) :
-    isFifo_(true),
-    maxSize_(maxSize)
-  {
-  }
-
-
-  SharedMessageQueue::~SharedMessageQueue()
-  {
-    for (Queue::iterator it = queue_.begin(); it != queue_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  void SharedMessageQueue::Enqueue(IDynamicObject* message)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (maxSize_ != 0 && queue_.size() > maxSize_)
-    {
-      if (isFifo_)
-      {
-        // Too many elements in the queue: Make room
-        delete queue_.front();
-        queue_.pop_front();
-      }
-      else
-      {
-        // Too many elements in the stack: Make room
-        delete queue_.back();
-        queue_.pop_back();
-      }
-    }
-
-    if (isFifo_)
-    {
-      // Queue policy (FIFO)
-      queue_.push_back(message);
-    }
-    else
-    {
-      // Stack policy (LIFO)
-      queue_.push_front(message);
-    }
-
-    elementAvailable_.notify_one();
-  }
-
-
-  IDynamicObject* SharedMessageQueue::Dequeue(int32_t millisecondsTimeout)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Wait for a message to arrive in the FIFO queue
-    while (queue_.empty())
-    {
-      if (millisecondsTimeout == 0)
-      {
-        elementAvailable_.wait(lock);
-      }
-      else
-      {
-        bool success = elementAvailable_.timed_wait
-          (lock, boost::posix_time::milliseconds(millisecondsTimeout));
-        if (!success)
-        {
-          return NULL;
-        }
-      }
-    }
-
-    std::unique_ptr<IDynamicObject> message(queue_.front());
-    queue_.pop_front();
-
-    if (queue_.empty())
-    {
-      emptied_.notify_all();
-    }
-
-    return message.release();
-  }
-
-
-
-  bool SharedMessageQueue::WaitEmpty(int32_t millisecondsTimeout)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    
-    // Wait for the queue to become empty
-    while (!queue_.empty())
-    {
-      if (millisecondsTimeout == 0)
-      {
-        emptied_.wait(lock);
-      }
-      else
-      {
-        if (!emptied_.timed_wait
-            (lock, boost::posix_time::milliseconds(millisecondsTimeout)))
-        {
-          return false;
-        }
-      }
-    }
-
-    return true;
-  }
-
-
-  void SharedMessageQueue::SetFifoPolicy()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    isFifo_ = true;
-  }
-
-  void SharedMessageQueue::SetLifoPolicy()
-  {
-    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::unique_ptr<IDynamicObject> message(queue_.front());
-        queue_.pop_front();
-      }
-
-      emptied_.notify_all();
-    }
-  }
-}
--- a/Core/MultiThreading/SharedMessageQueue.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#include <stdint.h>
-#include <list>
-#include <boost/thread.hpp>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC SharedMessageQueue : public boost::noncopyable
-  {
-  private:
-    typedef std::list<IDynamicObject*>  Queue;
-
-    bool isFifo_;
-    unsigned int maxSize_;
-    Queue queue_;
-    boost::mutex mutex_;
-    boost::condition_variable elementAvailable_;
-    boost::condition_variable emptied_;
-
-  public:
-    explicit SharedMessageQueue(unsigned int maxSize = 0);
-    
-    ~SharedMessageQueue();
-
-    // This transfers the ownership of the message
-    void Enqueue(IDynamicObject* message);
-
-    // The caller is responsible to delete the dequeud message!
-    IDynamicObject* Dequeue(int32_t millisecondsTimeout);
-
-    bool WaitEmpty(int32_t millisecondsTimeout);
-
-    bool IsFifoPolicy() const
-    {
-      return isFifo_;
-    }
-
-    bool IsLifoPolicy() const
-    {
-      return !isFifo_;
-    }
-
-    void SetFifoPolicy();
-
-    void SetLifoPolicy();
-
-    void Clear();
-  };
-}
--- a/Core/OrthancException.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Compatibility.h"
-#include "Enumerations.h"
-#include "Logging.h"
-#include "OrthancFramework.h"
-
-#include <stdint.h>
-#include <string>
-#include <memory>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC OrthancException
-  {
-  private:
-    OrthancException();  // Forbidden
-    
-    OrthancException& operator= (const OrthancException&);  // Forbidden
-
-    ErrorCode  errorCode_;
-    HttpStatus httpStatus_;
-
-    // New in Orthanc 1.5.0
-    std::unique_ptr<std::string>  details_;
-    
-  public:
-    OrthancException(const OrthancException& other) : 
-      errorCode_(other.errorCode_),
-      httpStatus_(other.httpStatus_)
-    {
-      if (other.details_.get() != NULL)
-      {
-        details_.reset(new std::string(*other.details_));
-      }
-    }
-
-    explicit OrthancException(ErrorCode errorCode) : 
-      errorCode_(errorCode),
-      httpStatus_(ConvertErrorCodeToHttpStatus(errorCode))
-    {
-    }
-
-    OrthancException(ErrorCode errorCode,
-                     const std::string& details,
-                     bool log = true) :
-      errorCode_(errorCode),
-      httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)),
-      details_(new std::string(details))
-    {
-#if ORTHANC_ENABLE_LOGGING == 1
-      if (log)
-      {
-        LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details;
-      }
-#endif
-    }
-
-    OrthancException(ErrorCode errorCode,
-                     HttpStatus httpStatus) :
-      errorCode_(errorCode),
-      httpStatus_(httpStatus)
-    {
-    }
-
-    OrthancException(ErrorCode errorCode,
-                     HttpStatus httpStatus,
-                     const std::string& details,
-                     bool log = true) :
-      errorCode_(errorCode),
-      httpStatus_(httpStatus),
-      details_(new std::string(details))
-    {
-#if ORTHANC_ENABLE_LOGGING == 1
-      if (log)
-      {
-        LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details;
-      }
-#endif
-    }
-
-    ErrorCode GetErrorCode() const
-    {
-      return errorCode_;
-    }
-
-    HttpStatus GetHttpStatus() const
-    {
-      return httpStatus_;
-    }
-
-    const char* What() const
-    {
-      return EnumerationToString(errorCode_);
-    }
-
-    bool HasDetails() const
-    {
-      return details_.get() != NULL;
-    }
-
-    const char* GetDetails() const
-    {
-      if (details_.get() == NULL)
-      {
-        return "";
-      }
-      else
-      {
-        return details_->c_str();
-      }
-    }
-  };
-}
--- a/Core/OrthancFramework.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#if !defined(ORTHANC_ENABLE_CURL)
-#  error Macro ORTHANC_ENABLE_CURL must be defined
-#endif
-
-#if !defined(ORTHANC_ENABLE_SSL)
-#  error Macro ORTHANC_ENABLE_SSL must be defined
-#endif
-
-#if !defined(ORTHANC_ENABLE_DCMTK)
-#  error Macro ORTHANC_ENABLE_DCMTK must be defined
-#endif
-
-#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING)
-#  error Macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
-#endif
-
-#if ORTHANC_ENABLE_CURL == 1
-#  include "HttpClient.h"
-#endif
-
-#if ORTHANC_ENABLE_DCMTK == 1
-#  include "DicomParsing/FromDcmtkBridge.h"
-#  if ORTHANC_ENABLE_DCMTK_NETWORKING == 1
-#    include <dcmtk/dcmnet/dul.h>
-#  endif
-#endif
-
-#include "Logging.h"
-#include "Toolbox.h"
-
-
-namespace Orthanc
-{
-  void InitializeFramework(const std::string& locale,
-                           bool loadPrivateDictionary)
-  {
-    Logging::Initialize();
-
-#if (ORTHANC_ENABLE_LOCALE == 1) && !defined(__EMSCRIPTEN__)  // No global locale in wasm/asm.js
-    if (locale.empty())
-    {
-      Toolbox::InitializeGlobalLocale(NULL);
-    }
-    else
-    {
-      Toolbox::InitializeGlobalLocale(locale.c_str());
-    }
-#endif
-
-    Toolbox::InitializeOpenSsl();
-
-#if ORTHANC_ENABLE_CURL == 1
-    HttpClient::GlobalInitialize();
-#endif
-
-#if ORTHANC_ENABLE_DCMTK == 1
-    FromDcmtkBridge::InitializeDictionary(true);
-    FromDcmtkBridge::InitializeCodecs();
-#endif
-
-#if (ORTHANC_ENABLE_DCMTK == 1 &&               \
-     ORTHANC_ENABLE_DCMTK_NETWORKING == 1)
-    /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
-    dcmDisableGethostbyaddr.set(OFTrue);
-#endif
-  }
-  
-
-  void FinalizeFramework()
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    FromDcmtkBridge::FinalizeCodecs();
-#endif
-
-#if ORTHANC_ENABLE_CURL == 1
-    HttpClient::GlobalFinalize();
-#endif
-    
-    Toolbox::FinalizeOpenSsl();
-
-#if (ORTHANC_ENABLE_LOCALE == 1) && !defined(__EMSCRIPTEN__)
-    Toolbox::FinalizeGlobalLocale();
-#endif
-    
-    Logging::Finalize();
-  }
-}
--- a/Core/OrthancFramework.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-/**
- * Besides the "pragma once" above that only protects this file,
- * define a macro to prevent including different versions of
- * "OrthancFramework.h"
- **/
-#ifndef __ORTHANC_FRAMEWORK_H
-#define __ORTHANC_FRAMEWORK_H
-
-#if !defined(ORTHANC_BUILDING_FRAMEWORK_LIBRARY)
-#  error The macro ORTHANC_BUILDING_FRAMEWORK_LIBRARY must be defined
-#endif
-
-/**
- * It is implied that if this file is used, we're building the Orthanc
- * framework (not using it as a shared library): We don't use the
- * common "BUILDING_DLL"
- * construction. https://gcc.gnu.org/wiki/Visibility
- **/
-#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
-#  if defined(_WIN32) || defined (__CYGWIN__)
-#    define ORTHANC_PUBLIC __declspec(dllexport)
-#    define ORTHANC_LOCAL
-#  else
-#    if __GNUC__ >= 4
-#      define ORTHANC_PUBLIC __attribute__((visibility ("default")))
-#      define ORTHANC_LOCAL  __attribute__((visibility ("hidden")))
-#    else
-#      define ORTHANC_PUBLIC
-#      define ORTHANC_LOCAL
-#      pragma warning Unknown dynamic link import/export semantics
-#    endif
-#  endif
-#else
-#  define ORTHANC_PUBLIC
-#  define ORTHANC_LOCAL
-#endif
-
-
-#include <string>
-
-namespace Orthanc
-{
-  ORTHANC_PUBLIC void InitializeFramework(const std::string& locale,
-                                          bool loadPrivateDictionary);
-  
-  ORTHANC_PUBLIC void FinalizeFramework();
-}
-
-
-#endif /* __ORTHANC_FRAMEWORK_H */
--- a/Core/Pkcs11.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,311 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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)
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot create an OpenSSL engine for PKCS#11");
-      }
-
-      // Create a PKCS#11 context using libp11
-      context_ = pkcs11_new();
-      if (!context_)
-      {
-        ENGINE_free(engine);
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot create a libp11 context for PKCS#11");
-      }
-
-      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()) ||
-
-#if OPENSSL_VERSION_NUMBER < 0x10100000L // OpenSSL 1.0.2
-          !ENGINE_set_ECDSA(engine, PKCS11_get_ecdsa_method()) ||
-          !ENGINE_set_ECDH(engine, PKCS11_get_ecdh_method()) ||
-#else
-          !ENGINE_set_EC(engine, PKCS11_get_ec_key_method()) ||
-#endif
-
-          // Make OpenSSL know about our PKCS#11 engine
-          !ENGINE_add(engine))
-      {
-        pkcs11_finish(context_);
-        ENGINE_free(engine);
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot initialize the OpenSSL engine for PKCS#11");
-      }
-
-      // 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_)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "The PKCS#11 engine has already been initialized");
-      }
-
-      if (module.empty() ||
-          !SystemToolbox::IsRegularFile(module))
-      {
-        throw OrthancException(
-          ErrorCode_InexistentFile,
-          "The PKCS#11 module must be a path to one shared library (DLL or .so)");
-      }
-
-      ENGINE* engine = LoadEngine();
-      if (!engine)
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot create an OpenSSL engine for PKCS#11");
-      }
-
-      if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0))
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot configure the OpenSSL dynamic engine for PKCS#11");
-      }
-
-      if (verbose)
-      {
-        ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0);
-      }
-
-      if (!pin.empty() &&
-          !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) 
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot set the PIN code for PKCS#11");
-      }
-  
-      if (!ENGINE_init(engine))
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot initialize the OpenSSL dynamic engine for PKCS#11");
-      }
-
-      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
-    }
-  }
-}
--- a/Core/Pkcs11.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
--- a/Core/PrecompiledHeaders.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(_WIN32) && !defined(NOMINMAX)
-#define NOMINMAX
-#endif
-
-#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
-
-#include "OrthancFramework.h"  // Must be the first one
-
-//#include <boost/date_time/posix_time/posix_time.hpp>
-//#include <boost/filesystem.hpp>
-#include <boost/lexical_cast.hpp>
-//#include <boost/locale.hpp>
-//#include <boost/regex.hpp>
-#include <boost/thread.hpp>
-#include <boost/thread/shared_mutex.hpp>
-
-#include <json/value.h>
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-#  include <pugixml.hpp>
-#endif
-
-#include "Compatibility.h"
-#include "Enumerations.h"
-#include "Logging.h"
-#include "OrthancException.h"
-#include "OrthancFramework.h"
-#include "Toolbox.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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,278 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApi.h"
-
-#include "../Logging.h"
-
-#include <stdlib.h>   // To define "_exit()" under Windows
-#include <stdio.h>
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-    class HttpHandlerVisitor : public RestApiHierarchy::IVisitor
-    {
-    private:
-      RestApi& api_;
-      RestApiOutput& output_;
-      RequestOrigin origin_;
-      const char* remoteIp_;
-      const char* username_;
-      HttpMethod method_;
-      const IHttpHandler::Arguments& headers_;
-      const IHttpHandler::Arguments& getArguments_;
-      const void* bodyData_;
-      size_t bodySize_;
-
-    public:
-      HttpHandlerVisitor(RestApi& api,
-                         RestApiOutput& output,
-                         RequestOrigin origin,
-                         const char* remoteIp,
-                         const char* username,
-                         HttpMethod method,
-                         const IHttpHandler::Arguments& headers,
-                         const IHttpHandler::Arguments& getArguments,
-                         const void* bodyData,
-                         size_t bodySize) :
-        api_(api),
-        output_(output),
-        origin_(origin),
-        remoteIp_(remoteIp),
-        username_(username),
-        method_(method),
-        headers_(headers),
-        getArguments_(getArguments),
-        bodyData_(bodyData),
-        bodySize_(bodySize)
-      {
-      }
-
-      virtual bool Visit(const RestApiHierarchy::Resource& resource,
-                         const UriComponents& uri,
-                         const IHttpHandler::Arguments& components,
-                         const UriComponents& trailing)
-      {
-        if (resource.HasHandler(method_))
-        {
-          switch (method_)
-          {
-            case HttpMethod_Get:
-            {
-              RestApiGetCall call(output_, api_, origin_, remoteIp_, username_, 
-                                  headers_, components, trailing, uri, getArguments_);
-              resource.Handle(call);
-              return true;
-            }
-
-            case HttpMethod_Post:
-            {
-              RestApiPostCall call(output_, api_, origin_, remoteIp_, username_, 
-                                   headers_, components, trailing, uri, bodyData_, bodySize_);
-              resource.Handle(call);
-              return true;
-            }
-
-            case HttpMethod_Delete:
-            {
-              RestApiDeleteCall call(output_, api_, origin_, remoteIp_, username_, 
-                                     headers_, components, trailing, uri);
-              resource.Handle(call);
-              return true;
-            }
-
-            case HttpMethod_Put:
-            {
-              RestApiPutCall call(output_, api_, origin_, remoteIp_, username_, 
-                                  headers_, components, trailing, uri, bodyData_, bodySize_);
-              resource.Handle(call);
-              return true;
-            }
-
-            default:
-              return false;
-          }
-        }
-
-        return false;
-      }
-    };
-  }
-
-
-
-  static void AddMethod(std::string& target,
-                        const std::string& method)
-  {
-    if (target.size() > 0)
-      target += "," + method;
-    else
-      target = method;
-  }
-
-  static std::string  MethodsToString(const std::set<HttpMethod>& methods)
-  {
-    std::string s;
-
-    if (methods.find(HttpMethod_Get) != methods.end())
-    {
-      AddMethod(s, "GET");
-    }
-
-    if (methods.find(HttpMethod_Post) != methods.end())
-    {
-      AddMethod(s, "POST");
-    }
-
-    if (methods.find(HttpMethod_Put) != methods.end())
-    {
-      AddMethod(s, "PUT");
-    }
-
-    if (methods.find(HttpMethod_Delete) != methods.end())
-    {
-      AddMethod(s, "DELETE");
-    }
-
-    return s;
-  }
-
-
-
-  bool RestApi::Handle(HttpOutput& output,
-                       RequestOrigin origin,
-                       const char* remoteIp,
-                       const char* username,
-                       HttpMethod method,
-                       const UriComponents& uri,
-                       const Arguments& headers,
-                       const GetArguments& getArguments,
-                       const void* bodyData,
-                       size_t bodySize)
-  {
-    RestApiOutput wrappedOutput(output, method);
-
-#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
-      Arguments::const_iterator it = headers.find("accept");
-      if (it != headers.end())
-      {
-        std::vector<std::string> accepted;
-        Toolbox::TokenizeString(accepted, it->second, ';');
-        for (size_t i = 0; i < accepted.size(); i++)
-        {
-          if (accepted[i] == MIME_XML)
-          {
-            wrappedOutput.SetConvertJsonToXml(true);
-          }
-
-          if (accepted[i] == MIME_JSON)
-          {
-            wrappedOutput.SetConvertJsonToXml(false);
-          }
-        }
-      }
-    }
-#endif
-
-    Arguments compiled;
-    HttpToolbox::CompileGetArguments(compiled, getArguments);
-
-    HttpHandlerVisitor visitor(*this, wrappedOutput, origin, remoteIp, username, 
-                               method, headers, compiled, bodyData, bodySize);
-
-    if (root_.LookupResource(uri, visitor))
-    {
-      wrappedOutput.Finalize();
-      return true;
-    }
-
-    std::set<HttpMethod> methods;
-    root_.GetAcceptedMethods(methods, uri);
-
-    if (methods.empty())
-    {
-      return false;  // This URI is not served by this REST API
-    }
-    else
-    {
-      LOG(INFO) << "REST method " << EnumerationToString(method) 
-                << " not allowed on: " << Toolbox::FlattenUri(uri);
-
-      output.SendMethodNotAllowed(MethodsToString(methods));
-
-      return true;
-    }
-  }
-
-  void RestApi::Register(const std::string& path,
-                         RestApiGetCall::Handler handler)
-  {
-    root_.Register(path, handler);
-  }
-
-  void RestApi::Register(const std::string& path,
-                         RestApiPutCall::Handler handler)
-  {
-    root_.Register(path, handler);
-  }
-
-  void RestApi::Register(const std::string& path,
-                         RestApiPostCall::Handler handler)
-  {
-    root_.Register(path, handler);
-  }
-
-  void RestApi::Register(const std::string& path,
-                         RestApiDeleteCall::Handler handler)
-  {
-    root_.Register(path, handler);
-  }
-  
-  void RestApi::AutoListChildren(RestApiGetCall& call)
-  {
-    RestApi& context = call.GetContext();
-
-    Json::Value directory;
-    if (context.root_.GetDirectory(directory, call.GetFullUri()))
-    {
-      call.GetOutput().AnswerJson(directory);
-    }    
-  }
-}
--- a/Core/RestApi/RestApi.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiHierarchy.h"
-#include "../Compatibility.h"
-
-#include <list>
-
-namespace Orthanc
-{
-  class RestApi : public IHttpHandler
-  {
-  private:
-    RestApiHierarchy root_;
-
-  public:
-    static void AutoListChildren(RestApiGetCall& call);
-
-    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                            RequestOrigin origin,
-                                            const char* remoteIp,
-                                            const char* username,
-                                            HttpMethod method,
-                                            const UriComponents& uri,
-                                            const Arguments& headers)
-    {
-      return false;
-    }
-
-    virtual bool Handle(HttpOutput& output,
-                        RequestOrigin origin,
-                        const char* remoteIp,
-                        const char* username,
-                        HttpMethod method,
-                        const UriComponents& uri,
-                        const Arguments& headers,
-                        const GetArguments& getArguments,
-                        const void* bodyData,
-                        size_t bodySize);
-
-    void Register(const std::string& path,
-                  RestApiGetCall::Handler handler);
-
-    void Register(const std::string& path,
-                  RestApiPutCall::Handler handler);
-
-    void Register(const std::string& path,
-                  RestApiPostCall::Handler handler);
-
-    void Register(const std::string& path,
-                  RestApiDeleteCall::Handler handler);
-  };
-}
--- a/Core/RestApi/RestApiCall.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiCall.h"
-
-namespace Orthanc
-{
-  bool RestApiCall::ParseJsonRequestInternal(Json::Value& result,
-                                             const void* body,
-                                             size_t size)
-  {
-    result.clear();
-    Json::Reader reader;
-    return reader.parse(reinterpret_cast<const char*>(body),
-                        reinterpret_cast<const char*>(body) + size, result);
-  }
-
-
-  std::string RestApiCall::FlattenUri() const
-  {
-    std::string s = "/";
-
-    for (size_t i = 0; i < fullUri_.size(); i++)
-    {
-      s += fullUri_[i] + "/";
-    }
-
-    return s;
-  }
-}
--- a/Core/RestApi/RestApiCall.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,148 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../HttpServer/IHttpHandler.h"
-#include "../HttpServer/HttpToolbox.h"
-#include "RestApiPath.h"
-#include "RestApiOutput.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class RestApi;
-
-  class RestApiCall : public boost::noncopyable
-  {
-  private:
-    RestApiOutput& output_;
-    RestApi& context_;
-    RequestOrigin origin_;
-    const char* remoteIp_;
-    const char* username_;
-    const IHttpHandler::Arguments& httpHeaders_;
-    const IHttpHandler::Arguments& uriComponents_;
-    const UriComponents& trailing_;
-    const UriComponents& fullUri_;
-
-  protected:
-    static bool ParseJsonRequestInternal(Json::Value& result,
-                                         const void* body,
-                                         size_t size);
-
-  public:
-    RestApiCall(RestApiOutput& output,
-                RestApi& context,
-                RequestOrigin origin,
-                const char* remoteIp,
-                const char* username,
-                const IHttpHandler::Arguments& httpHeaders,
-                const IHttpHandler::Arguments& uriComponents,
-                const UriComponents& trailing,
-                const UriComponents& fullUri) :
-      output_(output),
-      context_(context),
-      origin_(origin),
-      remoteIp_(remoteIp),
-      username_(username),
-      httpHeaders_(httpHeaders),
-      uriComponents_(uriComponents),
-      trailing_(trailing),
-      fullUri_(fullUri)
-    {
-    }
-
-    RestApiOutput& GetOutput()
-    {
-      return output_;
-    }
-
-    RestApi& GetContext()
-    {
-      return context_;
-    }
-    
-    const UriComponents& GetFullUri() const
-    {
-      return fullUri_;
-    }
-    
-    const UriComponents& GetTrailingUri() const
-    {
-      return trailing_;
-    }
-
-    std::string GetUriComponent(const std::string& name,
-                                const std::string& defaultValue) const
-    {
-      return HttpToolbox::GetArgument(uriComponents_, name, defaultValue);
-    }
-
-    std::string GetHttpHeader(const std::string& name,
-                              const std::string& defaultValue) const
-    {
-      return HttpToolbox::GetArgument(httpHeaders_, name, defaultValue);
-    }
-
-    const IHttpHandler::Arguments& GetHttpHeaders() const
-    {
-      return httpHeaders_;
-    }
-
-    void ParseCookies(IHttpHandler::Arguments& result) const
-    {
-      HttpToolbox::ParseCookies(result, httpHeaders_);
-    }
-
-    std::string FlattenUri() const;
-
-    RequestOrigin GetRequestOrigin() const
-    {
-      return origin_;
-    }
-
-    const char* GetRemoteIp() const
-    {
-      return remoteIp_;
-    }
-
-    const char* GetUsername() const
-    {
-      return username_;
-    }
-
-    virtual bool ParseJsonRequest(Json::Value& result) const = 0;
-  };
-}
--- a/Core/RestApi/RestApiDeleteCall.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiCall.h"
-
-namespace Orthanc
-{
-  class RestApiDeleteCall : public RestApiCall
-  {
-  public:
-    typedef void (*Handler) (RestApiDeleteCall& call);
-    
-    RestApiDeleteCall(RestApiOutput& output,
-                      RestApi& context,
-                      RequestOrigin origin,
-                      const char* remoteIp,
-                      const char* username,
-                      const IHttpHandler::Arguments& httpHeaders,
-                      const IHttpHandler::Arguments& uriComponents,
-                      const UriComponents& trailing,
-                      const UriComponents& fullUri) :
-      RestApiCall(output, context, origin, remoteIp, username,
-                  httpHeaders, uriComponents, trailing, fullUri)
-    {
-    }
-
-    virtual bool ParseJsonRequest(Json::Value& result) const
-    {
-      result.clear();
-      return true;
-    }
-  };
-}
--- a/Core/RestApi/RestApiGetCall.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiGetCall.h"
-
-namespace Orthanc
-{
-  bool RestApiGetCall::ParseJsonRequest(Json::Value& result) const
-  {
-    result.clear();
-
-    for (IHttpHandler::Arguments::const_iterator 
-           it = getArguments_.begin(); it != getArguments_.end(); ++it)
-    {
-      result[it->first] = it->second;
-    }
-
-    return true;
-  }
-}
--- a/Core/RestApi/RestApiGetCall.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiCall.h"
-
-namespace Orthanc
-{
-  class RestApiGetCall : public RestApiCall
-  {
-  private:
-    const IHttpHandler::Arguments& getArguments_;
-
-  public:
-    typedef void (*Handler) (RestApiGetCall& call);   
-
-    RestApiGetCall(RestApiOutput& output,
-                   RestApi& context,
-                   RequestOrigin origin,
-                   const char* remoteIp,
-                   const char* username,
-                   const IHttpHandler::Arguments& httpHeaders,
-                   const IHttpHandler::Arguments& uriComponents,
-                   const UriComponents& trailing,
-                   const UriComponents& fullUri,
-                   const IHttpHandler::Arguments& getArguments) :
-      RestApiCall(output, context, origin, remoteIp, username, 
-                  httpHeaders, uriComponents, trailing, fullUri),
-      getArguments_(getArguments)
-    {
-    }
-
-    std::string GetArgument(const std::string& name,
-                            const std::string& defaultValue) const
-    {
-      return HttpToolbox::GetArgument(getArguments_, name, defaultValue);
-    }
-
-    bool HasArgument(const std::string& name) const
-    {
-      return getArguments_.find(name) != getArguments_.end();
-    }
-
-    virtual bool ParseJsonRequest(Json::Value& result) const;
-  };
-}
--- a/Core/RestApi/RestApiHierarchy.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,476 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiHierarchy.h"
-
-#include "../OrthancException.h"
-
-#include <cassert>
-#include <stdio.h>
-
-namespace Orthanc
-{
-  RestApiHierarchy::Resource::Resource() : 
-    getHandler_(NULL), 
-    postHandler_(NULL),
-    putHandler_(NULL), 
-    deleteHandler_(NULL)
-  {
-  }
-
-
-  bool RestApiHierarchy::Resource::HasHandler(HttpMethod method) const
-  {
-    switch (method)
-    {
-      case HttpMethod_Get:
-        return getHandler_ != NULL;
-
-      case HttpMethod_Post:
-        return postHandler_ != NULL;
-
-      case HttpMethod_Put:
-        return putHandler_ != NULL;
-
-      case HttpMethod_Delete:
-        return deleteHandler_ != NULL;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool RestApiHierarchy::Resource::IsEmpty() const
-  {
-    return (getHandler_ == NULL &&
-            postHandler_ == NULL &&
-            putHandler_ == NULL &&
-            deleteHandler_ == NULL);
-  }
-
-
-  RestApiHierarchy& RestApiHierarchy::AddChild(Children& children,
-                                               const std::string& name)
-  {
-    Children::iterator it = children.find(name);
-
-    if (it == children.end())
-    {
-      // Create new child
-      RestApiHierarchy *child = new RestApiHierarchy;
-      children[name] = child;
-      return *child;
-    }
-    else
-    {
-      return *it->second;
-    }
-  }
-
-
-
-  bool RestApiHierarchy::Resource::Handle(RestApiGetCall& call) const
-  {
-    if (getHandler_ != NULL)
-    {
-      getHandler_(call);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  bool RestApiHierarchy::Resource::Handle(RestApiPutCall& call) const
-  {
-    if (putHandler_ != NULL)
-    {
-      putHandler_(call);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  bool RestApiHierarchy::Resource::Handle(RestApiPostCall& call) const
-  {
-    if (postHandler_ != NULL)
-    {
-      postHandler_(call);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  bool RestApiHierarchy::Resource::Handle(RestApiDeleteCall& call) const
-  {
-    if (deleteHandler_ != NULL)
-    {
-      deleteHandler_(call);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-
-  void RestApiHierarchy::DeleteChildren(Children& children)
-  {
-    for (Children::iterator it = children.begin();
-         it != children.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-
-  template <typename Handler>
-  void RestApiHierarchy::RegisterInternal(const RestApiPath& path,
-                                          Handler handler,
-                                          size_t level)
-  {
-    if (path.GetLevelCount() == level)
-    {
-      if (path.IsUniversalTrailing())
-      {
-        universalHandlers_.Register(handler);
-      }
-      else
-      {
-        handlers_.Register(handler);
-      }
-    }
-    else
-    {
-      RestApiHierarchy* child;
-      if (path.IsWildcardLevel(level))
-      {
-        child = &AddChild(wildcardChildren_, path.GetWildcardName(level));
-      }
-      else
-      {
-        child = &AddChild(children_, path.GetLevelName(level));
-      }
-
-      child->RegisterInternal(path, handler, level + 1);
-    }
-  }
-
-
-  bool RestApiHierarchy::LookupResource(IHttpHandler::Arguments& components,
-                                       const UriComponents& uri,
-                                       IVisitor& visitor,
-                                       size_t level)
-  {
-    if (uri.size() != 0 &&
-        level > uri.size())
-    {
-      return false;
-    }
-
-    UriComponents trailing;
-
-    // Look for an exact match on the resource of interest
-      if (uri.size() == 0 ||
-          level == uri.size())
-    {
-      if (!handlers_.IsEmpty() &&
-          visitor.Visit(handlers_, uri, components, trailing))
-      {
-        return true;
-      }
-    }
-
-
-    if (level < uri.size())  // A recursive call is possible
-    {
-      // Try and go down in the hierarchy, using an exact match for the child
-      Children::const_iterator child = children_.find(uri[level]);
-      if (child != children_.end())
-      {
-        if (child->second->LookupResource(components, uri, visitor, level + 1))
-        {
-          return true;
-        }
-      }
-
-      // Try and go down in the hierarchy, using wildcard rules for children
-      for (child = wildcardChildren_.begin();
-           child != wildcardChildren_.end(); ++child)
-      {
-        IHttpHandler::Arguments subComponents = components;
-        subComponents[child->first] = uri[level];
-
-        if (child->second->LookupResource(subComponents, uri, visitor, level + 1))
-        {
-          return true;
-        }        
-      }
-    }
-
-
-    // As a last resort, call the universal handlers, if any
-    if (!universalHandlers_.IsEmpty())
-    {
-      trailing.resize(uri.size() - level);
-      size_t pos = 0;
-      for (size_t i = level; i < uri.size(); i++, pos++)
-      {
-        trailing[pos] = uri[i];
-      }
-
-      assert(pos == trailing.size());
-
-      if (visitor.Visit(universalHandlers_, uri, components, trailing))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  bool RestApiHierarchy::CanGenerateDirectory() const
-  {
-    return (universalHandlers_.IsEmpty() &&
-            wildcardChildren_.empty());
-  }
-
-
-  bool RestApiHierarchy::GetDirectory(Json::Value& result,
-                                      const UriComponents& uri,
-                                      size_t level)
-  {
-    if (uri.size() == level)
-    {
-      if (CanGenerateDirectory())
-      {
-        result = Json::arrayValue;
-
-        for (Children::const_iterator it = children_.begin();
-             it != children_.end(); ++it)
-        {
-          result.append(it->first);
-        }
-
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-    Children::const_iterator child = children_.find(uri[level]);
-    if (child != children_.end())
-    {
-      if (child->second->GetDirectory(result, uri, level + 1))
-      {
-        return true;
-      }
-    }
-
-    for (child = wildcardChildren_.begin(); 
-         child != wildcardChildren_.end(); ++child)
-    {
-      if (child->second->GetDirectory(result, uri, level + 1))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-                       
-
-  RestApiHierarchy::~RestApiHierarchy()
-  {
-    DeleteChildren(children_);
-    DeleteChildren(wildcardChildren_);
-  }
-
-  void RestApiHierarchy::Register(const std::string& uri,
-                                  RestApiGetCall::Handler handler)
-  {
-    RestApiPath path(uri);
-    RegisterInternal(path, handler, 0);
-  }
-
-  void RestApiHierarchy::Register(const std::string& uri,
-                                  RestApiPutCall::Handler handler)
-  {
-    RestApiPath path(uri);
-    RegisterInternal(path, handler, 0);
-  }
-
-  void RestApiHierarchy::Register(const std::string& uri,
-                                  RestApiPostCall::Handler handler)
-  {
-    RestApiPath path(uri);
-    RegisterInternal(path, handler, 0);
-  }
-
-  void RestApiHierarchy::Register(const std::string& uri,
-                                  RestApiDeleteCall::Handler handler)
-  {
-    RestApiPath path(uri);
-    RegisterInternal(path, handler, 0);
-  }
-
-  void RestApiHierarchy::CreateSiteMap(Json::Value& target) const
-  {
-    target = Json::objectValue;
-
-    /*std::string s = " ";
-      if (handlers_.HasHandler(HttpMethod_Get))
-      {
-      s += "GET ";
-      }
-
-      if (handlers_.HasHandler(HttpMethod_Post))
-      {
-      s += "POST ";
-      }
-
-      if (handlers_.HasHandler(HttpMethod_Put))
-      {
-      s += "PUT ";
-      }
-
-      if (handlers_.HasHandler(HttpMethod_Delete))
-      {
-      s += "DELETE ";
-      }
-
-      target = s;*/
-      
-    for (Children::const_iterator it = children_.begin();
-         it != children_.end(); ++it)
-    {
-      it->second->CreateSiteMap(target[it->first]);
-    }
-      
-    for (Children::const_iterator it = wildcardChildren_.begin();
-         it != wildcardChildren_.end(); ++it)
-    {
-      it->second->CreateSiteMap(target["<" + it->first + ">"]);
-    }
-  }
-
-
-  bool RestApiHierarchy::LookupResource(const UriComponents& uri,
-                                        IVisitor& visitor)
-  {
-    IHttpHandler::Arguments components;
-    return LookupResource(components, uri, visitor, 0);
-  }    
-
-
-
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-
-    class AcceptedMethodsVisitor : public RestApiHierarchy::IVisitor
-    {
-    private:
-      std::set<HttpMethod>& methods_;
-
-    public:
-      AcceptedMethodsVisitor(std::set<HttpMethod>& methods) : methods_(methods)
-      {
-      }
-
-      virtual bool Visit(const RestApiHierarchy::Resource& resource,
-                         const UriComponents& uri,
-                         const IHttpHandler::Arguments& components,
-                         const UriComponents& trailing)
-      {
-        if (trailing.size() == 0)  // Ignore universal handlers
-        {
-          if (resource.HasHandler(HttpMethod_Get))
-          {
-            methods_.insert(HttpMethod_Get);
-          }
-
-          if (resource.HasHandler(HttpMethod_Post))
-          {
-            methods_.insert(HttpMethod_Post);
-          }
-
-          if (resource.HasHandler(HttpMethod_Put))
-          {
-            methods_.insert(HttpMethod_Put);
-          }
-
-          if (resource.HasHandler(HttpMethod_Delete))
-          {
-            methods_.insert(HttpMethod_Delete);
-          }
-        }
-
-        return false;  // Continue to check all the possible ways to access this URI
-      }
-    };
-  }
-
-  void RestApiHierarchy::GetAcceptedMethods(std::set<HttpMethod>& methods,
-                                            const UriComponents& uri)
-  {
-    IHttpHandler::Arguments components;
-    AcceptedMethodsVisitor visitor(methods);
-    if (LookupResource(components, uri, visitor, 0))
-    {
-      Json::Value d;
-      if (GetDirectory(d, uri))
-      {
-        methods.insert(HttpMethod_Get);
-      }
-    }
-  }
-}
--- a/Core/RestApi/RestApiHierarchy.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,165 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiGetCall.h"
-#include "RestApiPostCall.h"
-#include "RestApiPutCall.h"
-#include "RestApiDeleteCall.h"
-
-#include <set>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC RestApiHierarchy : public boost::noncopyable
-  {
-  public:
-    class ORTHANC_PUBLIC Resource : public boost::noncopyable
-    {
-    private:
-      RestApiGetCall::Handler     getHandler_;
-      RestApiPostCall::Handler    postHandler_;
-      RestApiPutCall::Handler     putHandler_;
-      RestApiDeleteCall::Handler  deleteHandler_;
-
-    public:
-      Resource();
-
-      bool HasHandler(HttpMethod method) const;
-
-      void Register(RestApiGetCall::Handler handler)
-      {
-        getHandler_ = handler;
-      }
-
-      void Register(RestApiPutCall::Handler handler)
-      {
-        putHandler_ = handler;
-      }
-
-      void Register(RestApiPostCall::Handler handler)
-      {
-        postHandler_ = handler;
-      }
-
-      void Register(RestApiDeleteCall::Handler handler)
-      {
-        deleteHandler_ = handler;
-      }
-
-      bool IsEmpty() const;
-
-      bool Handle(RestApiGetCall& call) const;
-
-      bool Handle(RestApiPutCall& call) const;
-
-      bool Handle(RestApiPostCall& call) const;
-
-      bool Handle(RestApiDeleteCall& call) const;
-    };
-
-
-    class IVisitor : public boost::noncopyable
-    {
-    public:
-      virtual ~IVisitor()
-      {
-      }
-
-      virtual bool Visit(const Resource& resource,
-                         const UriComponents& uri,
-                         const IHttpHandler::Arguments& components,
-                         const UriComponents& trailing) = 0;
-    };
-
-
-  private:
-    typedef std::map<std::string, RestApiHierarchy*>  Children;
-
-    Resource  handlers_;
-    Children  children_;
-    Children  wildcardChildren_;
-    Resource  universalHandlers_;
-
-    static RestApiHierarchy& AddChild(Children& children,
-                                      const std::string& name);
-
-    static void DeleteChildren(Children& children);
-
-    template <typename Handler>
-    void RegisterInternal(const RestApiPath& path,
-                          Handler handler,
-                          size_t level);
-
-    bool CanGenerateDirectory() const;
-
-    bool LookupResource(IHttpHandler::Arguments& components,
-                        const UriComponents& uri,
-                        IVisitor& visitor,
-                        size_t level);
-
-    bool GetDirectory(Json::Value& result,
-                      const UriComponents& uri,
-                      size_t level);
-
-  public:
-    ~RestApiHierarchy();
-
-    void Register(const std::string& uri,
-                  RestApiGetCall::Handler handler);
-
-    void Register(const std::string& uri,
-                  RestApiPutCall::Handler handler);
-
-    void Register(const std::string& uri,
-                  RestApiPostCall::Handler handler);
-
-    void Register(const std::string& uri,
-                  RestApiDeleteCall::Handler handler);
-
-    void CreateSiteMap(Json::Value& target) const;
-
-    bool GetDirectory(Json::Value& result,
-                      const UriComponents& uri)
-    {
-      return GetDirectory(result, uri, 0);
-    }
-
-    bool LookupResource(const UriComponents& uri,
-                        IVisitor& visitor);
-
-    void GetAcceptedMethods(std::set<HttpMethod>& methods,
-                            const UriComponents& uri);
-  };
-}
--- a/Core/RestApi/RestApiOutput.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,222 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiOutput.h"
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-#include "../Toolbox.h"
-
-#include <boost/lexical_cast.hpp>
-
-
-namespace Orthanc
-{
-  RestApiOutput::RestApiOutput(HttpOutput& output,
-                               HttpMethod method) : 
-    output_(output),
-    method_(method),
-    convertJsonToXml_(false)
-  {
-    alreadySent_ = false;
-  }
-
-  RestApiOutput::~RestApiOutput()
-  {
-  }
-
-  void RestApiOutput::Finalize()
-  {
-    if (!alreadySent_)
-    {
-      if (method_ == HttpMethod_Post)
-      {
-        output_.SendStatus(HttpStatus_400_BadRequest);
-      }
-      else
-      {
-        output_.SendStatus(HttpStatus_404_NotFound);
-      }
-    }
-  }
-  
-  void RestApiOutput::CheckStatus()
-  {
-    if (alreadySent_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-
-
-  void RestApiOutput::AnswerStream(IHttpStreamAnswer& stream)
-  {
-    CheckStatus();
-    output_.Answer(stream);
-    alreadySent_ = true;
-  }
-
-  void RestApiOutput::AnswerJson(const Json::Value& value)
-  {
-    CheckStatus();
-
-    if (convertJsonToXml_)
-    {
-#if ORTHANC_ENABLE_PUGIXML == 1
-      std::string s;
-      Toolbox::JsonToXml(s, value);
-
-      output_.SetContentType(MIME_XML_UTF8);
-      output_.Answer(s);
-#else
-      throw OrthancException(ErrorCode_InternalError,
-                             "Orthanc was compiled without XML support");
-#endif
-    }
-    else
-    {
-      Json::StyledWriter writer;
-      std::string s = writer.write(value);
-      
-      output_.SetContentType(MIME_JSON_UTF8);      
-      output_.Answer(s);
-    }
-
-    alreadySent_ = true;
-  }
-
-  void RestApiOutput::AnswerBuffer(const std::string& buffer,
-                                   MimeType contentType)
-  {
-    AnswerBuffer(buffer.size() == 0 ? NULL : buffer.c_str(),
-                 buffer.size(), contentType);
-  }
-
-  void RestApiOutput::AnswerBuffer(const void* buffer,
-                                   size_t length,
-                                   MimeType contentType)
-  {
-    CheckStatus();
-
-    if (convertJsonToXml_ &&
-        contentType == MimeType_Json)
-    {
-      Json::Value json;
-      Json::Reader reader;
-      if (reader.parse(reinterpret_cast<const char*>(buffer),
-                       reinterpret_cast<const char*>(buffer) + length, json))
-      {
-        AnswerJson(json);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "The REST API tries and answers with an invalid JSON file");
-      } 
-    }
-    else
-    {
-      output_.SetContentType(contentType);
-      output_.Answer(buffer, length);
-      alreadySent_ = true;
-    }
-  }
-
-  void RestApiOutput::Redirect(const std::string& path)
-  {
-    CheckStatus();
-    output_.Redirect(path);
-    alreadySent_ = true;
-  }
-
-  void RestApiOutput::SignalErrorInternal(HttpStatus status,
-					  const char* message,
-					  size_t messageSize)
-  {
-    if (status != HttpStatus_400_BadRequest &&
-        status != HttpStatus_403_Forbidden &&
-        status != HttpStatus_500_InternalServerError &&
-        status != HttpStatus_415_UnsupportedMediaType)
-    {
-      throw OrthancException(ErrorCode_BadHttpStatusInRest);
-    }
-
-    CheckStatus();
-    output_.SendStatus(status, message, messageSize);
-    alreadySent_ = true;    
-  }
-
-  void RestApiOutput::SignalError(HttpStatus status)
-  {
-    SignalErrorInternal(status, NULL, 0);
-  }
-
-  void RestApiOutput::SignalError(HttpStatus status,
-				  const std::string& message)
-  {
-    SignalErrorInternal(status, message.c_str(), message.size());
-  }
-
-  void RestApiOutput::SetCookie(const std::string& name,
-                                const std::string& value,
-                                unsigned int maxAge)
-  {
-    if (name.find(";") != std::string::npos ||
-        name.find(" ") != std::string::npos ||
-        value.find(";") != std::string::npos ||
-        value.find(" ") != std::string::npos)
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    CheckStatus();
-
-    std::string v = value + ";path=/";
-
-    if (maxAge != 0)
-    {
-      v += ";max-age=" + boost::lexical_cast<std::string>(maxAge);
-    }
-
-    output_.SetCookie(name, v);
-  }
-
-  void RestApiOutput::ResetCookie(const std::string& name)
-  {
-    // This marks the cookie to be deleted by the browser in 1 second,
-    // and before it actually gets deleted, its value is set to the
-    // empty string
-    SetCookie(name, "", 1);
-  }
-}
--- a/Core/RestApi/RestApiOutput.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../HttpServer/HttpOutput.h"
-#include "../HttpServer/HttpFileSender.h"
-
-#include <json/json.h>
-
-namespace Orthanc
-{
-  class RestApiOutput
-  {
-  private:
-    HttpOutput&  output_;
-    HttpMethod   method_;
-    bool         alreadySent_;
-    bool         convertJsonToXml_;
-
-    void CheckStatus();
-
-    void SignalErrorInternal(HttpStatus status,
-			     const char* message,
-			     size_t messageSize);
-
-  public:
-    RestApiOutput(HttpOutput& output,
-                  HttpMethod method);
-
-    ~RestApiOutput();
-
-    void SetConvertJsonToXml(bool convert)
-    {
-      convertJsonToXml_ = convert;
-    }
-
-    bool IsConvertJsonToXml() const
-    {
-      return convertJsonToXml_;
-    }
-
-    void AnswerStream(IHttpStreamAnswer& stream);
-
-    void AnswerJson(const Json::Value& value);
-
-    void AnswerBuffer(const std::string& buffer,
-                      MimeType contentType);
-
-    void AnswerBuffer(const void* buffer,
-                      size_t length,
-                      MimeType contentType);
-
-    void SignalError(HttpStatus status);
-
-    void SignalError(HttpStatus status,
-		     const std::string& message);
-
-    void Redirect(const std::string& path);
-
-    void SetCookie(const std::string& name,
-                   const std::string& value,
-                   unsigned int maxAge = 0);
-
-    void ResetCookie(const std::string& name);
-
-    void Finalize();
-  };
-}
--- a/Core/RestApi/RestApiPath.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,181 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiPath.h"
-
-#include "../OrthancException.h"
-
-#include <cassert>
-
-namespace Orthanc
-{
-  RestApiPath::RestApiPath(const std::string& uri)
-  {
-    Toolbox::SplitUriComponents(uri_, uri);
-
-    if (uri_.size() == 0)
-    {
-      hasTrailing_ = false;
-      return;
-    }
-
-    if (uri_.back() == "*")
-    {
-      hasTrailing_ = true;
-      uri_.pop_back();
-    }
-    else
-    {
-      hasTrailing_ = false;
-    }
-
-    components_.resize(uri_.size());
-    for (size_t i = 0; i < uri_.size(); i++)
-    {
-      size_t s = uri_[i].size();
-      assert(s > 0);
-
-      if (uri_[i][0] == '{' && 
-          uri_[i][s - 1] == '}')
-      {
-        components_[i] = uri_[i].substr(1, s - 2);
-        uri_[i] = "";
-      }
-      else
-      {
-        components_[i] = "";
-      }
-    }
-  }
-
-  bool RestApiPath::Match(IHttpHandler::Arguments& components,
-                          UriComponents& trailing,
-                          const std::string& uriRaw) const
-  {
-    UriComponents uri;
-    Toolbox::SplitUriComponents(uri, uriRaw);
-    return Match(components, trailing, uri);
-  }
-
-  bool RestApiPath::Match(IHttpHandler::Arguments& components,
-                          UriComponents& trailing,
-                          const UriComponents& uri) const
-  {
-    assert(uri_.size() == components_.size());
-
-    if (uri.size() < uri_.size())
-    {
-      return false;
-    }
-
-    if (!hasTrailing_ && uri.size() > uri_.size())
-    {
-      return false;
-    }
-
-    components.clear();
-    trailing.clear();
-
-    assert(uri_.size() <= uri.size());
-    for (size_t i = 0; i < uri_.size(); i++)
-    {
-      if (components_[i].size() == 0)
-      {
-        // This URI component is not a free parameter
-        if (uri_[i] != uri[i])
-        {
-          return false;
-        }
-      }
-      else
-      {
-        // This URI component is a free parameter
-        components[components_[i]] = uri[i];
-      }
-    }
-
-    if (hasTrailing_)
-    {
-      trailing.assign(uri.begin() + uri_.size(), uri.end());
-    }
-
-    return true;
-  }
-
-
-  bool RestApiPath::Match(const UriComponents& uri) const
-  {
-    IHttpHandler::Arguments components;
-    UriComponents trailing;
-    return Match(components, trailing, uri);
-  }
-
-
-  bool RestApiPath::IsWildcardLevel(size_t level) const
-  {
-    assert(uri_.size() == components_.size());
-
-    if (level >= uri_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    return uri_[level].length() == 0;
-  }
-
-  const std::string& RestApiPath::GetWildcardName(size_t level) const
-  {
-    assert(uri_.size() == components_.size());
-
-    if (!IsWildcardLevel(level))
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    return components_[level];
-  }
-
-  const std::string& RestApiPath::GetLevelName(size_t level) const
-  {
-    assert(uri_.size() == components_.size());
-
-    if (IsWildcardLevel(level))
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    return uri_[level];
-  }
-}
-
--- a/Core/RestApi/RestApiPath.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Toolbox.h"
-#include "../HttpServer/IHttpHandler.h"
-
-#include <map>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC RestApiPath : public boost::noncopyable
-  {
-  private:
-    UriComponents uri_;
-    bool hasTrailing_;
-    std::vector<std::string> components_;
-
-  public:
-    RestApiPath(const std::string& uri);
-
-    // This version is slower
-    bool Match(IHttpHandler::Arguments& components,
-               UriComponents& trailing,
-               const std::string& uriRaw) const;
-
-    bool Match(IHttpHandler::Arguments& components,
-               UriComponents& trailing,
-               const UriComponents& uri) const;
-
-    bool Match(const UriComponents& uri) const;
-
-    size_t GetLevelCount() const
-    {
-      return uri_.size();
-    }
-
-    bool IsWildcardLevel(size_t level) const;
-
-    bool IsUniversalTrailing() const
-    {
-      return hasTrailing_;
-    }
-
-    const std::string& GetWildcardName(size_t level) const;
-
-    const std::string& GetLevelName(size_t level) const;
-
-  };
-}
--- a/Core/RestApi/RestApiPostCall.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiCall.h"
-
-namespace Orthanc
-{
-  class RestApiPostCall : public RestApiCall
-  {
-  private:
-    const void* bodyData_;
-    size_t bodySize_;
-
-  public:
-    typedef void (*Handler) (RestApiPostCall& call);
-    
-    RestApiPostCall(RestApiOutput& output,
-                    RestApi& context,
-                    RequestOrigin origin,
-                    const char* remoteIp,
-                    const char* username,
-                    const IHttpHandler::Arguments& httpHeaders,
-                    const IHttpHandler::Arguments& uriComponents,
-                    const UriComponents& trailing,
-                    const UriComponents& fullUri,
-                    const void* bodyData,
-                    size_t bodySize) :
-      RestApiCall(output, context, origin, remoteIp, username, 
-                  httpHeaders, uriComponents, trailing, fullUri),
-      bodyData_(bodyData),
-      bodySize_(bodySize)
-    {
-    }
-
-    const void* GetBodyData() const
-    {
-      return bodyData_;
-    }
-
-    size_t GetBodySize() const
-    {
-      return bodySize_;
-    }
-
-    void BodyToString(std::string& result) const
-    {
-      result.assign(reinterpret_cast<const char*>(bodyData_), bodySize_);
-    }
-
-    virtual bool ParseJsonRequest(Json::Value& result) const
-    {
-      return ParseJsonRequestInternal(result, reinterpret_cast<const char*>(bodyData_), bodySize_);
-    }
-  };
-}
--- a/Core/RestApi/RestApiPutCall.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "RestApiCall.h"
-
-namespace Orthanc
-{
-  class RestApiPutCall : public RestApiCall
-  {
-  private:
-    const void* bodyData_;
-    size_t bodySize_;
-
-  public:
-    typedef void (*Handler) (RestApiPutCall& call);
-    
-    RestApiPutCall(RestApiOutput& output,
-                   RestApi& context,
-                   RequestOrigin origin,
-                   const char* remoteIp,
-                   const char* username,
-                   const IHttpHandler::Arguments& httpHeaders,
-                   const IHttpHandler::Arguments& uriComponents,
-                   const UriComponents& trailing,
-                   const UriComponents& fullUri,
-                   const void* bodyData,
-                   size_t bodySize) :
-      RestApiCall(output, context, origin, remoteIp, username,
-                  httpHeaders, uriComponents, trailing, fullUri),
-      bodyData_(bodyData),
-      bodySize_(bodySize)
-    {
-    }
-
-    const void* GetBodyData() const
-    {
-      return bodyData_;
-    }
-
-    size_t GetBodySize() const
-    {
-      return bodySize_;
-    }
-
-    void BodyToString(std::string& result) const
-    {
-      result.assign(reinterpret_cast<const char*>(bodyData_), bodySize_);
-    }
-
-    virtual bool ParseJsonRequest(Json::Value& result) const
-    {
-      return ParseJsonRequestInternal(result, reinterpret_cast<const char*>(bodyData_), bodySize_);
-    }      
-  };
-}
--- a/Core/SQLite/Connection.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,399 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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.
- **/
-
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../PrecompiledHeaders.h"
-#endif
-
-#include "Connection.h"
-#include "OrthancSQLiteException.h"
-
-#include <memory>
-#include <cassert>
-#include <string.h>
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../Logging.h"
-#endif
-
-#include "sqlite3.h"
-
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    Connection::Connection() :
-      db_(NULL),
-      transactionNesting_(0),
-      needsRollback_(false)
-    {
-    }
-
-
-    Connection::~Connection()
-    {
-      Close();
-    }
-
-
-    void Connection::CheckIsOpen() const
-    {
-      if (!db_)
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteNotOpened);
-      }
-    }
-
-    void Connection::Open(const std::string& path)
-    {
-      if (db_) 
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteAlreadyOpened);
-      }
-
-      int err = sqlite3_open(path.c_str(), &db_);
-      if (err != SQLITE_OK) 
-      {
-        Close();
-        db_ = NULL;
-        throw OrthancSQLiteException(ErrorCode_SQLiteCannotOpen);
-      }
-
-      // Execute PRAGMAs at this point
-      // http://www.sqlite.org/pragma.html
-      Execute("PRAGMA FOREIGN_KEYS=ON;");
-      Execute("PRAGMA RECURSIVE_TRIGGERS=ON;");
-    }
-
-    void Connection::OpenInMemory()
-    {
-      Open(":memory:");
-    }
-
-    void Connection::Close() 
-    {
-      ClearCache();
-
-      if (db_)
-      {
-        sqlite3_close(db_);
-        db_ = NULL;
-      }
-    }
-
-    void Connection::ClearCache()
-    {
-      for (CachedStatements::iterator 
-             it = cachedStatements_.begin(); 
-           it != cachedStatements_.end(); ++it)
-      {
-        delete it->second;
-      }
-
-      cachedStatements_.clear();
-    }
-
-
-    StatementReference& Connection::GetCachedStatement(const StatementId& id,
-                                                       const char* sql)
-    {
-      CachedStatements::iterator i = cachedStatements_.find(id);
-      if (i != cachedStatements_.end())
-      {
-        if (i->second->GetReferenceCount() >= 1)
-        {
-          throw OrthancSQLiteException(ErrorCode_SQLiteStatementAlreadyUsed);
-        }
-
-        return *i->second;
-      }
-      else
-      {
-        StatementReference* statement = new StatementReference(db_, sql);
-        cachedStatements_[id] = statement;
-        return *statement;
-      }
-    }
-
-
-    bool Connection::Execute(const char* sql) 
-    {
-#if ORTHANC_SQLITE_STANDALONE != 1
-      VLOG(1) << "SQLite::Connection::Execute " << sql;
-#endif
-
-      CheckIsOpen();
-
-      int error = sqlite3_exec(db_, sql, NULL, NULL, NULL);
-      if (error == SQLITE_ERROR)
-      {
-#if ORTHANC_SQLITE_STANDALONE != 1
-        LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_)
-                   << " (" << sqlite3_extended_errcode(db_) << ")";
-#endif
-
-        throw OrthancSQLiteException(ErrorCode_SQLiteExecute);
-      }
-      else
-      {
-        return error == SQLITE_OK;
-      }
-    }
-
-    int  Connection::ExecuteAndReturnErrorCode(const char* sql)
-    {
-      CheckIsOpen();
-      return sqlite3_exec(db_, sql, NULL, NULL, NULL);
-    }
-
-    // Info querying -------------------------------------------------------------
-
-    bool Connection::IsSQLValid(const char* sql) 
-    {
-      sqlite3_stmt* stmt = NULL;
-      if (sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL) != SQLITE_OK)
-        return false;
-
-      sqlite3_finalize(stmt);
-      return true;
-    }
-
-    bool Connection::DoesTableOrIndexExist(const char* name, 
-                                           const char* type) const
-    {
-      // Our SQL is non-mutating, so this cast is OK.
-      Statement statement(const_cast<Connection&>(*this), 
-                          "SELECT name FROM sqlite_master WHERE type=? AND name=?");
-      statement.BindString(0, type);
-      statement.BindString(1, name);
-      return statement.Step();  // Table exists if any row was returned.
-    }
-
-    bool Connection::DoesTableExist(const char* table_name) const
-    {
-      return DoesTableOrIndexExist(table_name, "table");
-    }
-
-    bool Connection::DoesIndexExist(const char* index_name) const
-    {
-      return DoesTableOrIndexExist(index_name, "index");
-    }
-
-    bool Connection::DoesColumnExist(const char* table_name, const char* column_name) const
-    {
-      std::string sql("PRAGMA TABLE_INFO(");
-      sql.append(table_name);
-      sql.append(")");
-
-      // Our SQL is non-mutating, so this cast is OK.
-      Statement statement(const_cast<Connection&>(*this), sql.c_str());
-
-      while (statement.Step()) {
-        if (!statement.ColumnString(1).compare(column_name))
-          return true;
-      }
-      return false;
-    }
-
-    int64_t Connection::GetLastInsertRowId() const
-    {
-      return sqlite3_last_insert_rowid(db_);
-    }
-
-    int Connection::GetLastChangeCount() const
-    {
-      return sqlite3_changes(db_);
-    }
-
-    int Connection::GetErrorCode() const 
-    {
-      return sqlite3_errcode(db_);
-    }
-
-    int Connection::GetLastErrno() const 
-    {
-      int err = 0;
-      if (SQLITE_OK != sqlite3_file_control(db_, NULL, SQLITE_LAST_ERRNO, &err))
-        return -2;
-
-      return err;
-    }
-
-    const char* Connection::GetErrorMessage() const 
-    {
-      return sqlite3_errmsg(db_);
-    }
-
-
-    bool Connection::BeginTransaction()
-    {
-      if (needsRollback_)
-      {
-        assert(transactionNesting_ > 0);
-
-        // When we're going to rollback, fail on this begin and don't actually
-        // mark us as entering the nested transaction.
-        return false;
-      }
-
-      bool success = true;
-      if (!transactionNesting_) 
-      {
-        needsRollback_ = false;
-
-        Statement begin(*this, SQLITE_FROM_HERE, "BEGIN TRANSACTION");
-        if (!begin.Run())
-          return false;
-      }
-      transactionNesting_++;
-      return success;
-    }
-
-    void Connection::RollbackTransaction()
-    {
-      if (!transactionNesting_)
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction);
-      }
-
-      transactionNesting_--;
-
-      if (transactionNesting_ > 0)
-      {
-        // Mark the outermost transaction as needing rollback.
-        needsRollback_ = true;
-        return;
-      }
-
-      DoRollback();
-    }
-
-    bool Connection::CommitTransaction() 
-    {
-      if (!transactionNesting_) 
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteCommitWithoutTransaction);
-      }
-      transactionNesting_--;
-
-      if (transactionNesting_ > 0) 
-      {
-        // Mark any nested transactions as failing after we've already got one.
-        return !needsRollback_;
-      }
-
-      if (needsRollback_) 
-      {
-        DoRollback();
-        return false;
-      }
-
-      Statement commit(*this, SQLITE_FROM_HERE, "COMMIT");
-      return commit.Run();
-    }
-
-    void Connection::DoRollback() 
-    {
-      Statement rollback(*this, SQLITE_FROM_HERE, "ROLLBACK");
-      rollback.Run();
-      needsRollback_ = false;
-    }
-
-
-
-
-
-
-    static void ScalarFunctionCaller(sqlite3_context* rawContext,
-                                     int argc,
-                                     sqlite3_value** argv)
-    {
-      FunctionContext context(rawContext, argc, argv);
-
-      void* payload = sqlite3_user_data(rawContext);
-      assert(payload != NULL);
-
-      IScalarFunction& func = *reinterpret_cast<IScalarFunction*>(payload);
-      func.Compute(context);
-    }
-
-
-    static void ScalarFunctionDestroyer(void* payload)
-    {
-      assert(payload != NULL);
-      delete reinterpret_cast<IScalarFunction*>(payload);
-    }
-
-
-    IScalarFunction* Connection::Register(IScalarFunction* func)
-    {
-      int err = sqlite3_create_function_v2(db_, 
-                                           func->GetName(), 
-                                           func->GetCardinality(),
-                                           SQLITE_UTF8, 
-                                           func,
-                                           ScalarFunctionCaller,
-                                           NULL,
-                                           NULL,
-                                           ScalarFunctionDestroyer);
-
-      if (err != SQLITE_OK)
-      {
-        delete func;
-        throw OrthancSQLiteException(ErrorCode_SQLiteRegisterFunction);
-      }
-
-      return func;
-    }
-
-
-    void Connection::FlushToDisk()
-    {
-#if ORTHANC_SQLITE_STANDALONE != 1
-      VLOG(1) << "SQLite::Connection::FlushToDisk";
-#endif
-
-      int err = sqlite3_wal_checkpoint(db_, NULL);
-
-      if (err != SQLITE_OK)
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteFlush);
-      }
-    }
-  }
-}
--- a/Core/SQLite/Connection.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-#include "Statement.h"
-#include "IScalarFunction.h"
-#include "SQLiteTypes.h"
-
-#include <string>
-#include <map>
-
-#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__)
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class ORTHANC_PUBLIC Connection : NonCopyable
-    {
-      friend class Statement;
-      friend class Transaction;
-
-    private:
-      // All cached statements. Keeping a reference to these statements means that
-      // they'll remain active.
-      typedef std::map<StatementId, StatementReference*>  CachedStatements;
-      CachedStatements cachedStatements_;
-
-      // The actual sqlite database. Will be NULL before Init has been called or if
-      // Init resulted in an error.
-      sqlite3* db_;
-
-      // Number of currently-nested transactions.
-      int transactionNesting_;
-
-      // True if any of the currently nested transactions have been rolled back.
-      // When we get to the outermost transaction, this will determine if we do
-      // a rollback instead of a commit.
-      bool needsRollback_;
-
-      void ClearCache();
-
-      void CheckIsOpen() const;
-
-      sqlite3* GetWrappedObject()
-      {
-        return db_;
-      }
-
-      StatementReference& GetCachedStatement(const StatementId& id,
-                                             const char* sql);
-
-      bool DoesTableOrIndexExist(const char* name, 
-                                 const char* type) const;
-
-      void DoRollback();
-
-    public:
-      // The database is opened by calling Open[InMemory](). Any uncommitted
-      // transactions will be rolled back when this object is deleted.
-      Connection();
-      ~Connection();
-
-      void Open(const std::string& path);
-
-      void OpenInMemory();
-
-      void Close();
-
-      bool Execute(const char* sql);
-
-      bool Execute(const std::string& sql)
-      {
-        return Execute(sql.c_str());
-      }
-
-      void FlushToDisk();
-
-      IScalarFunction* Register(IScalarFunction* func);  // Takes the ownership of the function
-
-      // Info querying -------------------------------------------------------------
-
-      // Used to check a |sql| statement for syntactic validity. If the
-      // statement is valid SQL, returns true.
-      bool IsSQLValid(const char* sql);
-
-      // Returns true if the given table exists.
-      bool DoesTableExist(const char* table_name) const;
-
-      // Returns true if the given index exists.
-      bool DoesIndexExist(const char* index_name) const;
-    
-      // Returns true if a column with the given name exists in the given table.
-      bool DoesColumnExist(const char* table_name, const char* column_name) const;
-
-      // Returns sqlite's internal ID for the last inserted row. Valid only
-      // immediately after an insert.
-      int64_t GetLastInsertRowId() const;
-
-      // Returns sqlite's count of the number of rows modified by the last
-      // statement executed. Will be 0 if no statement has executed or the database
-      // is closed.
-      int GetLastChangeCount() const;
-
-      // Errors --------------------------------------------------------------------
-
-      // Returns the error code associated with the last sqlite operation.
-      int GetErrorCode() const;
-
-      // Returns the errno associated with GetErrorCode().  See
-      // SQLITE_LAST_ERRNO in SQLite documentation.
-      int GetLastErrno() const;
-
-      // Returns a pointer to a statically allocated string associated with the
-      // last sqlite operation.
-      const char* GetErrorMessage() const;
-
-
-      // Diagnostics (for unit tests) ----------------------------------------------
-
-      int ExecuteAndReturnErrorCode(const char* sql);
-    
-      bool HasCachedStatement(const StatementId& id) const
-      {
-        return cachedStatements_.find(id) != cachedStatements_.end();
-      }
-
-      int GetTransactionNesting() const
-      {
-        return transactionNesting_;
-      }
-
-      // Transactions --------------------------------------------------------------
-
-      bool BeginTransaction();
-      void RollbackTransaction();
-      bool CommitTransaction();      
-    };
-  }
-}
--- a/Core/SQLite/FunctionContext.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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.
- **/
-
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../PrecompiledHeaders.h"
-#endif
-
-#include "FunctionContext.h"
-#include "OrthancSQLiteException.h"
-
-#include <string>
-
-#include "sqlite3.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    FunctionContext::FunctionContext(struct sqlite3_context* context,
-                                     int argc,
-                                     Internals::SQLiteValue** argv)
-    {
-      assert(context != NULL);
-      assert(argc >= 0);
-      assert(argv != NULL);
-
-      context_ = context;
-      argc_ = static_cast<unsigned int>(argc);
-      argv_ = argv;
-    }
-
-    void FunctionContext::CheckIndex(unsigned int index) const
-    {
-      if (index >= argc_)
-      {
-        throw OrthancSQLiteException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-    ColumnType FunctionContext::GetColumnType(unsigned int index) const
-    {
-      CheckIndex(index);
-      return static_cast<SQLite::ColumnType>(sqlite3_value_type(argv_[index]));
-    }
-
-    int FunctionContext::GetIntValue(unsigned int index) const
-    {
-      CheckIndex(index);
-      return sqlite3_value_int(argv_[index]);
-    }
-
-    int64_t FunctionContext::GetInt64Value(unsigned int index) const
-    {
-      CheckIndex(index);
-      return sqlite3_value_int64(argv_[index]);
-    }
-
-    double FunctionContext::GetDoubleValue(unsigned int index) const
-    {
-      CheckIndex(index);
-      return sqlite3_value_double(argv_[index]);
-    }
-
-    std::string FunctionContext::GetStringValue(unsigned int index) const
-    {
-      CheckIndex(index);
-      return std::string(reinterpret_cast<const char*>(sqlite3_value_text(argv_[index])));
-    }
-
-    bool FunctionContext::IsNullValue(unsigned int index) const
-    {
-      CheckIndex(index);
-      return sqlite3_value_type(argv_[index]) == SQLITE_NULL;
-    }
-  
-    void FunctionContext::SetNullResult()
-    {
-      sqlite3_result_null(context_);
-    }
-
-    void FunctionContext::SetIntResult(int value)
-    {
-      sqlite3_result_int(context_, value);
-    }
-
-    void FunctionContext::SetDoubleResult(double value)
-    {
-      sqlite3_result_double(context_, value);
-    }
-
-    void FunctionContext::SetStringResult(const std::string& str)
-    {
-      sqlite3_result_text(context_, str.data(), static_cast<int>(str.size()), SQLITE_TRANSIENT);
-    }
-  }
-}
--- a/Core/SQLite/FunctionContext.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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
-
-#include "Statement.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class ORTHANC_PUBLIC FunctionContext : public NonCopyable
-    {
-      friend class Connection;
-
-    private:
-      struct sqlite3_context* context_;
-      unsigned int argc_;
-      Internals::SQLiteValue** argv_;
-
-      void CheckIndex(unsigned int index) const;
-
-    public:
-      FunctionContext(struct sqlite3_context* context,
-                      int argc,
-                      Internals::SQLiteValue** argv);
-
-      ColumnType GetColumnType(unsigned int index) const;
- 
-      unsigned int GetParameterCount() const
-      {
-        return argc_;
-      }
-
-      int GetIntValue(unsigned int index) const;
-
-      int64_t GetInt64Value(unsigned int index) const;
-
-      double GetDoubleValue(unsigned int index) const;
-
-      std::string GetStringValue(unsigned int index) const;
-
-      bool IsNullValue(unsigned int index) const;
-  
-      void SetNullResult();
-
-      void SetIntResult(int value);
-
-      void SetDoubleResult(double value);
-
-      void SetStringResult(const std::string& str);
-    };
-  }
-}
--- a/Core/SQLite/IScalarFunction.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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
-
-#include "NonCopyable.h"
-#include "FunctionContext.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class IScalarFunction : public NonCopyable
-    {
-    public:
-      virtual ~IScalarFunction()
-      {
-      }
-
-      virtual const char* GetName() const = 0;
-
-      virtual unsigned int GetCardinality() const = 0;
-
-      virtual void Compute(FunctionContext& context) = 0;
-    };
-  }
-}
--- a/Core/SQLite/ITransaction.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-#include "NonCopyable.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class ITransaction : public NonCopyable
-    {
-    public:
-      virtual ~ITransaction()
-      {
-      }
-
-      // Begins the transaction. This uses the default sqlite "deferred" transaction
-      // type, which means that the DB lock is lazily acquired the next time the
-      // database is accessed, not in the begin transaction command.
-      virtual void Begin() = 0;
-
-      // Rolls back the transaction. This will happen automatically if you do
-      // nothing when the transaction goes out of scope.
-      virtual void Rollback() = 0;
-
-      // Commits the transaction, returning true on success.
-      virtual void Commit() = 0;
-    };
-  }
-}
--- a/Core/SQLite/NonCopyable.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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 Google Inc., 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
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    // This class mimics "boost::noncopyable"
-    class NonCopyable
-    {
-    private:
-      NonCopyable(const NonCopyable&);
-
-      NonCopyable& operator= (const NonCopyable&);
-
-    protected:
-      NonCopyable()
-      {
-      }
-
-      ~NonCopyable()
-      {
-      }
-    };
-  }
-}
--- a/Core/SQLite/OrthancSQLiteException.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-
-#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>
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    // Auto-generated by "Resources/GenerateErrorCodes.py"
-    enum ErrorCode
-    {
-      ErrorCode_ParameterOutOfRange,
-      ErrorCode_BadParameterType,
-      ErrorCode_SQLiteNotOpened,
-      ErrorCode_SQLiteAlreadyOpened,
-      ErrorCode_SQLiteCannotOpen,
-      ErrorCode_SQLiteStatementAlreadyUsed,
-      ErrorCode_SQLiteExecute,
-      ErrorCode_SQLiteRollbackWithoutTransaction,
-      ErrorCode_SQLiteCommitWithoutTransaction,
-      ErrorCode_SQLiteRegisterFunction,
-      ErrorCode_SQLiteFlush,
-      ErrorCode_SQLiteCannotRun,
-      ErrorCode_SQLiteCannotStep,
-      ErrorCode_SQLiteBindOutOfRange,
-      ErrorCode_SQLitePrepareStatement,
-      ErrorCode_SQLiteTransactionAlreadyStarted,
-      ErrorCode_SQLiteTransactionCommit,
-      ErrorCode_SQLiteTransactionBegin
-    };
-
-    class OrthancSQLiteException : public ::std::runtime_error
-    {
-    public:
-      OrthancSQLiteException(ErrorCode error) :
-        ::std::runtime_error(EnumerationToString(error))
-      {
-      }
-
-      // Auto-generated by "Resources/GenerateErrorCodes.py"
-      static const char* EnumerationToString(ErrorCode code)
-      {
-        switch (code)
-        {
-          case ErrorCode_ParameterOutOfRange:
-            return "Parameter out of range";
-
-          case ErrorCode_BadParameterType:
-            return "Bad type for a parameter";
-
-          case ErrorCode_SQLiteNotOpened:
-            return "SQLite: The database is not opened";
-
-          case ErrorCode_SQLiteAlreadyOpened:
-            return "SQLite: Connection is already open";
-
-          case ErrorCode_SQLiteCannotOpen:
-            return "SQLite: Unable to open the database";
-
-          case ErrorCode_SQLiteStatementAlreadyUsed:
-            return "SQLite: This cached statement is already being referred to";
-
-          case ErrorCode_SQLiteExecute:
-            return "SQLite: Cannot execute a command";
-
-          case ErrorCode_SQLiteRollbackWithoutTransaction:
-            return "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)";
-
-          case ErrorCode_SQLiteCommitWithoutTransaction:
-            return "SQLite: Committing a nonexistent transaction";
-
-          case ErrorCode_SQLiteRegisterFunction:
-            return "SQLite: Unable to register a function";
-
-          case ErrorCode_SQLiteFlush:
-            return "SQLite: Unable to flush the database";
-
-          case ErrorCode_SQLiteCannotRun:
-            return "SQLite: Cannot run a cached statement";
-
-          case ErrorCode_SQLiteCannotStep:
-            return "SQLite: Cannot step over a cached statement";
-
-          case ErrorCode_SQLiteBindOutOfRange:
-            return "SQLite: Bing a value while out of range (serious error)";
-
-          case ErrorCode_SQLitePrepareStatement:
-            return "SQLite: Cannot prepare a cached statement";
-
-          case ErrorCode_SQLiteTransactionAlreadyStarted:
-            return "SQLite: Beginning the same transaction twice";
-
-          case ErrorCode_SQLiteTransactionCommit:
-            return "SQLite: Failure when committing the transaction";
-
-          case ErrorCode_SQLiteTransactionBegin:
-            return "SQLite: Cannot start a transaction";
-
-          default:
-            return "Unknown error code";
-        }
-      }
-    };
-  }
-}
-
-#else
-#  include "../OrthancException.h"
-#  define OrthancSQLiteException ::Orthanc::OrthancException
-#endif
--- a/Core/SQLite/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-Introduction
-============
-
-The code in this folder is a standalone object-oriented wrapper around
-SQLite3. It is derived from the code of Chromium:
-
-http://src.chromium.org/viewvc/chrome/trunk/src/sql/
-http://maxradi.us/documents/sqlite/
-
-
-Main differences with Chromium
-==============================
-
-* The reference counting mechanism has been reimplemented to make it 
-  simpler.
-* The OrthancException class is used for the exception mechanisms.
-* A statement is always valid (is_valid() always return true).
-* The classes and the methods have been renamed to meet Orthanc's
-  coding conventions.
-
-
-Reuse in another software
-=========================
-
-To use the Orthanc SQLite wrapper in another project than Orthanc, you
-just have to define the "ORTHANC_SQLITE_STANDALONE" macro.
-
-All the C++ exceptions generated by the wrapper will be objects of the
-class "::Orthanc::SQLite::OrthancSQLiteException", that derives from
-the standard exception class "::std::runtime_error".
-
-
-Licensing
-=========
-
-The code in this folder is licensed under the 3-clause BSD license, in
-order to respect the original license of the code.
-
-It is pretty straightforward to extract the code from this folder and
-to include it in another project. 
--- a/Core/SQLite/SQLiteTypes.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,370 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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.
- **/
-
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../PrecompiledHeaders.h"
-#endif
-
-#include "Statement.h"
-#include "Connection.h"
-
-#include <string.h>
-#include <stdio.h>
-#include <algorithm>
-
-#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"
-
-#if defined(_MSC_VER)
-#define snprintf _snprintf
-#endif
-
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    int Statement::CheckError(int err, ErrorCode code) const
-    {
-      bool succeeded = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE);
-      if (!succeeded)
-      {
-#if ORTHANC_SQLITE_STANDALONE != 1
-        char buffer[128];
-        snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err);
-        LOG(ERROR) << buffer;
-#endif
-
-        throw OrthancSQLiteException(code);
-      }
-
-      return err;
-    }
-
-    void Statement::CheckOk(int err, ErrorCode code) const 
-    {
-      if (err == SQLITE_RANGE)
-      {
-        // Binding to a non-existent variable is evidence of a serious error.
-        throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange);
-      }
-      else if (err != SQLITE_OK)
-      {
-#if ORTHANC_SQLITE_STANDALONE != 1
-        char buffer[128];
-        snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err);
-        LOG(ERROR) << buffer;
-#endif
-
-        throw OrthancSQLiteException(code);
-      }
-    }
-
-
-    Statement::Statement(Connection& database,
-                         const StatementId& id,
-                         const std::string& sql) : 
-      reference_(database.GetCachedStatement(id, sql.c_str()))
-    {
-      Reset(true);
-      LOG_CREATE(sql);
-    }
-
-
-    Statement::Statement(Connection& database,
-                         const StatementId& id,
-                         const char* sql) : 
-      reference_(database.GetCachedStatement(id, sql))
-    {
-      Reset(true);
-      LOG_CREATE(sql);
-    }
-
-
-    Statement::Statement(Connection& database,
-                         const std::string& sql) :
-      reference_(database.GetWrappedObject(), sql.c_str())
-    {
-      LOG_CREATE(sql);
-    }
-
-
-    Statement::Statement(Connection& database,
-                         const char* sql) :
-      reference_(database.GetWrappedObject(), sql)
-    {
-      LOG_CREATE(sql);
-    }
-
-
-    bool Statement::Run()
-    {
-      LOG_APPLY(sqlite3_sql(GetStatement()));
-
-      return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotRun) == SQLITE_DONE;
-    }
-
-    bool Statement::Step()
-    {
-      LOG_APPLY(sqlite3_sql(GetStatement()));
-
-      return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotStep) == SQLITE_ROW;
-    }
-
-    void Statement::Reset(bool clear_bound_vars) 
-    {
-      // We don't call CheckError() here because sqlite3_reset() returns
-      // the last error that Step() caused thereby generating a second
-      // spurious error callback.
-      if (clear_bound_vars)
-        sqlite3_clear_bindings(GetStatement());
-      //VLOG(1) << "SQLite::Statement::Reset";
-      sqlite3_reset(GetStatement());
-    }
-
-    std::string Statement::GetOriginalSQLStatement()
-    {
-      return std::string(sqlite3_sql(GetStatement()));
-    }
-
-
-    void Statement::BindNull(int col)
-    {
-      CheckOk(sqlite3_bind_null(GetStatement(), col + 1),
-              ErrorCode_BadParameterType);
-    }
-
-    void Statement::BindBool(int col, bool val) 
-    {
-      BindInt(col, val ? 1 : 0);
-    }
-
-    void Statement::BindInt(int col, int val) 
-    {
-      CheckOk(sqlite3_bind_int(GetStatement(), col + 1, val),
-              ErrorCode_BadParameterType);
-    }
-
-    void Statement::BindInt64(int col, int64_t val) 
-    {
-      CheckOk(sqlite3_bind_int64(GetStatement(), col + 1, val),
-              ErrorCode_BadParameterType);
-    }
-
-    void Statement::BindDouble(int col, double val) 
-    {
-      CheckOk(sqlite3_bind_double(GetStatement(), col + 1, val),
-              ErrorCode_BadParameterType);
-    }
-
-    void Statement::BindCString(int col, const char* val) 
-    {
-      CheckOk(sqlite3_bind_text(GetStatement(), col + 1, val, -1, SQLITE_TRANSIENT),
-              ErrorCode_BadParameterType);
-    }
-
-    void Statement::BindString(int col, const std::string& val) 
-    {
-      CheckOk(sqlite3_bind_text(GetStatement(),
-                                col + 1,
-                                val.data(),
-                                static_cast<int>(val.size()),
-                                SQLITE_TRANSIENT),
-              ErrorCode_BadParameterType);
-    }
-
-    /*void Statement::BindString16(int col, const string16& value) 
-      {
-      BindString(col, UTF16ToUTF8(value));
-      }*/
-
-    void Statement::BindBlob(int col, const void* val, int val_len) 
-    {
-      CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, val_len, SQLITE_TRANSIENT),
-              ErrorCode_BadParameterType);
-    }
-
-
-    int Statement::ColumnCount() const 
-    {
-      return sqlite3_column_count(GetStatement());
-    }
-
-
-    ColumnType Statement::GetColumnType(int col) const 
-    {
-      // Verify that our enum matches sqlite's values.
-      assert(COLUMN_TYPE_INTEGER == SQLITE_INTEGER);
-      assert(COLUMN_TYPE_FLOAT == SQLITE_FLOAT);
-      assert(COLUMN_TYPE_TEXT == SQLITE_TEXT);
-      assert(COLUMN_TYPE_BLOB == SQLITE_BLOB);
-      assert(COLUMN_TYPE_NULL == SQLITE_NULL);
-
-      return static_cast<ColumnType>(sqlite3_column_type(GetStatement(), col));
-    }
-
-    ColumnType Statement::GetDeclaredColumnType(int col) const 
-    {
-      std::string column_type(sqlite3_column_decltype(GetStatement(), col));
-      std::transform(column_type.begin(), column_type.end(), column_type.begin(), tolower);
-
-      if (column_type == "integer")
-        return COLUMN_TYPE_INTEGER;
-      else if (column_type == "float")
-        return COLUMN_TYPE_FLOAT;
-      else if (column_type == "text")
-        return COLUMN_TYPE_TEXT;
-      else if (column_type == "blob")
-        return COLUMN_TYPE_BLOB;
-
-      return COLUMN_TYPE_NULL;
-    }
-
-    bool Statement::ColumnIsNull(int col) const 
-    {
-      return sqlite3_column_type(GetStatement(), col) == SQLITE_NULL;
-    }
-
-    bool Statement::ColumnBool(int col) const 
-    {
-      return !!ColumnInt(col);
-    }
-
-    int Statement::ColumnInt(int col) const 
-    {
-      return sqlite3_column_int(GetStatement(), col);
-    }
-
-    int64_t Statement::ColumnInt64(int col) const 
-    {
-      return sqlite3_column_int64(GetStatement(), col);
-    }
-
-    double Statement::ColumnDouble(int col) const 
-    {
-      return sqlite3_column_double(GetStatement(), col);
-    }
-
-    std::string Statement::ColumnString(int col) const 
-    {
-      const char* str = reinterpret_cast<const char*>(
-        sqlite3_column_text(GetStatement(), col));
-      int len = sqlite3_column_bytes(GetStatement(), col);
-
-      std::string result;
-      if (str && len > 0)
-        result.assign(str, len);
-      return result;
-    }
-
-    /*string16 Statement::ColumnString16(int col) const 
-      {
-      std::string s = ColumnString(col);
-      return !s.empty() ? UTF8ToUTF16(s) : string16();
-      }*/
-
-    int Statement::ColumnByteLength(int col) const 
-    {
-      return sqlite3_column_bytes(GetStatement(), col);
-    }
-
-    const void* Statement::ColumnBlob(int col) const 
-    {
-      return sqlite3_column_blob(GetStatement(), col);
-    }
-
-    bool Statement::ColumnBlobAsString(int col, std::string* blob) 
-    {
-      const void* p = ColumnBlob(col);
-      size_t len = ColumnByteLength(col);
-      blob->resize(len);
-      if (blob->size() != len) {
-        return false;
-      }
-      blob->assign(reinterpret_cast<const char*>(p), len);
-      return true;
-    }
-
-    /*bool Statement::ColumnBlobAsString16(int col, string16* val) const 
-      {
-      const void* data = ColumnBlob(col);
-      size_t len = ColumnByteLength(col) / sizeof(char16);
-      val->resize(len);
-      if (val->size() != len)
-      return false;
-      val->assign(reinterpret_cast<const char16*>(data), len);
-      return true;
-      }*/
-
-    /*bool Statement::ColumnBlobAsVector(int col, std::vector<char>* val) const 
-    {
-      val->clear();
-
-      const void* data = sqlite3_column_blob(GetStatement(), col);
-      int len = sqlite3_column_bytes(GetStatement(), col);
-      if (data && len > 0) {
-        val->resize(len);
-        memcpy(&(*val)[0], data, len);
-      }
-      return true;
-      }*/
-
-    /*bool Statement::ColumnBlobAsVector(
-      int col,
-      std::vector<unsigned char>* val) const 
-    {
-      return ColumnBlobAsVector(col, reinterpret_cast< std::vector<char>* >(val));
-      }*/
-
-  }
-}
--- a/Core/SQLite/Statement.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,174 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-#include "NonCopyable.h"
-#include "OrthancSQLiteException.h"
-#include "StatementId.h"
-#include "StatementReference.h"
-
-#include <vector>
-#include <stdint.h>
-
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-#  include <gtest/gtest_prod.h>
-#endif
-
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class Connection;
-
-    // Possible return values from ColumnType in a statement. These
-    // should match the values in sqlite3.h.
-    enum ColumnType 
-    {
-      COLUMN_TYPE_INTEGER = 1,
-      COLUMN_TYPE_FLOAT = 2,
-      COLUMN_TYPE_TEXT = 3,
-      COLUMN_TYPE_BLOB = 4,
-      COLUMN_TYPE_NULL = 5
-    };
-
-    class ORTHANC_PUBLIC Statement : public NonCopyable
-    {
-      friend class Connection;
-
-#if ORTHANC_BUILD_UNIT_TESTS == 1
-      FRIEND_TEST(SQLStatementTest, Run);
-      FRIEND_TEST(SQLStatementTest, Reset);
-#endif
-
-    private:
-      StatementReference  reference_;
-
-      int CheckError(int err, 
-                     ErrorCode code) const;
-
-      void CheckOk(int err, 
-                   ErrorCode code) const;
-
-      struct sqlite3_stmt* GetStatement() const
-      {
-        return reference_.GetWrappedObject();
-      }
-
-    public:
-      Statement(Connection& database,
-                const std::string& sql);
-
-      Statement(Connection& database,
-                const StatementId& id,
-                const std::string& sql);
-
-      Statement(Connection& database,
-                const char* sql);
-
-      Statement(Connection& database,
-                const StatementId& id,
-                const char* sql);
-
-      ~Statement()
-      {
-        Reset();
-      }
-
-      bool Run();
-
-      bool Step();
-
-      // Diagnostics --------------------------------------------------------------
-
-      std::string GetOriginalSQLStatement();
-
-
-      // Binding -------------------------------------------------------------------
-
-      // These all take a 0-based argument index
-      void BindNull(int col);
-      void BindBool(int col, bool val);
-      void BindInt(int col, int val);
-      void BindInt64(int col, int64_t val);
-      void BindDouble(int col, double val);
-      void BindCString(int col, const char* val);
-      void BindString(int col, const std::string& val);
-      //void BindString16(int col, const string16& value);
-      void BindBlob(int col, const void* value, int value_len);
-
-
-      // Retrieving ----------------------------------------------------------------
-
-      // Returns the number of output columns in the result.
-      int ColumnCount() const;
-
-      // Returns the type associated with the given column.
-      //
-      // Watch out: the type may be undefined if you've done something to cause a
-      // "type conversion." This means requesting the value of a column of a type
-      // where that type is not the native type. For safety, call ColumnType only
-      // on a column before getting the value out in any way.
-      ColumnType GetColumnType(int col) const;
-      ColumnType GetDeclaredColumnType(int col) const;
-
-      // These all take a 0-based argument index.
-      bool ColumnIsNull(int col) const ;
-      bool ColumnBool(int col) const;
-      int ColumnInt(int col) const;
-      int64_t ColumnInt64(int col) const;
-      double ColumnDouble(int col) const;
-      std::string ColumnString(int col) const;
-      //string16 ColumnString16(int col) const;
-
-      // When reading a blob, you can get a raw pointer to the underlying data,
-      // along with the length, or you can just ask us to copy the blob into a
-      // vector. Danger! ColumnBlob may return NULL if there is no data!
-      int ColumnByteLength(int col) const;
-      const void* ColumnBlob(int col) const;
-      bool ColumnBlobAsString(int col, std::string* blob);
-      //bool ColumnBlobAsString16(int col, string16* val) const;
-      //bool ColumnBlobAsVector(int col, std::vector<char>* val) const;
-      //bool ColumnBlobAsVector(int col, std::vector<unsigned char>* val) const;
-
-      // Resets the statement to its initial condition. This includes any current
-      // result row, and also the bound variables if the |clear_bound_vars| is true.
-      void Reset(bool clear_bound_vars = true);
-    };
-  }
-}
--- a/Core/SQLite/StatementId.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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.
- **/
-
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../PrecompiledHeaders.h"
-#endif
-
-#include "StatementId.h"
-
-#include <string.h>
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    bool StatementId::operator< (const StatementId& other) const
-    {
-      if (line_ != other.line_)
-        return line_ < other.line_;
-
-      return strcmp(file_, other.file_) < 0;
-    }
-  }
-}
--- a/Core/SQLite/StatementId.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-#if ORTHANC_SQLITE_STANDALONE == 1
-#  define ORTHANC_PUBLIC
-#else
-#  include "../OrthancFramework.h"
-#endif
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class ORTHANC_PUBLIC StatementId
-    {
-    private:
-      const char* file_;
-      int line_;
-
-      StatementId(); // Forbidden
-
-    public:
-      StatementId(const char* file, int line) : file_(file), line_(line)
-      {
-      }
-
-      bool operator< (const StatementId& other) const;
-    };
-  }
-}
--- a/Core/SQLite/StatementReference.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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.
- **/
-
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../PrecompiledHeaders.h"
-#endif
-
-#include "StatementReference.h"
-#include "OrthancSQLiteException.h"
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../Logging.h"
-#endif
-
-#include <string>
-#include <cassert>
-#include "sqlite3.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    bool StatementReference::IsRoot() const
-    {
-      return root_ == NULL;
-    }
-
-    StatementReference::StatementReference()
-    {
-      root_ = NULL;
-      refCount_ = 0;
-      statement_ = NULL;
-      assert(IsRoot());
-    }
-
-    StatementReference::StatementReference(sqlite3* database,
-                                           const char* sql)
-    {
-      if (database == NULL || sql == NULL)
-      {
-        throw OrthancSQLiteException(ErrorCode_ParameterOutOfRange);
-      }
-
-      root_ = NULL;
-      refCount_ = 0;
-
-      int error = sqlite3_prepare_v2(database, sql, -1, &statement_, NULL);
-      if (error != SQLITE_OK)
-      {
-#if ORTHANC_SQLITE_STANDALONE != 1
-        int extended = sqlite3_extended_errcode(database);
-        LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database) << " (" << extended << ")";
-        if (extended == SQLITE_IOERR_SHMSIZE  /* 4874 */)
-        {
-          LOG(ERROR) << "  This probably indicates that your filesystem is full";
-        }        
-#endif
-
-        throw OrthancSQLiteException(ErrorCode_SQLitePrepareStatement);
-      }
-
-      assert(IsRoot());
-    }
-
-    StatementReference::StatementReference(StatementReference& other)
-    {
-      refCount_ = 0;
-
-      if (other.IsRoot())
-      {
-        root_ = &other;
-      }
-      else
-      {
-        root_ = other.root_;
-      }
-
-      root_->refCount_++;
-      statement_ = root_->statement_;
-
-      assert(!IsRoot());
-    }
-
-    StatementReference::~StatementReference()
-    {
-      if (IsRoot())
-      {
-        if (refCount_ != 0)
-        {
-          // There remain references to this object. We cannot throw
-          // an exception because:
-          // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-          LOG(ERROR) << "Bad value of the reference counter";
-#endif
-        }
-        else if (statement_ != NULL)
-        {
-          sqlite3_finalize(statement_);
-        }
-      }
-      else
-      {
-        if (root_->refCount_ == 0)
-        {
-          // There remain references to this object. We cannot throw
-          // an exception because:
-          // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-          LOG(ERROR) << "Bad value of the reference counter";
-#endif
-        }
-        else
-        {
-          root_->refCount_--;
-        }
-      }
-    }
-
-    uint32_t StatementReference::GetReferenceCount() const
-    {
-      return refCount_;
-    }
-  }
-}
--- a/Core/SQLite/StatementReference.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-#if ORTHANC_SQLITE_STANDALONE == 1
-#  define ORTHANC_PUBLIC
-#else
-#  include "../OrthancFramework.h"
-#endif
-
-#include "NonCopyable.h"
-#include "SQLiteTypes.h"
-
-#include <stdint.h>
-#include <cassert>
-#include <stdlib.h>
-
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class ORTHANC_PUBLIC StatementReference : NonCopyable
-    {
-    private:
-      StatementReference* root_;   // Only used for non-root nodes
-      uint32_t refCount_;         // Only used for root node
-      struct sqlite3_stmt* statement_;
-
-      bool IsRoot() const;
-
-    public:
-      StatementReference();
-
-      StatementReference(sqlite3* database,
-                         const char* sql);
-
-      StatementReference(StatementReference& other);
-
-      ~StatementReference();
-
-      uint32_t GetReferenceCount() const;
-
-      struct sqlite3_stmt* GetWrappedObject() const
-      {
-        assert(statement_ != NULL);
-        return statement_;
-      }
-    };
-  }
-}
--- a/Core/SQLite/Transaction.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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.
- **/
-
-
-#if ORTHANC_SQLITE_STANDALONE != 1
-#include "../PrecompiledHeaders.h"
-#endif
-
-#include "Transaction.h"
-#include "OrthancSQLiteException.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    Transaction::Transaction(Connection& connection) :
-      connection_(connection),
-      isOpen_(false)
-    {
-    }
-
-    Transaction::~Transaction()
-    {
-      if (isOpen_)
-      {
-        connection_.RollbackTransaction();
-      }
-    }
-
-    void Transaction::Begin()
-    {
-      if (isOpen_) 
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteTransactionAlreadyStarted);
-      }
-
-      isOpen_ = connection_.BeginTransaction();
-      if (!isOpen_)
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteTransactionBegin);
-      }
-    }
-
-    void Transaction::Rollback() 
-    {
-      if (!isOpen_) 
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction);
-      }
-
-      isOpen_ = false;
-
-      connection_.RollbackTransaction();
-    }
-
-    void Transaction::Commit() 
-    {
-      if (!isOpen_) 
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction);
-      }
-
-      isOpen_ = false;
-
-      if (!connection_.CommitTransaction())
-      {
-        throw OrthancSQLiteException(ErrorCode_SQLiteTransactionCommit);
-      }
-    }
-  }
-}
--- a/Core/SQLite/Transaction.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- *
- * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
- * Medical Physics Department, CHU of Liege, Belgium
- *
- * Copyright (c) 2012 The Chromium Authors. 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., 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
-
-#include "Connection.h"
-#include "ITransaction.h"
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class ORTHANC_PUBLIC Transaction : public ITransaction
-    {
-    private:
-      Connection& connection_;
-
-      // True when the transaction is open, false when it's already been committed
-      // or rolled back.
-      bool isOpen_;
-
-    public:
-      explicit Transaction(Connection& connection);
-
-      virtual ~Transaction();
-
-      // Returns true when there is a transaction that has been successfully begun.
-      bool IsOpen() const { return isOpen_; }
-
-      virtual void Begin();
-
-      virtual void Rollback();
-
-      virtual void Commit();
-    };
-  }
-}
--- a/Core/SerializationToolbox.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,444 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#if ORTHANC_ENABLE_DCMTK == 1
-#  include "DicomParsing/FromDcmtkBridge.h"
-#endif
-
-namespace Orthanc
-{
-  static bool ParseTagInternal(DicomTag& tag,
-                               const char* name)
-  {
-#if ORTHANC_ENABLE_DCMTK == 1
-    try
-    {
-      tag = FromDcmtkBridge::ParseTag(name);
-      return true;
-    }
-    catch (OrthancException&)
-    {
-      return false;
-    }
-#else
-    return DicomTag::ParseHexadecimal(tag, name);
-#endif   
-  }
-
-    
-  std::string SerializationToolbox::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,
-                             "String value expected in field: " + field);
-    }
-    else
-    {
-      return value[field.c_str()].asString();
-    }
-  }
-
-
-  int SerializationToolbox::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,
-                             "Integer value expected in field: " + field);
-    }
-    else
-    {
-      return value[field.c_str()].asInt();
-    }    
-  }
-
-
-  int SerializationToolbox::ReadInteger(const Json::Value& value,
-                                        const std::string& field,
-                                        int defaultValue)
-  {
-    if (value.isMember(field.c_str()))
-    {
-      return ReadInteger(value, field);
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  unsigned int SerializationToolbox::ReadUnsignedInteger(const Json::Value& value,
-                                                         const std::string& field)
-  {
-    int tmp = ReadInteger(value, field);
-
-    if (tmp < 0)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Unsigned integer value expected in field: " + field);
-    }
-    else
-    {
-      return static_cast<unsigned int>(tmp);
-    }
-  }
-
-
-  bool SerializationToolbox::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,
-                             "Boolean value expected in field: " + field);
-    }
-    else
-    {
-      return value[field.c_str()].asBool();
-    }   
-  }
-
-  
-  void SerializationToolbox::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,
-                             "List of strings expected in field: " + field);
-    }
-
-    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,
-                               "List of strings expected in field: " + field);
-      }
-      else
-      {
-        target[i] = arr[i].asString();
-      }
-    }
-  }
-
-
-  void SerializationToolbox::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 SerializationToolbox::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 SerializationToolbox::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,
-                             "Set of DICOM tags expected in field: " + field);
-    }
-
-    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 ||
-          !ParseTagInternal(tag, arr[i].asCString()))
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Set of DICOM tags expected in field: " + field);
-      }
-      else
-      {
-        target.insert(tag);
-      }
-    }
-  }
-
-
-  void SerializationToolbox::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,
-                             "Associative array of strings to strings expected in field: " + field);
-    }
-
-    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,
-                               "Associative array of string to strings expected in field: " + field);
-      }
-      else
-      {
-        target[members[i]] = tmp.asString();
-      }
-    }
-  }
-
-
-  void SerializationToolbox::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,
-                             "Associative array of DICOM tags to strings expected in field: " + field);
-    }
-
-    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 (!ParseTagInternal(tag, members[i].c_str()) ||
-          tmp.type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Associative array of DICOM tags to strings expected in field: " + field);
-      }
-      else
-      {
-        target[tag] = tmp.asString();
-      }
-    }
-  }
-
-
-  void SerializationToolbox::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 SerializationToolbox::WriteListOfStrings(Json::Value& target,
-                                                const std::list<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::list<std::string>::const_iterator it = values.begin();
-         it != values.end(); ++it)
-    {
-      value.append(*it);
-    }
-  }
-
-
-  void SerializationToolbox::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 SerializationToolbox::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 SerializationToolbox::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 SerializationToolbox::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;
-    }
-  }
-}
--- a/Core/SerializationToolbox.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#include <json/value.h>
-#include <list>
-#include <map>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC SerializationToolbox
-  {
-  public:
-    static std::string ReadString(const Json::Value& value,
-                                  const std::string& field);
-
-    static int ReadInteger(const Json::Value& value,
-                           const std::string& field);
-
-    static int ReadInteger(const Json::Value& value,
-                           const std::string& field,
-                           int defaultValue);
-
-    static unsigned int ReadUnsignedInteger(const Json::Value& value,
-                                            const std::string& field);
-
-    static bool ReadBoolean(const Json::Value& value,
-                            const std::string& field);
-
-    static void ReadArrayOfStrings(std::vector<std::string>& target,
-                                   const Json::Value& value,
-                                   const std::string& field);
-
-    static void ReadListOfStrings(std::list<std::string>& target,
-                                  const Json::Value& value,
-                                  const std::string& field);
-
-    static void ReadSetOfStrings(std::set<std::string>& target,
-                                 const Json::Value& value,
-                                 const std::string& field);
-
-    static void ReadSetOfTags(std::set<DicomTag>& target,
-                              const Json::Value& value,
-                              const std::string& field);
-
-    static void ReadMapOfStrings(std::map<std::string, std::string>& values,
-                                 const Json::Value& target,
-                                 const std::string& field);
-
-    static void ReadMapOfTags(std::map<DicomTag, std::string>& values,
-                              const Json::Value& target,
-                              const std::string& field);
-
-    static void WriteArrayOfStrings(Json::Value& target,
-                                    const std::vector<std::string>& values,
-                                    const std::string& field);
-
-    static void WriteListOfStrings(Json::Value& target,
-                                   const std::list<std::string>& values,
-                                   const std::string& field);
-
-    static void WriteSetOfStrings(Json::Value& target,
-                                  const std::set<std::string>& values,
-                                  const std::string& field);
-
-    static void WriteSetOfTags(Json::Value& target,
-                               const std::set<DicomTag>& tags,
-                               const std::string& field);
-
-    static void WriteMapOfStrings(Json::Value& target,
-                                  const std::map<std::string, std::string>& values,
-                                  const std::string& field);
-
-    static void WriteMapOfTags(Json::Value& target,
-                               const std::map<DicomTag, std::string>& values,
-                               const std::string& field);
-  };
-}
--- a/Core/SharedLibrary.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,154 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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__)
-   
-    /**
-     * "RTLD_LOCAL" is the default, and is only present to be
-     * explicit. "RTLD_DEEPBIND" was added in Orthanc 1.6.0, in order
-     * to avoid crashes while loading plugins from the LSB binaries of
-     * the Orthanc core.
-     *
-     * BUT this had no effect, and this results in a crash if loading
-     * the Python 2.7 plugin => We disabled it again in Orthanc 1.6.1.
-     **/
-    
-#if 0 // && defined(RTLD_DEEPBIND)  // This is a GNU extension
-    // Disabled in Orthanc 1.6.1
-    handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
-#else
-    handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL);
-#endif
-
-    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)
-    {
-      throw OrthancException(
-        ErrorCode_SharedLibrary,
-        "Shared library does not expose function \"" + name + "\"");
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  bool SharedLibrary::HasFunction(const std::string& name)
-  {
-    return GetFunctionInternal(name) != NULL;
-  }
-}
--- a/Core/SharedLibrary.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#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 ORTHANC_PUBLIC 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);
-  };
-}
--- a/Core/SystemToolbox.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,764 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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()"
-#  include <stdlib.h>    // For "environ"
-#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>
-
-
-/*=========================================================================
-  The section below comes from the Boost 1.68.0 project:
-  https://github.com/boostorg/program_options/blob/boost-1.68.0/src/parsers.cpp
-  
-  Copyright Vladimir Prus 2002-2004.
-  Distributed under the Boost Software License, Version 1.0.
-  (See accompanying file LICENSE_1_0.txt
-  or copy at http://www.boost.org/LICENSE_1_0.txt)
-  =========================================================================*/
-
-// The 'environ' should be declared in some cases. E.g. Linux man page says:
-// (This variable must be declared in the user program, but is declared in 
-// the header file unistd.h in case the header files came from libc4 or libc5, 
-// and in case they came from glibc and _GNU_SOURCE was defined.) 
-// To be safe, declare it here.
-
-// It appears that on Mac OS X the 'environ' variable is not
-// available to dynamically linked libraries.
-// See: http://article.gmane.org/gmane.comp.lib.boost.devel/103843
-// See: http://lists.gnu.org/archive/html/bug-guile/2004-01/msg00013.html
-#if defined(__APPLE__) && defined(__DYNAMIC__)
-// The proper include for this is crt_externs.h, however it's not
-// available on iOS. The right replacement is not known. See
-// https://svn.boost.org/trac/boost/ticket/5053
-extern "C"
-{
-  extern char ***_NSGetEnviron(void);
-}
-#  define environ (*_NSGetEnviron()) 
-#else
-#  if defined(__MWERKS__)
-#    include <crtl.h>
-#  else
-#    if !defined(_WIN32) || defined(__COMO_VERSION__)
-extern char** environ;
-#    endif
-#  endif
-#endif
-
-
-/*=========================================================================
-  End of section from the Boost 1.68.0 project
-  =========================================================================*/
-
-
-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,
-                               bool log)
-  {
-    if (!IsRegularFile(path))
-    {
-      throw OrthancException(ErrorCode_RegularFileExpected,
-                             "The path does not point to a regular file: " + path,
-                             log);
-    }
-
-    boost::filesystem::ifstream f;
-    f.open(path, std::ifstream::in | std::ifstream::binary);
-    if (!f.good())
-    {
-      throw OrthancException(ErrorCode_InexistentFile,
-                             "File not found: " + path,
-                             log);
-    }
-
-    std::streamsize size = GetStreamSize(f);
-    content.resize(static_cast<size_t>(size));
-    if (size != 0)
-    {
-      f.read(&content[0], size);
-    }
-
-    f.close();
-  }
-
-
-  bool SystemToolbox::ReadHeader(std::string& header,
-                                 const std::string& path,
-                                 size_t headerSize)
-  {
-    if (!IsRegularFile(path))
-    {
-      throw OrthancException(ErrorCode_RegularFileExpected,
-                             "The path does not point to a regular file: " + path);
-    }
-
-    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(&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()
-      throw OrthancException(ErrorCode_SystemCommand, "Cannot fork a child process");
-    }
-    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)
-    {
-      throw OrthancException(ErrorCode_SystemCommand,
-                             "System command failed with status code " +
-                             boost::lexical_cast<std::string>(status));
-    }
-  }
-
-
-  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;
-    }
-  }
-
-
-  MimeType SystemToolbox::AutodetectMimeType(const std::string& path)
-  {
-    std::string extension = boost::filesystem::extension(path);
-    Toolbox::ToLowerCase(extension);
-
-    // http://en.wikipedia.org/wiki/Mime_types
-    // Text types
-    if (extension == ".txt")
-    {
-      return MimeType_PlainText;
-    }
-    else if (extension == ".html")
-    {
-      return MimeType_Html;
-    }
-    else if (extension == ".xml")
-    {
-      return MimeType_Xml;
-    }
-    else if (extension == ".css")
-    {
-      return MimeType_Css;
-    }
-
-    // Application types
-    else if (extension == ".js")
-    {
-      return MimeType_JavaScript;
-    }
-    else if (extension == ".json" ||
-             extension == ".nmf"  /* manifest */)
-    {
-      return MimeType_Json;
-    }
-    else if (extension == ".pdf")
-    {
-      return MimeType_Pdf;
-    }
-    else if (extension == ".wasm")
-    {
-      return MimeType_WebAssembly;
-    }
-    else if (extension == ".nexe")
-    {
-      return MimeType_NaCl;
-    }
-    else if (extension == ".pexe")
-    {
-      return MimeType_PNaCl;
-    }
-
-    // Images types
-    else if (extension == ".jpg" ||
-             extension == ".jpeg")
-    {
-      return MimeType_Jpeg;
-    }
-    else if (extension == ".gif")
-    {
-      return MimeType_Gif;
-    }
-    else if (extension == ".png")
-    {
-      return MimeType_Png;
-    }
-    else if (extension == ".pam")
-    {
-      return MimeType_Pam;
-    }
-    else if (extension == ".svg")
-    {
-      return MimeType_Svg;
-    }
-
-    // Various types
-    else if (extension == ".woff")
-    {
-      return MimeType_Woff;
-    }
-    else if (extension == ".woff2")
-    {
-      return MimeType_Woff2;
-    }
-
-    // Default type
-    else
-    {
-      LOG(INFO) << "Unknown MIME type for extension \"" << extension << "\"";
-      return MimeType_Binary;
-    }
-  }
-
-
-  void SystemToolbox::GetEnvironmentVariables(std::map<std::string, std::string>& env)
-  {
-    env.clear();
-    
-    for (char **p = environ; *p != NULL; p++)
-    {
-      std::string v(*p);
-      size_t pos = v.find('=');
-
-      if (pos != std::string::npos)
-      {
-        std::string key = v.substr(0, pos);
-        std::string value = v.substr(pos + 1);
-        env[key] = value;
-      } 
-    }
-  }
-
-
-  std::string SystemToolbox::InterpretRelativePath(const std::string& baseDirectory,
-                                                   const std::string& relativePath)
-  {
-    boost::filesystem::path base(baseDirectory);
-    boost::filesystem::path relative(relativePath);
-
-    /**
-       The following lines should be equivalent to this one: 
-
-       return (base / relative).string();
-
-       However, for some unknown reason, some versions of Boost do not
-       make the proper path resolution when "baseDirectory" is an
-       absolute path. So, a hack is used below.
-    **/
-
-    if (relative.is_absolute())
-    {
-      return relative.string();
-    }
-    else
-    {
-      return (base / relative).string();
-    }
-  }
-}
--- a/Core/SystemToolbox.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#include <map>
-#include <vector>
-#include <string>
-#include <stdint.h>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC SystemToolbox
-  {
-  public:
-    static void USleep(uint64_t microSeconds);
-
-    static ServerBarrierEvent ServerBarrier(const bool& stopFlag);
-
-    static ServerBarrierEvent ServerBarrier();
-
-    static void ReadFile(std::string& content,
-                         const std::string& path,
-                         bool log = true);
-
-    static bool ReadHeader(std::string& header,
-                           const std::string& path,
-                           size_t headerSize);
-
-    static void WriteFile(const void* content,
-                          size_t size,
-                          const std::string& path);
-
-    static void WriteFile(const std::string& content,
-                          const std::string& path);
-
-    static void RemoveFile(const std::string& path);
-
-    static uint64_t GetFileSize(const std::string& path);
-
-    static void MakeDirectory(const std::string& path);
-
-    static bool IsExistingFile(const std::string& path);
-
-    static std::string GetPathToExecutable();
-
-    static std::string GetDirectoryOfExecutable();
-
-    static void ExecuteSystemCommand(const std::string& command,
-                                     const std::vector<std::string>& arguments);
-
-    static int GetProcessId();
-
-    static bool IsRegularFile(const std::string& path);
-
-    static FILE* OpenFile(const std::string& path,
-                          FileMode mode);
-
-    static std::string GetNowIsoString(bool utc);
-
-    static void GetNowDicom(std::string& date,
-                            std::string& time,
-                            bool utc);
-
-    static unsigned int GetHardwareConcurrency();
-
-    static MimeType AutodetectMimeType(const std::string& path);
-
-    static void GetEnvironmentVariables(std::map<std::string, std::string>& env);
-
-    static std::string InterpretRelativePath(const std::string& baseDirectory,
-                                             const std::string& relativePath);
-  };
-}
--- a/Core/TemporaryFile.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,140 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancException.h"
-#include "SystemToolbox.h"
-#include "Toolbox.h"
-
-#include <boost/filesystem.hpp>
-
-namespace Orthanc
-{
-  static std::string CreateTemporaryPath(const char* temporaryDirectory,
-                                         const char* extension)
-  {
-    boost::filesystem::path dir;
-
-    if (temporaryDirectory == NULL)
-    {
-#if BOOST_HAS_FILESYSTEM_V3 == 1
-      dir = boost::filesystem::temp_directory_path();
-#elif defined(__linux__)
-      dir = "/tmp";
-#else
-#  error Support your platform here
-#endif
-    }
-    else
-    {
-      dir = temporaryDirectory;
-    }
-
-    // We use UUID to create unique path to temporary files
-    const std::string uuid = Orthanc::Toolbox::GenerateUuid();
-
-    // New in Orthanc 1.5.8: Prefix the process ID to the name of the
-    // temporary files, in order to locate orphan temporary files that
-    // were left by instances of Orthanc that exited in non-clean way
-    // https://groups.google.com/d/msg/orthanc-users/MSJX53bw6Lw/d3S3lRRLAwAJ
-    std::string filename = "Orthanc-" + boost::lexical_cast<std::string>(SystemToolbox::GetProcessId()) + "-" + uuid;
-
-    if (extension != NULL)
-    {
-      filename.append(extension);
-    }
-
-    dir /= filename;
-    return dir.string();
-  }
-
-
-  TemporaryFile::TemporaryFile() : 
-    path_(CreateTemporaryPath(NULL, NULL))
-  {
-  }
-
-
-  TemporaryFile::TemporaryFile(const std::string& temporaryDirectory,
-                               const std::string& extension) :
-    path_(CreateTemporaryPath(temporaryDirectory.c_str(), extension.c_str()))
-  {
-  }
-
-
-  TemporaryFile::~TemporaryFile()
-  {
-    boost::filesystem::remove(path_);
-  }
-
-
-  void TemporaryFile::Write(const std::string& content)
-  {
-    try
-    {
-      SystemToolbox::WriteFile(content, path_);
-    }
-    catch (OrthancException& e)
-    {
-      throw OrthancException(e.GetErrorCode(),
-                             "Can't create temporary file \"" + path_ +
-                             "\" with " + boost::lexical_cast<std::string>(content.size()) +
-                             " bytes: Check you have write access to the "
-                             "temporary directory and that it is not full");
-    }
-  }
-
-
-  void TemporaryFile::Read(std::string& content) const
-  {
-    try
-    {
-      SystemToolbox::ReadFile(content, path_);
-    }
-    catch (OrthancException& e)
-    {
-      throw OrthancException(e.GetErrorCode(),
-                             "Can't read temporary file \"" + path_ +
-                             "\": Another process has corrupted the temporary directory");
-    }
-  }
-
-
-  void TemporaryFile::Touch()
-  {
-    std::string empty;
-    Write(empty);
-  }
-}
--- a/Core/TemporaryFile.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#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 <boost/noncopyable.hpp>
-#include <string>
-
-namespace Orthanc
-{
-  class ORTHANC_PUBLIC TemporaryFile : public boost::noncopyable
-  {
-  private:
-    std::string path_;
-
-  public:
-    TemporaryFile();
-
-    TemporaryFile(const std::string& temporaryFolder,
-                  const std::string& extension);
-
-    ~TemporaryFile();
-
-    const std::string& GetPath() const
-    {
-      return path_;
-    }
-
-    void Write(const std::string& content);
-
-    void Read(std::string& content) const;
-
-    void Touch();
-  };
-}
--- a/Core/Toolbox.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2257 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "Toolbox.h"
-
-#include "Compatibility.h"
-#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>
-
-#if BOOST_VERSION >= 106600
-#  include <boost/uuid/detail/sha1.hpp>
-#else
-#  include <boost/uuid/sha1.hpp>
-#endif
- 
-#include <string>
-#include <stdint.h>
-#include <string.h>
-#include <algorithm>
-#include <ctype.h>
-
-
-#if ORTHANC_ENABLE_MD5 == 1
-// TODO - Could be replaced by <boost/uuid/detail/md5.hpp> starting
-// with Boost >= 1.66.0
-#  include "../Resources/ThirdParty/md5/md5.h"
-#endif
-
-#if ORTHANC_ENABLE_BASE64 == 1
-#  include "../Resources/ThirdParty/base64/base64.h"
-#endif
-
-#if ORTHANC_ENABLE_LOCALE == 1
-#  include <boost/locale.hpp>
-#endif
-
-#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
-
-
-#if defined(_MSC_VER) && (_MSC_VER < 1800)
-// Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013
-extern "C"
-{
-  int64_t _strtoi64(const char *nptr, char **endptr, int base);
-  int64_t strtoll(const char *nptr, char **endptr, int base)
-  {
-    return _strtoi64(nptr, endptr, base);
-  } 
-}
-#endif
-
-
-#if defined(_WIN32)
-#  include <windows.h>   // For ::Sleep
-#endif
-
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-#  include "ChunkedBuffer.h"
-#endif
-
-
-// Inclusions for UUID
-// http://stackoverflow.com/a/1626302
-
-extern "C"
-{
-#if defined(_WIN32)
-#  include <rpc.h>
-#else
-#  include <uuid/uuid.h>
-#endif
-}
-
-
-#if defined(ORTHANC_STATIC_ICU)
-#  if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1)
-#    include <OrthancFrameworkResources.h>
-#    include <unicode/udata.h>
-#    include <unicode/uloc.h>
-#    include "Compression/GzipCompressor.h"
-
-static std::string  globalIcuData_;
-
-extern "C"
-{
-  // This is dummy content for the "icudt58_dat" (resp. "icudt63_dat")
-  // global variable from the autogenerated "icudt58l_dat.c"
-  // (resp. "icudt63l_dat.c") file that contains a huge C array. In
-  // Orthanc, this array is compressed using gzip and attached as a
-  // resource, then uncompressed during the launch of Orthanc by
-  // static function "InitializeIcu()".
-  struct
-  {
-    double bogus;
-    uint8_t *bytes;
-  } U_ICUDATA_ENTRY_POINT = { 0.0, NULL };
-}
-
-#    if defined(__LSB_VERSION__)
-extern "C"
-{
-  /**
-   * The "tzname" global variable is declared as "extern" but is not
-   * defined in any compilation module, if using Linux Standard Base,
-   * as soon as OpenSSL or cURL is in use on Ubuntu >= 18.04 (glibc >=
-   * 2.27). The variable "__tzname" is always properly declared *and*
-   * defined. The reason is unclear, and is maybe a bug in the gcc 4.8
-   * linker that is used by LSB if facing a weak symbol (as "tzname").
-   * This makes Orthanc crash if the timezone is set to UTC.
-   * https://groups.google.com/d/msg/orthanc-users/0m8sxxwSm1E/2p8du_89CAAJ
-   **/
-  char *tzname[2] = { (char *) "GMT", (char *) "GMT" };
-}
-#    endif
-
-#  endif
-#endif
- 
-
-
-#if defined(__unix__) && ORTHANC_SANDBOXED != 1
-#  include "SystemToolbox.h"  // Check out "InitializeGlobalLocale()"
-#endif
-
-
-
-namespace Orthanc
-{
-  void Toolbox::LinesIterator::FindEndOfLine()
-  {
-    lineEnd_ = lineStart_;
-
-    while (lineEnd_ < content_.size() &&
-           content_[lineEnd_] != '\n' &&
-           content_[lineEnd_] != '\r')
-    {
-      lineEnd_ += 1;
-    }
-  }
-  
-
-  Toolbox::LinesIterator::LinesIterator(const std::string& content) :
-    content_(content),
-    lineStart_(0)
-  {
-    FindEndOfLine();
-  }
-
-    
-  bool Toolbox::LinesIterator::GetLine(std::string& target) const
-  {
-    assert(lineStart_ <= content_.size() &&
-           lineEnd_ <= content_.size() &&
-           lineStart_ <= lineEnd_);
-
-    if (lineStart_ == content_.size())
-    {
-      return false;
-    }
-    else
-    {
-      target = content_.substr(lineStart_, lineEnd_ - lineStart_);
-      return true;
-    }
-  }
-
-    
-  void Toolbox::LinesIterator::Next()
-  {
-    lineStart_ = lineEnd_;
-
-    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::ToUpperCase(std::string& s)
-  {
-    std::transform(s.begin(), s.end(), s.begin(), toupper);
-  }
-
-
-  void Toolbox::ToLowerCase(std::string& s)
-  {
-    std::transform(s.begin(), s.end(), s.begin(), tolower);
-  }
-
-
-  void Toolbox::ToUpperCase(std::string& result,
-                            const std::string& source)
-  {
-    result = source;
-    ToUpperCase(result);
-  }
-
-  void Toolbox::ToLowerCase(std::string& result,
-                            const std::string& source)
-  {
-    result = source;
-    ToLowerCase(result);
-  }
-
-
-  void Toolbox::SplitUriComponents(UriComponents& components,
-                                   const std::string& uri)
-  {
-    static const char URI_SEPARATOR = '/';
-
-    components.clear();
-
-    if (uri.size() == 0 ||
-        uri[0] != URI_SEPARATOR)
-    {
-      throw OrthancException(ErrorCode_UriSyntax);
-    }
-
-    // Count the number of slashes in the URI to make an assumption
-    // about the number of components in the URI
-    unsigned int estimatedSize = 0;
-    for (unsigned int i = 0; i < uri.size(); i++)
-    {
-      if (uri[i] == URI_SEPARATOR)
-        estimatedSize++;
-    }
-
-    components.reserve(estimatedSize - 1);
-
-    unsigned int start = 1;
-    unsigned int end = 1;
-    while (end < uri.size())
-    {
-      // This is the loop invariant
-      assert(uri[start - 1] == '/' && (end >= start));
-
-      if (uri[end] == '/')
-      {
-        components.push_back(std::string(&uri[start], end - start));
-        end++;
-        start = end;
-      }
-      else
-      {
-        end++;
-      }
-    }
-
-    if (start < uri.size())
-    {
-      components.push_back(std::string(&uri[start], end - start));
-    }
-
-    for (size_t i = 0; i < components.size(); i++)
-    {
-      if (components[i].size() == 0)
-      {
-        // Empty component, as in: "/coucou//e"
-        throw OrthancException(ErrorCode_UriSyntax);
-      }
-    }
-  }
-
-
-  void Toolbox::TruncateUri(UriComponents& target,
-                            const UriComponents& source,
-                            size_t fromLevel)
-  {
-    target.clear();
-
-    if (source.size() > fromLevel)
-    {
-      target.resize(source.size() - fromLevel);
-
-      size_t j = 0;
-      for (size_t i = fromLevel; i < source.size(); i++, j++)
-      {
-        target[j] = source[i];
-      }
-
-      assert(j == target.size());
-    }
-  }
-  
-
-
-  bool Toolbox::IsChildUri(const UriComponents& baseUri,
-                           const UriComponents& testedUri)
-  {
-    if (testedUri.size() < baseUri.size())
-    {
-      return false;
-    }
-
-    for (size_t i = 0; i < baseUri.size(); i++)
-    {
-      if (baseUri[i] != testedUri[i])
-        return false;
-    }
-
-    return true;
-  }
-
-
-  std::string Toolbox::FlattenUri(const UriComponents& components,
-                                  size_t fromLevel)
-  {
-    if (components.size() <= fromLevel)
-    {
-      return "/";
-    }
-    else
-    {
-      std::string r;
-
-      for (size_t i = fromLevel; i < components.size(); i++)
-      {
-        r += "/" + components[i];
-      }
-
-      return r;
-    }
-  }
-
-
-#if ORTHANC_ENABLE_MD5 == 1
-  static char GetHexadecimalCharacter(uint8_t value)
-  {
-    assert(value < 16);
-
-    if (value < 10)
-    {
-      return value + '0';
-    }
-    else
-    {
-      return (value - 10) + 'a';
-    }
-  }
-
-
-  void Toolbox::ComputeMD5(std::string& result,
-                           const std::string& data)
-  {
-    if (data.size() > 0)
-    {
-      ComputeMD5(result, &data[0], data.size());
-    }
-    else
-    {
-      ComputeMD5(result, NULL, 0);
-    }
-  }
-
-
-  void Toolbox::ComputeMD5(std::string& result,
-                           const void* data,
-                           size_t size)
-  {
-    md5_state_s state;
-    md5_init(&state);
-
-    if (size > 0)
-    {
-      md5_append(&state, 
-                 reinterpret_cast<const md5_byte_t*>(data), 
-                 static_cast<int>(size));
-    }
-
-    md5_byte_t actualHash[16];
-    md5_finish(&state, actualHash);
-
-    result.resize(32);
-    for (unsigned int i = 0; i < 16; i++)
-    {
-      result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16));
-      result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
-    }
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_BASE64 == 1
-  void Toolbox::EncodeBase64(std::string& result, 
-                             const std::string& data)
-  {
-    result.clear();
-    base64_encode(result, data);
-  }
-
-  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.clear();
-    base64_decode(result, data);
-  }
-
-
-  bool Toolbox::DecodeDataUriScheme(std::string& mime,
-                                    std::string& content,
-                                    const std::string& source)
-  {
-    boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)",
-                         boost::regex::icase /* case insensitive search */);
-
-    boost::cmatch what;
-    if (regex_match(source.c_str(), what, pattern))
-    {
-      mime = what[1];
-      DecodeBase64(content, what[2]);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  void Toolbox::EncodeDataUriScheme(std::string& result,
-                                    const std::string& mime,
-                                    const std::string& content)
-  {
-    result = "data:" + mime + ";base64,";
-    base64_encode(result, content);
-  }
-
-#endif
-
-
-#if ORTHANC_ENABLE_LOCALE == 1
-  static const char* GetBoostLocaleEncoding(const Encoding sourceEncoding)
-  {
-    switch (sourceEncoding)
-    {
-      case Encoding_Utf8:
-        return "UTF-8";
-
-      case Encoding_Ascii:
-        return "ASCII";
-
-      case Encoding_Latin1:
-        return "ISO-8859-1";
-
-      case Encoding_Latin2:
-        return "ISO-8859-2";
-
-      case Encoding_Latin3:
-        return "ISO-8859-3";
-
-      case Encoding_Latin4:
-        return "ISO-8859-4";
-
-      case Encoding_Latin5:
-        return "ISO-8859-9";
-
-      case Encoding_Cyrillic:
-        return "ISO-8859-5";
-
-      case Encoding_Windows1251:
-        return "WINDOWS-1251";
-
-      case Encoding_Arabic:
-        return "ISO-8859-6";
-
-      case Encoding_Greek:
-        return "ISO-8859-7";
-
-      case Encoding_Hebrew:
-        return "ISO-8859-8";
-        
-      case Encoding_Japanese:
-        return "SHIFT-JIS";
-
-      case Encoding_Chinese:
-        return "GB18030";
-
-      case Encoding_Thai:
-#if BOOST_LOCALE_WITH_ICU == 1
-        return "tis620.2533";
-#else
-        return "TIS620.2533-0";
-#endif
-
-      case Encoding_Korean:
-        return "ISO-IR-149";
-
-      case Encoding_JapaneseKanji:
-        return "JIS";
-
-      case Encoding_SimplifiedChinese:
-        return "GB2312";
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-#endif
-
-
-#if ORTHANC_ENABLE_LOCALE == 1
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
-  std::string Toolbox::ConvertToUtf8(const std::string& source,
-                                     Encoding sourceEncoding,
-                                     bool hasCodeExtensions)
-  {
-#if ORTHANC_STATIC_ICU == 1
-    if (globalIcuData_.empty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "Call Toolbox::InitializeGlobalLocale()");
-    }
-#endif
-
-    // The "::skip" flag makes boost skip invalid UTF-8
-    // characters. This can occur in badly-encoded DICOM files.
-    
-    try
-    {
-      if (sourceEncoding == Encoding_Ascii)
-      {
-        return ConvertToAscii(source);
-      }
-      else 
-      {
-        std::string s;
-        
-        if (sourceEncoding == Encoding_Utf8)
-        {
-          // Already in UTF-8: No conversion is required, but we ensure
-          // the output is correctly encoded
-          s = boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
-        }
-        else
-        {
-          const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
-          s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip);
-        }
-
-        if (hasCodeExtensions)
-        {
-          std::string t;
-          RemoveIso2022EscapeSequences(t, s);
-          return t;
-        }
-        else
-        {
-          return s;
-        }        
-      }
-    }
-    catch (std::runtime_error& e)
-    {
-      // Bad input string or bad encoding
-      LOG(INFO) << e.what();
-      return ConvertToAscii(source);
-    }
-  }
-#endif
-  
-
-#if ORTHANC_ENABLE_LOCALE == 1
-  std::string Toolbox::ConvertFromUtf8(const std::string& source,
-                                       Encoding targetEncoding)
-  {
-#if ORTHANC_STATIC_ICU == 1
-    if (globalIcuData_.empty())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "Call Toolbox::InitializeGlobalLocale()");
-    }
-#endif
-
-    // The "::skip" flag makes boost skip invalid UTF-8
-    // characters. This can occur in badly-encoded DICOM files.
-    
-    try
-    {
-      if (targetEncoding == Encoding_Utf8)
-      {
-        // Already in UTF-8: No conversion is required.
-        return boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
-      }
-      else if (targetEncoding == Encoding_Ascii)
-      {
-        return ConvertToAscii(source);
-      }
-      else
-      {
-        const char* encoding = GetBoostLocaleEncoding(targetEncoding);
-        return boost::locale::conv::from_utf<char>(source, encoding, boost::locale::conv::skip);
-      }
-    }
-    catch (std::runtime_error&)
-    {
-      // Bad input string or bad encoding
-      return ConvertToAscii(source);
-    }
-  }
-#endif
-
-
-  static bool IsAsciiCharacter(uint8_t c)
-  {
-    return (c != 0 &&
-            c <= 127 &&
-            (c == '\n' || !iscntrl(c)));
-  }
-
-
-  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 (!IsAsciiCharacter(*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;
-
-    result.reserve(source.size() + 1);
-    for (size_t i = 0; i < source.size(); i++)
-    {
-      if (IsAsciiCharacter(source[i]))
-      {
-        result.push_back(source[i]);
-      }
-    }
-
-    return result;
-  }
-
-
-  void Toolbox::ComputeSHA1(std::string& result,
-                            const void* data,
-                            size_t size)
-  {
-    boost::uuids::detail::sha1 sha1;
-
-    if (size > 0)
-    {
-      sha1.process_bytes(data, size);
-    }
-
-    unsigned int digest[5];
-
-    // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
-    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); 
-    
-    sha1.get_digest(digest);
-
-    result.resize(8 * 5 + 4);
-    sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x",
-            digest[0],
-            digest[1],
-            digest[2],
-            digest[3],
-            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 void* str,
-                       size_t size)
-  {
-    if (size == 0)
-    {
-      return false;
-    }
-
-    const char* start = reinterpret_cast<const char*>(str);
-    const char* end = start + size;
-
-    // Trim the beginning of the string
-    while (start < end)
-    {
-      if (*start == '\0' ||
-          isspace(*start))
-      {
-        start++;
-      }
-      else
-      {
-        break;
-      }
-    }
-
-    // Trim the trailing of the string
-    while (start < end)
-    {
-      if (*(end - 1) == '\0' ||
-          isspace(*(end - 1)))
-      {
-        end--;
-      }
-      else
-      {
-        break;
-      }
-    }
-
-    if (end - start != 44)
-    {
-      return false;
-    }
-
-    for (unsigned int i = 0; i < 44; i++)
-    {
-      if (i == 8 ||
-          i == 17 ||
-          i == 26 ||
-          i == 35)
-      {
-        if (start[i] != '-')
-          return false;
-      }
-      else
-      {
-        if (!isalnum(start[i]))
-          return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  bool Toolbox::IsSHA1(const std::string& s)
-  {
-    if (s.size() == 0)
-    {
-      return false;
-    }
-    else
-    {
-      return IsSHA1(s.c_str(), s.size());
-    }
-  }
-
-
-  std::string Toolbox::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);
-  }
-
-
-  static char Hex2Dec(char c)
-  {
-    return ((c >= '0' && c <= '9') ? c - '0' :
-            ((c >= 'a' && c <= 'f') ? c - 'a' + 10 : c - 'A' + 10));
-  }
-
-  void Toolbox::UrlDecode(std::string& s)
-  {
-    // http://en.wikipedia.org/wiki/Percent-encoding
-    // http://www.w3schools.com/tags/ref_urlencode.asp
-    // http://stackoverflow.com/questions/154536/encode-decode-urls-in-c
-
-    if (s.size() == 0)
-    {
-      return;
-    }
-
-    size_t source = 0;
-    size_t target = 0;
-
-    while (source < s.size())
-    {
-      if (s[source] == '%' &&
-          source + 2 < s.size() &&
-          isalnum(s[source + 1]) &&
-          isalnum(s[source + 2]))
-      {
-        s[target] = (Hex2Dec(s[source + 1]) << 4) | Hex2Dec(s[source + 2]);
-        source += 3;
-        target += 1;
-      }
-      else
-      {
-        if (s[source] == '+')
-          s[target] = ' ';
-        else
-          s[target] = s[source];
-
-        source++;
-        target++;
-      }
-    }
-
-    s.resize(target);
-  }
-
-
-  Endianness Toolbox::DetectEndianness()
-  {
-    // http://sourceforge.net/p/predef/wiki/Endianness/
-
-    uint32_t bufferView;
-
-    uint8_t* buffer = reinterpret_cast<uint8_t*>(&bufferView);
-
-    buffer[0] = 0x00;
-    buffer[1] = 0x01;
-    buffer[2] = 0x02;
-    buffer[3] = 0x03;
-
-    switch (bufferView) 
-    {
-      case 0x00010203: 
-        return Endianness_Big;
-
-      case 0x03020100: 
-        return Endianness_Little;
-        
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-  std::string Toolbox::WildcardToRegularExpression(const std::string& source)
-  {
-    // TODO - Speed up this with a regular expression
-
-    std::string result = source;
-
-    // Escape all special characters
-    boost::replace_all(result, "\\", "\\\\");
-    boost::replace_all(result, "^", "\\^");
-    boost::replace_all(result, ".", "\\.");
-    boost::replace_all(result, "$", "\\$");
-    boost::replace_all(result, "|", "\\|");
-    boost::replace_all(result, "(", "\\(");
-    boost::replace_all(result, ")", "\\)");
-    boost::replace_all(result, "[", "\\[");
-    boost::replace_all(result, "]", "\\]");
-    boost::replace_all(result, "+", "\\+");
-    boost::replace_all(result, "/", "\\/");
-    boost::replace_all(result, "{", "\\{");
-    boost::replace_all(result, "}", "\\}");
-
-    // Convert wildcards '*' and '?' to their regex equivalents
-    boost::replace_all(result, "?", ".");
-    boost::replace_all(result, "*", ".*");
-
-    return result;
-  }
-
-
-  void Toolbox::TokenizeString(std::vector<std::string>& result,
-                               const std::string& value,
-                               char separator)
-  {
-    size_t countSeparators = 0;
-    
-    for (size_t i = 0; i < value.size(); i++)
-    {
-      if (value[i] == separator)
-      {
-        countSeparators++;
-      }
-    }
-    
-    result.clear();
-    result.reserve(countSeparators + 1);
-
-    std::string currentItem;
-
-    for (size_t i = 0; i < value.size(); i++)
-    {
-      if (value[i] == separator)
-      {
-        result.push_back(currentItem);
-        currentItem.clear();
-      }
-      else
-      {
-        currentItem.push_back(value[i]);
-      }
-    }
-
-    result.push_back(currentItem);
-  }
-
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-  class ChunkedBufferWriter : public pugi::xml_writer
-  {
-  private:
-    ChunkedBuffer buffer_;
-
-  public:
-    virtual void write(const void *data, size_t size)
-    {
-      if (size > 0)
-      {
-        buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
-      }
-    }
-
-    void Flatten(std::string& s)
-    {
-      buffer_.Flatten(s);
-    }
-  };
-
-
-  static void JsonToXmlInternal(pugi::xml_node& target,
-                                const Json::Value& source,
-                                const std::string& arrayElement)
-  {
-    // http://jsoncpp.sourceforge.net/value_8h_source.html#l00030
-
-    switch (source.type())
-    {
-      case Json::nullValue:
-      {
-        target.append_child(pugi::node_pcdata).set_value("null");
-        break;
-      }
-
-      case Json::intValue:
-      {
-        std::string s = boost::lexical_cast<std::string>(source.asInt());
-        target.append_child(pugi::node_pcdata).set_value(s.c_str());
-        break;
-      }
-
-      case Json::uintValue:
-      {
-        std::string s = boost::lexical_cast<std::string>(source.asUInt());
-        target.append_child(pugi::node_pcdata).set_value(s.c_str());
-        break;
-      }
-
-      case Json::realValue:
-      {
-        std::string s = boost::lexical_cast<std::string>(source.asFloat());
-        target.append_child(pugi::node_pcdata).set_value(s.c_str());
-        break;
-      }
-
-      case Json::stringValue:
-      {
-        target.append_child(pugi::node_pcdata).set_value(source.asString().c_str());
-        break;
-      }
-
-      case Json::booleanValue:
-      {
-        target.append_child(pugi::node_pcdata).set_value(source.asBool() ? "true" : "false");
-        break;
-      }
-
-      case Json::arrayValue:
-      {
-        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
-        {
-          pugi::xml_node node = target.append_child();
-          node.set_name(arrayElement.c_str());
-          JsonToXmlInternal(node, source[i], arrayElement);
-        }
-        break;
-      }
-        
-      case Json::objectValue:
-      {
-        Json::Value::Members members = source.getMemberNames();
-
-        for (size_t i = 0; i < members.size(); i++)
-        {
-          pugi::xml_node node = target.append_child();
-          node.set_name(members[i].c_str());
-          JsonToXmlInternal(node, source[members[i]], arrayElement);          
-        }
-
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  void Toolbox::JsonToXml(std::string& target,
-                          const Json::Value& source,
-                          const std::string& rootElement,
-                          const std::string& arrayElement)
-  {
-    pugi::xml_document doc;
-
-    pugi::xml_node n = doc.append_child(rootElement.c_str());
-    JsonToXmlInternal(n, source, arrayElement);
-
-    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
-    decl.append_attribute("version").set_value("1.0");
-    decl.append_attribute("encoding").set_value("utf-8");
-
-    XmlToString(target, doc);
-  }
-
-  void Toolbox::XmlToString(std::string& target,
-                            const pugi::xml_document& source)
-  {
-    ChunkedBufferWriter writer;
-    source.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
-    writer.Flatten(target);
-  }
-#endif
-
-
-  
-  bool Toolbox::IsInteger(const std::string& str)
-  {
-    std::string s = StripSpaces(str);
-
-    if (s.size() == 0)
-    {
-      return false;
-    }
-
-    size_t pos = 0;
-    if (s[0] == '-')
-    {
-      if (s.size() == 1)
-      {
-        return false;
-      }
-
-      pos = 1;
-    }
-
-    while (pos < s.size())
-    {
-      if (!isdigit(s[pos]))
-      {
-        return false;
-      }
-
-      pos++;
-    }
-
-    return true;
-  }
-
-
-  void Toolbox::CopyJsonWithoutComments(Json::Value& target,
-                                        const Json::Value& source)
-  {
-    switch (source.type())
-    {
-      case Json::nullValue:
-        target = Json::nullValue;
-        break;
-
-      case Json::intValue:
-        target = source.asInt64();
-        break;
-
-      case Json::uintValue:
-        target = source.asUInt64();
-        break;
-
-      case Json::realValue:
-        target = source.asDouble();
-        break;
-
-      case Json::stringValue:
-        target = source.asString();
-        break;
-
-      case Json::booleanValue:
-        target = source.asBool();
-        break;
-
-      case Json::arrayValue:
-      {
-        target = Json::arrayValue;
-        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
-        {
-          Json::Value& item = target.append(Json::nullValue);
-          CopyJsonWithoutComments(item, source[i]);
-        }
-
-        break;
-      }
-
-      case Json::objectValue:
-      {
-        target = Json::objectValue;
-        Json::Value::Members members = source.getMemberNames();
-        for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
-        {
-          const std::string item = members[i];
-          CopyJsonWithoutComments(target[item], source[item]);
-        }
-
-        break;
-      }
-
-      default:
-        break;
-    }
-  }
-
-
-  bool Toolbox::StartsWith(const std::string& str,
-                           const std::string& prefix)
-  {
-    if (str.size() < prefix.size())
-    {
-      return false;
-    }
-    else
-    {
-      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;
-  }
-
-
-  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::unique_ptr<std::locale>  globalLocale_;
-
-  static bool SetGlobalLocale(const char* locale)
-  {
-    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& e)
-    {
-      LOG(ERROR) << "Cannot set globale locale to "
-                 << (locale ? std::string(locale) : "(null)")
-                 << ": " << e.what();
-      globalLocale_.reset(NULL);
-    }
-
-    return (globalLocale_.get() != NULL);
-  }
-
-  
-  static void InitializeIcu()
-  {
-#if ORTHANC_STATIC_ICU == 1
-    if (globalIcuData_.empty())
-    {
-      LOG(INFO) << "Setting up the ICU common data";
-
-      GzipCompressor compressor;
-      compressor.Uncompress(globalIcuData_,
-                            FrameworkResources::GetFileResourceBuffer(FrameworkResources::LIBICU_DATA),
-                            FrameworkResources::GetFileResourceSize(FrameworkResources::LIBICU_DATA));
-
-      std::string md5;
-      Toolbox::ComputeMD5(md5, globalIcuData_);
-
-      if (md5 != ORTHANC_ICU_DATA_MD5 ||
-          globalIcuData_.empty())
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot decode the ICU common data");
-      }
-
-      // "ICU data is designed to be 16-aligned"
-      // http://userguide.icu-project.org/icudata#TOC-Alignment
-
-      {
-        static const size_t ALIGN = 16;
-
-        UErrorCode status = U_ZERO_ERROR;
-
-        if (reinterpret_cast<intptr_t>(globalIcuData_.c_str()) % ALIGN == 0)
-        {
-          // Data is already properly aligned
-          udata_setCommonData(globalIcuData_.c_str(), &status);  
-        }
-        else
-        {
-          std::string aligned;
-          aligned.resize(globalIcuData_.size() + ALIGN - 1);
-
-          intptr_t offset = reinterpret_cast<intptr_t>(aligned.c_str()) % ALIGN;
-          if (offset != 0)
-          {
-            offset = ALIGN - offset;
-          }
-
-          if (offset + globalIcuData_.size() > aligned.size())
-          {
-            throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary");
-          }
-
-          // We don't use "memcpy()", as it expects its data to be aligned
-          const uint8_t* p = reinterpret_cast<uint8_t*>(&globalIcuData_[0]);
-          uint8_t* q = reinterpret_cast<uint8_t*>(&aligned[0]) + offset;
-          for (size_t i = 0; i < globalIcuData_.size(); i++, p++, q++)
-          {
-            *q = *p;
-          }
-        
-          globalIcuData_.swap(aligned);
-
-          const uint8_t* data = reinterpret_cast<const uint8_t*>(globalIcuData_.c_str()) + offset;
-        
-          if (reinterpret_cast<intptr_t>(data) % ALIGN != 0)
-          {
-            throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary");
-          }
-          else
-          {
-            udata_setCommonData(data, &status);  
-          }
-        }
-
-        if (status != U_ZERO_ERROR)
-        {
-          throw OrthancException(ErrorCode_InternalError, "Cannot initialize ICU");
-        }
-      }
-
-      if (Toolbox::DetectEndianness() != Endianness_Little)
-      {
-        // TODO - The data table must be swapped (uint16_t)
-        throw OrthancException(ErrorCode_NotImplemented);
-      }
-
-      // "First-use of ICU from a single thread before the
-      // multi-threaded use of ICU begins", to make sure everything is
-      // properly initialized (should not be mandatory in our
-      // case). We let boost handle calls to "u_init()" and "u_cleanup()".
-      // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination
-      uloc_getDefault();
-    }
-#endif
-  }
-  
-  void Toolbox::InitializeGlobalLocale(const char* locale)
-  {
-    InitializeIcu();
-
-#if defined(__unix__) && ORTHANC_SANDBOXED != 1
-    static const char* LOCALTIME = "/etc/localtime";
-    
-    if (!SystemToolbox::IsExistingFile(LOCALTIME))
-    {
-      // Check out file
-      // "boost_1_69_0/libs/locale/src/icu/time_zone.cpp": Direct
-      // access is made to this file if ICU is not used. Crash arises
-      // in Boost if the file is a symbolic link to a non-existing
-      // file (such as in Ubuntu 16.04 base Docker image).
-      throw OrthancException(
-        ErrorCode_InternalError,
-        "On UNIX-like systems, the file " + std::string(LOCALTIME) +
-        " must be present on the filesystem (install \"tzdata\" package on Debian)");
-    }
-#endif
-
-    // 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))
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot initialize global locale");
-    }
-
-  }
-
-
-  void Toolbox::FinalizeGlobalLocale()
-  {
-    globalLocale_.reset();
-  }
-
-
-  std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
-  {
-    bool error = (globalLocale_.get() == NULL);
-
-#if ORTHANC_STATIC_ICU == 1
-    if (globalIcuData_.empty())
-    {
-      error = true;
-    }
-#endif
-    
-    if (error)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "No global locale was set, call Toolbox::InitializeGlobalLocale()");
-    }
-
-    /**
-     * 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, boost::locale::conv::skip);
-    w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
-    return boost::locale::conv::utf_to_utf<char>(w, boost::locale::conv::skip);
-  }
-#endif
-
-
-
-#if ORTHANC_ENABLE_SSL == 0
-  /**
-   * OpenSSL is disabled
-   **/
-  void Toolbox::InitializeOpenSsl()
-  {
-  }
-  
-  void Toolbox::FinalizeOpenSsl()
-  {
-  }  
-
-
-#elif (ORTHANC_ENABLE_SSL == 1 &&               \
-       OPENSSL_VERSION_NUMBER < 0x10100000L) 
-  /**
-   * OpenSSL < 1.1.0
-   **/
-  void Toolbox::InitializeOpenSsl()
-  {
-    // https://wiki.openssl.org/index.php/Library_Initialization
-    SSL_library_init();
-    SSL_load_error_strings();
-    OpenSSL_add_all_algorithms();
-    ERR_load_crypto_strings();
-  }
-
-  void Toolbox::FinalizeOpenSsl()
-  {
-    // Finalize OpenSSL
-    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
-#ifdef FIPS_mode_set
-    FIPS_mode_set(0);
-#endif
-
-#if !defined(OPENSSL_NO_ENGINE)
-    ENGINE_cleanup();
-#endif
-    
-    CONF_modules_unload(1);
-    EVP_cleanup();
-    CRYPTO_cleanup_all_ex_data();
-    ERR_remove_state(0);
-    ERR_free_strings();
-  }
-
-  
-#elif (ORTHANC_ENABLE_SSL == 1 &&               \
-       OPENSSL_VERSION_NUMBER >= 0x10100000L) 
-  /**
-   * OpenSSL >= 1.1.0. In this case, the initialization is
-   * automatically done by the functions of OpenSSL.
-   * https://wiki.openssl.org/index.php/Library_Initialization
-   **/
-  void Toolbox::InitializeOpenSsl()
-  {
-  }
-
-  void Toolbox::FinalizeOpenSsl()
-  {
-  }
-
-#else
-#  error "Support your platform here"
-#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
-    uuid_t uuid;
-    uuid_generate_random ( uuid );
-    char s[37];
-    uuid_unparse ( uuid, s );
-#endif
-    return s;
-  }
-
-
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-
-    class VariableFormatter
-    {
-    public:
-      typedef std::map<std::string, std::string>   Dictionary;
-
-    private:
-      const Dictionary& dictionary_;
-
-    public:
-      VariableFormatter(const Dictionary& dictionary) :
-        dictionary_(dictionary)
-      {
-      }
-  
-      template<typename Out>
-      Out operator()(const boost::smatch& what,
-                     Out out) const
-      {
-        if (!what[1].str().empty())
-        {
-          // Variable without a default value
-          Dictionary::const_iterator found = dictionary_.find(what[1]);
-    
-          if (found != dictionary_.end())
-          {
-            const std::string& value = found->second;
-            out = std::copy(value.begin(), value.end(), out);
-          }
-        }
-        else
-        {
-          // Variable with a default value
-          std::string key;
-          std::string defaultValue;
-          
-          if (!what[2].str().empty())
-          {
-            key = what[2].str();
-            defaultValue = what[3].str();
-          }
-          else if (!what[4].str().empty())
-          {
-            key = what[4].str();
-            defaultValue = what[5].str();
-          }
-          else if (!what[6].str().empty())
-          {
-            key = what[6].str();
-            defaultValue = what[7].str();
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          Dictionary::const_iterator found = dictionary_.find(key);
-    
-          if (found == dictionary_.end())
-          {
-            out = std::copy(defaultValue.begin(), defaultValue.end(), out);
-          }
-          else
-          {
-            const std::string& value = found->second;
-            out = std::copy(value.begin(), value.end(), out);
-          }
-        }
-    
-        return out;
-      }
-    };
-  }
-
-  
-  std::string Toolbox::SubstituteVariables(const std::string& source,
-                                           const std::map<std::string, std::string>& dictionary)
-  {
-    const boost::regex pattern("\\$\\{([^:]*?)\\}|"                 // ${what[1]}
-                               "\\$\\{([^:]*?):-([^'\"]*?)\\}|"     // ${what[2]:-what[3]}
-                               "\\$\\{([^:]*?):-\"([^\"]*?)\"\\}|"  // ${what[4]:-"what[5]"}
-                               "\\$\\{([^:]*?):-'([^']*?)'\\}");    // ${what[6]:-'what[7]'}
-
-    VariableFormatter formatter(dictionary);
-
-    return boost::regex_replace(source, pattern, formatter);
-  }
-
-
-  namespace Iso2022
-  {
-    /**
-       Returns whether the string s contains a single-byte control message
-       at index i
-    **/
-    static inline bool IsControlMessage1(const std::string& s, size_t i)
-    {
-      if (i < s.size())
-      {
-        char c = s[i];
-        return
-          (c == '\x0f') || // Locking shift zero
-          (c == '\x0e');   // Locking shift one
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-    /**
-       Returns whether the string s contains a double-byte control message
-       at index i
-    **/
-    static inline size_t IsControlMessage2(const std::string& s, size_t i)
-    {
-      if (i + 1 < s.size())
-      {
-        char c1 = s[i];
-        char c2 = s[i + 1];
-        return (c1 == 0x1b) && (
-          (c2 == '\x6e') || // Locking shift two
-          (c2 == '\x6f') || // Locking shift three
-          (c2 == '\x4e') || // Single shift two (alt)
-          (c2 == '\x4f') || // Single shift three (alt)
-          (c2 == '\x7c') || // Locking shift three right
-          (c2 == '\x7d') || // Locking shift two right
-          (c2 == '\x7e')    // Locking shift one right
-          );
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-    /**
-       Returns whether the string s contains a triple-byte control message
-       at index i
-    **/
-    static inline size_t IsControlMessage3(const std::string& s, size_t i)
-    {
-      if (i + 2 < s.size())
-      {
-        char c1 = s[i];
-        char c2 = s[i + 1];
-        char c3 = s[i + 2];
-        return ((c1 == '\x8e' && c2 == 0x1b && c3 == '\x4e') ||
-                (c1 == '\x8f' && c2 == 0x1b && c3 == '\x4f'));
-      }
-      else
-      {
-        return false;
-      }
-    }
-
-    /**
-       This function returns true if the index i in the supplied string s:
-       - is valid
-       - contains the c character
-       This function returns false otherwise.
-    **/
-    static inline bool TestCharValue(
-      const std::string& s, size_t i, char c)
-    {
-      if (i < s.size())
-        return s[i] == c;
-      else
-        return false;
-    }
-
-    /**
-       This function returns true if the index i in the supplied string s:
-       - is valid
-       - has a c character that is >= cMin and <= cMax (included)
-       This function returns false otherwise.
-    **/
-    static inline bool TestCharRange(
-      const std::string& s, size_t i, char cMin, char cMax)
-    {
-      if (i < s.size())
-        return (s[i] >= cMin) && (s[i] <= cMax);
-      else
-        return false;
-    }
-
-    /**
-       This function returns the total length in bytes of the escape sequence
-       located in string s at index i, if there is one, or 0 otherwise.
-    **/
-    static inline size_t GetEscapeSequenceLength(const std::string& s, size_t i)
-    {
-      if (TestCharValue(s, i, 0x1b))
-      {
-        size_t j = i+1;
-
-        // advance reading cursor while we are in a sequence 
-        while (TestCharRange(s, j, '\x20', '\x2f'))
-          ++j;
-
-        // check there is a valid termination byte AND we're long enough (there
-        // must be at least one byte between 0x20 and 0x2f
-        if (TestCharRange(s, j, '\x30', '\x7f') && (j - i) >= 2)
-          return j - i + 1;
-        else
-          return 0;
-      }
-      else
-        return 0;
-    }
-  }
-
-  
-
-  /**
-     This function will strip all ISO/IEC 2022 control codes and escape
-     sequences.
-     Please see https://en.wikipedia.org/wiki/ISO/IEC_2022 (as of 2019-02)
-     for a list of those.
-
-     Please note that this operation is potentially destructive, because
-     it removes the character set information from the byte stream.
-
-     However, in the case where the encoding is unique, then suppressing
-     the escape sequences allows one to provide us with a clean string after
-     conversion to utf-8 with boost.
-  **/
-  void Toolbox::RemoveIso2022EscapeSequences(std::string& dest, const std::string& src)
-  {
-    // we need AT MOST the same size as the source string in the output
-    dest.clear();
-    if (dest.capacity() < src.size())
-      dest.reserve(src.size());
-
-    size_t i = 0;
-
-    // uint8_t view to the string
-    while (i < src.size())
-    {
-      size_t j = i;
-
-      // The i index will only be incremented if a message is detected
-      // in that case, the message is skipped and the index is set to the
-      // next position to read
-      if (Iso2022::IsControlMessage1(src, i))
-        i += 1;
-      else if (Iso2022::IsControlMessage2(src, i))
-        i += 2;
-      else if (Iso2022::IsControlMessage3(src, i))
-        i += 3;
-      else
-        i += Iso2022::GetEscapeSequenceLength(src, i);
-
-      // if the index was NOT incremented, this means there was no message at
-      // this location: we then may copy the character at this index and 
-      // increment the index to point to the next read position
-      if (j == i)
-      {
-        dest.push_back(src[i]);
-        i++;
-      }
-    }
-  }
-
-
-  void Toolbox::Utf8ToUnicodeCharacter(uint32_t& unicode,
-                                       size_t& length,
-                                       const std::string& utf8,
-                                       size_t position)
-  {
-    // https://en.wikipedia.org/wiki/UTF-8
-
-    static const uint8_t MASK_IS_1_BYTE = 0x80;     // printf '0x%x\n' "$((2#10000000))"
-    static const uint8_t TEST_IS_1_BYTE = 0x00;
- 
-    static const uint8_t MASK_IS_2_BYTES = 0xe0;    // printf '0x%x\n' "$((2#11100000))"
-    static const uint8_t TEST_IS_2_BYTES = 0xc0;    // printf '0x%x\n' "$((2#11000000))"
-
-    static const uint8_t MASK_IS_3_BYTES = 0xf0;    // printf '0x%x\n' "$((2#11110000))"
-    static const uint8_t TEST_IS_3_BYTES = 0xe0;    // printf '0x%x\n' "$((2#11100000))"
-
-    static const uint8_t MASK_IS_4_BYTES = 0xf8;    // printf '0x%x\n' "$((2#11111000))"
-    static const uint8_t TEST_IS_4_BYTES = 0xf0;    // printf '0x%x\n' "$((2#11110000))"
-
-    static const uint8_t MASK_CONTINUATION = 0xc0;  // printf '0x%x\n' "$((2#11000000))"
-    static const uint8_t TEST_CONTINUATION = 0x80;  // printf '0x%x\n' "$((2#10000000))"
-
-    if (position >= utf8.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    assert(sizeof(uint8_t) == sizeof(char));
-    const uint8_t* buffer = reinterpret_cast<const uint8_t*>(utf8.c_str()) + position;
-
-    if ((buffer[0] & MASK_IS_1_BYTE) == TEST_IS_1_BYTE)
-    {
-      length = 1;
-      unicode = buffer[0] & ~MASK_IS_1_BYTE;
-    }
-    else if ((buffer[0] & MASK_IS_2_BYTES) == TEST_IS_2_BYTES &&
-             position + 1 < utf8.size() &&
-             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION)
-    {
-      length = 2;
-      uint32_t a = buffer[0] & ~MASK_IS_2_BYTES;
-      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
-      unicode = (a << 6) | b;
-    }
-    else if ((buffer[0] & MASK_IS_3_BYTES) == TEST_IS_3_BYTES &&
-             position + 2 < utf8.size() &&
-             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION &&
-             (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION)
-    {
-      length = 3;
-      uint32_t a = buffer[0] & ~MASK_IS_3_BYTES;
-      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
-      uint32_t c = buffer[2] & ~MASK_CONTINUATION;
-      unicode = (a << 12) | (b << 6) | c;
-    }
-    else if ((buffer[0] & MASK_IS_4_BYTES) == TEST_IS_4_BYTES &&
-             position + 3 < utf8.size() &&
-             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION &&
-             (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION &&
-             (buffer[3] & MASK_CONTINUATION) == TEST_CONTINUATION)
-    {
-      length = 4;
-      uint32_t a = buffer[0] & ~MASK_IS_4_BYTES;
-      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
-      uint32_t c = buffer[2] & ~MASK_CONTINUATION;
-      uint32_t d = buffer[3] & ~MASK_CONTINUATION;
-      unicode = (a << 18) | (b << 12) | (c << 6) | d;
-    }
-    else
-    {
-      // This is not a valid UTF-8 encoding
-      throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string");
-    }
-  }
-
-
-  std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex)
-  {
-    /**
-     * NB: Focus of the code below is *not* efficiency, but
-     * readability!
-     **/
-    
-    for (size_t i = 0; i < hex.size(); i++)
-    {
-      const char c = hex[i];
-      if (!((c >= 'A' && c <= 'F') ||
-            (c >= 'a' && c <= 'f') ||
-            (c >= '0' && c <= '9')))
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
-                                        "Not an hexadecimal number");
-      }
-    }
-    
-    std::vector<uint8_t> decimal;
-    decimal.push_back(0);
-
-    for (size_t i = 0; i < hex.size(); i++)
-    {
-      uint8_t hexDigit = static_cast<uint8_t>(Hex2Dec(hex[i]));
-      assert(hexDigit <= 15);
-
-      for (size_t j = 0; j < decimal.size(); j++)
-      {
-        uint8_t val = static_cast<uint8_t>(decimal[j]) * 16 + hexDigit;  // Maximum: 9 * 16 + 15
-        assert(val <= 159 /* == 9 * 16 + 15 */);
-      
-        decimal[j] = val % 10;
-        hexDigit = val / 10;
-        assert(hexDigit <= 15 /* == 159 / 10 */);
-      }
-
-      while (hexDigit > 0)
-      {
-        decimal.push_back(hexDigit % 10);
-        hexDigit /= 10;
-      }
-    }
-
-    size_t start = 0;
-    while (start < decimal.size() &&
-           decimal[start] == '0')
-    {
-      start++;
-    }
-
-    std::string s;
-    s.reserve(decimal.size() - start);
-
-    for (size_t i = decimal.size(); i > start; i--)
-    {
-      s.push_back(decimal[i - 1] + '0');
-    }
-
-    return s;
-  }
-
-
-  std::string Toolbox::GenerateDicomPrivateUniqueIdentifier()
-  {
-    /**
-     * REFERENCE: "Creating a Privately Defined Unique Identifier
-     * (Informative)" / "UUID Derived UID"
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
-     * https://stackoverflow.com/a/46316162/881731
-     **/
-
-    std::string uuid = GenerateUuid();
-    assert(IsUuid(uuid) && uuid.size() == 36);
-
-    /**
-     * After removing the four dashes ("-") out of the 36-character
-     * UUID, we get a large hexadecimal number with 32 characters,
-     * each of those characters lying in the range [0,16[. The large
-     * number is thus in the [0,16^32[ = [0,256^16[ range. This number
-     * has a maximum of 39 decimal digits, as can be seen in Python:
-     * 
-     * # python -c 'import math; print(math.log(16**32))/math.log(10))'
-     * 38.531839445
-     *
-     * We now to convert the large hexadecimal number to a decimal
-     * number with up to 39 digits, remove the leading zeros, then
-     * prefix it with "2.25."
-     **/
-
-    // Remove the dashes
-    std::string hex = (uuid.substr(0, 8) +
-                       uuid.substr(9, 4) +
-                       uuid.substr(14, 4) +
-                       uuid.substr(19, 4) +
-                       uuid.substr(24, 12));
-    assert(hex.size() == 32);
-
-    return "2.25." + LargeHexadecimalToDecimal(hex);
-  }
-}
-
-
-
-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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,292 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#include <stdint.h>
-#include <vector>
-#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
-
-#if !defined(ORTHANC_ENABLE_SSL)
-#  error The macro ORTHANC_ENABLE_SSL 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
- **/
-
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-#  include <pugixml.hpp>
-#endif
-
-
-namespace Orthanc
-{
-  typedef std::vector<std::string> UriComponents;
-
-  class NullType
-  {
-  };
-
-  class ORTHANC_PUBLIC Toolbox
-  {
-  public:
-    class ORTHANC_PUBLIC LinesIterator
-    {
-    private:
-      const std::string& content_;
-      size_t             lineStart_;
-      size_t             lineEnd_;
-
-      void FindEndOfLine();
-  
-    public:
-      LinesIterator(const std::string& content);
-  
-      bool GetLine(std::string& target) const;
-
-      void Next();
-    };
-    
-    static void ToUpperCase(std::string& s);  // Inplace version
-
-    static void ToLowerCase(std::string& s);  // Inplace version
-
-    static void ToUpperCase(std::string& result,
-                            const std::string& source);
-
-    static void ToLowerCase(std::string& result,
-                            const std::string& source);
-
-    static void SplitUriComponents(UriComponents& components,
-                                   const std::string& uri);
-  
-    static void TruncateUri(UriComponents& target,
-                            const UriComponents& source,
-                            size_t fromLevel);
-  
-    static bool IsChildUri(const UriComponents& baseUri,
-                           const UriComponents& testedUri);
-
-    static std::string FlattenUri(const UriComponents& components,
-                                  size_t fromLevel = 0);
-
-#if ORTHANC_ENABLE_MD5 == 1
-    static void ComputeMD5(std::string& result,
-                           const std::string& data);
-
-    static void ComputeMD5(std::string& result,
-                           const void* data,
-                           size_t size);
-#endif
-
-    static void ComputeSHA1(std::string& result,
-                            const std::string& data);
-
-    static void ComputeSHA1(std::string& result,
-                            const void* data,
-                            size_t size);
-
-    static bool IsSHA1(const void* str,
-                       size_t size);
-
-    static bool IsSHA1(const std::string& s);
-
-#if ORTHANC_ENABLE_BASE64 == 1
-    static void DecodeBase64(std::string& result, 
-                             const std::string& data);
-
-    static void EncodeBase64(std::string& result, 
-                             const std::string& data);
-
-    static bool DecodeDataUriScheme(std::string& mime,
-                                    std::string& content,
-                                    const std::string& source);
-
-    static void EncodeDataUriScheme(std::string& result,
-                                    const std::string& mime,
-                                    const std::string& content);
-#endif
-
-#if ORTHANC_ENABLE_LOCALE == 1
-    static std::string ConvertToUtf8(const std::string& source,
-                                     Encoding sourceEncoding,
-                                     bool hasCodeExtensions);
-
-    static std::string ConvertFromUtf8(const std::string& source,
-                                       Encoding targetEncoding);
-#endif
-
-    static bool IsAsciiString(const void* data,
-                              size_t size);
-
-    static bool IsAsciiString(const std::string& s);
-
-    static std::string ConvertToAscii(const std::string& source);
-
-    static std::string StripSpaces(const std::string& source);
-
-    // In-place percent-decoding for URL
-    static void UrlDecode(std::string& s);
-
-    static Endianness DetectEndianness();
-
-    static std::string WildcardToRegularExpression(const std::string& s);
-
-    static void TokenizeString(std::vector<std::string>& result,
-                               const std::string& source,
-                               char separator);
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-    static void JsonToXml(std::string& target,
-                          const Json::Value& source,
-                          const std::string& rootElement = "root",
-                          const std::string& arrayElement = "item");
-#endif
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-    static void XmlToString(std::string& target,
-                            const pugi::xml_document& source);
-#endif
-
-    static bool IsInteger(const std::string& str);
-
-    static void CopyJsonWithoutComments(Json::Value& target,
-                                        const Json::Value& source);
-
-    static bool StartsWith(const std::string& str,
-                           const std::string& prefix);
-
-    static void UriEncode(std::string& target,
-                          const std::string& source);
-
-    static std::string GetJsonStringField(const ::Json::Value& json,
-                                          const std::string& key,
-                                          const std::string& defaultValue);
-
-    static bool GetJsonBooleanField(const ::Json::Value& json,
-                                    const std::string& key,
-                                    bool defaultValue);
-
-    static int GetJsonIntegerField(const ::Json::Value& json,
-                                   const std::string& key,
-                                   int defaultValue);
-
-    static unsigned int GetJsonUnsignedIntegerField(const ::Json::Value& json,
-                                                    const std::string& key,
-                                                    unsigned int defaultValue);
-
-    static bool IsUuid(const std::string& str);
-
-    static bool StartsWithUuid(const std::string& str);
-
-#if ORTHANC_ENABLE_LOCALE == 1
-    static void InitializeGlobalLocale(const char* locale);
-
-    static void FinalizeGlobalLocale();
-
-    static std::string ToUpperCaseWithAccents(const std::string& source);
-#endif
-
-    static void InitializeOpenSsl();
-
-    static void FinalizeOpenSsl();
-
-    static std::string GenerateUuid();
-
-    static std::string SubstituteVariables(const std::string& source,
-                                           const std::map<std::string, std::string>& dictionary);
-
-    static void RemoveIso2022EscapeSequences(std::string& dest,
-                                             const std::string& src);
-
-    static void Utf8ToUnicodeCharacter(uint32_t& unicode,
-                                       size_t& utf8Length,
-                                       const std::string& utf8,
-                                       size_t position);
-
-    static std::string LargeHexadecimalToDecimal(const std::string& hex);
-
-    // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
-    static std::string GenerateDicomPrivateUniqueIdentifier();
-  };
-}
-
-
-
-
-/**
- * The plain C, opaque data structure "OrthancLinesIterator" is a thin
- * wrapper around Orthanc::Toolbox::LinesIterator, and is only used by
- * "../Resources/Patches/dcmtk-dcdict_orthanc.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/WebServiceParameters.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,585 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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://"))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Bad URL: " + url);
-    }
-
-    // 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())
-    {
-      throw OrthancException(
-        ErrorCode_BadFileFormat,
-        "The password for the HTTPS certificate is not provided: " + certificateFile);
-    }
-
-    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)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "The HTTP password is not provided");
-    }
-    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))
-      {
-        switch (peer[*it].type())
-        {
-          case Json::stringValue:
-            userProperties_[*it] = peer[*it].asString();
-            break;
-
-          case Json::booleanValue:
-            userProperties_[*it] = peer[*it].asBool() ? "1" : "0";
-            break;
-
-          case Json::intValue:
-            userProperties_[*it] = boost::lexical_cast<std::string>(peer[*it].asInt());
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_BadFileFormat,
-                                   "User-defined properties associated with a Web service must be strings: " + *it);
-        }
-      }
-    }
-  }
-
-
-  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))
-    {
-      throw OrthancException(
-        ErrorCode_ParameterOutOfRange,
-        "Cannot use this reserved key to name an user property: " + key);
-    }
-    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::GetBooleanUserProperty(const std::string& key,
-                                                    bool defaultValue) const
-  {
-    Dictionary::const_iterator found = userProperties_.find(key);
-
-    if (found == userProperties_.end())
-    {
-      return defaultValue;
-    }
-    else if (found->second == "0" ||
-             found->second == "false")
-    {
-      return false;
-    }
-    else if (found->second == "1" ||
-             found->second == "true")
-    {
-      return true;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters "
-                             "of a Web service: Property \"" + key + "\" equals: " + found->second);
-    }    
-  }
-
-
-  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_))
-      {
-        throw OrthancException(ErrorCode_InexistentFile,
-                               "Cannot open certificate file: " + certificateFile_);
-      }
-
-      if (!certificateKeyFile_.empty() && 
-          !SystemToolbox::IsRegularFile(certificateKeyFile_))
-      {
-        throw OrthancException(ErrorCode_InexistentFile,
-                               "Cannot open key file: " + certificateKeyFile_);
-      }
-    }
-  }
-#endif
-
-
-  void WebServiceParameters::FormatPublic(Json::Value& target) const
-  {
-    target = Json::objectValue;
-
-    // Only return the public information identifying the destination.
-    // "Security"-related information such as passwords and HTTP
-    // headers are shown as "null" values.
-    target[KEY_URL] = url_;
-
-    if (!username_.empty())
-    {
-      target[KEY_USERNAME] = username_;
-      target[KEY_PASSWORD] = Json::nullValue;
-    }
-
-    if (!certificateFile_.empty())
-    {
-      target[KEY_CERTIFICATE_FILE] = certificateFile_;
-      target[KEY_CERTIFICATE_KEY_FILE] = Json::nullValue;
-      target[KEY_CERTIFICATE_KEY_PASSWORD] = Json::nullValue;      
-    }
-
-    target[KEY_PKCS11] = pkcs11Enabled_;
-
-    Json::Value headers = Json::arrayValue;
-      
-    for (Dictionary::const_iterator it = headers_.begin();
-         it != headers_.end(); ++it)
-    {
-      // Only list the HTTP headers, not their value
-      headers.append(it->first);
-    }
-
-    target[KEY_HTTP_HEADERS] = headers;
-
-    for (Dictionary::const_iterator it = userProperties_.begin();
-         it != userProperties_.end(); ++it)
-    {
-      target[it->first] = it->second;
-    }
-  }
-}
--- a/Core/WebServiceParameters.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,186 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFramework.h"
-
-#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 ORTHANC_PUBLIC 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 GetBooleanUserProperty(const std::string& key,
-                                bool defaultValue) 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
-
-    void FormatPublic(Json::Value& target) const;
-  };
-}
--- a/OrthancExplorer/explorer.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-ul.tree ul {
-    margin-left: 36px; 
-}
-
-#progress { 
-    position: relative; 
-    /*height: 2em; */
-    width: 100%; 
-    background-color: grey; 
-    height: 2.5em;
-}
-
-#progress .label {
-    z-index: 10; 
-    position: absolute; 
-    left:0; 
-    top: 0; 
-    width: 100%; 
-    font-weight: bold; 
-    text-align: center;  
-    text-shadow: none; 
-    padding: .5em;
-    color: white;
-}
-
-#progress .bar { 
-    z-index: 0; 
-    position: absolute; 
-    left:0; 
-    top: 0; 
-    height: 100%; 
-    width: 0%; 
-    background-color: green; 
-}
-
-.ui-title a {
-    text-decoration: none;
-    color: white !important;
-}
-
-.switch-container .ui-slider-switch {
-    width: 100%;
-}
\ No newline at end of file
--- a/OrthancExplorer/explorer.html	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,665 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-
-<head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <title>Orthanc Explorer</title>
-
-  <link rel="stylesheet" href="libs/jquery.mobile.min.css" />
-  <link rel="stylesheet" href="libs/jqtree.css" />
-  <link rel="stylesheet" href="libs/jquery.mobile.simpledialog.min.css" />
-  <link rel="stylesheet" href="libs/jquery-file-upload/css/style.css" />
-  <link rel="stylesheet" href="libs/jquery-file-upload/css/jquery.fileupload-ui.css" />
-  <link rel="stylesheet" href="libs/slimbox2/slimbox2.css" />
-
-  <script src="libs/jquery.min.js"></script>
-  <script src="libs/jquery.mobile.min.js"></script>
-  <script src="libs/jqm.page.params.js"></script>
-  <script src="libs/tree.jquery.js"></script>
-  <script src="libs/date.js"></script>
-  <script src="libs/jquery.mobile.simpledialog2.js"></script>
-  <script src="libs/slimbox2.js"></script>
-  <script src="libs/jquery.blockui.js"></script>
-
-  <!-- https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin -->
-  <script src="libs/jquery-file-upload/js/vendor/jquery.ui.widget.js"></script>
-  <script src="libs/jquery-file-upload/js/jquery.iframe-transport.js"></script>
-  <script src="libs/jquery-file-upload/js/jquery.fileupload.js"></script>
-
-  <link rel="stylesheet" href="explorer.css" />
-  <script src="file-upload.js"></script>
-  <script src="explorer.js"></script>
-  <script src="query-retrieve.js"></script>
-  <script src="../plugins/explorer.js"></script>
-</head>
-
-<body>
-  <div data-role="page" id="lookup">
-    <div data-role="header">
-      <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">
-      <div data-role="content" id="content" style="padding:0px">
-        <p align="center">
-          <a href="http://www.orthanc-server.com/" target="_blank" alt="Orthanc homepage">
-            <img src="orthanc-logo.png" alt="Orthanc" style="max-width:100%" />
-          </a>
-        </p>
-      </div>
-      
-      <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="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>
-      <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>
-        <!-- It's very difficult to style a "file" input so we
-        actually hide it and create a "proxy" button that is
-        forwarding its click to the "file" input -->
-        <input id="fileupload" type="file" name="files[]" data-url="../instances/" style="display:none" multiple>
-      </div>
-      <p>
-        <ul data-role="listview" data-inset="true">
-          <li id="fileupload-proxy" onclick="$('#fileupload').click()" data-icon="arrow-r" data-theme="e">
-            <a href="#">Select files to upload ...</a>
-          </li>
-          <li data-icon="arrow-r" data-theme="e"><a href="#" id="upload-button">Start the upload</a></li>
-          <!--li data-icon="gear" data-theme="e"><a href="#" id="upload-abort" class="ui-disabled">Abort the current upload</a></li-->
-          <li data-icon="delete" data-theme="d"><a href="#" id="upload-clear">Clear the pending uploads</a></li>
-        </ul>
-        <div id="progress" class="ui-corner-all">
-          <span class="bar ui-corner-all"></span>
-          <div class="label"></div>
-        </div>
-      </p>
-      <div class="ui-bar ui-bar-e" id="issue-21-warning">
-        <h3>Warning:</h3> Orthanc issue #21: On Firefox, especially on
-        Linux & OSX systems, files might be missing if using
-        drag-and-drop. Please use the "Select files to upload" button
-        instead, or use the command-line "ImportDicomFiles.py" script.
-      </div>
-      <ul id="upload-list" data-role="listview" data-inset="true">
-        <li data-role="list-divider">Drag and drop DICOM files here</li>
-      </ul>
-    </div>
-  </div>
-
-  <div data-role="page" id="patient">
-    <div data-role="header">
-      <h1><span class="orthanc-name"></span>Patient</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 class="ui-grid-a">
-        <div class="ui-block-a" style="width:30%">
-          <div style="padding-right:10px">
-            <ul data-role="listview" data-inset="true" data-theme="a" id="patient-info">
-            </ul>
-            <p>
-              <div class="switch-container">
-                <select name="protection" id="protection" data-role="slider">
-                  <option value="off">Unprotected</option>
-                  <option value="on">Protected</option>
-                </select>
-              </div>
-            </p>
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Interact</li>
-              <li data-icon="delete"><a href="#" id="patient-delete">Delete this patient</a></li>
-              <li data-icon="forward"><a href="#" id="patient-store">Send to remote modality</a></li>
-              <li data-icon="star"><a href="#" id="patient-anonymize">Anonymize</a></li>
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Access</li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="patient-anonymized-from">Before anonymization</a>
-              </li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="patient-modified-from">Before modification</a>
-              </li>
-              <li data-icon="gear"><a href="#" id="patient-archive">Download ZIP</a></li>
-              <li data-icon="gear"><a href="#" id="patient-media">Download DICOMDIR</a></li>
-            </ul>
-          </div>
-        </div>
-        <div class="ui-block-b" style="width:70%">
-          <div style="padding:10px">
-            <ul id="list-studies" data-role="listview" data-inset="true" data-filter="true">
-            </ul>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div data-role="page" id="study">
-    <div data-role="header">
-      <h1>
-        <span class="orthanc-name"></span>
-        <a href="#" class="patient-link">Patient</a> &raquo;
-        Study
-      </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 class="ui-grid-a">
-        <div class="ui-block-a" style="width:30%">
-          <div style="padding-right:10px">
-            <ul data-role="listview" data-inset="true" data-theme="a" id="study-info">
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Interact</li>
-              <li data-icon="delete"><a href="#" id="study-delete">Delete this study</a></li>
-              <li data-icon="forward"><a href="#" id="study-store">Send to DICOM modality</a></li>
-              <li data-icon="star"><a href="#" id="study-anonymize">Anonymize</a></li>
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Access</li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="study-anonymized-from">Before anonymization</a>
-              </li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="study-modified-from">Before modification</a>
-              </li>
-              <li data-icon="gear"><a href="#" id="study-archive">Download ZIP</a></li>
-              <li data-icon="gear"><a href="#" id="study-media">Download DICOMDIR</a></li>
-            </ul>
-          </div>
-        </div>
-        <div class="ui-block-b" style="width:70%">
-          <div style="padding:10px">
-            <ul id="list-series" data-role="listview" data-inset="true" data-filter="true">
-            </ul>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div data-role="page" id="series">
-    <div data-role="header">
-      <h1>
-        <span class="orthanc-name"></span>
-        <a href="#" class="patient-link">Patient</a> &raquo;
-        <a href="#" class="study-link">Study</a> &raquo;
-        Series
-      </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 class="ui-grid-a">
-        <div class="ui-block-a" style="width:30%">
-          <div style="padding-right:10px">
-            <ul data-role="listview" data-inset="true" data-theme="a" id="series-info">
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Interact</li>
-              <li data-icon="delete"><a href="#" id="series-delete">Delete this series</a></li>
-              <li data-icon="forward"><a href="#" id="series-store">Send to DICOM modality</a></li>
-              <li data-icon="star"><a href="#" id="series-anonymize">Anonymize</a></li>
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Access</li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="series-anonymized-from">Before anonymization</a>
-              </li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="series-modified-from">Before modification</a>
-              </li>
-              <li data-icon="search"><a href="#" id="series-preview">Preview this series</a></li>
-              <li data-icon="gear"><a href="#" id="series-archive">Download ZIP</a></li>
-              <li data-icon="gear"><a href="#" id="series-media">Download DICOMDIR</a></li>
-            </ul>
-          </div>
-        </div>
-        <div class="ui-block-b" style="width:70%">
-          <div style="padding:10px">
-            <ul id="list-instances" data-role="listview" data-inset="true" data-filter="true">
-            </ul>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div data-role="page" id="instance">
-    <div data-role="header">
-      <h1>
-        <span class="orthanc-name"></span>
-        <a href="#" class="patient-link">Patient</a> &raquo;
-        <a href="#" class="study-link">Study</a> &raquo;
-        <a href="#" class="series-link">Series</a> &raquo;
-        Instance
-      </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 class="ui-grid-a">
-        <div class="ui-block-a" style="width:30%">
-          <div style="padding-right:10px">
-            <ul data-role="listview" data-inset="true" data-theme="a" id="instance-info">
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Interact</li>
-              <li data-icon="delete"><a href="#" id="instance-delete">Delete this instance</a></li>
-              <li data-icon="forward"><a href="#" id="instance-store">Send to DICOM modality</a></li>
-            </ul>
-
-            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
-              <li data-role="list-divider">Access</li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="instance-anonymized-from">Before anonymization</a>
-              </li>
-              <li data-icon="info" data-theme="e" style="display:none">
-                <a href="#" id="instance-modified-from">Before modification</a>
-              </li>
-              <li data-icon="arrow-d"><a href="#" id="instance-download-dicom">Download the DICOM file</a></li>
-              <li data-icon="arrow-d"><a href="#" id="instance-download-json">Download the JSON file</a></li>
-              <li data-icon="search"><a href="#" id="instance-preview">Preview the instance</a></li>
-            </ul>
-          </div>
-        </div>
-        <div class="ui-block-b" style="width:70%">
-          <div style="padding:10px">
-            <div class="ui-body ui-body-b">
-              <h1>DICOM Tags</h1>
-              <p align="right">
-                <input type="checkbox" id="show-tag-name" checked="checked" class="custom" data-mini="true" />
-                <label for="show-tag-name">Show tag description</label>
-              </p>
-              <div id="dicom-tree"></div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div data-role="page" id="plugins">
-    <div data-role="header">
-      <h1><span class="orthanc-name"></span>Plugins</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-plugins" data-role="listview" data-inset="true" data-filter="true">
-      </ul>
-    </div>
-  </div>
-
-  <div data-role="page" id="query-retrieve">
-    <div data-role="header">
-      <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">
-        <div data-role="fieldcontain">
-          <label for="qr-server">DICOM server:</label>
-          <select name="qr-server" id="qr-server">
-          </select>
-        </div>
-
-        <div data-role="fieldcontain" id="qr-fields">
-          <fieldset data-role="controlgroup">
-            <legend>Field of interest:</legend>
-            <input type="radio" name="qr-field" id="qr-patient-id" value="PatientID" checked="checked" />
-            <label for="qr-patient-id">Patient ID</label>
-            <input type="radio" name="qr-field" id="qr-patient-name" value="PatientName" />
-            <label for="qr-patient-name">Patient Name</label>
-            <input type="radio" name="qr-field" id="qr-accession-number" value="AccessionNumber" />
-            <label for="qr-accession-number">Accession Number</label>
-            <input type="radio" name="qr-field" id="qr-study-description" value="StudyDescription" />
-            <label for="qr-study-description">Study Description</label>
-          </fieldset>
-        </div>
-
-        <div data-role="fieldcontain">
-          <label for="qr-value">Value for this field:</label>
-          <input type="text" name="qr-value" id="qr-value" value="*" />
-        </div>
-
-        <div data-role="fieldcontain">
-          <label for="qr-date">Study date:</label>
-          <select name="qr-date" id="qr-date">
-          </select>
-        </div>
-
-        <div data-role="fieldcontain" id="qr-modalities">
-          <div data-role="fieldcontain">
-            <fieldset data-role="controlgroup" data-type="horizontal">
-              <legend>Modalities:</legend>
-              <input type="checkbox" name="CR" id="qr-cr" class="custom" /> <label for="qr-cr">CR</label>
-              <input type="checkbox" name="CT" id="qr-ct" class="custom" /> <label for="qr-ct">CT</label>
-              <input type="checkbox" name="MR" id="qr-mr" class="custom" /> <label for="qr-mr">MR</label>
-              <input type="checkbox" name="NM" id="qr-nm" class="custom" /> <label for="qr-nm">NM</label>
-              <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>
-              <input type="checkbox" name="MG" id="qr-mg" class="custom" /> <label for="qr-mg">MG</label>
-            </fieldset>
-          </div>
-        </div>
-
-        <fieldset class="ui-grid-a">
-          <div class="ui-block-a">
-            <button id="qr-echo" data-theme="a">Test Echo</button>
-          </div>
-          <div class="ui-block-b">
-            <button id="qr-submit" type="submit" data-theme="b">Search studies</button>
-          </div>
-        </fieldset>
-      </form>
-    </div>
-  </div>
-
-
-  <div data-role="page" id="query-retrieve-2">
-    <div data-role="header">
-      <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">
-      <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
-      </ul>
-    </div>
-  </div>
-
-
-  <div data-role="page" id="query-retrieve-3">
-    <div data-role="header">
-      <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">
-      </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>
-  </div>
-
-  <div id="dicom-store" style="display:none;" class="ui-body-c">
-    <p align="center"><b>Sending to DICOM modality...</b></p>
-    <p><img src="libs/images/ajax-loader.gif" alt="" /></p>
-  </div>
-
-  <div id="info-retrieve" style="display:none;" class="ui-body-c">
-    <p align="center"><b>Retrieving images from DICOM modality...</b></p>
-    <p><img src="libs/images/ajax-loader.gif" alt="" /></p>
-  </div>
-
-  <div id="dialog" style="display:none">
-  </div>
-
-  <div id="template-insecure" style="display:none">
-    <div class="warning-insecure ui-body ui-body-e">
-      <h1>Insecure setup</h1>
-      <p>
-        Your Orthanc server is accepting remote connections, but is
-	using the default username and password, or has user
-	authentication explicitly turned off. Please carefully read
-	your logs and review your configuration, especially
-	options <tt>RemoteAccessAllowed</tt>, <tt>AuthenticationEnabled</tt>,
-	and <tt>RegisteredUsers</tt>.
-      </p>
-    </div>
-  </div>
-</body>
-
-</html>
--- a/OrthancExplorer/explorer.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1570 +0,0 @@
-// http://stackoverflow.com/questions/1663741/is-there-a-good-jquery-drag-and-drop-file-upload-plugin
-
-
-// Forbid the access to IE
-if ($.browser.msie)
-{
-  alert("Please use Mozilla Firefox or Google Chrome. Microsoft Internet Explorer is not supported.");
-}
-
-// http://jquerymobile.com/demos/1.1.0/docs/api/globalconfig.html
-//$.mobile.ajaxEnabled = false;
-//$.mobile.page.prototype.options.addBackBtn = true;
-//$.mobile.defaultPageTransition = 'slide';
-
-
-var LIMIT_RESOURCES = 100;
-
-var currentPage = '';
-var currentUuid = '';
-
-
-function DeepCopy(obj)
-{
-  return jQuery.extend(true, {}, obj);
-}
-
-
-function ChangePage(page, options)
-{
-  var first = true;
-  var value;
-
-  if (options) {
-    for (var key in options) {
-      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()
-{
-  if (currentPage == 'patient')
-    RefreshPatient();
-  else if (currentPage == 'study')
-    RefreshStudy();
-  else if (currentPage == 'series')
-    RefreshSeries();
-  else if (currentPage == 'instance')
-    RefreshInstance();
-}
-
-
-$(document).ready(function() {
-  var $tree = $('#dicom-tree');
-  $tree.tree({
-    autoEscape: false
-  });
-
-  $('#dicom-tree').bind(
-    'tree.click',
-    function(event) {
-      if (event.node.is_open)
-        $tree.tree('closeNode', event.node, true);
-      else
-        $tree.tree('openNode', event.node, true);
-    }
-  );
-
-  // Inject the template of the warning about insecure setup as the
-  // first child of each page
-  var insecure = $('#template-insecure').html();
-  $('[data-role="page"]>[data-role="content"]').prepend(insecure);
-  
-  currentPage = $.mobile.pageData.active;
-  currentUuid = $.mobile.pageData.uuid;
-  if (!(typeof currentPage === 'undefined') &&
-      !(typeof currentUuid === 'undefined') &&
-      currentPage.length > 0 && 
-      currentUuid.length > 0)
-  {
-    Refresh();
-  }
-});
-
-function GetAuthorizationTokensFromUrl() {
-  var urlVariables = window.location.search.substring(1).split('&');
-  var dict = {};
-
-  for (var i = 0; i < urlVariables.length; i++) {
-      var split = urlVariables[i].split('=');
-
-      if (split.length == 2 && (split[0] == "token" || split[0] == "auth-token" || split[0] == "authorization")) {
-        dict[split[0]] = split[1];
-      }
-  }
-  return dict;
-};
-
-var authorizationTokens = GetAuthorizationTokensFromUrl();
-
-/* Copy the authoziation toekn from the url search parameters into HTTP headers in every request to the Rest API.  
-Thanks to this behaviour, you may specify a ?token=xxx in your url and this will be passed 
-as the "token" header in every request to the API allowing you to use the authorization plugin */
-$.ajaxSetup(
-  {
-    headers : authorizationTokens
-  }
-);
-
-
-function SplitLongUid(s)
-{
-  return '<span>' + s.substr(0, s.length / 2) + '</span> <span>' + s.substr(s.length / 2, s.length - s.length / 2) + '</span>';
-}
-
-
-function ParseDicomDate(s)
-{
-  y = parseInt(s.substr(0, 4), 10);
-  m = parseInt(s.substr(4, 2), 10) - 1;
-  d = parseInt(s.substr(6, 2), 10);
-
-  if (y == null || m == null || d == null ||
-      !isFinite(y) || !isFinite(m) || !isFinite(d))
-  {
-    return null;
-  }
-
-  if (y < 1900 || y > 2100 ||
-      m < 0 || m >= 12 ||
-      d <= 0 || d >= 32)
-  {
-    return null;
-  }
-
-  return new Date(y, m, d);
-}
-
-
-function FormatDicomDate(s)
-{
-  if (s == undefined)
-    return "No date";
-
-  var d = ParseDicomDate(s);
-  if (d == null)
-    return '?';
-  else
-    return d.toString('dddd, MMMM d, yyyy');
-}
-
-function FormatFloatSequence(s)
-{
-  if (s == undefined || s.length == 0)
-    return "-";
-
-  if (s.indexOf("\\") == -1)
-    return s;
-
-  var oldValues = s.split("\\");
-  var newValues = [];
-  for (var i = 0; i < oldValues.length; i++)
-  {
-    newValues.push(parseFloat(oldValues[i]).toFixed(3));
-  }
-  return newValues.join("\\");
-}
-
-function Sort(arr, fieldExtractor, isInteger, reverse)
-{
-  var defaultValue;
-  if (isInteger)
-    defaultValue = 0;
-  else
-    defaultValue = '';
-
-  arr.sort(function(a, b) {
-    var ta = fieldExtractor(a);
-    var tb = fieldExtractor(b);
-    var order;
-
-    if (ta == undefined)
-      ta = defaultValue;
-
-    if (tb == undefined)
-      tb = defaultValue;
-
-    if (isInteger)
-    {
-      ta = parseInt(ta, 10);
-      tb = parseInt(tb, 10);
-      order = ta - tb;
-    }
-    else
-    {
-      if (ta < tb)
-        order = -1;
-      else if (ta > tb)
-        order = 1;
-      else
-        order = 0;
-    }
-
-    if (reverse)
-      return -order;
-    else
-      return order;
-  });
-}
-
-
-function SortOnDicomTag(arr, tag, isInteger, reverse)
-{
-  return Sort(arr, function(a) { 
-    return a.MainDicomTags[tag];
-  }, isInteger, reverse);
-}
-
-
-
-function GetResource(uri, callback)
-{
-  $.ajax({
-    url: '..' + uri,
-    dataType: 'json',
-    async: false,
-    cache: false,
-    success: function(s) {
-      callback(s);
-    }
-  });
-}
-
-
-function CompleteFormatting(node, link, isReverse, count)
-{
-  if (count != null)
-  {
-    node = node.add($('<span>')
-                    .addClass('ui-li-count')
-                    .text(count));
-  }
-  
-  if (link != null)
-  {
-    node = $('<a>').attr('href', link).append(node);
-
-    if (isReverse)
-      node.attr('data-direction', 'reverse')
-  }
-
-  node = $('<li>').append(node);
-
-  if (isReverse)
-    node.attr('data-icon', 'back');
-
-  return node;
-}
-
-
-function FormatMainDicomTags(target, tags, tagsToIgnore)
-{
-  var v;
-  
-  for (var i in tags)
-  {
-    if (tagsToIgnore.indexOf(i) == -1)
-    {
-      v = tags[i];
-
-      if (i == "PatientBirthDate" ||
-          i == "StudyDate" ||
-          i == "SeriesDate")
-      {
-        v = FormatDicomDate(v);
-      }
-      else if (i == "DicomStudyInstanceUID" ||
-               i == "DicomSeriesInstanceUID")
-      {
-        v = SplitLongUid(v);
-      }
-      else if (i == "ImagePositionPatient" ||
-               i == "ImageOrientationPatient")
-      {
-        v = FormatFloatSequence(v);
-      }
-      
-      target.append($('<p>')
-                    .text(i + ': ')
-                    .append($('<strong>').text(v)));
-    }
-  }
-}
-
-
-function FormatPatient(patient, link, isReverse)
-{
-  var node = $('<div>').append($('<h3>').text(patient.MainDicomTags.PatientName));
-
-  FormatMainDicomTags(node, patient.MainDicomTags, [ 
-    "PatientName"
-    // "OtherPatientIDs"
-  ]);
-    
-  return CompleteFormatting(node, link, isReverse, patient.Studies.length);
-}
-
-
-
-function FormatStudy(study, link, isReverse, includePatient)
-{
-  var label;
-  var node;
-
-  if (includePatient) {
-    label = study.Label;
-  } else {
-    label = study.MainDicomTags.StudyDescription;
-  }
-
-  node = $('<div>').append($('<h3>').text(label));
-
-  if (includePatient) {
-    FormatMainDicomTags(node, study.PatientMainDicomTags, [ 
-      'PatientName'
-    ]);
-  }
-    
-  FormatMainDicomTags(node, study.MainDicomTags, [ 
-     'StudyDescription', 
-     'StudyTime' 
-  ]);
-
-  return CompleteFormatting(node, link, isReverse, study.Series.length);
-}
-
-
-
-function FormatSeries(series, link, isReverse)
-{
-  var c;
-  var node;
-
-  if (series.ExpectedNumberOfInstances == null ||
-      series.Instances.length == series.ExpectedNumberOfInstances)
-  {
-    c = series.Instances.length;
-  }
-  else
-  {
-    c = series.Instances.length + '/' + series.ExpectedNumberOfInstances;
-  }
-  
-  node = $('<div>')
-      .append($('<h3>').text(series.MainDicomTags.SeriesDescription))
-      .append($('<p>').append($('<em>')
-                           .text('Status: ')
-                           .append($('<strong>').text(series.Status))));
-
-  FormatMainDicomTags(node, series.MainDicomTags, [ 
-     "SeriesDescription", 
-     "SeriesTime", 
-     "Manufacturer",
-     "ImagesInAcquisition",
-     "SeriesDate",
-     "ImageOrientationPatient"
-  ]);
-    
-  return CompleteFormatting(node, link, isReverse, c);
-}
-
-
-function FormatInstance(instance, link, isReverse)
-{
-  var node = $('<div>').append($('<h3>').text('Instance: ' + instance.IndexInSeries));
-
-  FormatMainDicomTags(node, instance.MainDicomTags, [
-    "AcquisitionNumber", 
-    "InstanceNumber", 
-    "InstanceCreationDate", 
-    "InstanceCreationTime"
-  ]);
-    
-  return CompleteFormatting(node, link, isReverse);
-}
-
-
-$('[data-role="page"]').live('pagebeforeshow', function() {
-  $.ajax({
-    url: '../system',
-    dataType: 'json',
-    async: false,
-    cache: false,
-    success: function(s) {
-      if (s.Name != "") {
-        $('.orthanc-name').html($('<a>')
-                                .addClass('ui-link')
-                                .attr('href', 'explorer.html')
-                                .text(s.Name)
-                                .append(' &raquo; '));
-      }
-
-      // New in Orthanc 1.5.8
-      if ('IsHttpServerSecure' in s &&
-          !s.IsHttpServerSecure) {
-        $('.warning-insecure').show();
-      } else {
-        $('.warning-insecure').hide();
-      }
-    }
-  });
-});
-
-
-
-$('#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() {
-  var lookup;
-
-  $('#lookup-result').hide();
-
-  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&since=0&limit=' + (LIMIT_RESOURCES + 1), function(patients) {
-    var target = $('#all-patients');
-    var count, showAlert, p;
-
-
-    $('li', target).remove();
-    
-    SortOnDicomTag(patients, 'PatientName', false, false);
-
-    if (patients.length <= LIMIT_RESOURCES) {
-      count = patients.length;
-      showAlert = false;
-    }
-    else {
-      count = LIMIT_RESOURCES;
-      showAlert = true;
-    }
-
-    for (var i = 0; i < count; i++) {
-      p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
-      target.append(p);
-    }
-
-    target.listview('refresh'); 
-
-    if (showAlert) {
-      $('#count-patients').text(LIMIT_RESOURCES);
-      $('#alert-patients').show();
-    } else {
-      $('#alert-patients').hide();
-    }
-  });
-});
-
-
-
-function FormatListOfStudies(targetId, alertId, countId, studies)
-{
-  var target = $(targetId);
-  var patient, study, s;
-  var count, showAlert;
-
-  $('li', target).remove();
-
-  for (var i = 0; i < studies.length; i++) {
-    patient = studies[i].PatientMainDicomTags.PatientName;
-    study = studies[i].MainDicomTags.StudyDescription;
-
-    s = "";
-    if (typeof patient === 'string') {
-      s = patient;
-    }
-
-    if (typeof study === 'string') {
-      if (s.length > 0) {
-        s += ' - ';
-      }
-
-      s += study;
-    }
-
-    studies[i]['Label'] = s;
-  }
-
-  Sort(studies, function(a) { return a.Label }, false, false);
-
-  if (studies.length <= LIMIT_RESOURCES) {
-    count = studies.length;
-    showAlert = false;
-  }
-  else {
-    count = LIMIT_RESOURCES;
-    showAlert = true;
-  }
-
-  for (var i = 0; i < count; i++) {
-    s = FormatStudy(studies[i], '#study?uuid=' + studies[i].ID, false, true);
-    target.append(s);
-  }
-
-  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);
-  });
-});
-
-
-
-function SetupAnonymizedOrModifiedFrom(buttonSelector, resource, resourceType, field)
-{
-  if (field in resource)
-  {
-    $(buttonSelector).closest('li').show();
-    $(buttonSelector).click(function(e) {
-      window.location.assign('explorer.html#' + resourceType + '?uuid=' + resource[field]);
-    });
-  }
-  else
-  {
-    $(buttonSelector).closest('li').hide();
-  }
-}
-
-
-
-function RefreshPatient()
-{
-  var pageData, target, v;
-
-  if ($.mobile.pageData) {
-    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();
-        $('#patient-info')
-          .append('<li data-role="list-divider">Patient</li>')
-          .append(FormatPatient(patient))
-          .listview('refresh');
-
-        target = $('#list-studies');
-        $('li', target).remove();
-        
-        for (var i = 0; i < studies.length; i++) {
-          if (i == 0 || studies[i].MainDicomTags.StudyDate != studies[i - 1].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));
-        }
-
-        SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', 'AnonymizedFrom');
-        SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', 'ModifiedFrom');
-
-        target.listview('refresh');
-
-        // Check whether this patient is protected
-        $.ajax({
-          url: '../patients/' + pageData.uuid + '/protected',
-          type: 'GET',
-          dataType: 'text',
-          async: false,
-          cache: false,
-          success: function (s) {
-            v = (s == '1') ? 'on' : 'off';
-            $('#protection').val(v).slider('refresh');
-          }
-        });
-
-        currentPage = 'patient';
-        currentUuid = pageData.uuid;
-      });
-    });
-  }
-}
-
-
-function RefreshStudy()
-{
-  var pageData, target;
-
-  if ($.mobile.pageData) {
-    pageData = DeepCopy($.mobile.pageData);
-
-    GetResource('/studies/' + pageData.uuid, function(study) {
-      GetResource('/patients/' + study.ParentPatient, function(patient) {
-        GetResource('/studies/' + pageData.uuid + '/series', function(series) {
-          SortOnDicomTag(series, 'SeriesDate', false, true);
-
-          $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID);
-          $('#study-info li').remove();
-          $('#study-info')
-            .append('<li data-role="list-divider">Patient</li>')
-            .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
-            .append('<li data-role="list-divider">Study</li>')
-            .append(FormatStudy(study))
-            .listview('refresh');
-
-          SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', 'AnonymizedFrom');
-          SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', 'ModifiedFrom');
-
-          target = $('#list-series');
-          $('li', target).remove();
-          for (var i = 0; i < series.length; i++) {
-            if (i == 0 || series[i].MainDicomTags.SeriesDate != series[i - 1].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 = pageData.uuid;
-        });
-      });
-    });
-  }
-}
-  
-
-function RefreshSeries() 
-{
-  var pageData, target;
-
-  if ($.mobile.pageData) {
-    pageData = DeepCopy($.mobile.pageData);
-
-    GetResource('/series/' + pageData.uuid, function(series) {
-      GetResource('/studies/' + series.ParentStudy, function(study) {
-        GetResource('/patients/' + study.ParentPatient, function(patient) {
-          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);
-            $('#series .study-link').attr('href', '#study?uuid=' + study.ID);
-
-            $('#series-info li').remove();
-            $('#series-info')
-              .append('<li data-role="list-divider">Patient</li>')
-              .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
-              .append('<li data-role="list-divider">Study</li>')
-              .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
-              .append('<li data-role="list-divider">Series</li>')
-              .append(FormatSeries(series))
-              .listview('refresh');
-
-            SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', 'AnonymizedFrom');
-            SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', 'ModifiedFrom');
-
-            target = $('#list-instances');
-            $('li', target).remove();
-            for (var i = 0; i < instances.length; i++) {
-              target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID));
-            }
-            target.listview('refresh');
-
-            currentPage = 'series';
-            currentUuid = pageData.uuid;
-          });
-        });
-      });
-    });
-  }
-}
-
-
-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)
-{
-  var result = [];
-  var label, c;
-
-  for (var i in dicom) {
-    if (dicom[i] != null) {
-      label = (i + '<span class="tag-name"> (<i>' +
-                   EscapeHtml(dicom[i]["Name"]) +
-                   '</i>)</span>: ');
-
-      if (dicom[i]["Type"] == 'String')
-      {
-        result.push({
-          label: label + '<strong>' + EscapeHtml(dicom[i]["Value"]) + '</strong>',
-          children: []
-        });
-      }
-      else if (dicom[i]["Type"] == 'TooLong')
-      {
-        result.push({
-          label: label + '<i>Too long</i>',
-          children: []
-        });
-      }
-      else if (dicom[i]["Type"] == 'Null')
-      {
-        result.push({
-          label: label + '<i>Null</i>',
-          children: []
-        });
-      }
-      else if (dicom[i]["Type"] == 'Sequence')
-      {
-        c = [];
-        for (var j = 0; j < dicom[i]["Value"].length; j++) {
-          c.push({
-            label: 'Item ' + j,
-            children: ConvertForTree(dicom[i]["Value"][j])
-          });
-        }
-
-        result.push({
-          label: label + '[]',
-          children: c
-        });
-      }
-    }
-  }
-
-  return result;
-}
-
-
-function RefreshInstance()
-{
-  var pageData;
-
-  if ($.mobile.pageData) {
-    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) {
-
-            $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID);
-            $('#instance .study-link').attr('href', '#study?uuid=' + study.ID);
-            $('#instance .series-link').attr('href', '#series?uuid=' + series.ID);
-            
-            $('#instance-info li').remove();
-            $('#instance-info')
-              .append('<li data-role="list-divider">Patient</li>')
-              .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
-              .append('<li data-role="list-divider">Study</li>')
-              .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
-              .append('<li data-role="list-divider">Series</li>')
-              .append(FormatSeries(series, '#series?uuid=' + series.ID, true))
-              .append('<li data-role="list-divider">Instance</li>')
-              .append(FormatInstance(instance))
-              .listview('refresh');
-
-            GetResource('/instances/' + instance.ID + '/tags', function(s) {
-              $('#dicom-tree').tree('loadData', ConvertForTree(s));
-            });
-
-            SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', 'AnonymizedFrom');
-            SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', 'ModifiedFrom');
-
-            currentPage = 'instance';
-            currentUuid = pageData.uuid;
-          });
-        });
-      });
-    });
-  }
-}
-
-$(document).live('pagebeforehide', function() {
-  currentPage = '';
-  currentUuid = '';
-});
-
-
-
-$('#patient').live('pagebeforeshow', RefreshPatient);
-$('#study').live('pagebeforeshow', RefreshStudy);
-$('#series').live('pagebeforeshow', RefreshSeries);
-$('#instance').live('pagebeforeshow', RefreshInstance);
-
-$(function() {
-  $(window).hashchange(function(e, data) {
-    // This fixes the navigation with the back button and with the anonymization
-    if ('uuid' in $.mobile.pageData &&
-        currentPage == $.mobile.pageData.active &&
-        currentUuid != $.mobile.pageData.uuid) {
-      Refresh();
-    }
-  });
-});
-
-
-
-
-
-function DeleteResource(path)
-{
-  $.ajax({
-    url: path,
-    type: 'DELETE',
-    dataType: 'json',
-    async: false,
-    success: function(s) {
-      var ancestor = s.RemainingAncestor;
-      if (ancestor == null)
-        $.mobile.changePage('#lookup');
-      else
-        $.mobile.changePage('#' + ancestor.Type.toLowerCase() + '?uuid=' + ancestor.ID);
-    }
-  });
-}
-
-
-
-function OpenDeleteResourceDialog(path, title)
-{
-  $(document).simpledialog2({ 
-    // http://dev.jtsage.com/jQM-SimpleDialog/demos2/
-    // http://dev.jtsage.com/jQM-SimpleDialog/demos2/options.html
-    mode: 'button',
-    animate: false,
-    headerText: title,
-    headerClose: true,
-    width: '500px',
-    buttons : {
-      'OK': {
-        click: function () { 
-          DeleteResource(path);
-        },
-        icon: "delete",
-        theme: "c"
-      },
-      'Cancel': {
-        click: function () { 
-        }
-      }
-    }
-  });
-}
-
-
-
-$('#instance-delete').live('click', function() {
-  OpenDeleteResourceDialog('../instances/' + $.mobile.pageData.uuid,
-                           'Delete this instance?');
-});
-
-$('#study-delete').live('click', function() {
-  OpenDeleteResourceDialog('../studies/' + $.mobile.pageData.uuid,
-                           'Delete this study?');
-});
-
-$('#series-delete').live('click', function() {
-  OpenDeleteResourceDialog('../series/' + $.mobile.pageData.uuid,
-                           'Delete this series?');
-});
-
-$('#patient-delete').live('click', function() {
-  OpenDeleteResourceDialog('../patients/' + $.mobile.pageData.uuid,
-                           'Delete this patient?');
-});
-
-
-$('#instance-download-dicom').live('click', function(e) {
-  // http://stackoverflow.com/a/1296101
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../instances/' + $.mobile.pageData.uuid + '/file';
-});
-
-$('#instance-download-json').live('click', function(e) {
-  // http://stackoverflow.com/a/1296101
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../instances/' + $.mobile.pageData.uuid + '/tags';
-});
-
-
-
-$('#instance-preview').live('click', function(e) {
-  var pageData, pdf, images;
-
-  if ($.mobile.pageData) {
-    pageData = DeepCopy($.mobile.pageData);
-
-    pdf = '../instances/' + pageData.uuid + '/pdf';
-    $.ajax({
-      url: pdf,
-      cache: false,
-      success: function(s) {
-        window.location.assign(pdf);
-      },
-      error: function() {
-        GetResource('/instances/' + pageData.uuid + '/frames', function(frames) {
-          if (frames.length == 1)
-          {
-            // Viewing a single-frame image
-            jQuery.slimbox('../instances/' + pageData.uuid + '/preview', '', {
-              overlayFadeDuration : 1,
-              resizeDuration : 1,
-              imageFadeDuration : 1
-            });
-          }
-          else
-          {
-            // Viewing a multi-frame image
-
-            images = [];
-            for (var i = 0; i < frames.length; i++) {
-              images.push([ '../instances/' + pageData.uuid + '/frames/' + i + '/preview' ]);
-            }
-
-            jQuery.slimbox(images, 0, {
-              overlayFadeDuration : 1,
-              resizeDuration : 1,
-              imageFadeDuration : 1,
-              loop : true
-            });
-          }
-        });
-      }
-    });
-  }
-});
-
-
-
-$('#series-preview').live('click', function(e) {
-  var pageData, images;
-
-  if ($.mobile.pageData) {
-    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);
-
-        images = [];
-        for (var i = 0; i < instances.length; i++) {
-          images.push([ '../instances/' + instances[i].ID + '/preview',
-                        (i + 1).toString() + '/' + instances.length.toString() ])
-        }
-
-        jQuery.slimbox(images, 0, {
-          overlayFadeDuration : 1,
-          resizeDuration : 1,
-          imageFadeDuration : 1,
-          loop : true
-        });
-      });
-    });
-  }
-});
-
-
-
-
-
-function ChooseDicomModality(callback)
-{
-  var clickedModality = '';
-  var clickedPeer = '';
-  var items = $('<ul>')
-    .attr('data-divider-theme', 'd')
-    .attr('data-role', 'listview');
-
-  // Retrieve the list of the known DICOM modalities
-  $.ajax({
-    url: '../modalities',
-    type: 'GET',
-    dataType: 'json',
-    async: false,
-    cache: false,
-    success: function(modalities) {
-      var name, item;
-      
-      if (modalities.length > 0)
-      {
-        items.append('<li data-role="list-divider">DICOM modalities</li>');
-
-        for (var i = 0; i < modalities.length; i++) {
-          name = modalities[i];
-          item = $('<li>')
-            .html('<a href="#" rel="close">' + name + '</a>')
-            .attr('name', name)
-            .click(function() { 
-              clickedModality = $(this).attr('name');
-            });
-          items.append(item);
-        }
-      }
-
-      // Retrieve the list of the known Orthanc peers
-      $.ajax({
-        url: '../peers',
-        type: 'GET',
-        dataType: 'json',
-        async: false,
-        cache: false,
-        success: function(peers) {
-          var name, item;
-
-          if (peers.length > 0)
-          {
-            items.append('<li data-role="list-divider">Orthanc peers</li>');
-
-            for (var i = 0; i < peers.length; i++) {
-              name = peers[i];
-              item = $('<li>')
-                .html('<a href="#" rel="close">' + name + '</a>')
-                .attr('name', name)
-                .click(function() { 
-                  clickedPeer = $(this).attr('name');
-                });
-              items.append(item);
-            }
-          }
-
-          // Launch the dialog
-          $('#dialog').simpledialog2({
-            mode: 'blank',
-            animate: false,
-            headerText: 'Choose target',
-            headerClose: true,
-            forceInput: false,
-            width: '100%',
-            blankContent: items,
-            callbackClose: function() {
-              var timer;
-              function WaitForDialogToClose() {
-                if (!$('#dialog').is(':visible')) {
-                  clearInterval(timer);
-                  callback(clickedModality, clickedPeer);
-                }
-              }
-              timer = setInterval(WaitForDialogToClose, 100);
-            }
-          });
-        }
-      });
-    }
-  });
-}
-
-
-$('#instance-store,#series-store,#study-store,#patient-store').live('click', function(e) {
-  ChooseDicomModality(function(modality, peer) {
-    var pageData = DeepCopy($.mobile.pageData);
-    var url, loading;
-
-    if (modality != '')
-    {
-      url = '../modalities/' + modality + '/store';
-      loading = '#dicom-store';
-    }
-
-    if (peer != '')
-    {
-      url = '../peers/' + peer + '/store';
-      loading = '#peer-store';
-    }
-
-    if (url != '') {
-      $.ajax({
-        url: url,
-        type: 'POST',
-        dataType: 'text',
-        data: pageData.uuid,
-        async: true,  // Necessary to block UI
-        beforeSend: function() {
-          $.blockUI({ message: $(loading) });
-        },
-        complete: function(s) {
-          $.unblockUI();
-        },
-        success: function(s) {
-        },
-        error: function() {
-          alert('Error during store');
-        }
-      });      
-    }
-  });
-});
-
-
-$('#show-tag-name').live('change', function(e) {
-  var checked = e.currentTarget.checked;
-  if (checked)
-    $('.tag-name').show();
-  else
-    $('.tag-name').hide();
-});
-
-
-$('#patient-archive').live('click', function(e) {
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../patients/' + $.mobile.pageData.uuid + '/archive';
-});
-
-$('#study-archive').live('click', function(e) {
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../studies/' + $.mobile.pageData.uuid + '/archive';
-});
-
-$('#series-archive').live('click', function(e) {
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive';
-});
-
-
-$('#patient-media').live('click', function(e) {
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../patients/' + $.mobile.pageData.uuid + '/media';
-});
-
-$('#study-media').live('click', function(e) {
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../studies/' + $.mobile.pageData.uuid + '/media';
-});
-
-$('#series-media').live('click', function(e) {
-  e.preventDefault();  //stop the browser from following
-  window.location.href = '../series/' + $.mobile.pageData.uuid + '/media';
-});
-
-
-
-$('#protection').live('change', function(e) {
-  var isProtected = e.target.value == "on";
-  $.ajax({
-    url: '../patients/' + $.mobile.pageData.uuid + '/protected',
-    type: 'PUT',
-    dataType: 'text',
-    data: isProtected ? '1' : '0',
-    async: false
-  });
-});
-
-
-
-function OpenAnonymizeResourceDialog(path, title)
-{
-  $(document).simpledialog2({ 
-    mode: 'button',
-    animate: false,
-    headerText: title,
-    headerClose: true,
-    width: '500px',
-    buttons : {
-      'OK': {
-        click: function () { 
-          $.ajax({
-            url: path + '/anonymize',
-            type: 'POST',
-            data: '{ "Keep" : [ "SeriesDescription", "StudyDescription" ] }',
-            dataType: 'json',
-            async: false,
-            cache: false,
-            success: function(s) {
-              // The following line does not work...
-              //$.mobile.changePage('explorer.html#patient?uuid=' + s.PatientID);
-
-              window.location.assign('explorer.html#patient?uuid=' + s.PatientID);
-              //window.location.reload();
-            }
-          });
-        },
-        icon: "delete",
-        theme: "c"
-      },
-      'Cancel': {
-        click: function () { 
-        }
-      }
-    }
-  });
-}
-
-$('#instance-anonymize').live('click', function() {
-  OpenAnonymizeResourceDialog('../instances/' + $.mobile.pageData.uuid,
-                              'Anonymize this instance?');
-});
-
-$('#study-anonymize').live('click', function() {
-  OpenAnonymizeResourceDialog('../studies/' + $.mobile.pageData.uuid,
-                              'Anonymize this study?');
-});
-
-$('#series-anonymize').live('click', function() {
-  OpenAnonymizeResourceDialog('../series/' + $.mobile.pageData.uuid,
-                              'Anonymize this series?');
-});
-
-$('#patient-anonymize').live('click', function() {
-  OpenAnonymizeResourceDialog('../patients/' + $.mobile.pageData.uuid,
-                              'Anonymize this patient?');
-});
-
-
-$('#plugins').live('pagebeforeshow', function() {
-  $.ajax({
-    url: '../plugins',
-    dataType: 'json',
-    async: false,
-    cache: false,
-    success: function(plugins) {
-      var target = $('#all-plugins');
-      $('li', target).remove();
-
-      plugins.map(function(id) {
-        return $.ajax({
-          url: '../plugins/' + id,
-          dataType: 'json',
-          async: false,
-          cache: false,
-          success: function(plugin) {
-            var li = $('<li>');
-            var item = li;
-
-            if ('RootUri' in plugin)
-            {
-              item = $('<a>');
-              li.append(item);
-              item.click(function() {
-                window.open(plugin.RootUri);
-              });
-            }
-
-            item.append($('<h1>').text(plugin.ID));
-            item.append($('<p>').text(plugin.Description));
-            item.append($('<span>').addClass('ui-li-count').text(plugin.Version));
-            target.append(li);
-          }
-        });
-      });
-
-      target.listview('refresh');
-    }
-  });
-});
-
-
-
-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');
-      var running, pending, inactive;
-
-      $('li', target).remove();
-
-      running = $('<li>')
-          .attr('data-role', 'list-divider')
-          .text('Currently running');
-
-      pending = $('<li>')
-          .attr('data-role', 'list-divider')
-          .text('Pending jobs');
-
-      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() {
-  var pageData, target;
-
-  if ($.mobile.pageData) {
-    pageData = DeepCopy($.mobile.pageData);
-
-    $.ajax({
-      url: '../jobs/' + pageData.uuid,
-      dataType: 'json',
-      async: false,
-      cache: false,
-      success: function(job) {
-        var block, value;
-        
-        target = $('#job-info');
-        $('li', target).remove();
-
-        target.append($('<li>')
-                      .attr('data-role', 'list-divider')
-                      .text('General information about the job'));
-
-        {                       
-          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'));
-
-        {
-          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-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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-var pendingUploads = [];
-var currentUpload = 0;
-var totalUpload = 0;
-var alreadyInitialized = false; // trying to debug Orthanc issue #1
-
-$(document).ready(function() {
-  if (alreadyInitialized) {
-    console.log("Orthanc issue #1: the fileupload has been initialized twice !");
-  } else {
-    alreadyInitialized = true;
-  }
-  
-  // Initialize the jQuery File Upload widget:
-  $('#fileupload').fileupload({
-    //dataType: 'json',
-    //maxChunkSize: 500,
-    //sequentialUploads: true,
-    limitConcurrentUploads: 3,
-    add: function (e, data) {
-      pendingUploads.push(data);
-    }
-  })
-    .bind('fileuploadstop', function(e, data) {
-      $('#upload-button').removeClass('ui-disabled');
-      //$('#upload-abort').addClass('ui-disabled');
-      $('#progress .bar').css('width', '100%');
-      if ($('#progress .label').text() != 'Failure')
-        $('#progress .label').text('Done');
-    })
-    .bind('fileuploadfail', function(e, data) {
-      $('#progress .bar')
-        .css('width', '100%')
-        .css('background-color', 'red');
-      $('#progress .label').text('Failure');
-    })
-    .bind('fileuploaddrop', function (e, data) {
-      console.log("dropped " + data.files.length + " files: ", data);
-      appendFilesToUploadList(data.files);
-    })
-    .bind('fileuploadsend', function (e, data) {
-      // Update the progress bar. Note: for some weird reason, the
-      // "fileuploadprogressall" does not work under Firefox.
-      var progress = parseInt(currentUpload / totalUploads * 100, 10);
-      currentUpload += 1;
-      $('#progress .label').text('Uploading: ' + progress + '%');
-      $('#progress .bar')
-        .css('width', progress + '%')
-        .css('background-color', 'green');
-    });
-});
-
-function appendFilesToUploadList(files) {
-  var target = $('#upload-list');
-  $.each(files, function (index, file) {
-    target.append('<li class="pending-file">' + file.name + '</li>');
-  });
-  target.listview('refresh');
-}
-
-$('#fileupload').live('change', function (e) {
-  appendFilesToUploadList(e.target.files);
-})
-
-
-function ClearUploadProgress()
-{
-  $('#progress .label').text('');
-  $('#progress .bar').css('width', '0%').css('background-color', '#333');
-}
-
-$('#upload').live('pagebeforeshow', function() {
-  if (navigator.userAgent.toLowerCase().indexOf('firefox') == -1) {
-    $("#issue-21-warning").css('display', 'none');
-  }
-
-  ClearUploadProgress();
-});
-
-$('#upload').live('pageshow', function() {
-  $('#fileupload').fileupload('enable');
-});
-
-$('#upload').live('pagehide', function() {
-  $('#fileupload').fileupload('disable');
-});
-
-
-$('#upload-button').live('click', function() {
-  var pu = pendingUploads;
-  pendingUploads = [];
-
-  $('.pending-file').remove();
-  $('#upload-list').listview('refresh');
-  ClearUploadProgress();
-
-  currentUpload = 1;
-  totalUploads = pu.length + 1;
-  if (pu.length > 0) {
-    $('#upload-button').addClass('ui-disabled');
-    //$('#upload-abort').removeClass('ui-disabled');
-  }
-
-  for (var i = 0; i < pu.length; i++) {
-    pu[i].submit();
-  }
-});
-
-$('#upload-clear').live('click', function() {
-  pendingUploads = [];
-  $('.pending-file').remove();
-  $('#upload-list').listview('refresh');
-});
-
-/*$('#upload-abort').live('click', function() {
-  $('#fileupload').fileupload().abort();
-  });*/
Binary file OrthancExplorer/images/unsupported.png has changed
--- a/OrthancExplorer/libs/date.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,104 +0,0 @@
-/**
- * Version: 1.0 Alpha-1 
- * Build Date: 13-Nov-2007
- * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
- * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. 
- * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
- */
-Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
-Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
-return-1;};Date.getDayNumberFromName=function(name){var n=Date.CultureInfo.dayNames,m=Date.CultureInfo.abbreviatedDayNames,o=Date.CultureInfo.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
-return-1;};Date.isLeapYear=function(year){return(((year%4===0)&&(year%100!==0))||(year%400===0));};Date.getDaysInMonth=function(year,month){return[31,(Date.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};Date.getTimezoneOffset=function(s,dst){return(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST[s.toUpperCase()]:Date.CultureInfo.abbreviatedTimeZoneStandard[s.toUpperCase()];};Date.getTimezoneAbbreviation=function(offset,dst){var n=(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST:Date.CultureInfo.abbreviatedTimeZoneStandard,p;for(p in n){if(n[p]===offset){return p;}}
-return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
-if(date instanceof Date&&!isNaN(date)){return(this>date)?1:(this<date)?-1:0;}else{throw new TypeError(date);}};Date.prototype.equals=function(date){return(this.compareTo(date)===0);};Date.prototype.between=function(start,end){var t=this.getTime();return t>=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
-var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
-if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
-if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
-if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
-if(x.month||x.months){this.addMonths(x.month||x.months);}
-if(x.year||x.years){this.addYears(x.year||x.years);}
-if(x.day||x.days){this.addDays(x.day||x.days);}
-return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(value<min||value>max){throw new RangeError(value+" is not a valid value for "+name+".");}
-return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
-if(!x.second&&x.second!==0){x.second=-1;}
-if(!x.minute&&x.minute!==0){x.minute=-1;}
-if(!x.hour&&x.hour!==0){x.hour=-1;}
-if(!x.day&&x.day!==0){x.day=-1;}
-if(!x.month&&x.month!==0){x.month=-1;}
-if(!x.year&&x.year!==0){x.year=-1;}
-if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
-if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
-if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
-if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
-if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
-if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
-if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
-if(x.timezone){this.setTimezone(x.timezone);}
-if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
-return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
-var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
-return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
-Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
-return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i<dx.length;i++){$D[dx[i]]=$D[dx[i].substring(0,3)]=df(i);}
-var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
-return this.moveToMonth(n,this._orient);};};for(var j=0;j<mx.length;j++){$D[mx[j]]=$D[mx[j].substring(0,3)]=mf(j);}
-var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
-return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$D[de]=$D[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}}());Date.prototype.toJSONString=function(){return this.toString("yyyy-MM-ddThh:mm:ssZ");};Date.prototype.toShortDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortDatePattern);};Date.prototype.toLongDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.longDatePattern);};Date.prototype.toShortTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortTimePattern);};Date.prototype.toLongTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.longTimePattern);};Date.prototype.getOrdinal=function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};
-(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
-break;}
-return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
-rx.push(r[0]);s=r[1];}
-return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
-return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
-throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
-return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
-if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
-try{r=(px[i].call(this,s));}catch(e){r=null;}
-if(r){return r;}}
-throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
-try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
-rx.push(r[0]);s=r[1];}
-return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
-return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
-rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
-s=q[1];}
-if(!r){throw new $P.Exception(s);}
-if(q){throw new $P.Exception(q[1]);}
-if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
-return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
-rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
-if(!last&&q[1].length===0){last=true;}
-if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
-p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
-if(rx[1].length<best[1].length){best=rx;}
-if(best[1].length===0){break;}}
-if(best[0].length===0){return best;}
-if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
-best[1]=q[1];}
-return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
-return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
-if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
-var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
-return rx;};Date.Grammar={};Date.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=((s.length==3)?Date.getMonthNumberFromName(s):(Number(s)-1));};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<Date.CultureInfo.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];var now=new Date();this.year=now.getFullYear();this.month=now.getMonth();this.day=1;this.hour=0;this.minute=0;this.second=0;for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
-this.hour=(this.meridian=="p"&&this.hour<13)?this.hour+12:this.hour;if(this.day>Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
-var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
-return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
-for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
-if(this.now){return new Date();}
-var today=Date.today();var method=null;var expression=!!(this.days!=null||this.orient||this.operator);if(expression){var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(this.weekday){this.unit="day";gap=(Date.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
-if(this.month){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
-if(!this.unit){this.unit="day";}
-if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
-if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
-this[this.unit+"s"]=this.value*orient;}
-return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
-if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
-if(this.month&&!this.day){this.day=1;}
-return today.set(this);}}};var _=Date.Parsing.Operators,g=Date.Grammar,t=Date.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=Date.CultureInfo.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
-fn=_C[keys]=_.any.apply(null,px);}
-return fn;};g.ctoken2=function(key){return _.rtoken(Date.CultureInfo.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.mm,g.ss],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^(\+|\-)?\s*\d\d\d\d?/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^(\+|\-)\s*\d\d\d\d/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[Date.CultureInfo.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw Date.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
-return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["yyyy-MM-ddTHH:mm:ss","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
-return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
-try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
-return((r[1].length===0)?r[0]:null);};Date.getParseFunction=function(fx){var fn=Date.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
-return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};
Binary file OrthancExplorer/libs/images/ajax-loader.gif has changed
Binary file OrthancExplorer/libs/images/icons-18-black.png has changed
Binary file OrthancExplorer/libs/images/icons-18-white.png has changed
Binary file OrthancExplorer/libs/images/icons-36-black.png has changed
Binary file OrthancExplorer/libs/images/icons-36-white.png has changed
--- a/OrthancExplorer/libs/images/notes.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-* The "ajax-loader.gif" was generated by the http://www.ajaxload.info/
-  Web site. "The Generated gifs can be used, modified and distributed
-  under the terms of the ​Do What The Fuck You Want To Public License."
-
-* The images "icons*" come from jQuery Mobile.
--- a/OrthancExplorer/libs/jqm.page.params.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-// jqm.page.params.js - version 0.1
-// Copyright (c) 2011, Kin Blas
-// 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 the <organization> 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 <COPYRIGHT HOLDER> 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.
-
-(function( $, window, undefined ) {
-
-// Given a query string, convert all the name/value pairs
-// into a property/value object. If a name appears more than
-// once in a query string, the value is automatically turned
-// into an array.
-function queryStringToObject( qstr )
-{
-	var result = {},
-		nvPairs = ( ( qstr || "" ).replace( /^\?/, "" ).split( /&/ ) ),
-		i, pair, n, v;
-
-	for ( i = 0; i < nvPairs.length; i++ ) {
-		var pstr = nvPairs[ i ];
-		if ( pstr ) {
-			pair = pstr.split( /=/ );
-			n = pair[ 0 ];
-			v = pair[ 1 ];
-			if ( result[ n ] === undefined ) {
-				result[ n ] = v;
-			} else {
-				if ( typeof result[ n ] !== "object" ) {
-					result[ n ] = [ result[ n ] ];
-				}
-				result[ n ].push( v );
-			}
-		}
-	}
-
-	return result;
-}
-
-// The idea here is to listen for any pagebeforechange notifications from
-// jQuery Mobile, and then muck with the toPage and options so that query
-// params can be passed to embedded/internal pages. So for example, if a
-// changePage() request for a URL like:
-//
-//    http://mycompany.com/myapp/#page-1?foo=1&bar=2
-//
-// is made, the page that will actually get shown is:
-//
-//    http://mycompany.com/myapp/#page-1
-//
-// The browser's location will still be updated to show the original URL.
-// The query params for the embedded page are also added as a property/value
-// object on the options object. You can access it from your page notifications
-// via data.options.pageData.
-$( document ).bind( "pagebeforechange", function( e, data ) {
-
-	// We only want to handle the case where we are being asked
-	// to go to a page by URL, and only if that URL is referring
-	// to an internal page by id.
-
-	if ( typeof data.toPage === "string" ) {
-		var u = $.mobile.path.parseUrl( data.toPage );
-		if ( $.mobile.path.isEmbeddedPage( u ) ) {
-			// The request is for an internal page, if the hash
-			// contains query (search) params, strip them off the
-			// toPage URL and then set options.dataUrl appropriately
-			// so the location.hash shows the originally requested URL
-			// that hash the query params in the hash.
-
-			var u2 = $.mobile.path.parseUrl( u.hash.replace( /^#/, "" ) );
-			if ( u2.search ) {
-				if ( !data.options.dataUrl ) {
-					data.options.dataUrl = data.toPage;
-				}
-				data.options.pageData = queryStringToObject( u2.search );
-				data.toPage = u.hrefNoHash + "#" + u2.pathname;
-			}
-		}
-	}
-});
-
-})( jQuery, window );
-
-
-
-
-// http://stackoverflow.com/a/8295488
-$(document).bind("pagebeforechange", function( event, data ) {
-  $.mobile.pageData = (data && data.options && data.options.pageData)
-    ? data.options.pageData
-    : {};
-
-  $.mobile.pageData.active = data.toPage[0].id;
-});
Binary file OrthancExplorer/libs/jqtree-icons.png has changed
--- a/OrthancExplorer/libs/jqtree.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-ul.tree {
-    margin-left: 12px;
-}
-
-ul.tree,
-ul.tree ul {
-    list-style: none outside;
-    margin-bottom: 0;
-    padding: 0;
-}
-
-ul.tree ul {
-    display: block;
-    margin-left: 12px;
-    margin-right: 0;
-}
-
-ul.tree li.closed > ul {
-    display: none;
-}
-
-ul.tree li {
-    clear: both;
-}
-
-ul.tree .toggler {
-    background-image: url(jqtree-icons.png);
-    background-repeat: no-repeat;
-    background-position: -8px 0;
-    width: 8px;
-    height: 8px;
-    display: block;
-    position: absolute;
-    left: -12px;
-    top: 30%;
-    text-indent: -9999px;
-    border-bottom: none;
-}
-
-ul.tree div {
-    cursor: pointer;
-}
-
-ul.tree .title {
-    color: #1C4257;
-    vertical-align: middle;
-}
-
-ul.tree li.folder {
-    margin-bottom: 4px;
-}
-
-ul.tree li.folder.closed {
-    margin-bottom: 1px;
-}
-
-ul.tree li.folder .title {
-    margin-left: 0;
-}
-
-ul.tree .toggler.closed {
-    background-position: 0 0;
-}
-
-span.tree-dragging {
-    color: #fff;
-    background: #000;
-    opacity: 0.6;
-    cursor: pointer;
-    padding: 2px 8px;
-}
-
-ul.tree li.ghost {
-    position: relative;
-    z-index: 10;
-    margin-right: 10px;
-}
-
-ul.tree li.ghost span {
-    display: block;
-}
-
-ul.tree li.ghost span.circle {
-    background-image: url(jqtree-icons.png);
-    background-repeat: no-repeat;
-    background-position: 0 -8px;
-    height: 8px;
-    width: 8px;
-    position: absolute;
-    top: -4px;
-    left: 2px;
-}
-
-ul.tree li.ghost span.line {
-    background-color: #0000ff;
-    height: 2px;
-    padding: 0;
-    position: absolute;
-    top: -1px;
-    left: 10px;
-    width: 100%;
-}
-
-ul.tree li.ghost.inside {
-    margin-left: 48px;
-}
-
-ul.tree span.tree-hit {
-    position: absolute;
-    display: block;
-}
-
-ul.tree span.border {
-    position: absolute;
-    display: block;
-    left: -2px;
-    top: 0;
-    border: solid 2px #0000ff;
-    -webkit-border-radius: 6px;
-    -moz-border-radius: 6px;
-    border-radius: 6px;
-    margin: 0;
-}
-
-ul.tree div {
-    width: 100%; /* todo: why is this in here? */
-    *width: auto; /* ie7 fix; issue 41 */
-    position: relative;
-}
-
-ul.tree li.selected > div,
-ul.tree li.selected > div:hover {
-    background-color: #97BDD6;
-    background: -webkit-gradient(linear, left top, left bottom, from(#BEE0F5), to(#89AFCA));
-    background: -moz-linear-gradient(top, #BEE0F5, #89AFCA);
-    background: -ms-linear-gradient(top, #BEE0F5, #89AFCA);
-    background: -o-linear-gradient(top, #BEE0F5, #89AFCA);
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
-}
-
-ul.tree .moving > div .title {
-    outline: dashed 1px #0000ff;
-}
--- a/OrthancExplorer/libs/jquery-file-upload/css/jquery.fileupload-ui.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-@charset 'UTF-8';
-/*
- * jQuery File Upload UI Plugin CSS 6.3
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2010, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-.fileinput-button {
-  position: relative;
-  overflow: hidden;
-  float: left;
-  margin-right: 4px;
-}
-.fileinput-button input {
-  position: absolute;
-  top: 0;
-  right: 0;
-  margin: 0;
-  border: solid transparent;
-  border-width: 0 0 100px 200px;
-  opacity: 0;
-  filter: alpha(opacity=0);
-  -moz-transform: translate(-300px, 0) scale(4);
-  direction: ltr;
-  cursor: pointer;
-}
-.fileupload-buttonbar .btn,
-.fileupload-buttonbar .toggle {
-  margin-bottom: 5px;
-}
-.files .progress {
-  width: 200px;
-}
-.progress-animated .bar {
-  background: url(../img/progressbar.gif) !important;
-  filter: none;
-}
-.fileupload-loading {
-  position: absolute;
-  left: 50%;
-  width: 128px;
-  height: 128px;
-  background: url(../img/loading.gif) center no-repeat;
-  display: none;
-}
-.fileupload-processing .fileupload-loading {
-  display: block;
-}
-
-/* Fix for IE 6: */
-*html .fileinput-button {
-  line-height: 22px;
-  margin: 1px -3px 0 0;
-}
-
-/* Fix for IE 7: */
-*+html .fileinput-button {
-  margin: 1px 0 0 0;
-}
-
-@media (max-width: 480px) {
-  .files .btn span {
-    display: none;
-  }
-  .files .preview * {
-    width: 40px;
-  }
-  .files .name * {
-    width: 80px;
-    display: inline-block;
-    word-wrap: break-word;
-  }
-  .files .progress {
-    width: 20px;
-  }
-  .files .delete {
-    width: 60px;
-  }
-}
--- a/OrthancExplorer/libs/jquery-file-upload/css/style.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-@charset 'UTF-8';
-/*
- * jQuery File Upload Plugin CSS Example 1.0
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2012, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-body{
-  padding-top: 60px;
-}
Binary file OrthancExplorer/libs/jquery-file-upload/img/loading.gif has changed
Binary file OrthancExplorer/libs/jquery-file-upload/img/progressbar.gif has changed
--- a/OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.postmessage-transport.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,117 +0,0 @@
-/*
- * jQuery postMessage Transport Plugin 1.1
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2011, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-/*jslint unparam: true, nomen: true */
-/*global define, window, document */
-
-(function (factory) {
-    'use strict';
-    if (typeof define === 'function' && define.amd) {
-        // Register as an anonymous AMD module:
-        define(['jquery'], factory);
-    } else {
-        // Browser globals:
-        factory(window.jQuery);
-    }
-}(function ($) {
-    'use strict';
-
-    var counter = 0,
-        names = [
-            'accepts',
-            'cache',
-            'contents',
-            'contentType',
-            'crossDomain',
-            'data',
-            'dataType',
-            'headers',
-            'ifModified',
-            'mimeType',
-            'password',
-            'processData',
-            'timeout',
-            'traditional',
-            'type',
-            'url',
-            'username'
-        ],
-        convert = function (p) {
-            return p;
-        };
-
-    $.ajaxSetup({
-        converters: {
-            'postmessage text': convert,
-            'postmessage json': convert,
-            'postmessage html': convert
-        }
-    });
-
-    $.ajaxTransport('postmessage', function (options) {
-        if (options.postMessage && window.postMessage) {
-            var iframe,
-                loc = $('<a>').prop('href', options.postMessage)[0],
-                target = loc.protocol + '//' + loc.host,
-                xhrUpload = options.xhr().upload;
-            return {
-                send: function (_, completeCallback) {
-                    var message = {
-                            id: 'postmessage-transport-' + (counter += 1)
-                        },
-                        eventName = 'message.' + message.id;
-                    iframe = $(
-                        '<iframe style="display:none;" src="' +
-                            options.postMessage + '" name="' +
-                            message.id + '"></iframe>'
-                    ).bind('load', function () {
-                        $.each(names, function (i, name) {
-                            message[name] = options[name];
-                        });
-                        message.dataType = message.dataType.replace('postmessage ', '');
-                        $(window).bind(eventName, function (e) {
-                            e = e.originalEvent;
-                            var data = e.data,
-                                ev;
-                            if (e.origin === target && data.id === message.id) {
-                                if (data.type === 'progress') {
-                                    ev = document.createEvent('Event');
-                                    ev.initEvent(data.type, false, true);
-                                    $.extend(ev, data);
-                                    xhrUpload.dispatchEvent(ev);
-                                } else {
-                                    completeCallback(
-                                        data.status,
-                                        data.statusText,
-                                        {postmessage: data.result},
-                                        data.headers
-                                    );
-                                    iframe.remove();
-                                    $(window).unbind(eventName);
-                                }
-                            }
-                        });
-                        iframe[0].contentWindow.postMessage(
-                            message,
-                            target
-                        );
-                    }).appendTo(document.body);
-                },
-                abort: function () {
-                    if (iframe) {
-                        iframe.remove();
-                    }
-                }
-            };
-        }
-    });
-
-}));
--- a/OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.xdr-transport.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/*
- * jQuery XDomainRequest Transport Plugin 1.1.2
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2011, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- *
- * Based on Julian Aubourg's ajaxHooks xdr.js:
- * https://github.com/jaubourg/ajaxHooks/
- */
-
-/*jslint unparam: true */
-/*global define, window, XDomainRequest */
-
-(function (factory) {
-    'use strict';
-    if (typeof define === 'function' && define.amd) {
-        // Register as an anonymous AMD module:
-        define(['jquery'], factory);
-    } else {
-        // Browser globals:
-        factory(window.jQuery);
-    }
-}(function ($) {
-    'use strict';
-    if (window.XDomainRequest && !$.support.cors) {
-        $.ajaxTransport(function (s) {
-            if (s.crossDomain && s.async) {
-                if (s.timeout) {
-                    s.xdrTimeout = s.timeout;
-                    delete s.timeout;
-                }
-                var xdr;
-                return {
-                    send: function (headers, completeCallback) {
-                        function callback(status, statusText, responses, responseHeaders) {
-                            xdr.onload = xdr.onerror = xdr.ontimeout = $.noop;
-                            xdr = null;
-                            completeCallback(status, statusText, responses, responseHeaders);
-                        }
-                        xdr = new XDomainRequest();
-                        // XDomainRequest only supports GET and POST:
-                        if (s.type === 'DELETE') {
-                            s.url = s.url + (/\?/.test(s.url) ? '&' : '?') +
-                                '_method=DELETE';
-                            s.type = 'POST';
-                        } else if (s.type === 'PUT') {
-                            s.url = s.url + (/\?/.test(s.url) ? '&' : '?') +
-                                '_method=PUT';
-                            s.type = 'POST';
-                        }
-                        xdr.open(s.type, s.url);
-                        xdr.onload = function () {
-                            callback(
-                                200,
-                                'OK',
-                                {text: xdr.responseText},
-                                'Content-Type: ' + xdr.contentType
-                            );
-                        };
-                        xdr.onerror = function () {
-                            callback(404, 'Not Found');
-                        };
-                        if (s.xdrTimeout) {
-                            xdr.ontimeout = function () {
-                                callback(0, 'timeout');
-                            };
-                            xdr.timeout = s.xdrTimeout;
-                        }
-                        xdr.send((s.hasContent && s.data) || null);
-                    },
-                    abort: function () {
-                        if (xdr) {
-                            xdr.onerror = $.noop();
-                            xdr.abort();
-                        }
-                    }
-                };
-            }
-        });
-    }
-}));
--- a/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-fp.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,219 +0,0 @@
-/*
- * jQuery File Upload File Processing Plugin 1.0
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2012, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-/*jslint nomen: true, unparam: true, regexp: true */
-/*global define, window, document */
-
-(function (factory) {
-    'use strict';
-    if (typeof define === 'function' && define.amd) {
-        // Register as an anonymous AMD module:
-        define([
-            'jquery',
-            'load-image',
-            'canvas-to-blob',
-            './jquery.fileupload'
-        ], factory);
-    } else {
-        // Browser globals:
-        factory(
-            window.jQuery,
-            window.loadImage
-        );
-    }
-}(function ($, loadImage) {
-    'use strict';
-
-    // The File Upload IP version extends the basic fileupload widget
-    // with file processing functionality:
-    $.widget('blueimpFP.fileupload', $.blueimp.fileupload, {
-
-        options: {
-            // The list of file processing actions:
-            process: [
-            /*
-                {
-                    action: 'load',
-                    fileTypes: /^image\/(gif|jpeg|png)$/,
-                    maxFileSize: 20000000 // 20MB
-                },
-                {
-                    action: 'resize',
-                    maxWidth: 1920,
-                    maxHeight: 1200,
-                    minWidth: 800,
-                    minHeight: 600
-                },
-                {
-                    action: 'save'
-                }
-            */
-            ],
-
-            // The add callback is invoked as soon as files are added to the
-            // fileupload widget (via file input selection, drag & drop or add
-            // API call). See the basic file upload widget for more information:
-            add: function (e, data) {
-                $(this).fileupload('process', data).done(function () {
-                    data.submit();
-                });
-            }
-        },
-
-        processActions: {
-            // Loads the image given via data.files and data.index
-            // as canvas element.
-            // Accepts the options fileTypes (regular expression)
-            // and maxFileSize (integer) to limit the files to load:
-            load: function (data, options) {
-                var that = this,
-                    file = data.files[data.index],
-                    dfd = $.Deferred();
-                if (window.HTMLCanvasElement &&
-                        window.HTMLCanvasElement.prototype.toBlob &&
-                        ($.type(options.maxFileSize) !== 'number' ||
-                            file.size < options.maxFileSize) &&
-                        (!options.fileTypes ||
-                            options.fileTypes.test(file.type))) {
-                    loadImage(
-                        file,
-                        function (canvas) {
-                            data.canvas = canvas;
-                            dfd.resolveWith(that, [data]);
-                        },
-                        {canvas: true}
-                    );
-                } else {
-                    dfd.rejectWith(that, [data]);
-                }
-                return dfd.promise();
-            },
-            // Resizes the image given as data.canvas and updates
-            // data.canvas with the resized image.
-            // Accepts the options maxWidth, maxHeight, minWidth and
-            // minHeight to scale the given image:
-            resize: function (data, options) {
-                if (data.canvas) {
-                    var canvas = loadImage.scale(data.canvas, options);
-                    if (canvas.width !== data.canvas.width ||
-                            canvas.height !== data.canvas.height) {
-                        data.canvas = canvas;
-                        data.processed = true;
-                    }
-                }
-                return data;
-            },
-            // Saves the processed image given as data.canvas
-            // inplace at data.index of data.files:
-            save: function (data, options) {
-                // Do nothing if no processing has happened:
-                if (!data.canvas || !data.processed) {
-                    return data;
-                }
-                var that = this,
-                    file = data.files[data.index],
-                    name = file.name,
-                    dfd = $.Deferred(),
-                    callback = function (blob) {
-                        if (!blob.name) {
-                            if (file.type === blob.type) {
-                                blob.name = file.name;
-                            } else if (file.name) {
-                                blob.name = file.name.replace(
-                                    /\..+$/,
-                                    '.' + blob.type.substr(6)
-                                );
-                            }
-                        }
-                        // Store the created blob at the position
-                        // of the original file in the files list:
-                        data.files[data.index] = blob;
-                        dfd.resolveWith(that, [data]);
-                    };
-                // Use canvas.mozGetAsFile directly, to retain the filename, as
-                // Gecko doesn't support the filename option for FormData.append:
-                if (data.canvas.mozGetAsFile) {
-                    callback(data.canvas.mozGetAsFile(
-                        (/^image\/(jpeg|png)$/.test(file.type) && name) ||
-                            ((name && name.replace(/\..+$/, '')) ||
-                                'blob') + '.png',
-                        file.type
-                    ));
-                } else {
-                    data.canvas.toBlob(callback, file.type);
-                }
-                return dfd.promise();
-            }
-        },
-
-        // Resizes the file at the given index and stores the created blob at
-        // the original position of the files list, returns a Promise object:
-        _processFile: function (files, index, options) {
-            var that = this,
-                dfd = $.Deferred().resolveWith(that, [{
-                    files: files,
-                    index: index
-                }]),
-                chain = dfd.promise();
-            that._processing += 1;
-            $.each(options.process, function (i, settings) {
-                chain = chain.pipe(function (data) {
-                    return that.processActions[settings.action]
-                        .call(this, data, settings);
-                });
-            });
-            chain.always(function () {
-                that._processing -= 1;
-                if (that._processing === 0) {
-                    that.element
-                        .removeClass('fileupload-processing');
-                }
-            });
-            if (that._processing === 1) {
-                that.element.addClass('fileupload-processing');
-            }
-            return chain;
-        },
-
-        // Processes the files given as files property of the data parameter,
-        // returns a Promise object that allows one to bind a done handler, which
-        // will be invoked after processing all files (inplace) is done:
-        process: function (data) {
-            var that = this,
-                options = $.extend({}, this.options, data);
-            if (options.process && options.process.length &&
-                    this._isXHRUpload(options)) {
-                $.each(data.files, function (index, file) {
-                    that._processingQueue = that._processingQueue.pipe(
-                        function () {
-                            var dfd = $.Deferred();
-                            that._processFile(data.files, index, options)
-                                .always(function () {
-                                    dfd.resolveWith(that);
-                                });
-                            return dfd.promise();
-                        }
-                    );
-                });
-            }
-            return this._processingQueue;
-        },
-
-        _create: function () {
-            $.blueimp.fileupload.prototype._create.call(this);
-            this._processing = 0;
-            this._processingQueue = $.Deferred().resolveWith(this)
-                .promise();
-        }
-
-    });
-
-}));
--- a/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-ui.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,702 +0,0 @@
-/*
- * jQuery File Upload User Interface Plugin 6.9.1
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2010, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-/*jslint nomen: true, unparam: true, regexp: true */
-/*global define, window, document, URL, webkitURL, FileReader */
-
-(function (factory) {
-    'use strict';
-    if (typeof define === 'function' && define.amd) {
-        // Register as an anonymous AMD module:
-        define([
-            'jquery',
-            'tmpl',
-            'load-image',
-            './jquery.fileupload-fp'
-        ], factory);
-    } else {
-        // Browser globals:
-        factory(
-            window.jQuery,
-            window.tmpl,
-            window.loadImage
-        );
-    }
-}(function ($, tmpl, loadImage) {
-    'use strict';
-
-    // The UI version extends the FP (file processing) version or the basic
-    // file upload widget and adds complete user interface interaction:
-    var parentWidget = ($.blueimpFP || $.blueimp).fileupload;
-    $.widget('blueimpUI.fileupload', parentWidget, {
-
-        options: {
-            // By default, files added to the widget are uploaded as soon
-            // as the user clicks on the start buttons. To enable automatic
-            // uploads, set the following option to true:
-            autoUpload: false,
-            // The following option limits the number of files that are
-            // allowed to be uploaded using this widget:
-            maxNumberOfFiles: undefined,
-            // The maximum allowed file size:
-            maxFileSize: undefined,
-            // The minimum allowed file size:
-            minFileSize: undefined,
-            // The regular expression for allowed file types, matches
-            // against either file type or file name:
-            acceptFileTypes:  /.+$/i,
-            // The regular expression to define for which files a preview
-            // image is shown, matched against the file type:
-            previewSourceFileTypes: /^image\/(gif|jpeg|png)$/,
-            // The maximum file size of images that are to be displayed as preview:
-            previewSourceMaxFileSize: 5000000, // 5MB
-            // The maximum width of the preview images:
-            previewMaxWidth: 80,
-            // The maximum height of the preview images:
-            previewMaxHeight: 80,
-            // By default, preview images are displayed as canvas elements
-            // if supported by the browser. Set the following option to false
-            // to always display preview images as img elements:
-            previewAsCanvas: true,
-            // The ID of the upload template:
-            uploadTemplateId: 'template-upload',
-            // The ID of the download template:
-            downloadTemplateId: 'template-download',
-            // The container for the list of files. If undefined, it is set to
-            // an element with class "files" inside of the widget element:
-            filesContainer: undefined,
-            // By default, files are appended to the files container.
-            // Set the following option to true, to prepend files instead:
-            prependFiles: false,
-            // The expected data type of the upload response, sets the dataType
-            // option of the $.ajax upload requests:
-            dataType: 'json',
-
-            // The add callback is invoked as soon as files are added to the fileupload
-            // widget (via file input selection, drag & drop or add API call).
-            // See the basic file upload widget for more information:
-            add: function (e, data) {
-                var that = $(this).data('fileupload'),
-                    options = that.options,
-                    files = data.files;
-                $(this).fileupload('process', data).done(function () {
-                    that._adjustMaxNumberOfFiles(-files.length);
-                    data.isAdjusted = true;
-                    data.files.valid = data.isValidated = that._validate(files);
-                    data.context = that._renderUpload(files).data('data', data);
-                    options.filesContainer[
-                        options.prependFiles ? 'prepend' : 'append'
-                    ](data.context);
-                    that._renderPreviews(files, data.context);
-                    that._forceReflow(data.context);
-                    that._transition(data.context).done(
-                        function () {
-                            if ((that._trigger('added', e, data) !== false) &&
-                                    (options.autoUpload || data.autoUpload) &&
-                                    data.autoUpload !== false && data.isValidated) {
-                                data.submit();
-                            }
-                        }
-                    );
-                });
-            },
-            // Callback for the start of each file upload request:
-            send: function (e, data) {
-                var that = $(this).data('fileupload');
-                if (!data.isValidated) {
-                    if (!data.isAdjusted) {
-                        that._adjustMaxNumberOfFiles(-data.files.length);
-                    }
-                    if (!that._validate(data.files)) {
-                        return false;
-                    }
-                }
-                if (data.context && data.dataType &&
-                        data.dataType.substr(0, 6) === 'iframe') {
-                    // Iframe Transport does not support progress events.
-                    // In lack of an indeterminate progress bar, we set
-                    // the progress to 100%, showing the full animated bar:
-                    data.context
-                        .find('.progress').addClass(
-                            !$.support.transition && 'progress-animated'
-                        )
-                        .attr('aria-valuenow', 100)
-                        .find('.bar').css(
-                            'width',
-                            '100%'
-                        );
-                }
-                return that._trigger('sent', e, data);
-            },
-            // Callback for successful uploads:
-            done: function (e, data) {
-                var that = $(this).data('fileupload'),
-                    template;
-                if (data.context) {
-                    data.context.each(function (index) {
-                        var file = ($.isArray(data.result) &&
-                                data.result[index]) || {error: 'emptyResult'};
-                        if (file.error) {
-                            that._adjustMaxNumberOfFiles(1);
-                        }
-                        that._transition($(this)).done(
-                            function () {
-                                var node = $(this);
-                                template = that._renderDownload([file])
-                                    .css('height', node.height())
-                                    .replaceAll(node);
-                                that._forceReflow(template);
-                                that._transition(template).done(
-                                    function () {
-                                        data.context = $(this);
-                                        that._trigger('completed', e, data);
-                                    }
-                                );
-                            }
-                        );
-                    });
-                } else {
-                    template = that._renderDownload(data.result)
-                        .appendTo(that.options.filesContainer);
-                    that._forceReflow(template);
-                    that._transition(template).done(
-                        function () {
-                            data.context = $(this);
-                            that._trigger('completed', e, data);
-                        }
-                    );
-                }
-            },
-            // Callback for failed (abort or error) uploads:
-            fail: function (e, data) {
-                var that = $(this).data('fileupload'),
-                    template;
-                that._adjustMaxNumberOfFiles(data.files.length);
-                if (data.context) {
-                    data.context.each(function (index) {
-                        if (data.errorThrown !== 'abort') {
-                            var file = data.files[index];
-                            file.error = file.error || data.errorThrown ||
-                                true;
-                            that._transition($(this)).done(
-                                function () {
-                                    var node = $(this);
-                                    template = that._renderDownload([file])
-                                        .replaceAll(node);
-                                    that._forceReflow(template);
-                                    that._transition(template).done(
-                                        function () {
-                                            data.context = $(this);
-                                            that._trigger('failed', e, data);
-                                        }
-                                    );
-                                }
-                            );
-                        } else {
-                            that._transition($(this)).done(
-                                function () {
-                                    $(this).remove();
-                                    that._trigger('failed', e, data);
-                                }
-                            );
-                        }
-                    });
-                } else if (data.errorThrown !== 'abort') {
-                    that._adjustMaxNumberOfFiles(-data.files.length);
-                    data.context = that._renderUpload(data.files)
-                        .appendTo(that.options.filesContainer)
-                        .data('data', data);
-                    that._forceReflow(data.context);
-                    that._transition(data.context).done(
-                        function () {
-                            data.context = $(this);
-                            that._trigger('failed', e, data);
-                        }
-                    );
-                } else {
-                    that._trigger('failed', e, data);
-                }
-            },
-            // Callback for upload progress events:
-            progress: function (e, data) {
-                if (data.context) {
-                    var progress = parseInt(data.loaded / data.total * 100, 10);
-                    data.context.find('.progress')
-                        .attr('aria-valuenow', progress)
-                        .find('.bar').css(
-                            'width',
-                            progress + '%'
-                        );
-                }
-            },
-            // Callback for global upload progress events:
-            progressall: function (e, data) {
-                var $this = $(this),
-                    progress = parseInt(data.loaded / data.total * 100, 10),
-                    globalProgressNode = $this.find('.fileupload-progress'),
-                    extendedProgressNode = globalProgressNode
-                        .find('.progress-extended');
-                if (extendedProgressNode.length) {
-                    extendedProgressNode.html(
-                        $this.data('fileupload')._renderExtendedProgress(data)
-                    );
-                }
-                globalProgressNode
-                    .find('.progress')
-                    .attr('aria-valuenow', progress)
-                    .find('.bar').css(
-                        'width',
-                        progress + '%'
-                    );
-            },
-            // Callback for uploads start, equivalent to the global ajaxStart event:
-            start: function (e) {
-                var that = $(this).data('fileupload');
-                that._transition($(this).find('.fileupload-progress')).done(
-                    function () {
-                        that._trigger('started', e);
-                    }
-                );
-            },
-            // Callback for uploads stop, equivalent to the global ajaxStop event:
-            stop: function (e) {
-                var that = $(this).data('fileupload');
-                that._transition($(this).find('.fileupload-progress')).done(
-                    function () {
-                        $(this).find('.progress')
-                            .attr('aria-valuenow', '0')
-                            .find('.bar').css('width', '0%');
-                        $(this).find('.progress-extended').html('&nbsp;');
-                        that._trigger('stopped', e);
-                    }
-                );
-            },
-            // Callback for file deletion:
-            destroy: function (e, data) {
-                var that = $(this).data('fileupload');
-                if (data.url) {
-                    $.ajax(data);
-                    that._adjustMaxNumberOfFiles(1);
-                }
-                that._transition(data.context).done(
-                    function () {
-                        $(this).remove();
-                        that._trigger('destroyed', e, data);
-                    }
-                );
-            }
-        },
-
-        // Link handler, that allows one to download files
-        // by drag & drop of the links to the desktop:
-        _enableDragToDesktop: function () {
-            var link = $(this),
-                url = link.prop('href'),
-                name = link.prop('download'),
-                type = 'application/octet-stream';
-            link.bind('dragstart', function (e) {
-                try {
-                    e.originalEvent.dataTransfer.setData(
-                        'DownloadURL',
-                        [type, name, url].join(':')
-                    );
-                } catch (err) {}
-            });
-        },
-
-        _adjustMaxNumberOfFiles: function (operand) {
-            if (typeof this.options.maxNumberOfFiles === 'number') {
-                this.options.maxNumberOfFiles += operand;
-                if (this.options.maxNumberOfFiles < 1) {
-                    this._disableFileInputButton();
-                } else {
-                    this._enableFileInputButton();
-                }
-            }
-        },
-
-        _formatFileSize: function (bytes) {
-            if (typeof bytes !== 'number') {
-                return '';
-            }
-            if (bytes >= 1000000000) {
-                return (bytes / 1000000000).toFixed(2) + ' GB';
-            }
-            if (bytes >= 1000000) {
-                return (bytes / 1000000).toFixed(2) + ' MB';
-            }
-            return (bytes / 1000).toFixed(2) + ' KB';
-        },
-
-        _formatBitrate: function (bits) {
-            if (typeof bits !== 'number') {
-                return '';
-            }
-            if (bits >= 1000000000) {
-                return (bits / 1000000000).toFixed(2) + ' Gbit/s';
-            }
-            if (bits >= 1000000) {
-                return (bits / 1000000).toFixed(2) + ' Mbit/s';
-            }
-            if (bits >= 1000) {
-                return (bits / 1000).toFixed(2) + ' kbit/s';
-            }
-            return bits + ' bit/s';
-        },
-
-        _formatTime: function (seconds) {
-            var date = new Date(seconds * 1000),
-                days = parseInt(seconds / 86400, 10);
-            days = days ? days + 'd ' : '';
-            return days +
-                ('0' + date.getUTCHours()).slice(-2) + ':' +
-                ('0' + date.getUTCMinutes()).slice(-2) + ':' +
-                ('0' + date.getUTCSeconds()).slice(-2);
-        },
-
-        _formatPercentage: function (floatValue) {
-            return (floatValue * 100).toFixed(2) + ' %';
-        },
-
-        _renderExtendedProgress: function (data) {
-            return this._formatBitrate(data.bitrate) + ' | ' +
-                this._formatTime(
-                    (data.total - data.loaded) * 8 / data.bitrate
-                ) + ' | ' +
-                this._formatPercentage(
-                    data.loaded / data.total
-                ) + ' | ' +
-                this._formatFileSize(data.loaded) + ' / ' +
-                this._formatFileSize(data.total);
-        },
-
-        _hasError: function (file) {
-            if (file.error) {
-                return file.error;
-            }
-            // The number of added files is subtracted from
-            // maxNumberOfFiles before validation, so we check if
-            // maxNumberOfFiles is below 0 (instead of below 1):
-            if (this.options.maxNumberOfFiles < 0) {
-                return 'maxNumberOfFiles';
-            }
-            // Files are accepted if either the file type or the file name
-            // matches against the acceptFileTypes regular expression, as
-            // only browsers with support for the File API report the type:
-            if (!(this.options.acceptFileTypes.test(file.type) ||
-                    this.options.acceptFileTypes.test(file.name))) {
-                return 'acceptFileTypes';
-            }
-            if (this.options.maxFileSize &&
-                    file.size > this.options.maxFileSize) {
-                return 'maxFileSize';
-            }
-            if (typeof file.size === 'number' &&
-                    file.size < this.options.minFileSize) {
-                return 'minFileSize';
-            }
-            return null;
-        },
-
-        _validate: function (files) {
-            var that = this,
-                valid = !!files.length;
-            $.each(files, function (index, file) {
-                file.error = that._hasError(file);
-                if (file.error) {
-                    valid = false;
-                }
-            });
-            return valid;
-        },
-
-        _renderTemplate: function (func, files) {
-            if (!func) {
-                return $();
-            }
-            var result = func({
-                files: files,
-                formatFileSize: this._formatFileSize,
-                options: this.options
-            });
-            if (result instanceof $) {
-                return result;
-            }
-            return $(this.options.templatesContainer).html(result).children();
-        },
-
-        _renderPreview: function (file, node) {
-            var that = this,
-                options = this.options,
-                dfd = $.Deferred();
-            return ((loadImage && loadImage(
-                file,
-                function (img) {
-                    node.append(img);
-                    that._forceReflow(node);
-                    that._transition(node).done(function () {
-                        dfd.resolveWith(node);
-                    });
-                    if (!$.contains(document.body, node[0])) {
-                        // If the element is not part of the DOM,
-                        // transition events are not triggered,
-                        // so we have to resolve manually:
-                        dfd.resolveWith(node);
-                    }
-                },
-                {
-                    maxWidth: options.previewMaxWidth,
-                    maxHeight: options.previewMaxHeight,
-                    canvas: options.previewAsCanvas
-                }
-            )) || dfd.resolveWith(node)) && dfd;
-        },
-
-        _renderPreviews: function (files, nodes) {
-            var that = this,
-                options = this.options;
-            nodes.find('.preview span').each(function (index, element) {
-                var file = files[index];
-                if (options.previewSourceFileTypes.test(file.type) &&
-                        ($.type(options.previewSourceMaxFileSize) !== 'number' ||
-                        file.size < options.previewSourceMaxFileSize)) {
-                    that._processingQueue = that._processingQueue.pipe(function () {
-                        var dfd = $.Deferred();
-                        that._renderPreview(file, $(element)).done(
-                            function () {
-                                dfd.resolveWith(that);
-                            }
-                        );
-                        return dfd.promise();
-                    });
-                }
-            });
-            return this._processingQueue;
-        },
-
-        _renderUpload: function (files) {
-            return this._renderTemplate(
-                this.options.uploadTemplate,
-                files
-            );
-        },
-
-        _renderDownload: function (files) {
-            return this._renderTemplate(
-                this.options.downloadTemplate,
-                files
-            ).find('a[download]').each(this._enableDragToDesktop).end();
-        },
-
-        _startHandler: function (e) {
-            e.preventDefault();
-            var button = $(this),
-                template = button.closest('.template-upload'),
-                data = template.data('data');
-            if (data && data.submit && !data.jqXHR && data.submit()) {
-                button.prop('disabled', true);
-            }
-        },
-
-        _cancelHandler: function (e) {
-            e.preventDefault();
-            var template = $(this).closest('.template-upload'),
-                data = template.data('data') || {};
-            if (!data.jqXHR) {
-                data.errorThrown = 'abort';
-                e.data.fileupload._trigger('fail', e, data);
-            } else {
-                data.jqXHR.abort();
-            }
-        },
-
-        _deleteHandler: function (e) {
-            e.preventDefault();
-            var button = $(this);
-            e.data.fileupload._trigger('destroy', e, {
-                context: button.closest('.template-download'),
-                url: button.attr('data-url'),
-                type: button.attr('data-type') || 'DELETE',
-                dataType: e.data.fileupload.options.dataType
-            });
-        },
-
-        _forceReflow: function (node) {
-            return $.support.transition && node.length &&
-                node[0].offsetWidth;
-        },
-
-        _transition: function (node) {
-            var dfd = $.Deferred();
-            if ($.support.transition && node.hasClass('fade')) {
-                node.bind(
-                    $.support.transition.end,
-                    function (e) {
-                        // Make sure we don't respond to other transitions events
-                        // in the container element, e.g. from button elements:
-                        if (e.target === node[0]) {
-                            node.unbind($.support.transition.end);
-                            dfd.resolveWith(node);
-                        }
-                    }
-                ).toggleClass('in');
-            } else {
-                node.toggleClass('in');
-                dfd.resolveWith(node);
-            }
-            return dfd;
-        },
-
-        _initButtonBarEventHandlers: function () {
-            var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'),
-                filesList = this.options.filesContainer,
-                ns = this.options.namespace;
-            fileUploadButtonBar.find('.start')
-                .bind('click.' + ns, function (e) {
-                    e.preventDefault();
-                    filesList.find('.start button').click();
-                });
-            fileUploadButtonBar.find('.cancel')
-                .bind('click.' + ns, function (e) {
-                    e.preventDefault();
-                    filesList.find('.cancel button').click();
-                });
-            fileUploadButtonBar.find('.delete')
-                .bind('click.' + ns, function (e) {
-                    e.preventDefault();
-                    filesList.find('.delete input:checked')
-                        .siblings('button').click();
-                    fileUploadButtonBar.find('.toggle')
-                        .prop('checked', false);
-                });
-            fileUploadButtonBar.find('.toggle')
-                .bind('change.' + ns, function (e) {
-                    filesList.find('.delete input').prop(
-                        'checked',
-                        $(this).is(':checked')
-                    );
-                });
-        },
-
-        _destroyButtonBarEventHandlers: function () {
-            this.element.find('.fileupload-buttonbar button')
-                .unbind('click.' + this.options.namespace);
-            this.element.find('.fileupload-buttonbar .toggle')
-                .unbind('change.' + this.options.namespace);
-        },
-
-        _initEventHandlers: function () {
-            parentWidget.prototype._initEventHandlers.call(this);
-            var eventData = {fileupload: this};
-            this.options.filesContainer
-                .delegate(
-                    '.start button',
-                    'click.' + this.options.namespace,
-                    eventData,
-                    this._startHandler
-                )
-                .delegate(
-                    '.cancel button',
-                    'click.' + this.options.namespace,
-                    eventData,
-                    this._cancelHandler
-                )
-                .delegate(
-                    '.delete button',
-                    'click.' + this.options.namespace,
-                    eventData,
-                    this._deleteHandler
-                );
-            this._initButtonBarEventHandlers();
-        },
-
-        _destroyEventHandlers: function () {
-            var options = this.options;
-            this._destroyButtonBarEventHandlers();
-            options.filesContainer
-                .undelegate('.start button', 'click.' + options.namespace)
-                .undelegate('.cancel button', 'click.' + options.namespace)
-                .undelegate('.delete button', 'click.' + options.namespace);
-            parentWidget.prototype._destroyEventHandlers.call(this);
-        },
-
-        _enableFileInputButton: function () {
-            this.element.find('.fileinput-button input')
-                .prop('disabled', false)
-                .parent().removeClass('disabled');
-        },
-
-        _disableFileInputButton: function () {
-            this.element.find('.fileinput-button input')
-                .prop('disabled', true)
-                .parent().addClass('disabled');
-        },
-
-        _initTemplates: function () {
-            var options = this.options;
-            options.templatesContainer = document.createElement(
-                options.filesContainer.prop('nodeName')
-            );
-            if (tmpl) {
-                if (options.uploadTemplateId) {
-                    options.uploadTemplate = tmpl(options.uploadTemplateId);
-                }
-                if (options.downloadTemplateId) {
-                    options.downloadTemplate = tmpl(options.downloadTemplateId);
-                }
-            }
-        },
-
-        _initFilesContainer: function () {
-            var options = this.options;
-            if (options.filesContainer === undefined) {
-                options.filesContainer = this.element.find('.files');
-            } else if (!(options.filesContainer instanceof $)) {
-                options.filesContainer = $(options.filesContainer);
-            }
-        },
-
-        _initSpecialOptions: function () {
-            parentWidget.prototype._initSpecialOptions.call(this);
-            this._initFilesContainer();
-            this._initTemplates();
-        },
-
-        _create: function () {
-            parentWidget.prototype._create.call(this);
-            this._refreshOptionsList.push(
-                'filesContainer',
-                'uploadTemplateId',
-                'downloadTemplateId'
-            );
-            if (!$.blueimpFP) {
-                this._processingQueue = $.Deferred().resolveWith(this).promise();
-                this.process = function () {
-                    return this._processingQueue;
-                };
-            }
-        },
-
-        enable: function () {
-            parentWidget.prototype.enable.call(this);
-            this.element.find('input, button').prop('disabled', false);
-            this._enableFileInputButton();
-        },
-
-        disable: function () {
-            this.element.find('input, button').prop('disabled', true);
-            this._disableFileInputButton();
-            parentWidget.prototype.disable.call(this);
-        }
-
-    });
-
-}));
--- a/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,968 +0,0 @@
-/*
- * jQuery File Upload Plugin 5.12
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2010, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-/*jslint nomen: true, unparam: true, regexp: true */
-/*global define, window, document, Blob, FormData, location */
-
-(function (factory) {
-    'use strict';
-    if (typeof define === 'function' && define.amd) {
-        // Register as an anonymous AMD module:
-        define([
-            'jquery',
-            'jquery.ui.widget'
-        ], factory);
-    } else {
-        // Browser globals:
-        factory(window.jQuery);
-    }
-}(function ($) {
-    'use strict';
-
-    // The FileReader API is not actually used, but works as feature detection,
-    // as e.g. Safari supports XHR file uploads via the FormData API,
-    // but not non-multipart XHR file uploads:
-    $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
-    $.support.xhrFormDataFileUpload = !!window.FormData;
-
-    // The fileupload widget listens for change events on file input fields defined
-    // via fileInput setting and paste or drop events of the given dropZone.
-    // In addition to the default jQuery Widget methods, the fileupload widget
-    // exposes the "add" and "send" methods, to add or directly send files using
-    // the fileupload API.
-    // By default, files added via file input selection, paste, drag & drop or
-    // "add" method are uploaded immediately, but it is possible to override
-    // the "add" callback option to queue file uploads.
-    $.widget('blueimp.fileupload', {
-
-        options: {
-            // The namespace used for event handler binding on the dropZone and
-            // fileInput collections.
-            // If not set, the name of the widget ("fileupload") is used.
-            namespace: undefined,
-            // The drop target collection, by the default the complete document.
-            // Set to null or an empty collection to disable drag & drop support:
-            dropZone: $(document),
-            // The file input field collection, that is listened for change events.
-            // If undefined, it is set to the file input fields inside
-            // of the widget element on plugin initialization.
-            // Set to null or an empty collection to disable the change listener.
-            fileInput: undefined,
-            // By default, the file input field is replaced with a clone after
-            // each input field change event. This is required for iframe transport
-            // queues and allows change events to be fired for the same file
-            // selection, but can be disabled by setting the following option to false:
-            replaceFileInput: true,
-            // The parameter name for the file form data (the request argument name).
-            // If undefined or empty, the name property of the file input field is
-            // used, or "files[]" if the file input name property is also empty,
-            // can be a string or an array of strings:
-            paramName: undefined,
-            // By default, each file of a selection is uploaded using an individual
-            // request for XHR type uploads. Set to false to upload file
-            // selections in one request each:
-            singleFileUploads: true,
-            // To limit the number of files uploaded with one XHR request,
-            // set the following option to an integer greater than 0:
-            limitMultiFileUploads: undefined,
-            // Set the following option to true to issue all file upload requests
-            // in a sequential order:
-            sequentialUploads: false,
-            // To limit the number of concurrent uploads,
-            // set the following option to an integer greater than 0:
-            limitConcurrentUploads: undefined,
-            // Set the following option to true to force iframe transport uploads:
-            forceIframeTransport: false,
-            // Set the following option to the location of a redirect url on the
-            // origin server, for cross-domain iframe transport uploads:
-            redirect: undefined,
-            // The parameter name for the redirect url, sent as part of the form
-            // data and set to 'redirect' if this option is empty:
-            redirectParamName: undefined,
-            // Set the following option to the location of a postMessage window,
-            // to enable postMessage transport uploads:
-            postMessage: undefined,
-            // By default, XHR file uploads are sent as multipart/form-data.
-            // The iframe transport is always using multipart/form-data.
-            // Set to false to enable non-multipart XHR uploads:
-            multipart: true,
-            // To upload large files in smaller chunks, set the following option
-            // to a preferred maximum chunk size. If set to 0, null or undefined,
-            // or the browser does not support the required Blob API, files will
-            // be uploaded as a whole.
-            maxChunkSize: undefined,
-            // When a non-multipart upload or a chunked multipart upload has been
-            // aborted, this option can be used to resume the upload by setting
-            // it to the size of the already uploaded bytes. This option is most
-            // useful when modifying the options object inside of the "add" or
-            // "send" callbacks, as the options are cloned for each file upload.
-            uploadedBytes: undefined,
-            // By default, failed (abort or error) file uploads are removed from the
-            // global progress calculation. Set the following option to false to
-            // prevent recalculating the global progress data:
-            recalculateProgress: true,
-            // Interval in milliseconds to calculate and trigger progress events:
-            progressInterval: 100,
-            // Interval in milliseconds to calculate progress bitrate:
-            bitrateInterval: 500,
-
-            // Additional form data to be sent along with the file uploads can be set
-            // using this option, which accepts an array of objects with name and
-            // value properties, a function returning such an array, a FormData
-            // object (for XHR file uploads), or a simple object.
-            // The form of the first fileInput is given as parameter to the function:
-            formData: function (form) {
-                return form.serializeArray();
-            },
-
-            // The add callback is invoked as soon as files are added to the fileupload
-            // widget (via file input selection, drag & drop, paste or add API call).
-            // If the singleFileUploads option is enabled, this callback will be
-            // called once for each file in the selection for XHR file uploads, else
-            // once for each file selection.
-            // The upload starts when the submit method is invoked on the data parameter.
-            // The data object contains a files property holding the added files
-            // and allows one to override plugin options as well as define ajax settings.
-            // Listeners for this callback can also be bound the following way:
-            // .bind('fileuploadadd', func);
-            // data.submit() returns a Promise object and allows one to attach additional
-            // handlers using jQuery's Deferred callbacks:
-            // data.submit().done(func).fail(func).always(func);
-            add: function (e, data) {
-                data.submit();
-            },
-
-            // Other callbacks:
-            // Callback for the submit event of each file upload:
-            // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
-            // Callback for the start of each file upload request:
-            // send: function (e, data) {}, // .bind('fileuploadsend', func);
-            // Callback for successful uploads:
-            // done: function (e, data) {}, // .bind('fileuploaddone', func);
-            // Callback for failed (abort or error) uploads:
-            // fail: function (e, data) {}, // .bind('fileuploadfail', func);
-            // Callback for completed (success, abort or error) requests:
-            // always: function (e, data) {}, // .bind('fileuploadalways', func);
-            // Callback for upload progress events:
-            // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
-            // Callback for global upload progress events:
-            // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
-            // Callback for uploads start, equivalent to the global ajaxStart event:
-            // start: function (e) {}, // .bind('fileuploadstart', func);
-            // Callback for uploads stop, equivalent to the global ajaxStop event:
-            // stop: function (e) {}, // .bind('fileuploadstop', func);
-            // Callback for change events of the fileInput collection:
-            // change: function (e, data) {}, // .bind('fileuploadchange', func);
-            // Callback for paste events to the dropZone collection:
-            // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
-            // Callback for drop events of the dropZone collection:
-            // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
-            // Callback for dragover events of the dropZone collection:
-            // dragover: function (e) {}, // .bind('fileuploaddragover', func);
-
-            // The plugin options are used as settings object for the ajax calls.
-            // The following are jQuery ajax settings required for the file uploads:
-            processData: false,
-            contentType: false,
-            cache: false
-        },
-
-        // A list of options that require a refresh after assigning a new value:
-        _refreshOptionsList: [
-            'namespace',
-            'dropZone',
-            'fileInput',
-            'multipart',
-            'forceIframeTransport'
-        ],
-
-        _BitrateTimer: function () {
-            this.timestamp = +(new Date());
-            this.loaded = 0;
-            this.bitrate = 0;
-            this.getBitrate = function (now, loaded, interval) {
-                var timeDiff = now - this.timestamp;
-                if (!this.bitrate || !interval || timeDiff > interval) {
-                    this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
-                    this.loaded = loaded;
-                    this.timestamp = now;
-                }
-                return this.bitrate;
-            };
-        },
-
-        _isXHRUpload: function (options) {
-            return !options.forceIframeTransport &&
-                ((!options.multipart && $.support.xhrFileUpload) ||
-                $.support.xhrFormDataFileUpload);
-        },
-
-        _getFormData: function (options) {
-            var formData;
-            if (typeof options.formData === 'function') {
-                return options.formData(options.form);
-            }
-			if ($.isArray(options.formData)) {
-                return options.formData;
-            }
-			if (options.formData) {
-                formData = [];
-                $.each(options.formData, function (name, value) {
-                    formData.push({name: name, value: value});
-                });
-                return formData;
-            }
-            return [];
-        },
-
-        _getTotal: function (files) {
-            var total = 0;
-            $.each(files, function (index, file) {
-                total += file.size || 1;
-            });
-            return total;
-        },
-
-        _onProgress: function (e, data) {
-            if (e.lengthComputable) {
-                var now = +(new Date()),
-                    total,
-                    loaded;
-                if (data._time && data.progressInterval &&
-                        (now - data._time < data.progressInterval) &&
-                        e.loaded !== e.total) {
-                    return;
-                }
-                data._time = now;
-                total = data.total || this._getTotal(data.files);
-                loaded = parseInt(
-                    e.loaded / e.total * (data.chunkSize || total),
-                    10
-                ) + (data.uploadedBytes || 0);
-                this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
-                data.lengthComputable = true;
-                data.loaded = loaded;
-                data.total = total;
-                data.bitrate = data._bitrateTimer.getBitrate(
-                    now,
-                    loaded,
-                    data.bitrateInterval
-                );
-                // Trigger a custom progress event with a total data property set
-                // to the file size(s) of the current upload and a loaded data
-                // property calculated accordingly:
-                this._trigger('progress', e, data);
-                // Trigger a global progress event for all current file uploads,
-                // including ajax calls queued for sequential file uploads:
-                this._trigger('progressall', e, {
-                    lengthComputable: true,
-                    loaded: this._loaded,
-                    total: this._total,
-                    bitrate: this._bitrateTimer.getBitrate(
-                        now,
-                        this._loaded,
-                        data.bitrateInterval
-                    )
-                });
-            }
-        },
-
-        _initProgressListener: function (options) {
-            var that = this,
-                xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
-            // Accesss to the native XHR object is required to add event listeners
-            // for the upload progress event:
-            if (xhr.upload) {
-                $(xhr.upload).bind('progress', function (e) {
-                    var oe = e.originalEvent;
-                    // Make sure the progress event properties get copied over:
-                    e.lengthComputable = oe.lengthComputable;
-                    e.loaded = oe.loaded;
-                    e.total = oe.total;
-                    that._onProgress(e, options);
-                });
-                options.xhr = function () {
-                    return xhr;
-                };
-            }
-        },
-
-        _initXHRData: function (options) {
-            var formData,
-                file = options.files[0],
-                // Ignore non-multipart setting if not supported:
-                multipart = options.multipart || !$.support.xhrFileUpload,
-                paramName = options.paramName[0];
-            if (!multipart || options.blob) {
-                // For non-multipart uploads and chunked uploads,
-                // file meta data is not part of the request body,
-                // so we transmit this data as part of the HTTP headers.
-                // For cross domain requests, these headers must be allowed
-                // via Access-Control-Allow-Headers or removed using
-                // the beforeSend callback:
-                options.headers = $.extend(options.headers, {
-                    'X-File-Name': file.name,
-                    'X-File-Type': file.type,
-                    'X-File-Size': file.size
-                });
-                if (!options.blob) {
-                    // Non-chunked non-multipart upload:
-                    options.contentType = file.type;
-                    options.data = file;
-                } else if (!multipart) {
-                    // Chunked non-multipart upload:
-                    options.contentType = 'application/octet-stream';
-                    options.data = options.blob;
-                }
-            }
-            if (multipart && $.support.xhrFormDataFileUpload) {
-                if (options.postMessage) {
-                    // window.postMessage does not allow sending FormData
-                    // objects, so we just add the File/Blob objects to
-                    // the formData array and let the postMessage window
-                    // create the FormData object out of this array:
-                    formData = this._getFormData(options);
-                    if (options.blob) {
-                        formData.push({
-                            name: paramName,
-                            value: options.blob
-                        });
-                    } else {
-                        $.each(options.files, function (index, file) {
-                            formData.push({
-                                name: options.paramName[index] || paramName,
-                                value: file
-                            });
-                        });
-                    }
-                } else {
-                    if (options.formData instanceof FormData) {
-                        formData = options.formData;
-                    } else {
-                        formData = new FormData();
-                        $.each(this._getFormData(options), function (index, field) {
-                            formData.append(field.name, field.value);
-                        });
-                    }
-                    if (options.blob) {
-                        formData.append(paramName, options.blob, file.name);
-                    } else {
-                        $.each(options.files, function (index, file) {
-                            // File objects are also Blob instances.
-                            // This check allows the tests to run with
-                            // dummy objects:
-                            if (file instanceof Blob) {
-                                formData.append(
-                                    options.paramName[index] || paramName,
-                                    file,
-                                    file.name
-                                );
-                            }
-                        });
-                    }
-                }
-                options.data = formData;
-            }
-            // Blob reference is not needed anymore, free memory:
-            options.blob = null;
-        },
-
-        _initIframeSettings: function (options) {
-            // Setting the dataType to iframe enables the iframe transport:
-            options.dataType = 'iframe ' + (options.dataType || '');
-            // The iframe transport accepts a serialized array as form data:
-            options.formData = this._getFormData(options);
-            // Add redirect url to form data on cross-domain uploads:
-            if (options.redirect && $('<a></a>').prop('href', options.url)
-                    .prop('host') !== location.host) {
-                options.formData.push({
-                    name: options.redirectParamName || 'redirect',
-                    value: options.redirect
-                });
-            }
-        },
-
-        _initDataSettings: function (options) {
-            if (this._isXHRUpload(options)) {
-                if (!this._chunkedUpload(options, true)) {
-                    if (!options.data) {
-                        this._initXHRData(options);
-                    }
-                    this._initProgressListener(options);
-                }
-                if (options.postMessage) {
-                    // Setting the dataType to postmessage enables the
-                    // postMessage transport:
-                    options.dataType = 'postmessage ' + (options.dataType || '');
-                }
-            } else {
-                this._initIframeSettings(options, 'iframe');
-            }
-        },
-
-        _getParamName: function (options) {
-            var fileInput = $(options.fileInput),
-                paramName = options.paramName;
-            if (!paramName) {
-                paramName = [];
-                fileInput.each(function () {
-                    var input = $(this),
-                        name = input.prop('name') || 'files[]',
-                        i = (input.prop('files') || [1]).length;
-                    while (i) {
-                        paramName.push(name);
-                        i -= 1;
-                    }
-                });
-                if (!paramName.length) {
-                    paramName = [fileInput.prop('name') || 'files[]'];
-                }
-            } else if (!$.isArray(paramName)) {
-                paramName = [paramName];
-            }
-            return paramName;
-        },
-
-        _initFormSettings: function (options) {
-            // Retrieve missing options from the input field and the
-            // associated form, if available:
-            if (!options.form || !options.form.length) {
-                options.form = $(options.fileInput.prop('form'));
-            }
-            options.paramName = this._getParamName(options);
-            if (!options.url) {
-                options.url = options.form.prop('action') || location.href;
-            }
-            // The HTTP request method must be "POST" or "PUT":
-            options.type = (options.type || options.form.prop('method') || '')
-                .toUpperCase();
-            if (options.type !== 'POST' && options.type !== 'PUT') {
-                options.type = 'POST';
-            }
-        },
-
-        _getAJAXSettings: function (data) {
-            var options = $.extend({}, this.options, data);
-            this._initFormSettings(options);
-            this._initDataSettings(options);
-            return options;
-        },
-
-        // Maps jqXHR callbacks to the equivalent
-        // methods of the given Promise object:
-        _enhancePromise: function (promise) {
-            promise.success = promise.done;
-            promise.error = promise.fail;
-            promise.complete = promise.always;
-            return promise;
-        },
-
-        // Creates and returns a Promise object enhanced with
-        // the jqXHR methods abort, success, error and complete:
-        _getXHRPromise: function (resolveOrReject, context, args) {
-            var dfd = $.Deferred(),
-                promise = dfd.promise();
-            context = context || this.options.context || promise;
-            if (resolveOrReject === true) {
-                dfd.resolveWith(context, args);
-            } else if (resolveOrReject === false) {
-                dfd.rejectWith(context, args);
-            }
-            promise.abort = dfd.promise;
-            return this._enhancePromise(promise);
-        },
-
-        // Uploads a file in multiple, sequential requests
-        // by splitting the file up in multiple blob chunks.
-        // If the second parameter is true, only tests if the file
-        // should be uploaded in chunks, but does not invoke any
-        // upload requests:
-        _chunkedUpload: function (options, testOnly) {
-            var that = this,
-                file = options.files[0],
-                fs = file.size,
-                ub = options.uploadedBytes = options.uploadedBytes || 0,
-                mcs = options.maxChunkSize || fs,
-                // Use the Blob methods with the slice implementation
-                // according to the W3C Blob API specification:
-                slice = file.webkitSlice || file.mozSlice || file.slice,
-                upload,
-                n,
-                jqXHR,
-                pipe;
-            if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
-                    options.data) {
-                return false;
-            }
-            if (testOnly) {
-                return true;
-            }
-            if (ub >= fs) {
-                file.error = 'uploadedBytes';
-                return this._getXHRPromise(
-                    false,
-                    options.context,
-                    [null, 'error', file.error]
-                );
-            }
-            // n is the number of blobs to upload,
-            // calculated via filesize, uploaded bytes and max chunk size:
-            n = Math.ceil((fs - ub) / mcs);
-            // The chunk upload method accepting the chunk number as parameter:
-            upload = function (i) {
-                if (!i) {
-                    return that._getXHRPromise(true, options.context);
-                }
-                // Upload the blobs in sequential order:
-                return upload(i -= 1).pipe(function () {
-                    // Clone the options object for each chunk upload:
-                    var o = $.extend({}, options);
-                    o.blob = slice.call(
-                        file,
-                        ub + i * mcs,
-                        ub + (i + 1) * mcs
-                    );
-                    // Store the current chunk size, as the blob itself
-                    // will be dereferenced after data processing:
-                    o.chunkSize = o.blob.size;
-                    // Process the upload data (the blob and potential form data):
-                    that._initXHRData(o);
-                    // Add progress listeners for this chunk upload:
-                    that._initProgressListener(o);
-                    jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
-                        .done(function () {
-                            // Create a progress event if upload is done and
-                            // no progress event has been invoked for this chunk:
-                            if (!o.loaded) {
-                                that._onProgress($.Event('progress', {
-                                    lengthComputable: true,
-                                    loaded: o.chunkSize,
-                                    total: o.chunkSize
-                                }), o);
-                            }
-                            options.uploadedBytes = o.uploadedBytes +=
-                                o.chunkSize;
-                        });
-                    return jqXHR;
-                });
-            };
-            // Return the piped Promise object, enhanced with an abort method,
-            // which is delegated to the jqXHR object of the current upload,
-            // and jqXHR callbacks mapped to the equivalent Promise methods:
-            pipe = upload(n);
-            pipe.abort = function () {
-                return jqXHR.abort();
-            };
-            return this._enhancePromise(pipe);
-        },
-
-        _beforeSend: function (e, data) {
-            if (this._active === 0) {
-                // the start callback is triggered when an upload starts
-                // and no other uploads are currently running,
-                // equivalent to the global ajaxStart event:
-                this._trigger('start');
-                // Set timer for global bitrate progress calculation:
-                this._bitrateTimer = new this._BitrateTimer();
-            }
-            this._active += 1;
-            // Initialize the global progress values:
-            this._loaded += data.uploadedBytes || 0;
-            this._total += this._getTotal(data.files);
-        },
-
-        _onDone: function (result, textStatus, jqXHR, options) {
-            if (!this._isXHRUpload(options)) {
-                // Create a progress event for each iframe load:
-                this._onProgress($.Event('progress', {
-                    lengthComputable: true,
-                    loaded: 1,
-                    total: 1
-                }), options);
-            }
-            options.result = result;
-            options.textStatus = textStatus;
-            options.jqXHR = jqXHR;
-            this._trigger('done', null, options);
-        },
-
-        _onFail: function (jqXHR, textStatus, errorThrown, options) {
-            options.jqXHR = jqXHR;
-            options.textStatus = textStatus;
-            options.errorThrown = errorThrown;
-            this._trigger('fail', null, options);
-            if (options.recalculateProgress) {
-                // Remove the failed (error or abort) file upload from
-                // the global progress calculation:
-                this._loaded -= options.loaded || options.uploadedBytes || 0;
-                this._total -= options.total || this._getTotal(options.files);
-            }
-        },
-
-        _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
-            this._active -= 1;
-            options.textStatus = textStatus;
-            if (jqXHRorError && jqXHRorError.always) {
-                options.jqXHR = jqXHRorError;
-                options.result = jqXHRorResult;
-            } else {
-                options.jqXHR = jqXHRorResult;
-                options.errorThrown = jqXHRorError;
-            }
-            this._trigger('always', null, options);
-            if (this._active === 0) {
-                // The stop callback is triggered when all uploads have
-                // been completed, equivalent to the global ajaxStop event:
-                this._trigger('stop');
-                // Reset the global progress values:
-                this._loaded = this._total = 0;
-                this._bitrateTimer = null;
-            }
-        },
-
-        _onSend: function (e, data) {
-            var that = this,
-                jqXHR,
-                slot,
-                pipe,
-                options = that._getAJAXSettings(data),
-                send = function (resolve, args) {
-                    that._sending += 1;
-                    // Set timer for bitrate progress calculation:
-                    options._bitrateTimer = new that._BitrateTimer();
-                    jqXHR = jqXHR || (
-                        (resolve !== false &&
-                        that._trigger('send', e, options) !== false &&
-                        (that._chunkedUpload(options) || $.ajax(options))) ||
-                        that._getXHRPromise(false, options.context, args)
-                    ).done(function (result, textStatus, jqXHR) {
-                        that._onDone(result, textStatus, jqXHR, options);
-                    }).fail(function (jqXHR, textStatus, errorThrown) {
-                        that._onFail(jqXHR, textStatus, errorThrown, options);
-                    }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
-                        that._sending -= 1;
-                        that._onAlways(
-                            jqXHRorResult,
-                            textStatus,
-                            jqXHRorError,
-                            options
-                        );
-                        if (options.limitConcurrentUploads &&
-                                options.limitConcurrentUploads > that._sending) {
-                            // Start the next queued upload,
-                            // that has not been aborted:
-                            var nextSlot = that._slots.shift();
-                            while (nextSlot) {
-                                if (!nextSlot.isRejected()) {
-                                    nextSlot.resolve();
-                                    break;
-                                }
-                                nextSlot = that._slots.shift();
-                            }
-                        }
-                    });
-                    return jqXHR;
-                };
-            this._beforeSend(e, options);
-            if (this.options.sequentialUploads ||
-                    (this.options.limitConcurrentUploads &&
-                    this.options.limitConcurrentUploads <= this._sending)) {
-                if (this.options.limitConcurrentUploads > 1) {
-                    slot = $.Deferred();
-                    this._slots.push(slot);
-                    pipe = slot.pipe(send);
-                } else {
-                    pipe = (this._sequence = this._sequence.pipe(send, send));
-                }
-                // Return the piped Promise object, enhanced with an abort method,
-                // which is delegated to the jqXHR object of the current upload,
-                // and jqXHR callbacks mapped to the equivalent Promise methods:
-                pipe.abort = function () {
-                    var args = [undefined, 'abort', 'abort'];
-                    if (!jqXHR) {
-                        if (slot) {
-                            slot.rejectWith(args);
-                        }
-                        return send(false, args);
-                    }
-                    return jqXHR.abort();
-                };
-                return this._enhancePromise(pipe);
-            }
-            return send();
-        },
-
-        _onAdd: function (e, data) {
-            var that = this,
-                result = true,
-                options = $.extend({}, this.options, data),
-                limit = options.limitMultiFileUploads,
-                paramName = this._getParamName(options),
-                paramNameSet,
-                paramNameSlice,
-                fileSet,
-                i;
-            if (!(options.singleFileUploads || limit) ||
-                    !this._isXHRUpload(options)) {
-                fileSet = [data.files];
-                paramNameSet = [paramName];
-            } else if (!options.singleFileUploads && limit) {
-                fileSet = [];
-                paramNameSet = [];
-                for (i = 0; i < data.files.length; i += limit) {
-                    fileSet.push(data.files.slice(i, i + limit));
-                    paramNameSlice = paramName.slice(i, i + limit);
-                    if (!paramNameSlice.length) {
-                        paramNameSlice = paramName;
-                    }
-                    paramNameSet.push(paramNameSlice);
-                }
-            } else {
-                paramNameSet = paramName;
-            }
-            data.originalFiles = data.files;
-            $.each(fileSet || data.files, function (index, element) {
-                var newData = $.extend({}, data);
-                newData.files = fileSet ? element : [element];
-                newData.paramName = paramNameSet[index];
-                newData.submit = function () {
-                    newData.jqXHR = this.jqXHR =
-                        (that._trigger('submit', e, this) !== false) &&
-                        that._onSend(e, this);
-                    return this.jqXHR;
-                };
-                return (result = that._trigger('add', e, newData));
-            });
-            return result;
-        },
-
-        // File Normalization for Gecko 1.9.1 (Firefox 3.5) support:
-        _normalizeFile: function (index, file) {
-            if (file.name === undefined && file.size === undefined) {
-                file.name = file.fileName;
-                file.size = file.fileSize;
-            }
-        },
-
-        _replaceFileInput: function (input) {
-            var inputClone = input.clone(true);
-            $('<form></form>').append(inputClone)[0].reset();
-            // Detaching allows one to insert the fileInput on another form
-            // without loosing the file input value:
-            input.after(inputClone).detach();
-            // Avoid memory leaks with the detached file input:
-            $.cleanData(input.unbind('remove'));
-            // Replace the original file input element in the fileInput
-            // collection with the clone, which has been copied including
-            // event handlers:
-            this.options.fileInput = this.options.fileInput.map(function (i, el) {
-                if (el === input[0]) {
-                    return inputClone[0];
-                }
-                return el;
-            });
-            // If the widget has been initialized on the file input itself,
-            // override this.element with the file input clone:
-            if (input[0] === this.element[0]) {
-                this.element = inputClone;
-            }
-        },
-
-        _getFileInputFiles: function (fileInput) {
-            fileInput = $(fileInput);
-            var files = $.each($.makeArray(fileInput.prop('files')), this._normalizeFile),
-                value;
-            if (!files.length) {
-                value = fileInput.prop('value');
-                if (!value) {
-                    return [];
-                }
-                // If the files property is not available, the browser does not
-                // support the File API and we add a pseudo File object with
-                // the input value as name with path information removed:
-                files = [{name: value.replace(/^.*\\/, '')}];
-            }
-            return files;
-        },
-
-        _onChange: function (e) {
-            var that = e.data.fileupload,
-                data = {
-                    fileInput: $(e.target),
-                    form: $(e.target.form)
-                };
-            data.files = that._getFileInputFiles(data.fileInput);
-            if (that.options.replaceFileInput) {
-                that._replaceFileInput(data.fileInput);
-            }
-            if (that._trigger('change', e, data) === false ||
-                    that._onAdd(e, data) === false) {
-                return false;
-            }
-        },
-
-        _onPaste: function (e) {
-            var that = e.data.fileupload,
-                cbd = e.originalEvent.clipboardData,
-                items = (cbd && cbd.items) || [],
-                data = {files: []};
-            $.each(items, function (index, item) {
-                var file = item.getAsFile && item.getAsFile();
-                if (file) {
-                    data.files.push(file);
-                }
-            });
-            if (that._trigger('paste', e, data) === false ||
-                    that._onAdd(e, data) === false) {
-                return false;
-            }
-        },
-
-        _onDrop: function (e) {
-            var that = e.data.fileupload,
-                dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
-                data = {
-                    files: $.each(
-                        $.makeArray(dataTransfer && dataTransfer.files),
-                        that._normalizeFile
-                    )
-                };
-            if (that._trigger('drop', e, data) === false ||
-                    that._onAdd(e, data) === false) {
-                return false;
-            }
-            e.preventDefault();
-        },
-
-        _onDragOver: function (e) {
-            var that = e.data.fileupload,
-                dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
-            if (that._trigger('dragover', e) === false) {
-                return false;
-            }
-            if (dataTransfer) {
-                dataTransfer.dropEffect = 'copy';
-            }
-            e.preventDefault();
-        },
-
-        _initEventHandlers: function () {
-            var ns = this.options.namespace;
-            if (this._isXHRUpload(this.options)) {
-                this.options.dropZone
-                    .bind('dragover.' + ns, {fileupload: this}, this._onDragOver)
-                    .bind('drop.' + ns, {fileupload: this}, this._onDrop)
-                    .bind('paste.' + ns, {fileupload: this}, this._onPaste);
-            }
-            this.options.fileInput
-                .bind('change.' + ns, {fileupload: this}, this._onChange);
-        },
-
-        _destroyEventHandlers: function () {
-            var ns = this.options.namespace;
-            this.options.dropZone
-                .unbind('dragover.' + ns, this._onDragOver)
-                .unbind('drop.' + ns, this._onDrop)
-                .unbind('paste.' + ns, this._onPaste);
-            this.options.fileInput
-                .unbind('change.' + ns, this._onChange);
-        },
-
-        _setOption: function (key, value) {
-            var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
-            if (refresh) {
-                this._destroyEventHandlers();
-            }
-            $.Widget.prototype._setOption.call(this, key, value);
-            if (refresh) {
-                this._initSpecialOptions();
-                this._initEventHandlers();
-            }
-        },
-
-        _initSpecialOptions: function () {
-            var options = this.options;
-            if (options.fileInput === undefined) {
-                options.fileInput = this.element.is('input:file') ?
-                        this.element : this.element.find('input:file');
-            } else if (!(options.fileInput instanceof $)) {
-                options.fileInput = $(options.fileInput);
-            }
-            if (!(options.dropZone instanceof $)) {
-                options.dropZone = $(options.dropZone);
-            }
-        },
-
-        _create: function () {
-            var options = this.options;
-            // Initialize options set via HTML5 data-attributes:
-            $.extend(options, $(this.element[0].cloneNode(false)).data());
-            options.namespace = options.namespace || this.widgetName;
-            this._initSpecialOptions();
-            this._slots = [];
-            this._sequence = this._getXHRPromise(true);
-            this._sending = this._active = this._loaded = this._total = 0;
-            this._initEventHandlers();
-        },
-
-        destroy: function () {
-            this._destroyEventHandlers();
-            $.Widget.prototype.destroy.call(this);
-        },
-
-        enable: function () {
-            $.Widget.prototype.enable.call(this);
-            this._initEventHandlers();
-        },
-
-        disable: function () {
-            this._destroyEventHandlers();
-            $.Widget.prototype.disable.call(this);
-        },
-
-        // This method is exposed to the widget API and allows adding files
-        // using the fileupload API. The data parameter accepts an object which
-        // must have a files property and can contain additional options:
-        // .fileupload('add', {files: filesList});
-        add: function (data) {
-            if (!data || this.options.disabled) {
-                return;
-            }
-            if (data.fileInput && !data.files) {
-                data.files = this._getFileInputFiles(data.fileInput);
-            } else {
-                data.files = $.each($.makeArray(data.files), this._normalizeFile);
-            }
-            this._onAdd(null, data);
-        },
-
-        // This method is exposed to the widget API and allows sending files
-        // using the fileupload API. The data parameter accepts an object which
-        // must have a files property and can contain additional options:
-        // .fileupload('send', {files: filesList});
-        // The method returns a Promise object for the file upload call.
-        send: function (data) {
-            if (data && !this.options.disabled) {
-                if (data.fileInput && !data.files) {
-                    data.files = this._getFileInputFiles(data.fileInput);
-                } else {
-                    data.files = $.each($.makeArray(data.files), this._normalizeFile);
-                }
-                if (data.files.length) {
-                    return this._onSend(null, data);
-                }
-            }
-            return this._getXHRPromise(false, data && data.context);
-        }
-
-    });
-
-}));
--- a/OrthancExplorer/libs/jquery-file-upload/js/jquery.iframe-transport.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,171 +0,0 @@
-/*
- * jQuery Iframe Transport Plugin 1.4
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2011, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-/*jslint unparam: true, nomen: true */
-/*global define, window, document */
-
-(function (factory) {
-    'use strict';
-    if (typeof define === 'function' && define.amd) {
-        // Register as an anonymous AMD module:
-        define(['jquery'], factory);
-    } else {
-        // Browser globals:
-        factory(window.jQuery);
-    }
-}(function ($) {
-    'use strict';
-
-    // Helper variable to create unique names for the transport iframes:
-    var counter = 0;
-
-    // The iframe transport accepts three additional options:
-    // options.fileInput: a jQuery collection of file input fields
-    // options.paramName: the parameter name for the file form data,
-    //  overrides the name property of the file input field(s),
-    //  can be a string or an array of strings.
-    // options.formData: an array of objects with name and value properties,
-    //  equivalent to the return data of .serializeArray(), e.g.:
-    //  [{name: 'a', value: 1}, {name: 'b', value: 2}]
-    $.ajaxTransport('iframe', function (options) {
-        if (options.async && (options.type === 'POST' || options.type === 'GET')) {
-            var form,
-                iframe;
-            return {
-                send: function (_, completeCallback) {
-                    form = $('<form style="display:none;"></form>');
-                    // javascript:false as initial iframe src
-                    // prevents warning popups on HTTPS in IE6.
-                    // IE versions below IE8 cannot set the name property of
-                    // elements that have already been added to the DOM,
-                    // so we set the name along with the iframe HTML markup:
-                    iframe = $(
-                        '<iframe src="javascript:false;" name="iframe-transport-' +
-                            (counter += 1) + '"></iframe>'
-                    ).bind('load', function () {
-                        var fileInputClones,
-                            paramNames = $.isArray(options.paramName) ?
-                                    options.paramName : [options.paramName];
-                        iframe
-                            .unbind('load')
-                            .bind('load', function () {
-                                var response;
-                                // Wrap in a try/catch block to catch exceptions thrown
-                                // when trying to access cross-domain iframe contents:
-                                try {
-                                    response = iframe.contents();
-                                    // Google Chrome and Firefox do not throw an
-                                    // exception when calling iframe.contents() on
-                                    // cross-domain requests, so we unify the response:
-                                    if (!response.length || !response[0].firstChild) {
-                                        throw new Error();
-                                    }
-                                } catch (e) {
-                                    response = undefined;
-                                }
-                                // The complete callback returns the
-                                // iframe content document as response object:
-                                completeCallback(
-                                    200,
-                                    'success',
-                                    {'iframe': response}
-                                );
-                                // Fix for IE endless progress bar activity bug
-                                // (happens on form submits to iframe targets):
-                                $('<iframe src="javascript:false;"></iframe>')
-                                    .appendTo(form);
-                                form.remove();
-                            });
-                        form
-                            .prop('target', iframe.prop('name'))
-                            .prop('action', options.url)
-                            .prop('method', options.type);
-                        if (options.formData) {
-                            $.each(options.formData, function (index, field) {
-                                $('<input type="hidden"/>')
-                                    .prop('name', field.name)
-                                    .val(field.value)
-                                    .appendTo(form);
-                            });
-                        }
-                        if (options.fileInput && options.fileInput.length &&
-                                options.type === 'POST') {
-                            fileInputClones = options.fileInput.clone();
-                            // Insert a clone for each file input field:
-                            options.fileInput.after(function (index) {
-                                return fileInputClones[index];
-                            });
-                            if (options.paramName) {
-                                options.fileInput.each(function (index) {
-                                    $(this).prop(
-                                        'name',
-                                        paramNames[index] || options.paramName
-                                    );
-                                });
-                            }
-                            // Appending the file input fields to the hidden form
-                            // removes them from their original location:
-                            form
-                                .append(options.fileInput)
-                                .prop('enctype', 'multipart/form-data')
-                                // enctype must be set as encoding for IE:
-                                .prop('encoding', 'multipart/form-data');
-                        }
-                        form.submit();
-                        // Insert the file input fields at their original location
-                        // by replacing the clones with the originals:
-                        if (fileInputClones && fileInputClones.length) {
-                            options.fileInput.each(function (index, input) {
-                                var clone = $(fileInputClones[index]);
-                                $(input).prop('name', clone.prop('name'));
-                                clone.replaceWith(input);
-                            });
-                        }
-                    });
-                    form.append(iframe).appendTo(document.body);
-                },
-                abort: function () {
-                    if (iframe) {
-                        // javascript:false as iframe src aborts the request
-                        // and prevents warning popups on HTTPS in IE6.
-                        // concat is used to avoid the "Script URL" JSLint error:
-                        iframe
-                            .unbind('load')
-                            .prop('src', 'javascript'.concat(':false;'));
-                    }
-                    if (form) {
-                        form.remove();
-                    }
-                }
-            };
-        }
-    });
-
-    // The iframe transport returns the iframe content document as response.
-    // The following adds converters from iframe to text, json, html, and script:
-    $.ajaxSetup({
-        converters: {
-            'iframe text': function (iframe) {
-                return $(iframe[0].body).text();
-            },
-            'iframe json': function (iframe) {
-                return $.parseJSON($(iframe[0].body).text());
-            },
-            'iframe html': function (iframe) {
-                return $(iframe[0].body).html();
-            },
-            'iframe script': function (iframe) {
-                return $.globalEval($(iframe[0].body).text());
-            }
-        }
-    });
-
-}));
--- a/OrthancExplorer/libs/jquery-file-upload/js/locale.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-/*
- * jQuery File Upload Plugin Localization Example 6.5.1
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2012, Sebastian Tschan
- * https://blueimp.net
- *
- * Licensed under the MIT license:
- * http://www.opensource.org/licenses/MIT
- */
-
-/*global window */
-
-window.locale = {
-    "fileupload": {
-        "errors": {
-            "maxFileSize": "File is too big",
-            "minFileSize": "File is too small",
-            "acceptFileTypes": "Filetype not allowed",
-            "maxNumberOfFiles": "Max number of files exceeded",
-            "uploadedBytes": "Uploaded bytes exceed file size",
-            "emptyResult": "Empty file upload result"
-        },
-        "error": "Error",
-        "start": "Start",
-        "cancel": "Cancel",
-        "destroy": "Delete"
-    }
-};
--- a/OrthancExplorer/libs/jquery-file-upload/js/vendor/jquery.ui.widget.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,282 +0,0 @@
-/*
- * jQuery UI Widget 1.8.18+amd
- * https://github.com/blueimp/jQuery-File-Upload
- *
- * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
- * Dual licensed under the MIT or GPL Version 2 licenses.
- * http://jquery.org/license
- *
- * http://docs.jquery.com/UI/Widget
- */
-
-(function (factory) {
-    if (typeof define === "function" && define.amd) {
-        // Register as an anonymous AMD module:
-        define(["jquery"], factory);
-    } else {
-        // Browser globals:
-        factory(jQuery);
-    }
-}(function( $, undefined ) {
-
-// jQuery 1.4+
-if ( $.cleanData ) {
-	var _cleanData = $.cleanData;
-	$.cleanData = function( elems ) {
-		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
-			try {
-				$( elem ).triggerHandler( "remove" );
-			// http://bugs.jquery.com/ticket/8235
-			} catch( e ) {}
-		}
-		_cleanData( elems );
-	};
-} else {
-	var _remove = $.fn.remove;
-	$.fn.remove = function( selector, keepData ) {
-		return this.each(function() {
-			if ( !keepData ) {
-				if ( !selector || $.filter( selector, [ this ] ).length ) {
-					$( "*", this ).add( [ this ] ).each(function() {
-						try {
-							$( this ).triggerHandler( "remove" );
-						// http://bugs.jquery.com/ticket/8235
-						} catch( e ) {}
-					});
-				}
-			}
-			return _remove.call( $(this), selector, keepData );
-		});
-	};
-}
-
-$.widget = function( name, base, prototype ) {
-	var namespace = name.split( "." )[ 0 ],
-		fullName;
-	name = name.split( "." )[ 1 ];
-	fullName = namespace + "-" + name;
-
-	if ( !prototype ) {
-		prototype = base;
-		base = $.Widget;
-	}
-
-	// create selector for plugin
-	$.expr[ ":" ][ fullName ] = function( elem ) {
-		return !!$.data( elem, name );
-	};
-
-	$[ namespace ] = $[ namespace ] || {};
-	$[ namespace ][ name ] = function( options, element ) {
-		// allow instantiation without initializing for simple inheritance
-		if ( arguments.length ) {
-			this._createWidget( options, element );
-		}
-	};
-
-	var basePrototype = new base();
-	// we need to make the options hash a property directly on the new instance
-	// otherwise we'll modify the options hash on the prototype that we're
-	// inheriting from
-//	$.each( basePrototype, function( key, val ) {
-//		if ( $.isPlainObject(val) ) {
-//			basePrototype[ key ] = $.extend( {}, val );
-//		}
-//	});
-	basePrototype.options = $.extend( true, {}, basePrototype.options );
-	$[ namespace ][ name ].prototype = $.extend( true, basePrototype, {
-		namespace: namespace,
-		widgetName: name,
-		widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name,
-		widgetBaseClass: fullName
-	}, prototype );
-
-	$.widget.bridge( name, $[ namespace ][ name ] );
-};
-
-$.widget.bridge = function( name, object ) {
-	$.fn[ name ] = function( options ) {
-		var isMethodCall = typeof options === "string",
-			args = Array.prototype.slice.call( arguments, 1 ),
-			returnValue = this;
-
-		// allow multiple hashes to be passed on init
-		options = !isMethodCall && args.length ?
-			$.extend.apply( null, [ true, options ].concat(args) ) :
-			options;
-
-		// prevent calls to internal methods
-		if ( isMethodCall && options.charAt( 0 ) === "_" ) {
-			return returnValue;
-		}
-
-		if ( isMethodCall ) {
-			this.each(function() {
-				var instance = $.data( this, name ),
-					methodValue = instance && $.isFunction( instance[options] ) ?
-						instance[ options ].apply( instance, args ) :
-						instance;
-				// TODO: add this back in 1.9 and use $.error() (see #5972)
-//				if ( !instance ) {
-//					throw "cannot call methods on " + name + " prior to initialization; " +
-//						"attempted to call method '" + options + "'";
-//				}
-//				if ( !$.isFunction( instance[options] ) ) {
-//					throw "no such method '" + options + "' for " + name + " widget instance";
-//				}
-//				var methodValue = instance[ options ].apply( instance, args );
-				if ( methodValue !== instance && methodValue !== undefined ) {
-					returnValue = methodValue;
-					return false;
-				}
-			});
-		} else {
-			this.each(function() {
-				var instance = $.data( this, name );
-				if ( instance ) {
-					instance.option( options || {} )._init();
-				} else {
-					$.data( this, name, new object( options, this ) );
-				}
-			});
-		}
-
-		return returnValue;
-	};
-};
-
-$.Widget = function( options, element ) {
-	// allow instantiation without initializing for simple inheritance
-	if ( arguments.length ) {
-		this._createWidget( options, element );
-	}
-};
-
-$.Widget.prototype = {
-	widgetName: "widget",
-	widgetEventPrefix: "",
-	options: {
-		disabled: false
-	},
-	_createWidget: function( options, element ) {
-		// $.widget.bridge stores the plugin instance, but we do it anyway
-		// so that it's stored even before the _create function runs
-		$.data( element, this.widgetName, this );
-		this.element = $( element );
-		this.options = $.extend( true, {},
-			this.options,
-			this._getCreateOptions(),
-			options );
-
-		var self = this;
-		this.element.bind( "remove." + this.widgetName, function() {
-			self.destroy();
-		});
-
-		this._create();
-		this._trigger( "create" );
-		this._init();
-	},
-	_getCreateOptions: function() {
-		return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ];
-	},
-	_create: function() {},
-	_init: function() {},
-
-	destroy: function() {
-		this.element
-			.unbind( "." + this.widgetName )
-			.removeData( this.widgetName );
-		this.widget()
-			.unbind( "." + this.widgetName )
-			.removeAttr( "aria-disabled" )
-			.removeClass(
-				this.widgetBaseClass + "-disabled " +
-				"ui-state-disabled" );
-	},
-
-	widget: function() {
-		return this.element;
-	},
-
-	option: function( key, value ) {
-		var options = key;
-
-		if ( arguments.length === 0 ) {
-			// don't return a reference to the internal hash
-			return $.extend( {}, this.options );
-		}
-
-		if  (typeof key === "string" ) {
-			if ( value === undefined ) {
-				return this.options[ key ];
-			}
-			options = {};
-			options[ key ] = value;
-		}
-
-		this._setOptions( options );
-
-		return this;
-	},
-	_setOptions: function( options ) {
-		var self = this;
-		$.each( options, function( key, value ) {
-			self._setOption( key, value );
-		});
-
-		return this;
-	},
-	_setOption: function( key, value ) {
-		this.options[ key ] = value;
-
-		if ( key === "disabled" ) {
-			this.widget()
-				[ value ? "addClass" : "removeClass"](
-					this.widgetBaseClass + "-disabled" + " " +
-					"ui-state-disabled" )
-				.attr( "aria-disabled", value );
-		}
-
-		return this;
-	},
-
-	enable: function() {
-		return this._setOption( "disabled", false );
-	},
-	disable: function() {
-		return this._setOption( "disabled", true );
-	},
-
-	_trigger: function( type, event, data ) {
-		var prop, orig,
-			callback = this.options[ type ];
-
-		data = data || {};
-		event = $.Event( event );
-		event.type = ( type === this.widgetEventPrefix ?
-			type :
-			this.widgetEventPrefix + type ).toLowerCase();
-		// the original event may come from any element
-		// so we need to reset the target on the new event
-		event.target = this.element[ 0 ];
-
-		// copy original event properties over to the new event
-		orig = event.originalEvent;
-		if ( orig ) {
-			for ( prop in orig ) {
-				if ( !( prop in event ) ) {
-					event[ prop ] = orig[ prop ];
-				}
-			}
-		}
-
-		this.element.trigger( event, data );
-
-		return !( $.isFunction(callback) &&
-			callback.call( this.element[0], event, data ) === false ||
-			event.isDefaultPrevented() );
-	}
-};
-
-}));
--- a/OrthancExplorer/libs/jquery.blockui.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,499 +0,0 @@
-/*!
- * jQuery blockUI plugin
- * Version 2.39 (23-MAY-2011)
- * @requires jQuery v1.2.3 or later
- *
- * Examples at: http://malsup.com/jquery/block/
- * Copyright (c) 2007-2010 M. Alsup
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- * Thanks to Amir-Hossein Sobhi for some excellent contributions!
- */
-
-;(function($) {
-
-if (/1\.(0|1|2)\.(0|1|2)/.test($.fn.jquery) || /^1.1/.test($.fn.jquery)) {
-	alert('blockUI requires jQuery v1.2.3 or later!  You are using v' + $.fn.jquery);
-	return;
-}
-
-$.fn._fadeIn = $.fn.fadeIn;
-
-var noOp = function() {};
-
-// this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
-// retarded userAgent strings on Vista)
-var mode = document.documentMode || 0;
-var setExpr = $.browser.msie && (($.browser.version < 8 && !mode) || mode < 8);
-var ie6 = $.browser.msie && /MSIE 6.0/.test(navigator.userAgent) && !mode;
-
-// global $ methods for blocking/unblocking the entire page
-$.blockUI   = function(opts) { install(window, opts); };
-$.unblockUI = function(opts) { remove(window, opts); };
-
-// convenience method for quick growl-like notifications  (http://www.google.com/search?q=growl)
-$.growlUI = function(title, message, timeout, onClose) {
-	var $m = $('<div class="growlUI"></div>');
-	if (title) $m.append('<h1>'+title+'</h1>');
-	if (message) $m.append('<h2>'+message+'</h2>');
-	if (timeout == undefined) timeout = 3000;
-	$.blockUI({
-		message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
-		timeout: timeout, showOverlay: false,
-		onUnblock: onClose, 
-		css: $.blockUI.defaults.growlCSS
-	});
-};
-
-// plugin method for blocking element content
-$.fn.block = function(opts) {
-	return this.unblock({ fadeOut: 0 }).each(function() {
-		if ($.css(this,'position') == 'static')
-			this.style.position = 'relative';
-		if ($.browser.msie)
-			this.style.zoom = 1; // force 'hasLayout'
-		install(this, opts);
-	});
-};
-
-// plugin method for unblocking element content
-$.fn.unblock = function(opts) {
-	return this.each(function() {
-		remove(this, opts);
-	});
-};
-
-$.blockUI.version = 2.39; // 2nd generation blocking at no extra cost!
-
-// override these in your code to change the default behavior and style
-$.blockUI.defaults = {
-	// message displayed when blocking (use null for no message)
-	message:  '<h1>Please wait...</h1>',
-
-	title: null,	  // title string; only used when theme == true
-	draggable: true,  // only used when theme == true (requires jquery-ui.js to be loaded)
-	
-	theme: false, // set to true to use with jQuery UI themes
-	
-	// styles for the message when blocking; if you wish to disable
-	// these and use an external stylesheet then do this in your code:
-	// $.blockUI.defaults.css = {};
-	css: {
-		padding:	0,
-		margin:		0,
-		width:		'30%',
-		top:		'40%',
-		left:		'35%',
-		textAlign:	'center',
-		color:		'#000',
-		border:		'3px solid #aaa',
-		backgroundColor:'#fff',
-		cursor:		'wait'
-	},
-	
-	// minimal style set used when themes are used
-	themedCSS: {
-		width:	'30%',
-		top:	'40%',
-		left:	'35%'
-	},
-
-	// styles for the overlay
-	overlayCSS:  {
-		backgroundColor: '#000',
-		opacity:	  	 0.6,
-		cursor:		  	 'wait'
-	},
-
-	// styles applied when using $.growlUI
-	growlCSS: {
-		width:  	'350px',
-		top:		'10px',
-		left:   	'',
-		right:  	'10px',
-		border: 	'none',
-		padding:	'5px',
-		opacity:	0.6,
-		cursor: 	'default',
-		color:		'#fff',
-		backgroundColor: '#000',
-		'-webkit-border-radius': '10px',
-		'-moz-border-radius':	 '10px',
-		'border-radius': 		 '10px'
-	},
-	
-	// IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
-	// (hat tip to Jorge H. N. de Vasconcelos)
-	iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',
-
-	// force usage of iframe in non-IE browsers (handy for blocking applets)
-	forceIframe: false,
-
-	// z-index for the blocking overlay
-	baseZ: 1000,
-
-	// set these to true to have the message automatically centered
-	centerX: true, // <-- only effects element blocking (page block controlled via css above)
-	centerY: true,
-
-	// allow body element to be stetched in ie6; this makes blocking look better
-	// on "short" pages.  disable if you wish to prevent changes to the body height
-	allowBodyStretch: true,
-
-	// enable if you want key and mouse events to be disabled for content that is blocked
-	bindEvents: true,
-
-	// be default blockUI will supress tab navigation from leaving blocking content
-	// (if bindEvents is true)
-	constrainTabKey: true,
-
-	// fadeIn time in millis; set to 0 to disable fadeIn on block
-	fadeIn:  200,
-
-	// fadeOut time in millis; set to 0 to disable fadeOut on unblock
-	fadeOut:  400,
-
-	// time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
-	timeout: 0,
-
-	// disable if you don't want to show the overlay
-	showOverlay: true,
-
-	// if true, focus will be placed in the first available input field when
-	// page blocking
-	focusInput: true,
-
-	// suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
-	applyPlatformOpacityRules: true,
-	
-	// callback method invoked when fadeIn has completed and blocking message is visible
-	onBlock: null,
-
-	// callback method invoked when unblocking has completed; the callback is
-	// passed the element that has been unblocked (which is the window object for page
-	// blocks) and the options that were passed to the unblock call:
-	//	 onUnblock(element, options)
-	onUnblock: null,
-
-	// don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
-	quirksmodeOffsetHack: 4,
-
-	// class name of the message block
-	blockMsgClass: 'blockMsg'
-};
-
-// private data and functions follow...
-
-var pageBlock = null;
-var pageBlockEls = [];
-
-function install(el, opts) {
-	var full = (el == window);
-	var msg = opts && opts.message !== undefined ? opts.message : undefined;
-	opts = $.extend({}, $.blockUI.defaults, opts || {});
-	opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
-	var css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
-	var themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
-	msg = msg === undefined ? opts.message : msg;
-
-	// remove the current block (if there is one)
-	if (full && pageBlock)
-		remove(window, {fadeOut:0});
-
-	// if an existing element is being used as the blocking content then we capture
-	// its current place in the DOM (and current display style) so we can restore
-	// it when we unblock
-	if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
-		var node = msg.jquery ? msg[0] : msg;
-		var data = {};
-		$(el).data('blockUI.history', data);
-		data.el = node;
-		data.parent = node.parentNode;
-		data.display = node.style.display;
-		data.position = node.style.position;
-		if (data.parent)
-			data.parent.removeChild(node);
-	}
-
-	$(el).data('blockUI.onUnblock', opts.onUnblock);
-	var z = opts.baseZ;
-
-	// blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
-	// layer1 is the iframe layer which is used to supress bleed through of underlying content
-	// layer2 is the overlay layer which has opacity and a wait cursor (by default)
-	// layer3 is the message content that is displayed while blocking
-
-	var lyr1 = ($.browser.msie || opts.forceIframe) 
-		? $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>')
-		: $('<div class="blockUI" style="display:none"></div>');
-	
-	var lyr2 = opts.theme 
-	 	? $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>')
-	 	: $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
-
-	var lyr3, s;
-	if (opts.theme && full) {
-		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">' +
-				'<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>' +
-				'<div class="ui-widget-content ui-dialog-content"></div>' +
-			'</div>';
-	}
-	else if (opts.theme) {
-		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">' +
-				'<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>' +
-				'<div class="ui-widget-content ui-dialog-content"></div>' +
-			'</div>';
-	}
-	else if (full) {
-		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
-	}			 
-	else {
-		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
-	}
-	lyr3 = $(s);
-
-	// if we have a message, style it
-	if (msg) {
-		if (opts.theme) {
-			lyr3.css(themedCSS);
-			lyr3.addClass('ui-widget-content');
-		}
-		else 
-			lyr3.css(css);
-	}
-
-	// style the overlay
-	if (!opts.theme && (!opts.applyPlatformOpacityRules || !($.browser.mozilla && /Linux/.test(navigator.platform))))
-		lyr2.css(opts.overlayCSS);
-	lyr2.css('position', full ? 'fixed' : 'absolute');
-
-	// make iframe layer transparent in IE
-	if ($.browser.msie || opts.forceIframe)
-		lyr1.css('opacity',0.0);
-
-	//$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
-	var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el);
-	$.each(layers, function() {
-		this.appendTo($par);
-	});
-	
-	if (opts.theme && opts.draggable && $.fn.draggable) {
-		lyr3.draggable({
-			handle: '.ui-dialog-titlebar',
-			cancel: 'li'
-		});
-	}
-
-	// ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
-	var expr = setExpr && (!$.boxModel || $('object,embed', full ? null : el).length > 0);
-	if (ie6 || expr) {
-		// give body 100% height
-		if (full && opts.allowBodyStretch && $.boxModel)
-			$('html,body').css('height','100%');
-
-		// fix ie6 issue when blocked element has a border width
-		if ((ie6 || !$.boxModel) && !full) {
-			var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth');
-			var fixT = t ? '(0 - '+t+')' : 0;
-			var fixL = l ? '(0 - '+l+')' : 0;
-		}
-
-		// simulate fixed position
-		$.each([lyr1,lyr2,lyr3], function(i,o) {
-			var s = o[0].style;
-			s.position = 'absolute';
-			if (i < 2) {
-				full ? s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"')
-					 : s.setExpression('height','this.parentNode.offsetHeight + "px"');
-				full ? s.setExpression('width','jQuery.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"')
-					 : s.setExpression('width','this.parentNode.offsetWidth + "px"');
-				if (fixL) s.setExpression('left', fixL);
-				if (fixT) s.setExpression('top', fixT);
-			}
-			else if (opts.centerY) {
-				if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
-				s.marginTop = 0;
-			}
-			else if (!opts.centerY && full) {
-				var top = (opts.css && opts.css.top) ? parseInt(opts.css.top) : 0;
-				var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"';
-				s.setExpression('top',expression);
-			}
-		});
-	}
-
-	// show the message
-	if (msg) {
-		if (opts.theme)
-			lyr3.find('.ui-widget-content').append(msg);
-		else
-			lyr3.append(msg);
-		if (msg.jquery || msg.nodeType)
-			$(msg).show();
-	}
-
-	if (($.browser.msie || opts.forceIframe) && opts.showOverlay)
-		lyr1.show(); // opacity is zero
-	if (opts.fadeIn) {
-		var cb = opts.onBlock ? opts.onBlock : noOp;
-		var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
-		var cb2 = msg ? cb : noOp;
-		if (opts.showOverlay)
-			lyr2._fadeIn(opts.fadeIn, cb1);
-		if (msg)
-			lyr3._fadeIn(opts.fadeIn, cb2);
-	}
-	else {
-		if (opts.showOverlay)
-			lyr2.show();
-		if (msg)
-			lyr3.show();
-		if (opts.onBlock)
-			opts.onBlock();
-	}
-
-	// bind key and mouse events
-	bind(1, el, opts);
-
-	if (full) {
-		pageBlock = lyr3[0];
-		pageBlockEls = $(':input:enabled:visible',pageBlock);
-		if (opts.focusInput)
-			setTimeout(focus, 20);
-	}
-	else
-		center(lyr3[0], opts.centerX, opts.centerY);
-
-	if (opts.timeout) {
-		// auto-unblock
-		var to = setTimeout(function() {
-			full ? $.unblockUI(opts) : $(el).unblock(opts);
-		}, opts.timeout);
-		$(el).data('blockUI.timeout', to);
-	}
-};
-
-// remove the block
-function remove(el, opts) {
-	var full = (el == window);
-	var $el = $(el);
-	var data = $el.data('blockUI.history');
-	var to = $el.data('blockUI.timeout');
-	if (to) {
-		clearTimeout(to);
-		$el.removeData('blockUI.timeout');
-	}
-	opts = $.extend({}, $.blockUI.defaults, opts || {});
-	bind(0, el, opts); // unbind events
-
-	if (opts.onUnblock === null) {
-		opts.onUnblock = $el.data('blockUI.onUnblock');
-		$el.removeData('blockUI.onUnblock');
-	}
-
-	var els;
-	if (full) // crazy selector to handle odd field errors in ie6/7
-		els = $('body').children().filter('.blockUI').add('body > .blockUI');
-	else
-		els = $('.blockUI', el);
-
-	if (full)
-		pageBlock = pageBlockEls = null;
-
-	if (opts.fadeOut) {
-		els.fadeOut(opts.fadeOut);
-		setTimeout(function() { reset(els,data,opts,el); }, opts.fadeOut);
-	}
-	else
-		reset(els, data, opts, el);
-};
-
-// move blocking element back into the DOM where it started
-function reset(els,data,opts,el) {
-	els.each(function(i,o) {
-		// remove via DOM calls so we don't lose event handlers
-		if (this.parentNode)
-			this.parentNode.removeChild(this);
-	});
-
-	if (data && data.el) {
-		data.el.style.display = data.display;
-		data.el.style.position = data.position;
-		if (data.parent)
-			data.parent.appendChild(data.el);
-		$(el).removeData('blockUI.history');
-	}
-
-	if (typeof opts.onUnblock == 'function')
-		opts.onUnblock(el,opts);
-};
-
-// bind/unbind the handler
-function bind(b, el, opts) {
-	var full = el == window, $el = $(el);
-
-	// don't bother unbinding if there is nothing to unbind
-	if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
-		return;
-	if (!full)
-		$el.data('blockUI.isBlocked', b);
-
-	// don't bind events when overlay is not in use or if bindEvents is false
-	if (!opts.bindEvents || (b && !opts.showOverlay)) 
-		return;
-
-	// bind anchors and inputs for mouse and key events
-	var events = 'mousedown mouseup keydown keypress';
-	b ? $(document).bind(events, opts, handler) : $(document).unbind(events, handler);
-
-// former impl...
-//	   var $e = $('a,:input');
-//	   b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
-};
-
-// event handler to suppress keyboard/mouse events when blocking
-function handler(e) {
-	// allow tab navigation (conditionally)
-	if (e.keyCode && e.keyCode == 9) {
-		if (pageBlock && e.data.constrainTabKey) {
-			var els = pageBlockEls;
-			var fwd = !e.shiftKey && e.target === els[els.length-1];
-			var back = e.shiftKey && e.target === els[0];
-			if (fwd || back) {
-				setTimeout(function(){focus(back)},10);
-				return false;
-			}
-		}
-	}
-	var opts = e.data;
-	// allow events within the message content
-	if ($(e.target).parents('div.' + opts.blockMsgClass).length > 0)
-		return true;
-
-	// allow events for content that is not being blocked
-	return $(e.target).parents().children().filter('div.blockUI').length == 0;
-};
-
-function focus(back) {
-	if (!pageBlockEls)
-		return;
-	var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0];
-	if (e)
-		e.focus();
-};
-
-function center(el, x, y) {
-	var p = el.parentNode, s = el.style;
-	var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth');
-	var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth');
-	if (x) s.left = l > 0 ? (l+'px') : '0';
-	if (y) s.top  = t > 0 ? (t+'px') : '0';
-};
-
-function sz(el, p) {
-	return parseInt($.css(el,p))||0;
-};
-
-})(jQuery);
--- a/OrthancExplorer/libs/jquery.min.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-/*! jQuery v1.7.2 jquery.com | jquery.org/license */
-(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"<!doctype html>":"")+"<html><body>"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function ca(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function b_(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):b_(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&f.type(b)==="object")for(var e in b)b_(a+"["+e+"]",b[e],c,d);else d(a,b)}function b$(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function bZ(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bZ(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bZ(a,c,d,e,"*",g));return l}function bY(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?1:0,g=4;if(d>0){if(c!=="border")for(;e<g;e+=2)c||(d-=parseFloat(f.css(a,"padding"+bx[e]))||0),c==="margin"?d+=parseFloat(f.css(a,c+bx[e]))||0:d-=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0;return d+"px"}d=by(a,b);if(d<0||d==null)d=a.style[b];if(bt.test(d))return d;d=parseFloat(d)||0;if(c)for(;e<g;e+=2)d+=parseFloat(f.css(a,"padding"+bx[e]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+bx[e]))||0);return d+"px"}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;b.nodeType===1&&(b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?b.outerHTML=a.outerHTML:c!=="input"||a.type!=="checkbox"&&a.type!=="radio"?c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text):(a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value)),b.removeAttribute(f.expando),b.removeAttribute("_submit_attached"),b.removeAttribute("_change_attached"))}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c,i[c][d])}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h,i){var j,k=d==null,l=0,m=a.length;if(d&&typeof d=="object"){for(l in d)e.access(a,c,l,d[l],1,h,f);g=1}else if(f!==b){j=i===b&&e.isFunction(f),k&&(j?(j=c,c=function(a,b,c){return j.call(e(a),c)}):(c.call(a,f),c=null));if(c)for(;l<m;l++)c(a[l],d,j?f.call(a[l],l,c(a[l],d)):f,i);g=1}return g?a:k?c.call(a):m?c(a[0],d):h},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m,n=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?n(g):h==="function"&&(!a.unique||!p.has(g))&&c.push(g)},o=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,j=!0,m=k||0,k=0,l=c.length;for(;c&&m<l;m++)if(c[m].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}j=!1,c&&(a.once?e===!0?p.disable():c=[]:d&&d.length&&(e=d.shift(),p.fireWith(e[0],e[1])))},p={add:function(){if(c){var a=c.length;n(arguments),j?l=c.length:e&&e!==!0&&(k=a,o(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){j&&f<=l&&(l--,f<=m&&m--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&p.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(j?a.once||d.push([b,c]):(!a.once||!e)&&o(b,c));return this},fire:function(){p.fireWith(this,arguments);return this},fired:function(){return!!i}};return p};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p=c.createElement("div"),q=c.documentElement;p.setAttribute("className","t"),p.innerHTML="   <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="<div "+n+"display:block;'><div style='"+t+"0;display:block;overflow:hidden;'></div></div>"+"<table "+n+"' cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="<table><tr><td style='"+t+"0;display:none'></td><td>t</td></tr></table>",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="<div style='width:5px;'></div>",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h,i,j=this[0],k=0,m=null;if(a===b){if(this.length){m=f.data(j);if(j.nodeType===1&&!f._data(j,"parsedAttrs")){g=j.attributes;for(i=g.length;k<i;k++)h=g[k].name,h.indexOf("data-")===0&&(h=f.camelCase(h.substring(5)),l(j,h,m[h]));f._data(j,"parsedAttrs",!0)}}return m}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!";return f.access(this,function(c){if(c===b){m=this.triggerHandler("getData"+e,[d[0]]),m===b&&j&&(m=f.data(j,a),m=l(j,a,m));return m===b&&d[1]?this.data(d[0]):m}d[1]=c,this.each(function(){var b=f(this);b.triggerHandler("setData"+e,d),f.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length<d)return f.queue(this[0],a);return c===b?this:this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise(c)}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,f.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i<g;i++)e=d[i],e&&(c=f.propFix[e]||e,h=u.test(e),h||f.attr(a,e,""),a.removeAttribute(v?e:c),h&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0,coords:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(
-a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:g&&G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=f.event.special[c.type]||{},j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(!i.preDispatch||i.preDispatch.call(this,c)!==!1){if(e&&(!c.button||c.type!=="click")){n=f(this),n.context=this.ownerDocument||this;for(m=c.target;m!=this;m=m.parentNode||this)if(m.disabled!==!0){p={},r=[],n[0]=m;for(k=0;k<e;k++)s=d[k],t=s.selector,p[t]===b&&(p[t]=s.quick?H(m,s.quick):n.is(t)),p[t]&&r.push(s);r.length&&j.push({elem:m,matches:r})}}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){q=j[k],c.currentTarget=q.elem;for(l=0;l<q.matches.length&&!c.isImmediatePropagationStopped();l++){s=q.matches[l];if(h||!c.namespace&&!s.namespace||c.namespace_re&&c.namespace_re.test(s.namespace))c.data=s.data,c.handleObj=s,o=((f.event.special[s.origType]||{}).handle||s.handler).apply(q.elem,g),o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()))}}i.postDispatch&&i.postDispatch.call(this,c);return c.result}},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),d._submit_attached=!0)})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9||d===11){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.globalPOS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")[\\s/>]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f
-.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(f.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(g){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,function(a,b){b.src?f.ajax({type:"GET",global:!1,url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1></$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]==="<table>"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i<u;i++)bn(l[i]);else bn(l);l.nodeType?j.push(l):j=f.merge(j,l)}if(d){g=function(a){return!a.type||be.test(a.type)};for(k=0;j[k];k++){h=j[k];if(e&&f.nodeName(h,"script")&&(!h.type||be.test(h.type)))e.push(h.parentNode?h.parentNode.removeChild(h):h);else{if(h.nodeType===1){var v=f.grep(h.getElementsByTagName("script"),g);j.splice.apply(j,[k+1,0].concat(v))}d.appendChild(h)}}}return j},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bp=/alpha\([^)]*\)/i,bq=/opacity=([^)]*)/,br=/([A-Z]|^ms)/g,bs=/^[\-+]?(?:\d*\.)?\d+$/i,bt=/^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i,bu=/^([\-+])=([\-+.\de]+)/,bv=/^margin/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Top","Right","Bottom","Left"],by,bz,bA;f.fn.css=function(a,c){return f.access(this,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)},a,c,arguments.length>1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),(e===""&&f.css(d,"display")==="none"||!f.contains(d.ownerDocument.documentElement,d))&&f._data(d,"olddisplay",cu(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ct("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(ct("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o,p,q;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]);if((k=f.cssHooks[g])&&"expand"in k){l=k.expand(a[g]),delete a[g];for(i in l)i in a||(a[i]=l[i])}}for(g in a){h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cu(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cm.test(h)?(q=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),q?(f._data(this,"toggle"+i,q==="show"?"hide":"show"),j[q]()):j[h]()):(m=cn.exec(h),n=j.cur(),m?(o=parseFloat(m[2]),p=m[3]||(f.cssNumber[i]?"":"px"),p!=="px"&&(f.style(this,i,(o||1)+p),n=(o||1)/j.cur()*n,f.style(this,i,n+p)),m[1]&&(o=(m[1]==="-="?-1:1)*o+n),j.custom(n,o,p)):j.custom(n,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:ct("show",1),slideUp:ct("hide",1),slideToggle:ct("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a){return a},swing:function(a){return-Math.cos(a*Math.PI)/2+.5}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cq||cr(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){f._data(e.elem,"fxshow"+e.prop)===b&&(e.options.hide?f._data(e.elem,"fxshow"+e.prop,e.start):e.options.show&&f._data(e.elem,"fxshow"+e.prop,e.end))},h()&&f.timers.push(h)&&!co&&(co=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cq||cr(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(co),co=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(cp.concat.apply([],cp),function(a,b){b.indexOf("margin")&&(f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)})}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cv,cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?cv=function(a,b,c,d){try{d=a.getBoundingClientRect()}catch(e){}if(!d||!f.contains(c,a))return d?{top:d.top,left:d.left}:{top:0,left:0};var g=b.body,h=cy(b),i=c.clientTop||g.clientTop||0,j=c.clientLeft||g.clientLeft||0,k=h.pageYOffset||f.support.boxModel&&c.scrollTop||g.scrollTop,l=h.pageXOffset||f.support.boxModel&&c.scrollLeft||g.scrollLeft,m=d.top+k-i,n=d.left+l-j;return{top:m,left:n}}:cv=function(a,b,c){var d,e=a.offsetParent,g=a,h=b.body,i=b.defaultView,j=i?i.getComputedStyle(a,null):a.currentStyle,k=a.offsetTop,l=a.offsetLeft;while((a=a.parentNode)&&a!==h&&a!==c){if(f.support.fixedPosition&&j.position==="fixed")break;d=i?i.getComputedStyle(a,null):a.currentStyle,k-=a.scrollTop,l-=a.scrollLeft,a===e&&(k+=a.offsetTop,l+=a.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(a.nodeName))&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),g=e,e=a.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&d.overflow!=="visible"&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),j=d}if(j.position==="relative"||j.position==="static")k+=h.offsetTop,l+=h.offsetLeft;f.support.fixedPosition&&j.position==="fixed"&&(k+=Math.max(c.scrollTop,h.scrollTop),l+=Math.max(c.scrollLeft,h.scrollLeft));return{top:k,left:l}},f.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){f.offset.setOffset(this,a,b)});var c=this[0],d=c&&c.ownerDocument;if(!d)return null;if(c===d.body)return f.offset.bodyOffset(c);return cv(c,d,d.documentElement)},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window);
\ No newline at end of file
--- a/OrthancExplorer/libs/jquery.mobile.min.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-/*! jQuery Mobile v1.1.0 db342b1f315c282692791aa870455901fdb46a55 jquerymobile.com | jquery.org/license */
-.ui-bar-a{border:1px solid #333;background:#111;color:#fff;font-weight:bold;text-shadow:0 -1px 1px #000;background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#111));background-image:-webkit-linear-gradient(#3c3c3c,#111);background-image:-moz-linear-gradient(#3c3c3c,#111);background-image:-ms-linear-gradient(#3c3c3c,#111);background-image:-o-linear-gradient(#3c3c3c,#111);background-image:linear-gradient(#3c3c3c,#111)}.ui-bar-a,.ui-bar-a input,.ui-bar-a select,.ui-bar-a textarea,.ui-bar-a button{font-family:Helvetica,Arial,sans-serif}.ui-bar-a .ui-link-inherit{color:#fff}.ui-bar-a .ui-link{color:#7cc4e7;font-weight:bold}.ui-bar-a .ui-link:hover{color:#2489ce}.ui-bar-a .ui-link:active{color:#2489ce}.ui-bar-a .ui-link:visited{color:#2489ce}.ui-body-a,.ui-overlay-a{border:1px solid #444;background:#222;color:#fff;text-shadow:0 1px 1px #111;font-weight:normal;background-image:-webkit-gradient(linear,left top,left bottom,from(#444),to(#222));background-image:-webkit-linear-gradient(#444,#222);background-image:-moz-linear-gradient(#444,#222);background-image:-ms-linear-gradient(#444,#222);background-image:-o-linear-gradient(#444,#222);background-image:linear-gradient(#444,#222)}.ui-overlay-a{background-image:none;border-width:0}.ui-body-a,.ui-body-a input,.ui-body-a select,.ui-body-a textarea,.ui-body-a button{font-family:Helvetica,Arial,sans-serif}.ui-body-a .ui-link-inherit{color:#fff}.ui-body-a .ui-link{color:#2489ce;font-weight:bold}.ui-body-a .ui-link:hover{color:#2489ce}.ui-body-a .ui-link:active{color:#2489ce}.ui-body-a .ui-link:visited{color:#2489ce}.ui-btn-up-a{border:1px solid #111;background:#333;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from(#444),to(#2d2d2d));background-image:-webkit-linear-gradient(#444,#2d2d2d);background-image:-moz-linear-gradient(#444,#2d2d2d);background-image:-ms-linear-gradient(#444,#2d2d2d);background-image:-o-linear-gradient(#444,#2d2d2d);background-image:linear-gradient(#444,#2d2d2d)}.ui-btn-up-a a.ui-link-inherit{color:#fff}.ui-btn-hover-a{border:1px solid #000;background:#444;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from(#555),to(#383838));background-image:-webkit-linear-gradient(#555,#383838);background-image:-moz-linear-gradient(#555,#383838);background-image:-ms-linear-gradient(#555,#383838);background-image:-o-linear-gradient(#555,#383838);background-image:linear-gradient(#555,#383838)}.ui-btn-hover-a a.ui-link-inherit{color:#fff}.ui-btn-down-a{border:1px solid #000;background:#222;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from(#202020),to(#2c2c2c));background-image:-webkit-linear-gradient(#202020,#2c2c2c);background-image:-moz-linear-gradient(#202020,#2c2c2c);background-image:-ms-linear-gradient(#202020,#2c2c2c);background-image:-o-linear-gradient(#202020,#2c2c2c);background-image:linear-gradient(#202020,#2c2c2c)}.ui-btn-down-a a.ui-link-inherit{color:#fff}.ui-btn-up-a,.ui-btn-hover-a,.ui-btn-down-a{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-b{border:1px solid #456f9a;background:#5e87b0;color:#fff;font-weight:bold;text-shadow:0 1px 1px #3e6790;background-image:-webkit-gradient(linear,left top,left bottom,from(#6facd5),to(#497bae));background-image:-webkit-linear-gradient(#6facd5,#497bae);background-image:-moz-linear-gradient(#6facd5,#497bae);background-image:-ms-linear-gradient(#6facd5,#497bae);background-image:-o-linear-gradient(#6facd5,#497bae);background-image:linear-gradient(#6facd5,#497bae)}.ui-bar-b,.ui-bar-b input,.ui-bar-b select,.ui-bar-b textarea,.ui-bar-b button{font-family:Helvetica,Arial,sans-serif}.ui-bar-b .ui-link-inherit{color:#fff}.ui-bar-b .ui-link{color:#ddf0f8;font-weight:bold}.ui-bar-b .ui-link:hover{color:#ddf0f8}.ui-bar-b .ui-link:active{color:#ddf0f8}.ui-bar-b .ui-link:visited{color:#ddf0f8}.ui-body-b,.ui-overlay-b{border:1px solid #999;background:#f3f3f3;color:#222;text-shadow:0 1px 0 #fff;font-weight:normal;background-image:-webkit-gradient(linear,left top,left bottom,from(#ddd),to(#ccc));background-image:-webkit-linear-gradient(#ddd,#ccc);background-image:-moz-linear-gradient(#ddd,#ccc);background-image:-ms-linear-gradient(#ddd,#ccc);background-image:-o-linear-gradient(#ddd,#ccc);background-image:linear-gradient(#ddd,#ccc)}.ui-overlay-b{background-image:none;border-width:0}.ui-body-b,.ui-body-b input,.ui-body-b select,.ui-body-b textarea,.ui-body-b button{font-family:Helvetica,Arial,sans-serif}.ui-body-b .ui-link-inherit{color:#333}.ui-body-b .ui-link{color:#2489ce;font-weight:bold}.ui-body-b .ui-link:hover{color:#2489ce}.ui-body-b .ui-link:active{color:#2489ce}.ui-body-b .ui-link:visited{color:#2489ce}.ui-btn-up-b{border:1px solid #044062;background:#396b9e;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from(#5f9cc5),to(#396b9e));background-image:-webkit-linear-gradient(#5f9cc5,#396b9e);background-image:-moz-linear-gradient(#5f9cc5,#396b9e);background-image:-ms-linear-gradient(#5f9cc5,#396b9e);background-image:-o-linear-gradient(#5f9cc5,#396b9e);background-image:linear-gradient(#5f9cc5,#396b9e)}.ui-btn-up-b a.ui-link-inherit{color:#fff}.ui-btn-hover-b{border:1px solid #00415e;background:#4b88b6;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from(#6facd5),to(#4272a4));background-image:-webkit-linear-gradient(#6facd5,#4272a4);background-image:-moz-linear-gradient(#6facd5,#4272a4);background-image:-ms-linear-gradient(#6facd5,#4272a4);background-image:-o-linear-gradient(#6facd5,#4272a4);background-image:linear-gradient(#6facd5,#4272a4)}.ui-btn-hover-b a.ui-link-inherit{color:#fff}.ui-btn-down-b{border:1px solid #225377;background:#4e89c5;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from(#295b8e),to(#3e79b5));background-image:-webkit-linear-gradient(#295b8e,#3e79b5);background-image:-moz-linear-gradient(#295b8e,#3e79b5);background-image:-ms-linear-gradient(#295b8e,#3e79b5);background-image:-o-linear-gradient(#295b8e,#3e79b5);background-image:linear-gradient(#295b8e,#3e79b5)}.ui-btn-down-b a.ui-link-inherit{color:#fff}.ui-btn-up-b,.ui-btn-hover-b,.ui-btn-down-b{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-c{border:1px solid #b3b3b3;background:#eee;color:#3e3e3e;font-weight:bold;text-shadow:0 1px 1px #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#f0f0f0),to(#ddd));background-image:-webkit-linear-gradient(#f0f0f0,#ddd);background-image:-moz-linear-gradient(#f0f0f0,#ddd);background-image:-ms-linear-gradient(#f0f0f0,#ddd);background-image:-o-linear-gradient(#f0f0f0,#ddd);background-image:linear-gradient(#f0f0f0,#ddd)}.ui-bar-c .ui-link-inherit{color:#3e3e3e}.ui-bar-c .ui-link{color:#7cc4e7;font-weight:bold}.ui-bar-c .ui-link:hover{color:#2489ce}.ui-bar-c .ui-link:active{color:#2489ce}.ui-bar-c .ui-link:visited{color:#2489ce}.ui-bar-c,.ui-bar-c input,.ui-bar-c select,.ui-bar-c textarea,.ui-bar-c button{font-family:Helvetica,Arial,sans-serif}.ui-body-c,.ui-overlay-c{border:1px solid #aaa;color:#333;text-shadow:0 1px 0 #fff;background:#f9f9f9;background-image:-webkit-gradient(linear,left top,left bottom,from(#f9f9f9),to(#eee));background-image:-webkit-linear-gradient(#f9f9f9,#eee);background-image:-moz-linear-gradient(#f9f9f9,#eee);background-image:-ms-linear-gradient(#f9f9f9,#eee);background-image:-o-linear-gradient(#f9f9f9,#eee);background-image:linear-gradient(#f9f9f9,#eee)}.ui-overlay-c{background-image:none;border-width:0}.ui-body-c,.ui-body-c input,.ui-body-c select,.ui-body-c textarea,.ui-body-c button{font-family:Helvetica,Arial,sans-serif}.ui-body-c .ui-link-inherit{color:#333}.ui-body-c .ui-link{color:#2489ce;font-weight:bold}.ui-body-c .ui-link:hover{color:#2489ce}.ui-body-c .ui-link:active{color:#2489ce}.ui-body-c .ui-link:visited{color:#2489ce}.ui-btn-up-c{border:1px solid #ccc;background:#eee;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f1f1f1));background-image:-webkit-linear-gradient(#fff,#f1f1f1);background-image:-moz-linear-gradient(#fff,#f1f1f1);background-image:-ms-linear-gradient(#fff,#f1f1f1);background-image:-o-linear-gradient(#fff,#f1f1f1);background-image:linear-gradient(#fff,#f1f1f1)}.ui-btn-up-c a.ui-link-inherit{color:#2f3e46}.ui-btn-hover-c{border:1px solid #bbb;background:#dfdfdf;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f6f6),to(#e0e0e0));background-image:-webkit-linear-gradient(#f9f9f9,#e0e0e0);background-image:-moz-linear-gradient(#f6f6f6,#e0e0e0);background-image:-ms-linear-gradient(#f6f6f6,#e0e0e0);background-image:-o-linear-gradient(#f6f6f6,#e0e0e0);background-image:linear-gradient(#f6f6f6,#e0e0e0)}.ui-btn-hover-c a.ui-link-inherit{color:#2f3e46}.ui-btn-down-c{border:1px solid #bbb;background:#d6d6d6;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#d0d0d0),to(#dfdfdf));background-image:-webkit-linear-gradient(#d0d0d0,#dfdfdf);background-image:-moz-linear-gradient(#d0d0d0,#dfdfdf);background-image:-ms-linear-gradient(#d0d0d0,#dfdfdf);background-image:-o-linear-gradient(#d0d0d0,#dfdfdf);background-image:linear-gradient(#d0d0d0,#dfdfdf)}.ui-btn-down-c a.ui-link-inherit{color:#2f3e46}.ui-btn-up-c,.ui-btn-hover-c,.ui-btn-down-c{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-d{border:1px solid #bbb;background:#bbb;color:#333;text-shadow:0 1px 0 #eee;background-image:-webkit-gradient(linear,left top,left bottom,from(#ddd),to(#bbb));background-image:-webkit-linear-gradient(#ddd,#bbb);background-image:-moz-linear-gradient(#ddd,#bbb);background-image:-ms-linear-gradient(#ddd,#bbb);background-image:-o-linear-gradient(#ddd,#bbb);background-image:linear-gradient(#ddd,#bbb)}.ui-bar-d,.ui-bar-d input,.ui-bar-d select,.ui-bar-d textarea,.ui-bar-d button{font-family:Helvetica,Arial,sans-serif}.ui-bar-d .ui-link-inherit{color:#333}.ui-bar-d .ui-link{color:#2489ce;font-weight:bold}.ui-bar-d .ui-link:hover{color:#2489ce}.ui-bar-d .ui-link:active{color:#2489ce}.ui-bar-d .ui-link:visited{color:#2489ce}.ui-body-d,.ui-overlay-d{border:1px solid #bbb;color:#333;text-shadow:0 1px 0 #fff;background:#fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#fff));background-image:-webkit-linear-gradient(#fff,#fff);background-image:-moz-linear-gradient(#fff,#fff);background-image:-ms-linear-gradient(#fff,#fff);background-image:-o-linear-gradient(#fff,#fff);background-image:linear-gradient(#fff,#fff)}.ui-overlay-d{background-image:none;border-width:0}.ui-body-d,.ui-body-d input,.ui-body-d select,.ui-body-d textarea,.ui-body-d button{font-family:Helvetica,Arial,sans-serif}.ui-body-d .ui-link-inherit{color:#333}.ui-body-d .ui-link{color:#2489ce;font-weight:bold}.ui-body-d .ui-link:hover{color:#2489ce}.ui-body-d .ui-link:active{color:#2489ce}.ui-body-d .ui-link:visited{color:#2489ce}.ui-btn-up-d{border:1px solid #bbb;background:#fff;font-weight:bold;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#f6f6f6));background-image:-webkit-linear-gradient(#fafafa,#f6f6f6);background-image:-moz-linear-gradient(#fafafa,#f6f6f6);background-image:-ms-linear-gradient(#fafafa,#f6f6f6);background-image:-o-linear-gradient(#fafafa,#f6f6f6);background-image:linear-gradient(#fafafa,#f6f6f6)}.ui-btn-up-d a.ui-link-inherit{color:#333}.ui-btn-hover-d{border:1px solid #aaa;background:#eee;font-weight:bold;color:#333;cursor:pointer;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#fff));background-image:-webkit-linear-gradient(#eee,#fff);background-image:-moz-linear-gradient(#eee,#fff);background-image:-ms-linear-gradient(#eee,#fff);background-image:-o-linear-gradient(#eee,#fff);background-image:linear-gradient(#eee,#fff)}.ui-btn-hover-d a.ui-link-inherit{color:#333}.ui-btn-down-d{border:1px solid #aaa;background:#eee;font-weight:bold;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#e5e5e5),to(#f2f2f2));background-image:-webkit-linear-gradient(#e5e5e5,#f2f2f2);background-image:-moz-linear-gradient(#e5e5e5,#f2f2f2);background-image:-ms-linear-gradient(#e5e5e5,#f2f2f2);background-image:-o-linear-gradient(#e5e5e5,#f2f2f2);background-image:linear-gradient(#e5e5e5,#f2f2f2)}.ui-btn-down-d a.ui-link-inherit{color:#333}.ui-btn-up-d,.ui-btn-hover-d,.ui-btn-down-d{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-e{border:1px solid #f7c942;background:#fadb4e;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fceda7),to(#fbef7e));background-image:-webkit-linear-gradient(#fceda7,#fbef7e);background-image:-moz-linear-gradient(#fceda7,#fbef7e);background-image:-ms-linear-gradient(#fceda7,#fbef7e);background-image:-o-linear-gradient(#fceda7,#fbef7e);background-image:linear-gradient(#fceda7,#fbef7e)}.ui-bar-e,.ui-bar-e input,.ui-bar-e select,.ui-bar-e textarea,.ui-bar-e button{font-family:Helvetica,Arial,sans-serif}.ui-bar-e .ui-link-inherit{color:#333}.ui-bar-e .ui-link{color:#2489ce;font-weight:bold}.ui-bar-e .ui-link:hover{color:#2489ce}.ui-bar-e .ui-link:active{color:#2489ce}.ui-bar-e .ui-link:visited{color:#2489ce}.ui-body-e,.ui-overlay-e{border:1px solid #f7c942;color:#222;text-shadow:0 1px 0 #fff;background:#fff9df;background-image:-webkit-gradient(linear,left top,left bottom,from(#fffadf),to(#fff3a5));background-image:-webkit-linear-gradient(#fffadf,#fff3a5);background-image:-moz-linear-gradient(#fffadf,#fff3a5);background-image:-ms-linear-gradient(#fffadf,#fff3a5);background-image:-o-linear-gradient(#fffadf,#fff3a5);background-image:linear-gradient(#fffadf,#fff3a5)}.ui-overlay-e{background-image:none;border-width:0}.ui-body-e,.ui-body-e input,.ui-body-e select,.ui-body-e textarea,.ui-body-e button{font-family:Helvetica,Arial,sans-serif}.ui-body-e .ui-link-inherit{color:#333}.ui-body-e .ui-link{color:#2489ce;font-weight:bold}.ui-body-e .ui-link:hover{color:#2489ce}.ui-body-e .ui-link:active{color:#2489ce}.ui-body-e .ui-link:visited{color:#2489ce}.ui-btn-up-e{border:1px solid #f4c63f;background:#fadb4e;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#ffefaa),to(#ffe155));background-image:-webkit-linear-gradient(#ffefaa,#ffe155);background-image:-moz-linear-gradient(#ffefaa,#ffe155);background-image:-ms-linear-gradient(#ffefaa,#ffe155);background-image:-o-linear-gradient(#ffefaa,#ffe155);background-image:linear-gradient(#ffefaa,#ffe155)}.ui-btn-up-e a.ui-link-inherit{color:#222}.ui-btn-hover-e{border:1px solid #f2c43d;background:#fbe26f;font-weight:bold;color:#111;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff5ba),to(#fbdd52));background-image:-webkit-linear-gradient(#fff5ba,#fbdd52);background-image:-moz-linear-gradient(#fff5ba,#fbdd52);background-image:-ms-linear-gradient(#fff5ba,#fbdd52);background-image:-o-linear-gradient(#fff5ba,#fbdd52);background-image:linear-gradient(#fff5ba,#fbdd52)}.ui-btn-hover-e a.ui-link-inherit{color:#333}.ui-btn-down-e{border:1px solid #f2c43d;background:#fceda7;font-weight:bold;color:#111;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#f8d94c),to(#fadb4e));background-image:-webkit-linear-gradient(#f8d94c,#fadb4e);background-image:-moz-linear-gradient(#f8d94c,#fadb4e);background-image:-ms-linear-gradient(#f8d94c,#fadb4e);background-image:-o-linear-gradient(#f8d94c,#fadb4e);background-image:linear-gradient(#f8d94c,#fadb4e)}.ui-btn-down-e a.ui-link-inherit{color:#333}.ui-btn-up-e,.ui-btn-hover-e,.ui-btn-down-e{font-family:Helvetica,Arial,sans-serif;text-decoration:none}a.ui-link-inherit{text-decoration:none!important}.ui-btn-active{border:1px solid #2373a5;background:#5393c5;font-weight:bold;color:#fff;cursor:pointer;text-shadow:0 1px 1px #3373a5;text-decoration:none;background-image:-webkit-gradient(linear,left top,left bottom,from(#5393c5),to(#6facd5));background-image:-webkit-linear-gradient(#5393c5,#6facd5);background-image:-moz-linear-gradient(#5393c5,#6facd5);background-image:-ms-linear-gradient(#5393c5,#6facd5);background-image:-o-linear-gradient(#5393c5,#6facd5);background-image:linear-gradient(#5393c5,#6facd5);font-family:Helvetica,Arial,sans-serif}.ui-btn-active a.ui-link-inherit{color:#fff}.ui-btn-inner{border-top:1px solid #fff;border-color:rgba(255,255,255,.3)}.ui-corner-tl{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em}.ui-corner-tr{-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em}.ui-corner-bl{-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em}.ui-corner-br{-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-top{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em;-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em}.ui-corner-bottom{-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em;-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-right{-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em;-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-left{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em;-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em}.ui-corner-all{-moz-border-radius:.6em;-webkit-border-radius:.6em;border-radius:.6em}.ui-corner-none{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.ui-br{border-bottom:#828282;border-bottom:rgba(130,130,130,.3);border-bottom-width:1px;border-bottom-style:solid}.ui-disabled{opacity:.3}.ui-disabled,.ui-disabled a{cursor:default!important;pointer-events:none}.ui-disabled .ui-btn-text{-ms-filter:"alpha(opacity=30)";filter:alpha(opacity=30);zoom:1}.ui-icon,.ui-icon-searchfield:after{background:#666;background:rgba(0,0,0,.4);background-image:url(images/icons-18-white.png);background-repeat:no-repeat;-moz-border-radius:9px;-webkit-border-radius:9px;border-radius:9px}.ui-icon-alt{background:#fff;background:rgba(255,255,255,.3);background-image:url(images/icons-18-black.png);background-repeat:no-repeat}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-resolution:240dpi){.ui-icon-plus,.ui-icon-minus,.ui-icon-delete,.ui-icon-arrow-r,.ui-icon-arrow-l,.ui-icon-arrow-u,.ui-icon-arrow-d,.ui-icon-check,.ui-icon-gear,.ui-icon-refresh,.ui-icon-forward,.ui-icon-back,.ui-icon-grid,.ui-icon-star,.ui-icon-alert,.ui-icon-info,.ui-icon-home,.ui-icon-search,.ui-icon-searchfield:after,.ui-icon-checkbox-off,.ui-icon-checkbox-on,.ui-icon-radio-off,.ui-icon-radio-on{background-image:url(images/icons-36-white.png);-moz-background-size:776px 18px;-o-background-size:776px 18px;-webkit-background-size:776px 18px;background-size:776px 18px}.ui-icon-alt{background-image:url(images/icons-36-black.png)}}.ui-icon-plus{background-position:-0 50%}.ui-icon-minus{background-position:-36px 50%}.ui-icon-delete{background-position:-72px 50%}.ui-icon-arrow-r{background-position:-108px 50%}.ui-icon-arrow-l{background-position:-144px 50%}.ui-icon-arrow-u{background-position:-180px 50%}.ui-icon-arrow-d{background-position:-216px 50%}.ui-icon-check{background-position:-252px 50%}.ui-icon-gear{background-position:-288px 50%}.ui-icon-refresh{background-position:-324px 50%}.ui-icon-forward{background-position:-360px 50%}.ui-icon-back{background-position:-396px 50%}.ui-icon-grid{background-position:-432px 50%}.ui-icon-star{background-position:-468px 50%}.ui-icon-alert{background-position:-504px 50%}.ui-icon-info{background-position:-540px 50%}.ui-icon-home{background-position:-576px 50%}.ui-icon-search,.ui-icon-searchfield:after{background-position:-612px 50%}.ui-icon-checkbox-off{background-position:-684px 50%}.ui-icon-checkbox-on{background-position:-648px 50%}.ui-icon-radio-off{background-position:-756px 50%}.ui-icon-radio-on{background-position:-720px 50%}.ui-checkbox .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.ui-icon-checkbox-off,.ui-icon-radio-off{background-color:transparent}.ui-checkbox-on .ui-icon,.ui-radio-on .ui-icon{background-color:#4596ce}.ui-icon-loading{background:url(images/ajax-loader.gif);background-size:46px 46px}.ui-btn-corner-tl{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em}.ui-btn-corner-tr{-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em}.ui-btn-corner-bl{-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em}.ui-btn-corner-br{-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em}.ui-btn-corner-top{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em;-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em}.ui-btn-corner-bottom{-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em;-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em}.ui-btn-corner-right{-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em;-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em}.ui-btn-corner-left{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em;-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em}.ui-btn-corner-all{-moz-border-radius:1em;-webkit-border-radius:1em;border-radius:1em}.ui-corner-tl,.ui-corner-tr,.ui-corner-bl,.ui-corner-br,.ui-corner-top,.ui-corner-bottom,.ui-corner-right,.ui-corner-left,.ui-corner-all,.ui-btn-corner-tl,.ui-btn-corner-tr,.ui-btn-corner-bl,.ui-btn-corner-br,.ui-btn-corner-top,.ui-btn-corner-bottom,.ui-btn-corner-right,.ui-btn-corner-left,.ui-btn-corner-all{-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.ui-overlay{background:#666;opacity:.5;filter:Alpha(Opacity=50);position:absolute;width:100%;height:100%}.ui-overlay-shadow{-moz-box-shadow:0 0 12px rgba(0,0,0,.6);-webkit-box-shadow:0 0 12px rgba(0,0,0,.6);box-shadow:0 0 12px rgba(0,0,0,.6)}.ui-shadow{-moz-box-shadow:0 1px 4px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.3);box-shadow:0 1px 4px rgba(0,0,0,.3)}.ui-bar-a .ui-shadow,.ui-bar-b .ui-shadow,.ui-bar-c .ui-shadow{-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.ui-shadow-inset{-moz-box-shadow:inset 0 1px 4px rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 4px rgba(0,0,0,.2);box-shadow:inset 0 1px 4px rgba(0,0,0,.2)}.ui-icon-shadow{-moz-box-shadow:0 1px 0 rgba(255,255,255,.4);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.4);box-shadow:0 1px 0 rgba(255,255,255,.4)}.ui-btn:focus{outline:0}.ui-focus,.ui-btn:focus{-moz-box-shadow:0 0 12px #387bbe;-webkit-box-shadow:0 0 12px #387bbe;box-shadow:0 0 12px #387bbe}.ui-mobile-nosupport-boxshadow *{-moz-box-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui-mobile-nosupport-boxshadow .ui-focus,.ui-mobile-nosupport-boxshadow .ui-btn:focus{outline-width:1px;outline-style:dotted}.ui-mobile,.ui-mobile body{height:99.9%}.ui-mobile fieldset,.ui-page{padding:0;margin:0}.ui-mobile a img,.ui-mobile fieldset{border-width:0}.ui-mobile-viewport{margin:0;overflow-x:visible;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}body.ui-mobile-viewport,div.ui-mobile-viewport{overflow-x:hidden}.ui-mobile [data-role=page],.ui-mobile [data-role=dialog],.ui-page{top:0;left:0;width:100%;min-height:100%;position:absolute;display:none;border:0}.ui-mobile .ui-page-active{display:block;overflow:visible}.ui-page{outline:0}@media screen and (orientation:portrait){.ui-mobile,.ui-mobile .ui-page{min-height:420px}}@media screen and (orientation:landscape){.ui-mobile,.ui-mobile .ui-page{min-height:300px}}.ui-loading .ui-loader{display:block}.ui-loader{display:none;z-index:9999999;position:fixed;top:50%;box-shadow:0 1px 1px -1px #fff;left:50%;border:0}.ui-loader-default{background:0;opacity:.18;width:46px;height:46px;margin-left:-23px;margin-top:-23px}.ui-loader-verbose{width:200px;opacity:.88;height:auto;margin-left:-110px;margin-top:-43px;padding:10px}.ui-loader-default h1{font-size:0;width:0;height:0;overflow:hidden}.ui-loader-verbose h1{font-size:16px;margin:0;text-align:center}.ui-loader .ui-icon{background-color:#000;display:block;margin:0;width:44px;height:44px;padding:1px;-webkit-border-radius:36px;-moz-border-radius:36px;border-radius:36px}.ui-loader-verbose .ui-icon{margin:0 auto 10px;opacity:.75}.ui-loader-textonly{padding:15px;margin-left:-115px}.ui-loader-textonly .ui-icon{display:none}.ui-loader-fakefix{position:absolute}.ui-mobile-rendering>*{visibility:hidden}.ui-bar,.ui-body{position:relative;padding:.4em 15px;overflow:hidden;display:block;clear:both}.ui-bar{font-size:16px;margin:0}.ui-bar h1,.ui-bar h2,.ui-bar h3,.ui-bar h4,.ui-bar h5,.ui-bar h6{margin:0;padding:0;font-size:16px;display:inline-block}.ui-header,.ui-footer{position:relative;border-left-width:0;border-right-width:0}.ui-header .ui-btn-left,.ui-header .ui-btn-right,.ui-footer .ui-btn-left,.ui-footer .ui-btn-right{position:absolute;top:3px}.ui-header .ui-btn-left,.ui-footer .ui-btn-left{left:5px}.ui-header .ui-btn-right,.ui-footer .ui-btn-right{right:5px}.ui-footer .ui-btn-icon-notext,.ui-header .ui-btn-icon-notext{top:6px}.ui-header .ui-title,.ui-footer .ui-title{min-height:1.1em;text-align:center;font-size:16px;display:block;margin:.6em 30% .8em;padding:0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;outline:0!important}.ui-footer .ui-title{margin:.6em 15px .8em}.ui-content{border-width:0;overflow:visible;overflow-x:hidden;padding:15px}.ui-icon{width:18px;height:18px}.ui-nojs{position:absolute;left:-9999px}.ui-hide-label label,.ui-hidden-accessible{position:absolute!important;left:-9999px;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ui-mobile-viewport-transitioning,.ui-mobile-viewport-transitioning .ui-page{width:100%;height:100%;overflow:hidden}.in{-webkit-animation-timing-function:ease-out;-webkit-animation-duration:350ms;-moz-animation-timing-function:ease-out;-moz-animation-duration:350ms}.out{-webkit-animation-timing-function:ease-in;-webkit-animation-duration:225ms;-moz-animation-timing-function:ease-in;-moz-animation-duration:225}@-webkit-keyframes fadein{from{opacity:0}to{opacity:1}}@-moz-keyframes fadein{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadeout{from{opacity:1}to{opacity:0}}@-moz-keyframes fadeout{from{opacity:1}to{opacity:0}}.fade.out{opacity:0;-webkit-animation-duration:125ms;-webkit-animation-name:fadeout;-moz-animation-duration:125ms;-moz-animation-name:fadeout}.fade.in{opacity:1;-webkit-animation-duration:225ms;-webkit-animation-name:fadein;-moz-animation-duration:225ms;-moz-animation-name:fadein}.pop{-webkit-transform-origin:50% 50%;-moz-transform-origin:50% 50%}.pop.in{-webkit-transform:scale(1);-moz-transform:scale(1);opacity:1;-webkit-animation-name:popin;-moz-animation-name:popin;-webkit-animation-duration:350ms;-moz-animation-duration:350ms}.pop.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;opacity:0;-webkit-animation-duration:100ms;-moz-animation-duration:100ms}.pop.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein}.pop.out.reverse{-webkit-transform:scale(.8);-moz-transform:scale(.8);-webkit-animation-name:popout;-moz-animation-name:popout}@-webkit-keyframes popin{from{-webkit-transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);opacity:1}}@-moz-keyframes popin{from{-moz-transform:scale(.8);opacity:0}to{-moz-transform:scale(1);opacity:1}}@-webkit-keyframes popout{from{-webkit-transform:scale(1);opacity:1}to{-webkit-transform:scale(.8);opacity:0}}@-moz-keyframes popout{from{-moz-transform:scale(1);opacity:1}to{-moz-transform:scale(.8);opacity:0}}@-webkit-keyframes slideinfromright{from{-webkit-transform:translateX(100%)}to{-webkit-transform:translateX(0)}}@-moz-keyframes slideinfromright{from{-moz-transform:translateX(100%)}to{-moz-transform:translateX(0)}}@-webkit-keyframes slideinfromleft{from{-webkit-transform:translateX(-100%)}to{-webkit-transform:translateX(0)}}@-moz-keyframes slideinfromleft{from{-moz-transform:translateX(-100%)}to{-moz-transform:translateX(0)}}@-webkit-keyframes slideouttoleft{from{-webkit-transform:translateX(0)}to{-webkit-transform:translateX(-100%)}}@-moz-keyframes slideouttoleft{from{-moz-transform:translateX(0)}to{-moz-transform:translateX(-100%)}}@-webkit-keyframes slideouttoright{from{-webkit-transform:translateX(0)}to{-webkit-transform:translateX(100%)}}@-moz-keyframes slideouttoright{from{-moz-transform:translateX(0)}to{-moz-transform:translateX(100%)}}.slide.out,.slide.in{-webkit-animation-timing-function:ease-out;-webkit-animation-duration:350ms;-moz-animation-timing-function:ease-out;-moz-animation-duration:350ms}.slide.out{-webkit-transform:translateX(-100%);-webkit-animation-name:slideouttoleft;-moz-transform:translateX(-100%);-moz-animation-name:slideouttoleft}.slide.in{-webkit-transform:translateX(0);-webkit-animation-name:slideinfromright;-moz-transform:translateX(0);-moz-animation-name:slideinfromright}.slide.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:slideouttoright;-moz-transform:translateX(100%);-moz-animation-name:slideouttoright}.slide.in.reverse{-webkit-transform:translateX(0);-webkit-animation-name:slideinfromleft;-moz-transform:translateX(0);-moz-animation-name:slideinfromleft}.slidefade.out{-webkit-transform:translateX(-100%);-webkit-animation-name:slideouttoleft;-moz-transform:translateX(-100%);-moz-animation-name:slideouttoleft;-webkit-animation-duration:225ms;-moz-animation-duration:225ms}.slidefade.in{-webkit-transform:translateX(0);-webkit-animation-name:fadein;-moz-transform:translateX(0);-moz-animation-name:fadein;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}.slidefade.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:slideouttoright;-moz-transform:translateX(100%);-moz-animation-name:slideouttoright;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}.slidefade.in.reverse{-webkit-transform:translateX(0);-webkit-animation-name:fadein;-moz-transform:translateX(0);-moz-animation-name:fadein;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}.slidedown.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;-webkit-animation-duration:100ms;-moz-animation-duration:100ms}.slidedown.in{-webkit-transform:translateY(0);-webkit-animation-name:slideinfromtop;-moz-transform:translateY(0);-moz-animation-name:slideinfromtop;-webkit-animation-duration:250ms;-moz-animation-duration:250ms}.slidedown.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein;-webkit-animation-duration:150ms;-moz-animation-duration:150ms}.slidedown.out.reverse{-webkit-transform:translateY(-100%);-moz-transform:translateY(-100%);-webkit-animation-name:slideouttotop;-moz-animation-name:slideouttotop;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}@-webkit-keyframes slideinfromtop{from{-webkit-transform:translateY(-100%)}to{-webkit-transform:translateY(0)}}@-moz-keyframes slideinfromtop{from{-moz-transform:translateY(-100%)}to{-moz-transform:translateY(0)}}@-webkit-keyframes slideouttotop{from{-webkit-transform:translateY(0)}to{-webkit-transform:translateY(-100%)}}@-moz-keyframes slideouttotop{from{-moz-transform:translateY(0)}to{-moz-transform:translateY(-100%)}}.slideup.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;-webkit-animation-duration:100ms;-moz-animation-duration:100ms}.slideup.in{-webkit-transform:translateY(0);-webkit-animation-name:slideinfrombottom;-moz-transform:translateY(0);-moz-animation-name:slideinfrombottom;-webkit-animation-duration:250ms;-moz-animation-duration:250ms}.slideup.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein;-webkit-animation-duration:150ms;-moz-animation-duration:150ms}.slideup.out.reverse{-webkit-transform:translateY(100%);-moz-transform:translateY(100%);-webkit-animation-name:slideouttobottom;-moz-animation-name:slideouttobottom;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}@-webkit-keyframes slideinfrombottom{from{-webkit-transform:translateY(100%)}to{-webkit-transform:translateY(0)}}@-moz-keyframes slideinfrombottom{from{-moz-transform:translateY(100%)}to{-moz-transform:translateY(0)}}@-webkit-keyframes slideouttobottom{from{-webkit-transform:translateY(0)}to{-webkit-transform:translateY(100%)}}@-moz-keyframes slideouttobottom{from{-moz-transform:translateY(0)}to{-moz-transform:translateY(100%)}}.viewport-flip{-webkit-perspective:1000;-moz-perspective:1000;position:absolute}.flip{-webkit-backface-visibility:hidden;-webkit-transform:translateX(0);-moz-backface-visibility:hidden;-moz-transform:translateX(0)}.flip.out{-webkit-transform:rotateY(-90deg) scale(.9);-webkit-animation-name:flipouttoleft;-webkit-animation-duration:175ms;-moz-transform:rotateY(-90deg) scale(.9);-moz-animation-name:flipouttoleft;-moz-animation-duration:175ms}.flip.in{-webkit-animation-name:flipintoright;-webkit-animation-duration:225ms;-moz-animation-name:flipintoright;-moz-animation-duration:225ms}.flip.out.reverse{-webkit-transform:rotateY(90deg) scale(.9);-webkit-animation-name:flipouttoright;-moz-transform:rotateY(90deg) scale(.9);-moz-animation-name:flipouttoright}.flip.in.reverse{-webkit-animation-name:flipintoleft;-moz-animation-name:flipintoleft}@-webkit-keyframes flipouttoleft{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(-90deg) scale(.9)}}@-moz-keyframes flipouttoleft{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(-90deg) scale(.9)}}@-webkit-keyframes flipouttoright{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(90deg) scale(.9)}}@-moz-keyframes flipouttoright{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(90deg) scale(.9)}}@-webkit-keyframes flipintoleft{from{-webkit-transform:rotateY(-90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoleft{from{-moz-transform:rotateY(-90deg) scale(.9)}to{-moz-transform:rotateY(0)}}@-webkit-keyframes flipintoright{from{-webkit-transform:rotateY(90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoright{from{-moz-transform:rotateY(90deg) scale(.9)}to{-moz-transform:rotateY(0)}}.viewport-turn{-webkit-perspective:1000;-moz-perspective:1000;position:absolute}.turn{-webkit-backface-visibility:hidden;-webkit-transform:translateX(0);-webkit-transform-origin:0 0;-moz-backface-visibility:hidden;-moz-transform:translateX(0);-moz-transform-origin:0 0}.turn.out{-webkit-transform:rotateY(-90deg) scale(.9);-webkit-animation-name:flipouttoleft;-moz-transform:rotateY(-90deg) scale(.9);-moz-animation-name:flipouttoleft;-webkit-animation-duration:125ms;-moz-animation-duration:125ms}.turn.in{-webkit-animation-name:flipintoright;-moz-animation-name:flipintoright;-webkit-animation-duration:250ms;-moz-animation-duration:250ms}.turn.out.reverse{-webkit-transform:rotateY(90deg) scale(.9);-webkit-animation-name:flipouttoright;-moz-transform:rotateY(90deg) scale(.9);-moz-animation-name:flipouttoright}.turn.in.reverse{-webkit-animation-name:flipintoleft;-moz-animation-name:flipintoleft}@-webkit-keyframes flipouttoleft{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(-90deg) scale(.9)}}@-moz-keyframes flipouttoleft{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(-90deg) scale(.9)}}@-webkit-keyframes flipouttoright{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(90deg) scale(.9)}}@-moz-keyframes flipouttoright{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(90deg) scale(.9)}}@-webkit-keyframes flipintoleft{from{-webkit-transform:rotateY(-90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoleft{from{-moz-transform:rotateY(-90deg) scale(.9)}to{-moz-transform:rotateY(0)}}@-webkit-keyframes flipintoright{from{-webkit-transform:rotateY(90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoright{from{-moz-transform:rotateY(90deg) scale(.9)}to{-moz-transform:rotateY(0)}}.flow{-webkit-transform-origin:50% 30%;-moz-transform-origin:50% 30%;-webkit-box-shadow:0 0 20px rgba(0,0,0,.4);-moz-box-shadow:0 0 20px rgba(0,0,0,.4)}.ui-dialog.flow{-webkit-transform-origin:none;-moz-transform-origin:none;-webkit-box-shadow:none;-moz-box-shadow:none}.flow.out{-webkit-transform:translateX(-100%) scale(.7);-webkit-animation-name:flowouttoleft;-webkit-animation-timing-function:ease;-webkit-animation-duration:350ms;-moz-transform:translateX(-100%) scale(.7);-moz-animation-name:flowouttoleft;-moz-animation-timing-function:ease;-moz-animation-duration:350ms}.flow.in{-webkit-transform:translateX(0) scale(1);-webkit-animation-name:flowinfromright;-webkit-animation-timing-function:ease;-webkit-animation-duration:350ms;-moz-transform:translateX(0) scale(1);-moz-animation-name:flowinfromright;-moz-animation-timing-function:ease;-moz-animation-duration:350ms}.flow.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:flowouttoright;-moz-transform:translateX(100%);-moz-animation-name:flowouttoright}.flow.in.reverse{-webkit-animation-name:flowinfromleft;-moz-animation-name:flowinfromleft}@-webkit-keyframes flowouttoleft{0%{-webkit-transform:translateX(0) scale(1)}60%,70%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(-100%) scale(.7)}}@-moz-keyframes flowouttoleft{0%{-moz-transform:translateX(0) scale(1)}60%,70%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(-100%) scale(.7)}}@-webkit-keyframes flowouttoright{0%{-webkit-transform:translateX(0) scale(1)}60%,70%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(100%) scale(.7)}}@-moz-keyframes flowouttoright{0%{-moz-transform:translateX(0) scale(1)}60%,70%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(100%) scale(.7)}}@-webkit-keyframes flowinfromleft{0%{-webkit-transform:translateX(-100%) scale(.7)}30%,40%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(0) scale(1)}}@-moz-keyframes flowinfromleft{0%{-moz-transform:translateX(-100%) scale(.7)}30%,40%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(0) scale(1)}}@-webkit-keyframes flowinfromright{0%{-webkit-transform:translateX(100%) scale(.7)}30%,40%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(0) scale(1)}}@-moz-keyframes flowinfromright{0%{-moz-transform:translateX(100%) scale(.7)}30%,40%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(0) scale(1)}}.ui-grid-a,.ui-grid-b,.ui-grid-c,.ui-grid-d{overflow:hidden}.ui-block-a,.ui-block-b,.ui-block-c,.ui-block-d,.ui-block-e{margin:0;padding:0;border:0;float:left;min-height:1px}.ui-grid-solo .ui-block-a{width:100%;float:none}.ui-grid-a .ui-block-a,.ui-grid-a .ui-block-b{width:50%}.ui-grid-a .ui-block-a{clear:left}.ui-grid-b .ui-block-a,.ui-grid-b .ui-block-b,.ui-grid-b .ui-block-c{width:33.333%}.ui-grid-b .ui-block-a{clear:left}.ui-grid-c .ui-block-a,.ui-grid-c .ui-block-b,.ui-grid-c .ui-block-c,.ui-grid-c .ui-block-d{width:25%}.ui-grid-c .ui-block-a{clear:left}.ui-grid-d .ui-block-a,.ui-grid-d .ui-block-b,.ui-grid-d .ui-block-c,.ui-grid-d .ui-block-d,.ui-grid-d .ui-block-e{width:20%}.ui-grid-d .ui-block-a{clear:left}.ui-header-fixed,.ui-footer-fixed{left:0;right:0;width:100%;position:fixed;z-index:1000}.ui-header-fixed{top:0}.ui-footer-fixed{bottom:0}.ui-header-fullscreen,.ui-footer-fullscreen{opacity:.9}.ui-page-header-fixed{padding-top:2.5em}.ui-page-footer-fixed{padding-bottom:3em}.ui-page-header-fullscreen .ui-content,.ui-page-footer-fullscreen .ui-content{padding:0}.ui-fixed-hidden{position:absolute}.ui-page-header-fullscreen .ui-fixed-hidden,.ui-page-footer-fullscreen .ui-fixed-hidden{left:-99999em}.ui-header-fixed .ui-btn,.ui-footer-fixed .ui-btn{z-index:10}.ui-navbar{overflow:hidden}.ui-navbar ul,.ui-navbar-expanded ul{list-style:none;padding:0;margin:0;position:relative;display:block;border:0}.ui-navbar-collapsed ul{float:left;width:75%;margin-right:-2px}.ui-navbar-collapsed .ui-navbar-toggle{float:left;width:25%}.ui-navbar li.ui-navbar-truncate{position:absolute;left:-9999px;top:-9999px}.ui-navbar li .ui-btn,.ui-navbar .ui-navbar-toggle .ui-btn{display:block;font-size:12px;text-align:center;margin:0;border-right-width:0;max-width:100%}.ui-navbar li .ui-btn{margin-right:-1px}.ui-navbar li .ui-btn:last-child{margin-right:0}.ui-header .ui-navbar li .ui-btn,.ui-header .ui-navbar .ui-navbar-toggle .ui-btn,.ui-footer .ui-navbar li .ui-btn,.ui-footer .ui-navbar .ui-navbar-toggle .ui-btn{border-top-width:0;border-bottom-width:0}.ui-navbar .ui-btn-inner{padding-left:2px;padding-right:2px}.ui-navbar-noicons li .ui-btn .ui-btn-inner,.ui-navbar-noicons .ui-navbar-toggle .ui-btn-inner{padding-top:.8em;padding-bottom:.9em}.ui-navbar-expanded .ui-btn{margin:0;font-size:14px}.ui-navbar-expanded .ui-btn-inner{padding-left:5px;padding-right:5px}.ui-navbar-expanded .ui-btn-icon-top .ui-btn-inner{padding:45px 5px 15px;text-align:center}.ui-navbar-expanded .ui-btn-icon-top .ui-icon{top:15px}.ui-navbar-expanded .ui-btn-icon-bottom .ui-btn-inner{padding:15px 5px 45px;text-align:center}.ui-navbar-expanded .ui-btn-icon-bottom .ui-icon{bottom:15px}.ui-navbar-expanded li .ui-btn .ui-btn-inner{min-height:2.5em}.ui-navbar-expanded .ui-navbar-noicons .ui-btn .ui-btn-inner{padding-top:1.8em;padding-bottom:1.9em}.ui-btn{display:block;text-align:center;cursor:pointer;position:relative;margin:.5em 5px;padding:0}.ui-mini{margin:.25em 5px}.ui-btn-inner{padding:.6em 20px;min-width:.75em;display:block;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;position:relative;zoom:1}.ui-btn input,.ui-btn button{z-index:2}.ui-btn-left,.ui-btn-right,.ui-btn-inline{display:inline-block}.ui-btn-block{display:block}.ui-header .ui-btn,.ui-footer .ui-btn{display:inline-block;margin:0}.ui-header .ui-btn-inner,.ui-footer .ui-btn-inner,.ui-mini .ui-btn-inner{font-size:12.5px;padding:.55em 11px .5em}.ui-header .ui-fullsize .ui-btn-inner,.ui-footer .ui-fullsize .ui-btn-inner{font-size:16px;padding:.6em 25px}.ui-btn-icon-notext{width:24px;height:24px}.ui-btn-icon-notext .ui-btn-inner{padding:0;height:100%}.ui-btn-icon-notext .ui-btn-inner .ui-icon{margin:2px 1px 2px 3px}.ui-btn-text{position:relative;z-index:1;width:100%}.ui-btn-icon-notext .ui-btn-text{position:absolute;left:-9999px}.ui-btn-icon-left .ui-btn-inner{padding-left:40px}.ui-btn-icon-right .ui-btn-inner{padding-right:40px}.ui-btn-icon-top .ui-btn-inner{padding-top:40px}.ui-btn-icon-bottom .ui-btn-inner{padding-bottom:40px}.ui-header .ui-btn-icon-left .ui-btn-inner,.ui-footer .ui-btn-icon-left .ui-btn-inner,.ui-mini .ui-btn-icon-left .ui-btn-inner{padding-left:30px}.ui-header .ui-btn-icon-right .ui-btn-inner,.ui-footer .ui-btn-icon-right .ui-btn-inner,.ui-mini .ui-btn-icon-right .ui-btn-inner{padding-right:30px}.ui-header .ui-btn-icon-top .ui-btn-inner,.ui-footer .ui-btn-icon-top .ui-btn-inner,.ui-mini .ui-btn-icon-top .ui-btn-inner{padding:30px 3px .5em 3px}.ui-header .ui-btn-icon-bottom .ui-btn-inner,.ui-footer .ui-btn-icon-bottom .ui-btn-inner,.ui-mini .ui-btn-icon-bottom .ui-btn-inner{padding:.55em 3px 30px 3px}.ui-btn-icon-notext .ui-icon{display:block;z-index:0}.ui-btn-icon-left .ui-btn-inner .ui-icon,.ui-btn-icon-right .ui-btn-inner .ui-icon{position:absolute;top:50%;margin-top:-9px}.ui-btn-icon-top .ui-btn-inner .ui-icon,.ui-btn-icon-bottom .ui-btn-inner .ui-icon{position:absolute;left:50%;margin-left:-9px}.ui-btn-icon-left .ui-icon{left:10px}.ui-btn-icon-right .ui-icon{right:10px}.ui-btn-icon-top .ui-icon{top:10px}.ui-btn-icon-bottom .ui-icon{top:auto;bottom:10px}.ui-header .ui-btn-icon-left .ui-icon,.ui-footer .ui-btn-icon-left .ui-icon,.ui-mini.ui-btn-icon-left .ui-icon,.ui-mini .ui-btn-icon-left .ui-icon{left:5px}.ui-header .ui-btn-icon-right .ui-icon,.ui-footer .ui-btn-icon-right .ui-icon,.ui-mini.ui-btn-icon-right .ui-icon,.ui-mini .ui-btn-icon-right .ui-icon{right:5px}.ui-header .ui-btn-icon-top .ui-icon,.ui-footer .ui-btn-icon-top .ui-icon,.ui-mini.ui-btn-icon-top .ui-icon,.ui-mini .ui-btn-icon-top .ui-icon{top:5px}.ui-header .ui-btn-icon-bottom .ui-icon,.ui-footer .ui-btn-icon-bottom .ui-icon,.ui-mini.ui-btn-icon-bottom .ui-icon,.ui-mini .ui-btn-icon-bottom .ui-icon{bottom:5px}.ui-btn-hidden{position:absolute;top:0;left:0;width:100%;height:100%;-webkit-appearance:button;opacity:.1;cursor:pointer;background:#fff;background:rgba(255,255,255,0);filter:Alpha(Opacity=.0001);font-size:1px;border:0;text-indent:-9999px}.ui-collapsible{margin:.5em 0}.ui-collapsible-heading{font-size:16px;display:block;margin:0 -8px;padding:0;border-width:0 0 1px 0;position:relative}.ui-collapsible-heading a{text-align:left;margin:0}.ui-collapsible-heading .ui-btn-inner,.ui-collapsible-heading .ui-btn-icon-left .ui-btn-inner{padding-left:40px}.ui-collapsible-heading .ui-btn-icon-right .ui-btn-inner{padding-left:12px;padding-right:40px}.ui-collapsible-heading .ui-btn-icon-top .ui-btn-inner,.ui-collapsible-heading .ui-btn-icon-bottom .ui-btn-inner{padding-right:40px;text-align:center}.ui-collapsible-heading a span.ui-btn{position:absolute;left:6px;top:50%;margin:-12px 0 0 0;width:20px;height:20px;padding:1px 0 1px 2px;text-indent:-9999px}.ui-collapsible-heading a span.ui-btn .ui-btn-inner{padding:10px 0}.ui-collapsible-heading a span.ui-btn .ui-icon{left:0;margin-top:-10px}.ui-collapsible-heading-status{position:absolute;top:-9999px;left:0}.ui-collapsible-content{display:block;margin:0 -8px;padding:10px 16px;border-top:0;background-image:none;font-weight:normal}.ui-collapsible-content-collapsed{display:none}.ui-collapsible-set{margin:.5em 0}.ui-collapsible-set .ui-collapsible{margin:-1px 0 0}.ui-controlgroup,fieldset.ui-controlgroup{padding:0;margin:0 0 .5em;zoom:1}.ui-bar .ui-controlgroup{margin:0 .3em}.ui-controlgroup-label{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .4em}.ui-controlgroup-controls{display:block;width:100%}.ui-controlgroup li{list-style:none}.ui-controlgroup-vertical .ui-btn,.ui-controlgroup-vertical .ui-checkbox,.ui-controlgroup-vertical .ui-radio{margin:0;border-bottom-width:0}.ui-controlgroup-controls label.ui-select{position:absolute;left:-9999px}.ui-controlgroup-vertical .ui-controlgroup-last{border-bottom-width:1px}.ui-controlgroup-horizontal{padding:0}.ui-controlgroup-horizontal .ui-btn-inner{text-align:center}.ui-controlgroup-horizontal .ui-btn,.ui-controlgroup-horizontal .ui-select{display:inline-block;margin:0 -6px 0 0}.ui-controlgroup-horizontal .ui-checkbox,.ui-controlgroup-horizontal .ui-radio{float:left;clear:none;margin:0 -1px 0 0}.ui-controlgroup-horizontal .ui-checkbox .ui-btn,.ui-controlgroup-horizontal .ui-radio .ui-btn,.ui-controlgroup-horizontal .ui-checkbox:last-child,.ui-controlgroup-horizontal .ui-radio:last-child{margin-right:0}.ui-controlgroup-horizontal .ui-controlgroup-last{margin-right:0}.ui-controlgroup .ui-checkbox label,.ui-controlgroup .ui-radio label{font-size:16px}@media all and (min-width:450px){.ui-field-contain .ui-controlgroup-label{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-controlgroup-controls{width:60%;display:inline-block}.ui-field-contain .ui-controlgroup .ui-select{width:100%}.ui-field-contain .ui-controlgroup-horizontal .ui-select{width:auto}}.ui-dialog{background:none!important}.ui-dialog-contain{width:92.5%;max-width:500px;margin:10% auto 15px auto;padding:0}.ui-dialog .ui-header{margin-top:15%;border:0;overflow:hidden}.ui-dialog .ui-header,.ui-dialog .ui-content,.ui-dialog .ui-footer{display:block;position:relative;width:auto}.ui-dialog .ui-header,.ui-dialog .ui-footer{z-index:10;padding:0}.ui-dialog .ui-footer{padding:0 15px}.ui-dialog .ui-content{padding:15px}.ui-dialog{margin-top:-15px}.ui-checkbox,.ui-radio{position:relative;clear:both;margin:.2em 0 .5em;z-index:1}.ui-checkbox .ui-btn,.ui-radio .ui-btn{margin:0;text-align:left;z-index:2}.ui-checkbox .ui-btn-inner,.ui-radio .ui-btn-inner{white-space:normal}.ui-checkbox .ui-btn-icon-left .ui-btn-inner,.ui-radio .ui-btn-icon-left .ui-btn-inner{padding-left:45px}.ui-checkbox .ui-mini.ui-btn-icon-left .ui-btn-inner,.ui-radio .ui-mini.ui-btn-icon-left .ui-btn-inner{padding-left:36px}.ui-checkbox .ui-btn-icon-right .ui-btn-inner,.ui-radio .ui-btn-icon-right .ui-btn-inner{padding-right:45px}.ui-checkbox .ui-mini.ui-btn-icon-right .ui-btn-inner,.ui-radio .ui-mini.ui-btn-icon-right .ui-btn-inner{padding-right:36px}.ui-checkbox .ui-btn-icon-top .ui-btn-inner,.ui-radio .ui-btn-icon-top .ui-btn-inner{padding-right:0;padding-left:0;text-align:center}.ui-checkbox .ui-btn-icon-bottom .ui-btn-inner,.ui-radio .ui-btn-icon-bottom .ui-btn-inner{padding-right:0;padding-left:0;text-align:center}.ui-checkbox .ui-icon,.ui-radio .ui-icon{top:1.1em}.ui-checkbox .ui-btn-icon-left .ui-icon,.ui-radio .ui-btn-icon-left .ui-icon{left:15px}.ui-checkbox .ui-mini.ui-btn-icon-left .ui-icon,.ui-radio .ui-mini.ui-btn-icon-left .ui-icon{left:9px}.ui-checkbox .ui-btn-icon-right .ui-icon,.ui-radio .ui-btn-icon-right .ui-icon{right:15px}.ui-checkbox .ui-mini.ui-btn-icon-right .ui-icon,.ui-radio .ui-mini.ui-btn-icon-right .ui-icon{right:9px}.ui-checkbox .ui-btn-icon-top .ui-icon,.ui-radio .ui-btn-icon-top .ui-icon{top:10px}.ui-checkbox .ui-btn-icon-bottom .ui-icon,.ui-radio .ui-btn-icon-bottom .ui-icon{top:auto;bottom:10px}.ui-checkbox .ui-btn-icon-right .ui-icon,.ui-radio .ui-btn-icon-right .ui-icon{right:15px}.ui-checkbox .ui-mini.ui-btn-icon-right .ui-icon,.ui-radio .ui-mini.ui-btn-icon-right .ui-icon{right:9px}.ui-checkbox input,.ui-radio input{position:absolute;left:20px;top:50%;width:10px;height:10px;margin:-5px 0 0 0;outline:0!important;z-index:1}.ui-field-contain,fieldset.ui-field-contain{padding:.8em 0;margin:0;border-width:0 0 1px 0;overflow:visible}.ui-field-contain:first-child{border-top-width:0}.ui-header .ui-field-contain-left,.ui-header .ui-field-contain-right{position:absolute;top:0;width:25%}.ui-header .ui-field-contain-left{left:1em}.ui-header .ui-field-contain-right{right:1em}@media all and (min-width:450px){.ui-field-contain,.ui-mobile fieldset.ui-field-contain{border-width:0;padding:0;margin:1em 0}}.ui-select{display:block;position:relative}.ui-select select{position:absolute;left:-9999px;top:-9999px}.ui-select .ui-btn{overflow:hidden;opacity:1;margin:0}.ui-select .ui-btn select{cursor:pointer;-webkit-appearance:button;left:0;top:0;width:100%;min-height:1.5em;min-height:100%;height:3em;max-height:100%;opacity:0;-ms-filter:"alpha(opacity=0)";filter:alpha(opacity=0);z-index:2}.ui-select .ui-disabled{opacity:.3}@-moz-document url-prefix(){.ui-select .ui-btn select{opacity:.0001}}.ui-select .ui-btn select.ui-select-nativeonly{opacity:1;text-indent:0}.ui-select .ui-btn-icon-right .ui-btn-inner{padding-right:45px}.ui-select .ui-btn-icon-right .ui-icon{right:15px}.ui-select .ui-mini.ui-btn-icon-right .ui-icon{right:7px}label.ui-select{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block}.ui-select .ui-btn-text,.ui-selectmenu .ui-btn-text{display:block;min-height:1em;overflow:hidden!important}.ui-select .ui-btn-text{text-overflow:ellipsis}.ui-selectmenu{position:absolute;padding:0;z-index:1100!important;width:80%;max-width:350px;padding:6px}.ui-selectmenu .ui-listview{margin:0}.ui-selectmenu .ui-btn.ui-li-divider{cursor:default}.ui-selectmenu-hidden{top:-9999px;left:-9999px}.ui-selectmenu-screen{position:absolute;top:0;left:0;width:100%;height:100%;z-index:99}.ui-screen-hidden,.ui-selectmenu-list .ui-li .ui-icon{display:none}.ui-selectmenu-list .ui-li .ui-icon{display:block}.ui-li.ui-selectmenu-placeholder{display:none}.ui-selectmenu .ui-header .ui-title{margin:.6em 46px .8em}@media all and (min-width:450px){.ui-field-contain label.ui-select{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-select{width:60%;display:inline-block}}.ui-selectmenu .ui-header h1:after{content:'.';visibility:hidden}label.ui-input-text{font-size:16px;line-height:1.4;display:block;font-weight:normal;margin:0 0 .3em}input.ui-input-text,textarea.ui-input-text{background-image:none;padding:.4em;line-height:1.4;font-size:16px;display:block;width:97%;outline:0}.ui-header input.ui-input-text,.ui-footer input.ui-input-text{margin-left:1.25%;padding:.4em 1%;width:95.5%}input.ui-input-text{-webkit-appearance:none}textarea.ui-input-text{height:50px;-webkit-transition:height 200ms linear;-moz-transition:height 200ms linear;-o-transition:height 200ms linear;transition:height 200ms linear}.ui-input-search{padding:0 30px;background-image:none;position:relative}.ui-icon-searchfield:after{position:absolute;left:7px;top:50%;margin-top:-9px;content:"";width:18px;height:18px;opacity:.5}.ui-input-search input.ui-input-text{border:0;width:98%;padding:.4em 0;margin:0;display:block;background:transparent none;outline:0!important}.ui-input-search .ui-input-clear{position:absolute;right:0;top:50%;margin-top:-13px}.ui-mini .ui-input-clear{right:-3px}.ui-input-search .ui-input-clear-hidden{display:none}input.ui-mini,.ui-mini input,textarea.ui-mini{font-size:14px}textarea.ui-mini{height:45px}@media all and (min-width:450px){.ui-field-contain label.ui-input-text{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain input.ui-input-text,.ui-field-contain textarea.ui-input-text,.ui-field-contain .ui-input-search{width:60%;display:inline-block}.ui-field-contain .ui-input-search{width:50%}.ui-hide-label input.ui-input-text,.ui-hide-label textarea.ui-input-text,.ui-hide-label .ui-input-search{padding:.4em;width:97%}.ui-input-search input.ui-input-text{width:98%}}.ui-listview{margin:0;counter-reset:listnumbering}.ui-content .ui-listview{margin:-15px}.ui-content .ui-listview-inset{margin:1em 0}.ui-listview,.ui-li{list-style:none;padding:0}.ui-li,.ui-li.ui-field-contain{display:block;margin:0;position:relative;overflow:visible;text-align:left;border-width:0;border-top-width:1px}.ui-li .ui-btn-text a.ui-link-inherit{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ui-li-divider,.ui-li-static{padding:.5em 15px;font-size:14px;font-weight:bold}.ui-li-divider{counter-reset:listnumbering}ol.ui-listview .ui-link-inherit:before,ol.ui-listview .ui-li-static:before,.ui-li-dec{font-size:.8em;display:inline-block;padding-right:.3em;font-weight:normal;counter-increment:listnumbering;content:counter(listnumbering) ". "}ol.ui-listview .ui-li-jsnumbering:before{content:""!important}.ui-listview-inset .ui-li{border-right-width:1px;border-left-width:1px}.ui-li:last-child,.ui-li.ui-field-contain:last-child{border-bottom-width:1px}.ui-li>.ui-btn-inner{display:block;position:relative;padding:0}.ui-li .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li{padding:.7em 15px .7em 15px;display:block}.ui-li-has-thumb .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-thumb{min-height:60px;padding-left:100px}.ui-li-has-icon .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-icon{min-height:20px;padding-left:40px}.ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-count{padding-right:45px}.ui-li-has-arrow .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-arrow{padding-right:30px}.ui-li-has-arrow.ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-arrow.ui-li-has-count{padding-right:75px}.ui-li-has-count .ui-btn-text{padding-right:15px}.ui-li-heading{font-size:16px;font-weight:bold;display:block;margin:.6em 0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ui-li-desc{font-size:12px;font-weight:normal;display:block;margin:-.5em 0 .6em;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ui-li-thumb,.ui-listview .ui-li-icon{position:absolute;left:1px;top:0;max-height:80px;max-width:80px}.ui-listview .ui-li-icon{max-height:40px;max-width:40px;left:10px;top:.9em}.ui-li-thumb,.ui-listview .ui-li-icon,.ui-li-content{float:left;margin-right:10px}.ui-li-aside{float:right;width:50%;text-align:right;margin:.3em 0}@media all and (min-width:480px){.ui-li-aside{width:45%}}.ui-li-divider{cursor:default}.ui-li-has-alt .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-alt{padding-right:95px}.ui-li-has-count .ui-li-count{position:absolute;font-size:11px;font-weight:bold;padding:.2em .5em;top:50%;margin-top:-.9em;right:48px}.ui-li-divider .ui-li-count,.ui-li-static .ui-li-count{right:10px}.ui-li-has-alt .ui-li-count{right:55px}.ui-li-link-alt{position:absolute;width:40px;height:100%;border-width:0;border-left-width:1px;top:0;right:0;margin:0;padding:0;z-index:2}.ui-li-link-alt .ui-btn{overflow:hidden;position:absolute;right:8px;top:50%;margin:-11px 0 0 0;border-bottom-width:1px;z-index:-1}.ui-li-link-alt .ui-btn-inner{padding:0;height:100%;position:absolute;width:100%;top:0;left:0}.ui-li-link-alt .ui-btn .ui-icon{right:50%;margin-right:-9px}.ui-listview * .ui-btn-inner>.ui-btn>.ui-btn-inner{border-top:0}.ui-listview-filter{border-width:0;overflow:hidden;margin:-15px -15px 15px -15px}.ui-listview-filter .ui-input-search{margin:5px;width:auto;display:block}.ui-listview-filter-inset{margin:-15px -5px -15px -5px;background:transparent}.ui-li.ui-screen-hidden{display:none}@media only screen and (min-device-width:768px) and (max-device-width:1024px){.ui-li .ui-btn-text{overflow:visible}}label.ui-slider{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block}input.ui-slider-input,.ui-field-contain input.ui-slider-input{display:inline-block;width:50px}select.ui-slider-switch{display:none}div.ui-slider{position:relative;display:inline-block;overflow:visible;height:15px;padding:0;margin:0 2% 0 20px;top:4px;width:65%}div.ui-slider-mini{height:12px;margin-left:10px}div.ui-slider-bg{border:0;height:100%;padding-right:8px}.ui-controlgroup a.ui-slider-handle,a.ui-slider-handle{position:absolute;z-index:1;top:50%;width:28px;height:28px;margin-top:-15px;margin-left:-15px;outline:0}a.ui-slider-handle .ui-btn-inner{padding:0;height:100%}div.ui-slider-mini a.ui-slider-handle{height:14px;width:14px;margin:-8px 0 0 -7px}div.ui-slider-mini a.ui-slider-handle .ui-btn-inner{height:30px;width:30px;padding:0;margin:-9px 0 0 -9px}@media all and (min-width:450px){.ui-field-contain label.ui-slider{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain div.ui-slider{width:43%}.ui-field-contain div.ui-slider-switch{width:5.5em}}div.ui-slider-switch{height:32px;margin-left:0;width:5.8em}a.ui-slider-handle-snapping{-webkit-transition:left 70ms linear;-moz-transition:left 70ms linear}div.ui-slider-switch .ui-slider-handle{margin-top:1px}.ui-slider-inneroffset{margin:0 16px;position:relative;z-index:1}div.ui-slider-switch.ui-slider-mini{width:5em;height:29px}div.ui-slider-switch.ui-slider-mini .ui-slider-inneroffset{margin:0 15px 0 14px}div.ui-slider-switch.ui-slider-mini .ui-slider-handle{width:25px;height:25px;margin:1px 0 0 -13px}div.ui-slider-switch.ui-slider-mini a.ui-slider-handle .ui-btn-inner{height:30px;width:30px;padding:0;margin:0}span.ui-slider-label{position:absolute;text-align:center;width:100%;overflow:hidden;font-size:16px;top:0;line-height:2;min-height:100%;border-width:0;white-space:nowrap}.ui-slider-mini span.ui-slider-label{font-size:14px}span.ui-slider-label-a{z-index:1;left:0;text-indent:-1.5em}span.ui-slider-label-b{z-index:0;right:0;text-indent:1.5em}.ui-slider-inline{width:120px;display:inline-block}
\ No newline at end of file
--- a/OrthancExplorer/libs/jquery.mobile.min.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,177 +0,0 @@
-/*! jQuery Mobile v1.1.0 db342b1f315c282692791aa870455901fdb46a55 jquerymobile.com | jquery.org/license */
-(function(D,s,k){typeof define==="function"&&define.amd?define(["jquery"],function(a){k(a,D,s);return a.mobile}):k(D.jQuery,D,s)})(this,document,function(D,s,k){(function(a,c,b,e){function f(a){for(;a&&typeof a.originalEvent!=="undefined";)a=a.originalEvent;return a}function d(b){for(var d={},f,g;b;){f=a.data(b,t);for(g in f)if(f[g])d[g]=d.hasVirtualBinding=true;b=b.parentNode}return d}function g(){y&&(clearTimeout(y),y=0);y=setTimeout(function(){F=y=0;C.length=0;z=false;G=true},a.vmouse.resetTimerDuration)}
-function h(b,d,g){var c,h;if(!(h=g&&g[b])){if(g=!g)a:{for(g=d.target;g;){if((h=a.data(g,t))&&(!b||h[b]))break a;g=g.parentNode}g=null}h=g}if(h){c=d;var g=c.type,z,j;c=a.Event(c);c.type=b;h=c.originalEvent;z=a.event.props;g.search(/^(mouse|click)/)>-1&&(z=w);if(h)for(j=z.length;j;)b=z[--j],c[b]=h[b];if(g.search(/mouse(down|up)|click/)>-1&&!c.which)c.which=1;if(g.search(/^touch/)!==-1&&(b=f(h),g=b.touches,b=b.changedTouches,g=g&&g.length?g[0]:b&&b.length?b[0]:e))for(h=0,len=u.length;h<len;h++)b=u[h],
-c[b]=g[b];a(d.target).trigger(c)}return c}function j(b){var d=a.data(b.target,x);if(!z&&(!F||F!==d))if(d=h("v"+b.type,b))d.isDefaultPrevented()&&b.preventDefault(),d.isPropagationStopped()&&b.stopPropagation(),d.isImmediatePropagationStopped()&&b.stopImmediatePropagation()}function o(b){var g=f(b).touches,c;if(g&&g.length===1&&(c=b.target,g=d(c),g.hasVirtualBinding))F=L++,a.data(c,x,F),y&&(clearTimeout(y),y=0),A=G=false,c=f(b).touches[0],s=c.pageX,E=c.pageY,h("vmouseover",b,g),h("vmousedown",b,g)}
-function m(a){G||(A||h("vmousecancel",a,d(a.target)),A=true,g())}function p(b){if(!G){var c=f(b).touches[0],e=A,z=a.vmouse.moveDistanceThreshold;A=A||Math.abs(c.pageX-s)>z||Math.abs(c.pageY-E)>z;flags=d(b.target);A&&!e&&h("vmousecancel",b,flags);h("vmousemove",b,flags);g()}}function l(a){if(!G){G=true;var b=d(a.target),c;h("vmouseup",a,b);if(!A&&(c=h("vclick",a,b))&&c.isDefaultPrevented())c=f(a).changedTouches[0],C.push({touchID:F,x:c.clientX,y:c.clientY}),z=true;h("vmouseout",a,b);A=false;g()}}function r(b){var b=
-a.data(b,t),d;if(b)for(d in b)if(b[d])return true;return false}function n(){}function k(b){var d=b.substr(1);return{setup:function(){r(this)||a.data(this,t,{});a.data(this,t)[b]=true;v[b]=(v[b]||0)+1;v[b]===1&&H.bind(d,j);a(this).bind(d,n);if(K)v.touchstart=(v.touchstart||0)+1,v.touchstart===1&&H.bind("touchstart",o).bind("touchend",l).bind("touchmove",p).bind("scroll",m)},teardown:function(){--v[b];v[b]||H.unbind(d,j);K&&(--v.touchstart,v.touchstart||H.unbind("touchstart",o).unbind("touchmove",p).unbind("touchend",
-l).unbind("scroll",m));var f=a(this),g=a.data(this,t);g&&(g[b]=false);f.unbind(d,n);r(this)||f.removeData(t)}}}var t="virtualMouseBindings",x="virtualTouchID",c="vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(" "),u="clientX clientY pageX pageY screenX screenY".split(" "),w=a.event.props.concat(a.event.mouseHooks?a.event.mouseHooks.props:[]),v={},y=0,s=0,E=0,A=false,C=[],z=false,G=false,K="addEventListener"in b,H=a(b),L=1,F=0;a.vmouse={moveDistanceThreshold:10,clickDistanceThreshold:10,
-resetTimerDuration:1500};for(var I=0;I<c.length;I++)a.event.special[c[I]]=k(c[I]);K&&b.addEventListener("click",function(b){var d=C.length,f=b.target,g,c,e,h,z;if(d){g=b.clientX;c=b.clientY;threshold=a.vmouse.clickDistanceThreshold;for(e=f;e;){for(h=0;h<d;h++)if(z=C[h],e===f&&Math.abs(z.x-g)<threshold&&Math.abs(z.y-c)<threshold||a.data(e,x)===z.touchID){b.preventDefault();b.stopPropagation();return}e=e.parentNode}}},true)})(jQuery,s,k);(function(a,c,b){function e(a){a=a||location.href;return"#"+a.replace(/^[^#]*#?(.*)$/,
-"$1")}var f="hashchange",d=k,g,h=a.event.special,j=d.documentMode,o="on"+f in c&&(j===b||j>7);a.fn[f]=function(a){return a?this.bind(f,a):this.trigger(f)};a.fn[f].delay=50;h[f]=a.extend(h[f],{setup:function(){if(o)return false;a(g.start)},teardown:function(){if(o)return false;a(g.stop)}});g=function(){function g(){var b=e(),d=t(r);if(b!==r)k(r=b,d),a(c).trigger(f);else if(d!==r)location.href=location.href.replace(/#.*/,"")+d;j=setTimeout(g,a.fn[f].delay)}var h={},j,r=e(),n=function(a){return a},k=
-n,t=n;h.start=function(){j||g()};h.stop=function(){j&&clearTimeout(j);j=b};a.browser.msie&&!o&&function(){var b,c;h.start=function(){if(!b)c=(c=a.fn[f].src)&&c+e(),b=a('<iframe tabindex="-1" title="empty"/>').hide().one("load",function(){c||k(e());g()}).attr("src",c||"javascript:0").insertAfter("body")[0].contentWindow,d.onpropertychange=function(){try{if(event.propertyName==="title")b.document.title=d.title}catch(a){}}};h.stop=n;t=function(){return e(b.location.href)};k=function(g,c){var e=b.document,
-h=a.fn[f].domain;if(g!==c)e.title=d.title,e.open(),h&&e.write('<script>document.domain="'+h+'"<\/script>'),e.close(),b.location.hash=g}}();return h}()})(jQuery,this);(function(a,c){if(a.cleanData){var b=a.cleanData;a.cleanData=function(f){for(var d=0,g;(g=f[d])!=null;d++)a(g).triggerHandler("remove");b(f)}}else{var e=a.fn.remove;a.fn.remove=function(b,d){return this.each(function(){d||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){a(this).triggerHandler("remove")});return e.call(a(this),
-b,d)})}}a.widget=function(b,d,g){var c=b.split(".")[0],e,b=b.split(".")[1];e=c+"-"+b;if(!g)g=d,d=a.Widget;a.expr[":"][e]=function(d){return!!a.data(d,b)};a[c]=a[c]||{};a[c][b]=function(a,b){arguments.length&&this._createWidget(a,b)};d=new d;d.options=a.extend(true,{},d.options);a[c][b].prototype=a.extend(true,d,{namespace:c,widgetName:b,widgetEventPrefix:a[c][b].prototype.widgetEventPrefix||b,widgetBaseClass:e},g);a.widget.bridge(b,a[c][b])};a.widget.bridge=function(b,d){a.fn[b]=function(g){var e=
-typeof g==="string",j=Array.prototype.slice.call(arguments,1),o=this,g=!e&&j.length?a.extend.apply(null,[true,g].concat(j)):g;if(e&&g.charAt(0)==="_")return o;e?this.each(function(){var d=a.data(this,b);if(!d)throw"cannot call methods on "+b+" prior to initialization; attempted to call method '"+g+"'";if(!a.isFunction(d[g]))throw"no such method '"+g+"' for "+b+" widget instance";var e=d[g].apply(d,j);if(e!==d&&e!==c)return o=e,false}):this.each(function(){var c=a.data(this,b);c?c.option(g||{})._init():
-a.data(this,b,new d(g,this))});return o}};a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)};a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:false},_createWidget:function(b,d){a.data(d,this.widgetName,this);this.element=a(d);this.options=a.extend(true,{},this.options,this._getCreateOptions(),b);var g=this;this.element.bind("remove."+this.widgetName,function(){g.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){var b=
-{};a.metadata&&(b=a.metadata.get(element)[this.widgetName]);return b},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(b,d){var g=b;if(arguments.length===0)return a.extend({},this.options);if(typeof b==="string"){if(d===c)return this.options[b];
-g={};g[b]=d}this._setOptions(g);return this},_setOptions:function(b){var d=this;a.each(b,function(a,b){d._setOption(a,b)});return this},_setOption:function(a,b){this.options[a]=b;a==="disabled"&&this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",b);return this},enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(b,d,g){var c=this.options[b],d=a.Event(d);
-d.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase();g=g||{};if(d.originalEvent)for(var b=a.event.props.length,e;b;)e=a.event.props[--b],d[e]=d.originalEvent[e];this.element.trigger(d,g);return!(a.isFunction(c)&&c.call(this.element[0],d,g)===false||d.isDefaultPrevented())}}})(jQuery);(function(a,c){a.widget("mobile.widget",{_createWidget:function(){a.Widget.prototype._createWidget.apply(this,arguments);this._trigger("init")},_getCreateOptions:function(){var b=this.element,
-e={};a.each(this.options,function(a){var d=b.jqmData(a.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()}));d!==c&&(e[a]=d)});return e},enhanceWithin:function(b,c){this.enhance(a(this.options.initSelector,a(b)),c)},enhance:function(b,c){var f,d=a(b),d=a.mobile.enhanceable(d);c&&d.length&&(f=(f=a.mobile.closestPageData(d))&&f.keepNativeSelector()||"",d=d.not(f));d[this.widgetName]()},raise:function(a){throw"Widget ["+this.widgetName+"]: "+a;}})})(jQuery);(function(a,c){var b={};a.mobile=a.extend({},
-{version:"1.1.0",ns:"",subPageUrlKey:"ui-page",activePageClass:"ui-page-active",activeBtnClass:"ui-btn-active",focusClass:"ui-focus",ajaxEnabled:true,hashListeningEnabled:true,linkBindingEnabled:true,defaultPageTransition:"fade",maxTransitionWidth:false,minScrollBack:250,touchOverflowEnabled:false,defaultDialogTransition:"pop",loadingMessage:"loading",pageLoadErrorMessage:"Error Loading Page",loadingMessageTextVisible:false,loadingMessageTheme:"a",pageLoadErrorMessageTheme:"e",autoInitializePage:true,
-pushStateEnabled:true,ignoreContentEnabled:false,orientationChangeEnabled:true,buttonMarkup:{hoverDelay:200},keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91},silentScroll:function(b){if(a.type(b)!==
-"number")b=a.mobile.defaultHomeScroll;a.event.special.scrollstart.enabled=false;setTimeout(function(){c.scrollTo(0,b);a(k).trigger("silentscroll",{x:0,y:b})},20);setTimeout(function(){a.event.special.scrollstart.enabled=true},150)},nsNormalizeDict:b,nsNormalize:function(d){return!d?void 0:b[d]||(b[d]=a.camelCase(a.mobile.ns+d))},getInheritedTheme:function(a,b){for(var c=a[0],e="",f=/ui-(bar|body|overlay)-([a-z])\b/,m,p;c;){m=c.className||"";if((p=f.exec(m))&&(e=p[2]))break;c=c.parentNode}return e||
-b||"a"},closestPageData:function(a){return a.closest(':jqmData(role="page"), :jqmData(role="dialog")').data("page")},enhanceable:function(a){return this.haveParents(a,"enhance")},hijackable:function(a){return this.haveParents(a,"ajax")},haveParents:function(b,c){if(!a.mobile.ignoreContentEnabled)return b;for(var e=b.length,f=a(),o,m,p,l=0;l<e;l++){m=b.eq(l);p=false;for(o=b[l];o;){if((o.getAttribute?o.getAttribute("data-"+a.mobile.ns+c):"")==="false"){p=true;break}o=o.parentNode}p||(f=f.add(m))}return f}},
-a.mobile);a.fn.jqmData=function(b,c){var f;typeof b!="undefined"&&(b&&(b=a.mobile.nsNormalize(b)),f=this.data.apply(this,arguments.length<2?[b]:[b,c]));return f};a.jqmData=function(b,c,f){var e;typeof c!="undefined"&&(e=a.data(b,c?a.mobile.nsNormalize(c):c,f));return e};a.fn.jqmRemoveData=function(b){return this.removeData(a.mobile.nsNormalize(b))};a.jqmRemoveData=function(b,c){return a.removeData(b,a.mobile.nsNormalize(c))};a.fn.removeWithDependents=function(){a.removeWithDependents(this)};a.removeWithDependents=
-function(b){b=a(b);(b.jqmData("dependents")||a()).remove();b.remove()};a.fn.addDependents=function(b){a.addDependents(a(this),b)};a.addDependents=function(b,c){var f=a(b).jqmData("dependents")||a();a(b).jqmData("dependents",a.merge(f,c))};a.fn.getEncodedText=function(){return a("<div/>").text(a(this).text()).html()};a.fn.jqmEnhanceable=function(){return a.mobile.enhanceable(this)};a.fn.jqmHijackable=function(){return a.mobile.hijackable(this)};var e=a.find,f=/:jqmData\(([^)]*)\)/g;a.find=function(b,
-c,h,j){b=b.replace(f,"[data-"+(a.mobile.ns||"")+"$1]");return e.call(this,b,c,h,j)};a.extend(a.find,e);a.find.matches=function(b,c){return a.find(b,null,null,c)};a.find.matchesSelector=function(b,c){return a.find(c,null,null,[b]).length>0}})(jQuery,this);(function(a){a(s);var c=a("html");a.mobile.media=function(){var b={},e=a("<div id='jquery-mediatest'>"),f=a("<body>").append(e);return function(a){if(!(a in b)){var g=k.createElement("style"),h="@media "+a+" { #jquery-mediatest { position:absolute; } }";
-g.type="text/css";g.styleSheet?g.styleSheet.cssText=h:g.appendChild(k.createTextNode(h));c.prepend(f).prepend(g);b[a]=e.css("position")==="absolute";f.add(g).remove()}return b[a]}}()})(jQuery);(function(a,c){function b(a){var b=a.charAt(0).toUpperCase()+a.substr(1),a=(a+" "+g.join(b+" ")+b).split(" "),f;for(f in a)if(d[a[f]]!==c)return true}function e(a,b,c){var d=k.createElement("div"),c=c?[c]:g,f;for(i=0;i<c.length;i++){var e=c[i],h="-"+e.charAt(0).toLowerCase()+e.substr(1)+"-"+a+": "+b+";",e=e.charAt(0).toUpperCase()+
-e.substr(1)+(a.charAt(0).toUpperCase()+a.substr(1));d.setAttribute("style",h);d.style[e]&&(f=true)}return!!f}var f=a("<body>").prependTo("html"),d=f[0].style,g=["Webkit","Moz","O"],h="palmGetResource"in s,j=s.operamini&&{}.toString.call(s.operamini)==="[object OperaMini]",o=s.blackberry;a.extend(a.mobile,{browser:{}});a.mobile.browser.ie=function(){for(var a=3,b=k.createElement("div"),c=b.all||[];b.innerHTML="<\!--[if gt IE "+ ++a+"]><br><![endif]--\>",c[0];);return a>4?a:!a}();a.extend(a.support,
-{orientation:"orientation"in s&&"onorientationchange"in s,touch:"ontouchend"in k,cssTransitions:"WebKitTransitionEvent"in s||e("transition","height 100ms linear"),pushState:"pushState"in history&&"replaceState"in history,mediaquery:a.mobile.media("only all"),cssPseudoElement:!!b("content"),touchOverflow:!!b("overflowScrolling"),cssTransform3d:e("perspective","10px","moz")||a.mobile.media("(-"+g.join("-transform-3d),(-")+"-transform-3d),(transform-3d)"),boxShadow:!!b("boxShadow")&&!o,scrollTop:("pageXOffset"in
-s||"scrollTop"in k.documentElement||"scrollTop"in f[0])&&!h&&!j,dynamicBaseTag:function(){var b=location.protocol+"//"+location.host+location.pathname+"ui-dir/",c=a("head base"),d=null,e="",g;c.length?e=c.attr("href"):c=d=a("<base>",{href:b}).appendTo("head");g=a("<a href='testurl' />").prependTo(f)[0].href;c[0].href=e||location.pathname;d&&d.remove();return g.indexOf(b)===0}()});f.remove();h=function(){var a=s.navigator.userAgent;return a.indexOf("Nokia")>-1&&(a.indexOf("Symbian/3")>-1||a.indexOf("Series60/5")>
--1)&&a.indexOf("AppleWebKit")>-1&&a.match(/(BrowserNG|NokiaBrowser)\/7\.[0-3]/)}();a.mobile.gradeA=function(){return a.support.mediaquery||a.mobile.browser.ie&&a.mobile.browser.ie>=7};a.mobile.ajaxBlacklist=s.blackberry&&!s.WebKitPoint||j||h;h&&a(function(){a("head link[rel='stylesheet']").attr("rel","alternate stylesheet").attr("rel","stylesheet")});a.support.boxShadow||a("html").addClass("ui-mobile-nosupport-boxshadow")})(jQuery);(function(a,c,b){function e(b,c,d){var f=d.type;d.type=c;a.event.handle.call(b,
-d);d.type=f}a.each("touchstart touchmove touchend orientationchange throttledresize tap taphold swipe swipeleft swiperight scrollstart scrollstop".split(" "),function(b,c){a.fn[c]=function(a){return a?this.bind(c,a):this.trigger(c)};a.attrFn[c]=true});var f=a.support.touch,d=f?"touchstart":"mousedown",g=f?"touchend":"mouseup",h=f?"touchmove":"mousemove";a.event.special.scrollstart={enabled:true,setup:function(){function b(a,f){d=f;e(c,d?"scrollstart":"scrollstop",a)}var c=this,d,f;a(c).bind("touchmove scroll",
-function(c){a.event.special.scrollstart.enabled&&(d||b(c,true),clearTimeout(f),f=setTimeout(function(){b(c,false)},50))})}};a.event.special.tap={setup:function(){var b=this,c=a(b);c.bind("vmousedown",function(d){function f(){clearTimeout(q)}function g(){f();c.unbind("vclick",h).unbind("vmouseup",f);a(k).unbind("vmousecancel",g)}function h(a){g();n==a.target&&e(b,"tap",a)}if(d.which&&d.which!==1)return false;var n=d.target,q;c.bind("vmouseup",f).bind("vclick",h);a(k).bind("vmousecancel",g);q=setTimeout(function(){e(b,
-"taphold",a.Event("taphold",{target:n}))},750)})}};a.event.special.swipe={scrollSupressionThreshold:10,durationThreshold:1E3,horizontalDistanceThreshold:30,verticalDistanceThreshold:75,setup:function(){var c=a(this);c.bind(d,function(d){function f(b){if(l){var c=b.originalEvent.touches?b.originalEvent.touches[0]:b;k={time:(new Date).getTime(),coords:[c.pageX,c.pageY]};Math.abs(l.coords[0]-k.coords[0])>a.event.special.swipe.scrollSupressionThreshold&&b.preventDefault()}}var e=d.originalEvent.touches?
-d.originalEvent.touches[0]:d,l={time:(new Date).getTime(),coords:[e.pageX,e.pageY],origin:a(d.target)},k;c.bind(h,f).one(g,function(){c.unbind(h,f);l&&k&&k.time-l.time<a.event.special.swipe.durationThreshold&&Math.abs(l.coords[0]-k.coords[0])>a.event.special.swipe.horizontalDistanceThreshold&&Math.abs(l.coords[1]-k.coords[1])<a.event.special.swipe.verticalDistanceThreshold&&l.origin.trigger("swipe").trigger(l.coords[0]>k.coords[0]?"swipeleft":"swiperight");l=k=b})})}};(function(a,b){function c(){var a=
-f();a!==e&&(e=a,d.trigger("orientationchange"))}var d=a(b),f,e,g,h,t={0:true,180:true};if(a.support.orientation&&(g=b.innerWidth||a(b).width(),h=b.innerHeight||a(b).height(),g=g>h&&g-h>50,h=t[b.orientation],g&&h||!g&&!h))t={"-90":true,90:true};a.event.special.orientationchange={setup:function(){if(a.support.orientation&&a.mobile.orientationChangeEnabled)return false;e=f();d.bind("throttledresize",c)},teardown:function(){if(a.support.orientation&&a.mobile.orientationChangeEnabled)return false;d.unbind("throttledresize",
-c)},add:function(a){var b=a.handler;a.handler=function(a){a.orientation=f();return b.apply(this,arguments)}}};a.event.special.orientationchange.orientation=f=function(){var c=true,c=k.documentElement;return(c=a.support.orientation?t[b.orientation]:c&&c.clientWidth/c.clientHeight<1.1)?"portrait":"landscape"}})(jQuery,c);(function(){a.event.special.throttledresize={setup:function(){a(this).bind("resize",b)},teardown:function(){a(this).unbind("resize",b)}};var b=function(){f=(new Date).getTime();g=f-
-c;g>=250?(c=f,a(this).trigger("throttledresize")):(d&&clearTimeout(d),d=setTimeout(b,250-g))},c=0,d,f,g})();a.each({scrollstop:"scrollstart",taphold:"tap",swipeleft:"swipe",swiperight:"swipe"},function(b,c){a.event.special[b]={setup:function(){a(this).bind(c,a.noop)}}})})(jQuery,this);(function(a){a.widget("mobile.page",a.mobile.widget,{options:{theme:"c",domCache:false,keepNativeDefault:":jqmData(role='none'), :jqmData(role='nojs')"},_create:function(){var a=this;if(a._trigger("beforecreate")===
-false)return false;a.element.attr("tabindex","0").addClass("ui-page ui-body-"+a.options.theme).bind("pagebeforehide",function(){a.removeContainerBackground()}).bind("pagebeforeshow",function(){a.setContainerBackground()})},removeContainerBackground:function(){a.mobile.pageContainer.removeClass("ui-overlay-"+a.mobile.getInheritedTheme(this.element.parent()))},setContainerBackground:function(c){this.options.theme&&a.mobile.pageContainer.addClass("ui-overlay-"+(c||this.options.theme))},keepNativeSelector:function(){var c=
-this.options;return c.keepNative&&a.trim(c.keepNative)&&c.keepNative!==c.keepNativeDefault?[c.keepNative,c.keepNativeDefault].join(", "):c.keepNativeDefault}})})(jQuery);(function(a,c,b){var e=function(d){d===b&&(d=true);return function(b,f,e,o){var k=new a.Deferred,p=f?" reverse":"",l=a.mobile.urlHistory.getActive().lastScroll||a.mobile.defaultHomeScroll,r=a.mobile.getScreenHeight(),n=a.mobile.maxTransitionWidth!==false&&a(c).width()>a.mobile.maxTransitionWidth,q=!a.support.cssTransitions||n||!b||
-b==="none",t=function(){a.mobile.pageContainer.toggleClass("ui-mobile-viewport-transitioning viewport-"+b)},x=function(){a.event.special.scrollstart.enabled=false;c.scrollTo(0,l);setTimeout(function(){a.event.special.scrollstart.enabled=true},150)},u=function(){o.removeClass(a.mobile.activePageClass+" out in reverse "+b).height("")},n=function(){o&&d&&u();e.addClass(a.mobile.activePageClass);a.mobile.focusPage(e);e.height(r+l);x();q||e.animationComplete(w);e.addClass(b+" in"+p);q&&w()},w=function(){d||
-o&&u();e.removeClass("out in reverse "+b).height("");t();a(c).scrollTop()!==l&&x();k.resolve(b,f,e,o,true)};t();o&&!q?(d?o.animationComplete(n):n(),o.height(r+a(c).scrollTop()).addClass(b+" out"+p)):n();return k.promise()}},f=e(),e=e(false);a.mobile.defaultTransitionHandler=f;a.mobile.transitionHandlers={"default":a.mobile.defaultTransitionHandler,sequential:f,simultaneous:e};a.mobile.transitionFallbacks={}})(jQuery,this);(function(a,c){function b(b){r&&(!r.closest(".ui-page-active").length||b)&&
-r.removeClass(a.mobile.activeBtnClass);r=null}function e(){t=false;q.length>0&&a.mobile.changePage.apply(null,q.pop())}function f(b,c,d,f){c&&c.data("page")._trigger("beforehide",null,{nextPage:b});b.data("page")._trigger("beforeshow",null,{prevPage:c||a("")});a.mobile.hidePageLoadingMsg();d&&!a.support.cssTransform3d&&a.mobile.transitionFallbacks[d]&&(d=a.mobile.transitionFallbacks[d]);d=(a.mobile.transitionHandlers[d||"default"]||a.mobile.defaultTransitionHandler)(d,f,b,c);d.done(function(){c&&
-c.data("page")._trigger("hide",null,{nextPage:b});b.data("page")._trigger("show",null,{prevPage:c||a("")})});return d}function d(){return s.innerHeight||a(s).height()}function g(){var b=a("."+a.mobile.activePageClass),c=parseFloat(b.css("padding-top")),f=parseFloat(b.css("padding-bottom"));b.css("min-height",d()-c-f)}function h(b,c){c&&b.attr("data-"+a.mobile.ns+"role",c);b.page()}function j(a){for(;a;){if(typeof a.nodeName==="string"&&a.nodeName.toLowerCase()=="a")break;a=a.parentNode}return a}function o(b){var b=
-a(b).closest(".ui-page").jqmData("url"),c=v.hrefNoHash;if(!b||!l.isPath(b))b=c;return l.makeUrlAbsolute(b,c)}var m=a(s);a("html");var p=a("head"),l={urlParseRE:/^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,parseUrl:function(b){if(a.type(b)==="object")return b;b=l.urlParseRE.exec(b||"")||[];return{href:b[0]||"",hrefNoHash:b[1]||"",hrefNoSearch:b[2]||"",domain:b[3]||"",
-protocol:b[4]||"",doubleSlash:b[5]||"",authority:b[6]||"",username:b[8]||"",password:b[9]||"",host:b[10]||"",hostname:b[11]||"",port:b[12]||"",pathname:b[13]||"",directory:b[14]||"",filename:b[15]||"",search:b[16]||"",hash:b[17]||""}},makePathAbsolute:function(a,b){if(a&&a.charAt(0)==="/")return a;for(var a=a||"",c=(b=b?b.replace(/^\/|(\/[^\/]*|[^\/]+)$/g,""):"")?b.split("/"):[],d=a.split("/"),f=0;f<d.length;f++){var e=d[f];switch(e){case ".":break;case "..":c.length&&c.pop();break;default:c.push(e)}}return"/"+
-c.join("/")},isSameDomain:function(a,b){return l.parseUrl(a).domain===l.parseUrl(b).domain},isRelativeUrl:function(a){return l.parseUrl(a).protocol===""},isAbsoluteUrl:function(a){return l.parseUrl(a).protocol!==""},makeUrlAbsolute:function(a,b){if(!l.isRelativeUrl(a))return a;var c=l.parseUrl(a),d=l.parseUrl(b),f=c.protocol||d.protocol,e=c.protocol?c.doubleSlash:c.doubleSlash||d.doubleSlash,g=c.authority||d.authority,h=c.pathname!=="",j=l.makePathAbsolute(c.pathname||d.filename,d.pathname);return f+
-e+g+j+(c.search||!h&&d.search||"")+c.hash},addSearchParams:function(b,c){var d=l.parseUrl(b),f=typeof c==="object"?a.param(c):c,e=d.search||"?";return d.hrefNoSearch+e+(e.charAt(e.length-1)!=="?"?"&":"")+f+(d.hash||"")},convertUrlToDataUrl:function(a){var b=l.parseUrl(a);if(l.isEmbeddedPage(b))return b.hash.split(x)[0].replace(/^#/,"");else if(l.isSameDomain(b,v))return b.hrefNoHash.replace(v.domain,"");return a},get:function(a){if(a===c)a=location.hash;return l.stripHash(a).replace(/[^\/]*\.[^\/*]+$/,
-"")},getFilePath:function(b){var c="&"+a.mobile.subPageUrlKey;return b&&b.split(c)[0].split(x)[0]},set:function(a){location.hash=a},isPath:function(a){return/\//.test(a)},clean:function(a){return a.replace(v.domain,"")},stripHash:function(a){return a.replace(/^#/,"")},cleanHash:function(a){return l.stripHash(a.replace(/\?.*$/,"").replace(x,""))},isExternal:function(a){a=l.parseUrl(a);return a.protocol&&a.domain!==w.domain?true:false},hasProtocol:function(a){return/^(:?\w+:)/.test(a)},isFirstPageUrl:function(b){var b=
-l.parseUrl(l.makeUrlAbsolute(b,v)),d=a.mobile.firstPage,d=d&&d[0]?d[0].id:c;return(b.hrefNoHash===w.hrefNoHash||y&&b.hrefNoHash===v.hrefNoHash)&&(!b.hash||b.hash==="#"||d&&b.hash.replace(/^#/,"")===d)},isEmbeddedPage:function(a){a=l.parseUrl(a);return a.protocol!==""?a.hash&&(a.hrefNoHash===w.hrefNoHash||y&&a.hrefNoHash===v.hrefNoHash):/^#/.test(a.href)}},r=null,n={stack:[],activeIndex:0,getActive:function(){return n.stack[n.activeIndex]},getPrev:function(){return n.stack[n.activeIndex-1]},getNext:function(){return n.stack[n.activeIndex+
-1]},addNew:function(a,b,c,d,f){n.getNext()&&n.clearForward();n.stack.push({url:a,transition:b,title:c,pageUrl:d,role:f});n.activeIndex=n.stack.length-1},clearForward:function(){n.stack=n.stack.slice(0,n.activeIndex+1)},directHashChange:function(b){var d,f,e;this.getActive();a.each(n.stack,function(a,c){b.currentUrl===c.url&&(d=a<n.activeIndex,f=!d,e=a)});this.activeIndex=e!==c?e:this.activeIndex;d?(b.either||b.isBack)(true):f&&(b.either||b.isForward)(false)},ignoreNextHashChange:false},q=[],t=false,
-x="&ui-state=dialog",u=p.children("base"),w=l.parseUrl(location.href),v=u.length?l.parseUrl(l.makeUrlAbsolute(u.attr("href"),w.href)):w,y=w.hrefNoHash!==v.hrefNoHash,B=a.support.dynamicBaseTag?{element:u.length?u:a("<base>",{href:v.hrefNoHash}).prependTo(p),set:function(a){B.element.attr("href",l.makeUrlAbsolute(a,v))},reset:function(){B.element.attr("href",v.hrefNoHash)}}:c;a.mobile.focusPage=function(a){var b=a.find("[autofocus]"),c=a.find(".ui-title:eq(0)");b.length?b.focus():c.length?c.focus():
-a.focus()};var E=true,A,C;A=function(){if(E){var b=a.mobile.urlHistory.getActive();if(b){var c=m.scrollTop();b.lastScroll=c<a.mobile.minScrollBack?a.mobile.defaultHomeScroll:c}}};C=function(){setTimeout(A,100)};m.bind(a.support.pushState?"popstate":"hashchange",function(){E=false});m.one(a.support.pushState?"popstate":"hashchange",function(){E=true});m.one("pagecontainercreate",function(){a.mobile.pageContainer.bind("pagechange",function(){E=true;m.unbind("scrollstop",C);m.bind("scrollstop",C)})});
-m.bind("scrollstop",C);a.mobile.getScreenHeight=d;a.fn.animationComplete=function(b){return a.support.cssTransitions?a(this).one("webkitAnimationEnd animationend",b):(setTimeout(b,0),a(this))};a.mobile.path=l;a.mobile.base=B;a.mobile.urlHistory=n;a.mobile.dialogHashKey=x;a.mobile.allowCrossDomainPages=false;a.mobile.getDocumentUrl=function(b){return b?a.extend({},w):w.href};a.mobile.getDocumentBase=function(b){return b?a.extend({},v):v.href};a.mobile._bindPageRemove=function(){var b=a(this);!b.data("page").options.domCache&&
-b.is(":jqmData(external-page='true')")&&b.bind("pagehide.remove",function(){var b=a(this),c=new a.Event("pageremove");b.trigger(c);c.isDefaultPrevented()||b.removeWithDependents()})};a.mobile.loadPage=function(b,d){var f=a.Deferred(),e=a.extend({},a.mobile.loadPage.defaults,d),g=null,j=null,k=l.makeUrlAbsolute(b,a.mobile.activePage&&o(a.mobile.activePage)||v.hrefNoHash);if(e.data&&e.type==="get")k=l.addSearchParams(k,e.data),e.data=c;if(e.data&&e.type==="post")e.reloadPage=true;var u=l.getFilePath(k),
-n=l.convertUrlToDataUrl(k);e.pageContainer=e.pageContainer||a.mobile.pageContainer;g=e.pageContainer.children(":jqmData(url='"+n+"')");g.length===0&&n&&!l.isPath(n)&&(g=e.pageContainer.children("#"+n).attr("data-"+a.mobile.ns+"url",n));if(g.length===0)if(a.mobile.firstPage&&l.isFirstPageUrl(u))a.mobile.firstPage.parent().length&&(g=a(a.mobile.firstPage));else if(l.isEmbeddedPage(u))return f.reject(k,d),f.promise();B&&B.reset();if(g.length){if(!e.reloadPage)return h(g,e.role),f.resolve(k,d,g),f.promise();
-j=g}var m=e.pageContainer,x=new a.Event("pagebeforeload"),p={url:b,absUrl:k,dataUrl:n,deferred:f,options:e};m.trigger(x,p);if(x.isDefaultPrevented())return f.promise();if(e.showLoadMsg)var r=setTimeout(function(){a.mobile.showPageLoadingMsg()},e.loadMsgDelay);!a.mobile.allowCrossDomainPages&&!l.isSameDomain(w,k)?f.reject(k,d):a.ajax({url:u,type:e.type,data:e.data,dataType:"html",success:function(c,o,m){var x=a("<div></div>"),v=c.match(/<title[^>]*>([^<]*)/)&&RegExp.$1,w=RegExp("\\bdata-"+a.mobile.ns+
-"url=[\"']?([^\"'>]*)[\"']?");RegExp("(<[^>]+\\bdata-"+a.mobile.ns+"role=[\"']?page[\"']?[^>]*>)").test(c)&&RegExp.$1&&w.test(RegExp.$1)&&RegExp.$1&&(b=u=l.getFilePath(RegExp.$1));B&&B.set(u);x.get(0).innerHTML=c;g=x.find(":jqmData(role='page'), :jqmData(role='dialog')").first();g.length||(g=a("<div data-"+a.mobile.ns+"role='page'>"+c.split(/<\/?body[^>]*>/gmi)[1]+"</div>"));v&&!g.jqmData("title")&&(~v.indexOf("&")&&(v=a("<div>"+v+"</div>").text()),g.jqmData("title",v));if(!a.support.dynamicBaseTag){var q=
-l.get(u);g.find("[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]").each(function(){var b=a(this).is("[href]")?"href":a(this).is("[src]")?"src":"action",c=a(this).attr(b),c=c.replace(location.protocol+"//"+location.host+location.pathname,"");/^(\w+:|#|\/)/.test(c)||a(this).attr(b,q+c)})}g.attr("data-"+a.mobile.ns+"url",l.convertUrlToDataUrl(u)).attr("data-"+a.mobile.ns+"external-page",true).appendTo(e.pageContainer);g.one("pagecreate",a.mobile._bindPageRemove);h(g,e.role);k.indexOf("&"+
-a.mobile.subPageUrlKey)>-1&&(g=e.pageContainer.children(":jqmData(url='"+n+"')"));e.showLoadMsg&&(clearTimeout(r),a.mobile.hidePageLoadingMsg());p.xhr=m;p.textStatus=o;p.page=g;e.pageContainer.trigger("pageload",p);f.resolve(k,d,g,j)},error:function(b,c,g){B&&B.set(l.get());p.xhr=b;p.textStatus=c;p.errorThrown=g;b=new a.Event("pageloadfailed");e.pageContainer.trigger(b,p);b.isDefaultPrevented()||(e.showLoadMsg&&(clearTimeout(r),a.mobile.hidePageLoadingMsg(),a.mobile.showPageLoadingMsg(a.mobile.pageLoadErrorMessageTheme,
-a.mobile.pageLoadErrorMessage,true),setTimeout(a.mobile.hidePageLoadingMsg,1500)),f.reject(k,d))}});return f.promise()};a.mobile.loadPage.defaults={type:"get",data:c,reloadPage:false,role:c,showLoadMsg:false,pageContainer:c,loadMsgDelay:50};a.mobile.changePage=function(d,g){if(t)q.unshift(arguments);else{var j=a.extend({},a.mobile.changePage.defaults,g);j.pageContainer=j.pageContainer||a.mobile.pageContainer;j.fromPage=j.fromPage||a.mobile.activePage;var u=j.pageContainer,o=new a.Event("pagebeforechange"),
-m={toPage:d,options:j};u.trigger(o,m);if(!o.isDefaultPrevented())if(d=m.toPage,t=true,typeof d=="string")a.mobile.loadPage(d,j).done(function(b,c,d,e){t=false;c.duplicateCachedPage=e;a.mobile.changePage(d,c)}).fail(function(){t=false;b(true);e();j.pageContainer.trigger("pagechangefailed",m)});else{if(d[0]===a.mobile.firstPage[0]&&!j.dataUrl)j.dataUrl=w.hrefNoHash;var o=j.fromPage,p=j.dataUrl&&l.convertUrlToDataUrl(j.dataUrl)||d.jqmData("url"),v=p;l.getFilePath(p);var r=n.getActive(),s=n.activeIndex===
-0,y=0,B=k.title,A=j.role==="dialog"||d.jqmData("role")==="dialog";if(o&&o[0]===d[0]&&!j.allowSamePageTransition)t=false,u.trigger("pagechange",m);else{h(d,j.role);j.fromHashChange&&n.directHashChange({currentUrl:p,isBack:function(){y=-1},isForward:function(){y=1}});try{k.activeElement&&k.activeElement.nodeName.toLowerCase()!="body"?a(k.activeElement).blur():a("input:focus, textarea:focus, select:focus").blur()}catch(E){}A&&r&&(p=(r.url||"")+x);if(j.changeHash!==false&&p)n.ignoreNextHashChange=true,
-l.set(p);var C=!r?B:d.jqmData("title")||d.children(":jqmData(role='header')").find(".ui-title").getEncodedText();C&&B==k.title&&(B=C);d.jqmData("title")||d.jqmData("title",B);j.transition=j.transition||(y&&!s?r.transition:c)||(A?a.mobile.defaultDialogTransition:a.mobile.defaultPageTransition);y||n.addNew(p,j.transition,B,v,j.role);k.title=n.getActive().title;a.mobile.activePage=d;j.reverse=j.reverse||y<0;f(d,o,j.transition,j.reverse).done(function(c,f,g,h,l){b();j.duplicateCachedPage&&j.duplicateCachedPage.remove();
-l||a.mobile.focusPage(d);e();u.trigger("pagechange",m)})}}}};a.mobile.changePage.defaults={transition:c,reverse:false,changeHash:true,fromHashChange:false,role:c,duplicateCachedPage:c,pageContainer:c,showLoadMsg:true,dataUrl:c,fromPage:c,allowSamePageTransition:false};a.mobile._registerInternalEvents=function(){a(k).delegate("form","submit",function(b){var c=a(this);if(a.mobile.ajaxEnabled&&!c.is(":jqmData(ajax='false')")&&c.jqmHijackable().length){var d=c.attr("method"),e=c.attr("target"),f=c.attr("action");
-if(!f&&(f=o(c),f===v.hrefNoHash))f=w.hrefNoSearch;f=l.makeUrlAbsolute(f,o(c));!l.isExternal(f)&&!e&&(a.mobile.changePage(f,{type:d&&d.length&&d.toLowerCase()||"get",data:c.serialize(),transition:c.jqmData("transition"),direction:c.jqmData("direction"),reloadPage:true}),b.preventDefault())}});a(k).bind("vclick",function(c){if(!(c.which>1)&&a.mobile.linkBindingEnabled&&(c=j(c.target),a(c).jqmHijackable().length&&c&&l.parseUrl(c.getAttribute("href")||"#").hash!=="#"))b(true),r=a(c).closest(".ui-btn").not(".ui-disabled"),
-r.addClass(a.mobile.activeBtnClass),a("."+a.mobile.activePageClass+" .ui-btn").not(c).blur(),a(c).jqmData("href",a(c).attr("href")).attr("href","#")});a(k).bind("click",function(d){if(a.mobile.linkBindingEnabled){var f=j(d.target),e=a(f),g;if(f&&!(d.which>1)&&e.jqmHijackable().length){g=function(){s.setTimeout(function(){b(true)},200)};e.jqmData("href")&&e.attr("href",e.jqmData("href"));if(e.is(":jqmData(rel='back')"))return s.history.back(),false;var h=o(e),f=l.makeUrlAbsolute(e.attr("href")||"#",
-h);if(!a.mobile.ajaxEnabled&&!l.isEmbeddedPage(f))g();else{if(f.search("#")!=-1)if(f=f.replace(/[^#]*#/,""))f=l.isPath(f)?l.makeUrlAbsolute(f,h):l.makeUrlAbsolute("#"+f,w.hrefNoHash);else{d.preventDefault();return}var h=e.is("[rel='external']")||e.is(":jqmData(ajax='false')")||e.is("[target]"),k=a.mobile.allowCrossDomainPages&&w.protocol==="file:"&&f.search(/^https?:/)!=-1;h||l.isExternal(f)&&!k?g():(g=e.jqmData("transition"),h=(h=e.jqmData("direction"))&&h==="reverse"||e.jqmData("back"),e=e.attr("data-"+
-a.mobile.ns+"rel")||c,a.mobile.changePage(f,{transition:g,reverse:h,role:e}),d.preventDefault())}}}});a(k).delegate(".ui-page","pageshow.prefetch",function(){var b=[];a(this).find("a:jqmData(prefetch)").each(function(){var c=a(this),d=c.attr("href");d&&a.inArray(d,b)===-1&&(b.push(d),a.mobile.loadPage(d,{role:c.attr("data-"+a.mobile.ns+"rel")}))})});a.mobile._handleHashChange=function(b){var d=l.stripHash(b),f={transition:a.mobile.urlHistory.stack.length===0?"none":c,changeHash:false,fromHashChange:true};
-if(!a.mobile.hashListeningEnabled||n.ignoreNextHashChange)n.ignoreNextHashChange=false;else{if(n.stack.length>1&&d.indexOf(x)>-1)if(a.mobile.activePage.is(".ui-dialog"))n.directHashChange({currentUrl:d,either:function(b){var c=a.mobile.urlHistory.getActive();d=c.pageUrl;a.extend(f,{role:c.role,transition:c.transition,reverse:b})}});else{n.directHashChange({currentUrl:d,isBack:function(){s.history.back()},isForward:function(){s.history.forward()}});return}d?(d=typeof d==="string"&&!l.isPath(d)?l.makeUrlAbsolute("#"+
-d,v):d,a.mobile.changePage(d,f)):a.mobile.changePage(a.mobile.firstPage,f)}};m.bind("hashchange",function(){a.mobile._handleHashChange(location.hash)});a(k).bind("pageshow",g);a(s).bind("throttledresize",g)}})(jQuery);(function(a,c){var b={},e=a(c),f=a.mobile.path.parseUrl(location.href);a.extend(b,{initialFilePath:f.pathname+f.search,initialHref:f.hrefNoHash,state:function(){return{hash:location.hash||"#"+b.initialFilePath,title:k.title,initialHref:b.initialHref}},resetUIKeys:function(b){var c="&"+
-a.mobile.subPageUrlKey,f=b.indexOf(a.mobile.dialogHashKey);f>-1?b=b.slice(0,f)+"#"+b.slice(f):b.indexOf(c)>-1&&(b=b.split(c).join("#"+c));return b},hashValueAfterReset:function(c){c=b.resetUIKeys(c);return a.mobile.path.parseUrl(c).hash},nextHashChangePrevented:function(c){a.mobile.urlHistory.ignoreNextHashChange=c;b.onHashChangeDisabled=c},onHashChange:function(){if(!b.onHashChangeDisabled){var c,f;c=location.hash;var e=a.mobile.path.isPath(c),j=e?location.href:a.mobile.getDocumentUrl();c=e?c.replace("#",
-""):c;f=b.state();c=a.mobile.path.makeUrlAbsolute(c,j);e&&(c=b.resetUIKeys(c));history.replaceState(f,k.title,c)}},onPopState:function(c){var c=c.originalEvent.state,f,h;if(c){f=b.hashValueAfterReset(a.mobile.urlHistory.getActive().url);h=b.hashValueAfterReset(c.hash.replace("#",""));if(f=f!==h)e.one("hashchange.pushstate",function(){b.nextHashChangePrevented(false)});b.nextHashChangePrevented(false);a.mobile._handleHashChange(c.hash);f&&b.nextHashChangePrevented(true)}},init:function(){e.bind("hashchange",
-b.onHashChange);e.bind("popstate",b.onPopState);location.hash===""&&history.replaceState(b.state(),k.title,location.href)}});a(function(){a.mobile.pushStateEnabled&&a.support.pushState&&b.init()})})(jQuery,this);jQuery.mobile.transitionFallbacks.pop="fade";(function(a){a.mobile.transitionHandlers.slide=a.mobile.transitionHandlers.simultaneous;a.mobile.transitionFallbacks.slide="fade"})(jQuery,this);jQuery.mobile.transitionFallbacks.slidedown="fade";jQuery.mobile.transitionFallbacks.slideup="fade";
-jQuery.mobile.transitionFallbacks.flip="fade";jQuery.mobile.transitionFallbacks.flow="fade";jQuery.mobile.transitionFallbacks.turn="fade";(function(a){a.mobile.page.prototype.options.degradeInputs={color:false,date:false,datetime:false,"datetime-local":false,email:false,month:false,number:false,range:"number",search:"text",tel:false,time:false,url:false,week:false};a(k).bind("pagecreate create",function(c){var b=a.mobile.closestPageData(a(c.target)),e;if(b)e=b.options,a(c.target).find("input").not(b.keepNativeSelector()).each(function(){var b=
-a(this),c=this.getAttribute("type"),g=e.degradeInputs[c]||"text";if(e.degradeInputs[c]){var h=a("<div>").html(b.clone()).html(),j=h.indexOf(" type=")>-1;b.replaceWith(h.replace(j?/\s+type=["']?\w+['"]?/:/\/?>/,' type="'+g+'" data-'+a.mobile.ns+'type="'+c+'"'+(j?"":">")))}})})})(jQuery);(function(a,c){a.widget("mobile.dialog",a.mobile.widget,{options:{closeBtnText:"Close",overlayTheme:"a",initSelector:":jqmData(role='dialog')"},_create:function(){var b=this,c=this.element,f=a("<a href='#' data-"+a.mobile.ns+
-"icon='delete' data-"+a.mobile.ns+"iconpos='notext'>"+this.options.closeBtnText+"</a>"),d=a("<div/>",{role:"dialog","class":"ui-dialog-contain ui-corner-all ui-overlay-shadow"});c.addClass("ui-dialog ui-overlay-"+this.options.overlayTheme);c.wrapInner(d).children().find(":jqmData(role='header')").prepend(f).end().children(":first-child").addClass("ui-corner-top").end().children(":last-child").addClass("ui-corner-bottom");f.bind("click",function(){b.close()});c.bind("vclick submit",function(b){var b=
-a(b.target).closest(b.type==="vclick"?"a":"form"),c;b.length&&!b.jqmData("transition")&&(c=a.mobile.urlHistory.getActive()||{},b.attr("data-"+a.mobile.ns+"transition",c.transition||a.mobile.defaultDialogTransition).attr("data-"+a.mobile.ns+"direction","reverse"))}).bind("pagehide",function(){a(this).find("."+a.mobile.activeBtnClass).removeClass(a.mobile.activeBtnClass)}).bind("pagebeforeshow",function(){b.options.overlayTheme&&b.element.page("removeContainerBackground").page("setContainerBackground",
-b.options.overlayTheme)})},close:function(){c.history.back()}});a(k).delegate(a.mobile.dialog.prototype.options.initSelector,"pagecreate",function(){a.mobile.dialog.prototype.enhance(this)})})(jQuery,this);(function(a){a.fn.fieldcontain=function(){return this.addClass("ui-field-contain ui-body ui-br")};a(k).bind("pagecreate create",function(c){a(":jqmData(role='fieldcontain')",c.target).jqmEnhanceable().fieldcontain()})})(jQuery);(function(a){a.fn.grid=function(c){return this.each(function(){var b=
-a(this),e=a.extend({grid:null},c),f=b.children(),d={solo:1,a:2,b:3,c:4,d:5},e=e.grid;if(!e)if(f.length<=5)for(var g in d)d[g]===f.length&&(e=g);else e="a";d=d[e];b.addClass("ui-grid-"+e);f.filter(":nth-child("+d+"n+1)").addClass("ui-block-a");d>1&&f.filter(":nth-child("+d+"n+2)").addClass("ui-block-b");d>2&&f.filter(":nth-child(3n+3)").addClass("ui-block-c");d>3&&f.filter(":nth-child(4n+4)").addClass("ui-block-d");d>4&&f.filter(":nth-child(5n+5)").addClass("ui-block-e")})}})(jQuery);(function(a){a(k).bind("pagecreate create",
-function(c){a(":jqmData(role='nojs')",c.target).addClass("ui-nojs")})})(jQuery);(function(a,c){function b(a){for(var b;a;){if((b=typeof a.className==="string"&&a.className+" ")&&b.indexOf("ui-btn ")>-1&&b.indexOf("ui-disabled ")<0)break;a=a.parentNode}return a}a.fn.buttonMarkup=function(b){for(var b=b&&a.type(b)=="object"?b:{},d=0;d<this.length;d++){var g=this.eq(d),h=g[0],j=a.extend({},a.fn.buttonMarkup.defaults,{icon:b.icon!==c?b.icon:g.jqmData("icon"),iconpos:b.iconpos!==c?b.iconpos:g.jqmData("iconpos"),
-theme:b.theme!==c?b.theme:g.jqmData("theme")||a.mobile.getInheritedTheme(g,"c"),inline:b.inline!==c?b.inline:g.jqmData("inline"),shadow:b.shadow!==c?b.shadow:g.jqmData("shadow"),corners:b.corners!==c?b.corners:g.jqmData("corners"),iconshadow:b.iconshadow!==c?b.iconshadow:g.jqmData("iconshadow"),mini:b.mini!==c?b.mini:g.jqmData("mini")},b),o="ui-btn-inner",m,p,l,r,n,q;a.each(j,function(b,c){h.setAttribute("data-"+a.mobile.ns+b,c);g.jqmData(b,c)});(q=a.data(h.tagName==="INPUT"||h.tagName==="BUTTON"?
-h.parentNode:h,"buttonElements"))?(h=q.outer,g=a(h),l=q.inner,r=q.text,a(q.icon).remove(),q.icon=null):(l=k.createElement(j.wrapperEls),r=k.createElement(j.wrapperEls));n=j.icon?k.createElement("span"):null;e&&!q&&e();if(!j.theme)j.theme=a.mobile.getInheritedTheme(g,"c");m="ui-btn ui-btn-up-"+j.theme;m+=j.inline?" ui-btn-inline":"";m+=j.shadow?" ui-shadow":"";m+=j.corners?" ui-btn-corner-all":"";j.mini!==c&&(m+=j.mini?" ui-mini":" ui-fullsize");j.inline!==c&&(m+=j.inline===false?" ui-btn-block":" ui-btn-inline");
-if(j.icon)j.icon="ui-icon-"+j.icon,j.iconpos=j.iconpos||"left",p="ui-icon "+j.icon,j.iconshadow&&(p+=" ui-icon-shadow");j.iconpos&&(m+=" ui-btn-icon-"+j.iconpos,j.iconpos=="notext"&&!g.attr("title")&&g.attr("title",g.getEncodedText()));o+=j.corners?" ui-btn-corner-all":"";j.iconpos&&j.iconpos==="notext"&&!g.attr("title")&&g.attr("title",g.getEncodedText());q&&g.removeClass(q.bcls||"");g.removeClass("ui-link").addClass(m);l.className=o;r.className="ui-btn-text";q||l.appendChild(r);if(n&&(n.className=
-p,!q||!q.icon))n.appendChild(k.createTextNode("\u00a0")),l.appendChild(n);for(;h.firstChild&&!q;)r.appendChild(h.firstChild);q||h.appendChild(l);q={bcls:m,outer:h,inner:l,text:r,icon:n};a.data(h,"buttonElements",q);a.data(l,"buttonElements",q);a.data(r,"buttonElements",q);n&&a.data(n,"buttonElements",q)}return this};a.fn.buttonMarkup.defaults={corners:true,shadow:true,iconshadow:true,wrapperEls:"span"};var e=function(){var c=a.mobile.buttonMarkup.hoverDelay,d,g;a(k).bind({"vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart":function(e){var j,
-k=a(b(e.target)),e=e.type;if(k.length)if(j=k.attr("data-"+a.mobile.ns+"theme"),e==="vmousedown")a.support.touch?d=setTimeout(function(){k.removeClass("ui-btn-up-"+j).addClass("ui-btn-down-"+j)},c):k.removeClass("ui-btn-up-"+j).addClass("ui-btn-down-"+j);else if(e==="vmousecancel"||e==="vmouseup")k.removeClass("ui-btn-down-"+j).addClass("ui-btn-up-"+j);else if(e==="vmouseover"||e==="focus")a.support.touch?g=setTimeout(function(){k.removeClass("ui-btn-up-"+j).addClass("ui-btn-hover-"+j)},c):k.removeClass("ui-btn-up-"+
-j).addClass("ui-btn-hover-"+j);else if(e==="vmouseout"||e==="blur"||e==="scrollstart")k.removeClass("ui-btn-hover-"+j+" ui-btn-down-"+j).addClass("ui-btn-up-"+j),d&&clearTimeout(d),g&&clearTimeout(g)},"focusin focus":function(c){a(b(c.target)).addClass(a.mobile.focusClass)},"focusout blur":function(c){a(b(c.target)).removeClass(a.mobile.focusClass)}});e=null};a(k).bind("pagecreate create",function(b){a(":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a",
-b.target).not(".ui-btn, :jqmData(role='none'), :jqmData(role='nojs')").buttonMarkup()})})(jQuery);(function(a){a.mobile.page.prototype.options.backBtnText="Back";a.mobile.page.prototype.options.addBackBtn=false;a.mobile.page.prototype.options.backBtnTheme=null;a.mobile.page.prototype.options.headerTheme="a";a.mobile.page.prototype.options.footerTheme="a";a.mobile.page.prototype.options.contentTheme=null;a(k).delegate(":jqmData(role='page'), :jqmData(role='dialog')","pagecreate",function(){var c=a(this),
-b=c.data("page").options,e=c.jqmData("role"),f=b.theme;a(":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')",this).jqmEnhanceable().each(function(){var d=a(this),g=d.jqmData("role"),h=d.jqmData("theme"),j=h||b.contentTheme||e==="dialog"&&f,k;d.addClass("ui-"+g);if(g==="header"||g==="footer"){var m=h||(g==="header"?b.headerTheme:b.footerTheme)||f;d.addClass("ui-bar-"+m).attr("role",g==="header"?"banner":"contentinfo");g==="header"&&(h=d.children("a"),k=h.hasClass("ui-btn-left"),
-j=h.hasClass("ui-btn-right"),k=k||h.eq(0).not(".ui-btn-right").addClass("ui-btn-left").length,j||h.eq(1).addClass("ui-btn-right"));b.addBackBtn&&g==="header"&&a(".ui-page").length>1&&c.jqmData("url")!==a.mobile.path.stripHash(location.hash)&&!k&&a("<a href='#' class='ui-btn-left' data-"+a.mobile.ns+"rel='back' data-"+a.mobile.ns+"icon='arrow-l'>"+b.backBtnText+"</a>").attr("data-"+a.mobile.ns+"theme",b.backBtnTheme||m).prependTo(d);d.children("h1, h2, h3, h4, h5, h6").addClass("ui-title").attr({role:"heading",
-"aria-level":"1"})}else g==="content"&&(j&&d.addClass("ui-body-"+j),d.attr("role","main"))})})})(jQuery);(function(a){a.widget("mobile.collapsible",a.mobile.widget,{options:{expandCueText:" click to expand contents",collapseCueText:" click to collapse contents",collapsed:true,heading:"h1,h2,h3,h4,h5,h6,legend",theme:null,contentTheme:null,iconTheme:"d",mini:false,initSelector:":jqmData(role='collapsible')"},_create:function(){var c=this.element,b=this.options,e=c.addClass("ui-collapsible"),f=c.children(b.heading).first(),
-d=e.wrapInner("<div class='ui-collapsible-content'></div>").find(".ui-collapsible-content"),g=c.closest(":jqmData(role='collapsible-set')").addClass("ui-collapsible-set");f.is("legend")&&(f=a("<div role='heading'>"+f.html()+"</div>").insertBefore(f),f.next().remove());if(g.length){if(!b.theme)b.theme=g.jqmData("theme")||a.mobile.getInheritedTheme(g,"c");if(!b.contentTheme)b.contentTheme=g.jqmData("content-theme");if(!b.iconPos)b.iconPos=g.jqmData("iconpos");if(!b.mini)b.mini=g.jqmData("mini")}d.addClass(b.contentTheme?
-"ui-body-"+b.contentTheme:"");f.insertBefore(d).addClass("ui-collapsible-heading").append("<span class='ui-collapsible-heading-status'></span>").wrapInner("<a href='#' class='ui-collapsible-heading-toggle'></a>").find("a").first().buttonMarkup({shadow:false,corners:false,iconpos:c.jqmData("iconpos")||b.iconPos||"left",icon:"plus",mini:b.mini,theme:b.theme}).add(".ui-btn-inner",c).addClass("ui-corner-top ui-corner-bottom");e.bind("expand collapse",function(c){if(!c.isDefaultPrevented()){c.preventDefault();
-var j=a(this),c=c.type==="collapse",k=b.contentTheme;f.toggleClass("ui-collapsible-heading-collapsed",c).find(".ui-collapsible-heading-status").text(c?b.expandCueText:b.collapseCueText).end().find(".ui-icon").toggleClass("ui-icon-minus",!c).toggleClass("ui-icon-plus",c);j.toggleClass("ui-collapsible-collapsed",c);d.toggleClass("ui-collapsible-content-collapsed",c).attr("aria-hidden",c);if(k&&(!g.length||e.jqmData("collapsible-last")))f.find("a").first().add(f.find(".ui-btn-inner")).toggleClass("ui-corner-bottom",
-c),d.toggleClass("ui-corner-bottom",!c);d.trigger("updatelayout")}}).trigger(b.collapsed?"collapse":"expand");f.bind("click",function(a){var b=f.is(".ui-collapsible-heading-collapsed")?"expand":"collapse";e.trigger(b);a.preventDefault()})}});a(k).bind("pagecreate create",function(c){a.mobile.collapsible.prototype.enhanceWithin(c.target)})})(jQuery);(function(a,c){a.widget("mobile.collapsibleset",a.mobile.widget,{options:{initSelector:":jqmData(role='collapsible-set')"},_create:function(){var b=this.element.addClass("ui-collapsible-set"),
-e=this.options;if(!e.theme)e.theme=a.mobile.getInheritedTheme(b,"c");if(!e.contentTheme)e.contentTheme=b.jqmData("content-theme");if(!e.corners)e.corners=b.jqmData("corners")===c?true:false;b.jqmData("collapsiblebound")||b.jqmData("collapsiblebound",true).bind("expand collapse",function(b){var c=b.type==="collapse",b=a(b.target).closest(".ui-collapsible"),e=b.data("collapsible");e.options.contentTheme&&b.jqmData("collapsible-last")&&(b.find(e.options.heading).first().find("a").first().add(".ui-btn-inner").toggleClass("ui-corner-bottom",
-c),b.find(".ui-collapsible-content").toggleClass("ui-corner-bottom",!c))}).bind("expand",function(b){a(b.target).closest(".ui-collapsible").siblings(".ui-collapsible").trigger("collapse")})},_init:function(){this.refresh()},refresh:function(){var b=this.options,c=this.element.children(":jqmData(role='collapsible')");a.mobile.collapsible.prototype.enhance(c.not(".ui-collapsible"));c.each(function(){a(this).find(a.mobile.collapsible.prototype.options.heading).find("a").first().add(".ui-btn-inner").removeClass("ui-corner-top ui-corner-bottom")});
-c.first().find("a").first().addClass(b.corners?"ui-corner-top":"").find(".ui-btn-inner").addClass("ui-corner-top");c.last().jqmData("collapsible-last",true).find("a").first().addClass(b.corners?"ui-corner-bottom":"").find(".ui-btn-inner").addClass("ui-corner-bottom")}});a(k).bind("pagecreate create",function(b){a.mobile.collapsibleset.prototype.enhanceWithin(b.target)})})(jQuery);(function(a,c){a.widget("mobile.navbar",a.mobile.widget,{options:{iconpos:"top",grid:null,initSelector:":jqmData(role='navbar')"},
-_create:function(){var b=this.element,e=b.find("a"),f=e.filter(":jqmData(icon)").length?this.options.iconpos:c;b.addClass("ui-navbar").attr("role","navigation").find("ul").jqmEnhanceable().grid({grid:this.options.grid});f||b.addClass("ui-navbar-noicons");e.buttonMarkup({corners:false,shadow:false,inline:true,iconpos:f});b.delegate("a","vclick",function(b){a(b.target).hasClass("ui-disabled")||(e.removeClass(a.mobile.activeBtnClass),a(this).addClass(a.mobile.activeBtnClass))});b.closest(".ui-page").bind("pagebeforeshow",
-function(){e.filter(".ui-state-persist").addClass(a.mobile.activeBtnClass)})}});a(k).bind("pagecreate create",function(b){a.mobile.navbar.prototype.enhanceWithin(b.target)})})(jQuery);(function(a){var c={};a.widget("mobile.listview",a.mobile.widget,{options:{theme:null,countTheme:"c",headerTheme:"b",dividerTheme:"b",splitIcon:"arrow-r",splitTheme:"b",mini:false,inset:false,initSelector:":jqmData(role='listview')"},_create:function(){var a="";a+=this.options.inset?" ui-listview-inset ui-corner-all ui-shadow ":
-"";a+=this.element.jqmData("mini")||this.options.mini===true?" ui-mini":"";this.element.addClass(function(c,f){return f+" ui-listview "+a});this.refresh(true)},_removeCorners:function(a,c){a=a.add(a.find(".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb"));c==="top"?a.removeClass("ui-corner-top ui-corner-tr ui-corner-tl"):c==="bottom"?a.removeClass("ui-corner-bottom ui-corner-br ui-corner-bl"):a.removeClass("ui-corner-top ui-corner-tr ui-corner-tl ui-corner-bottom ui-corner-br ui-corner-bl")},_refreshCorners:function(a){var c,
-f;this.options.inset&&(c=this.element.children("li"),f=a?c.not(".ui-screen-hidden"):c.filter(":visible"),this._removeCorners(c),c=f.first().addClass("ui-corner-top"),c.add(c.find(".ui-btn-inner").not(".ui-li-link-alt span:first-child")).addClass("ui-corner-top").end().find(".ui-li-link-alt, .ui-li-link-alt span:first-child").addClass("ui-corner-tr").end().find(".ui-li-thumb").not(".ui-li-icon").addClass("ui-corner-tl"),f=f.last().addClass("ui-corner-bottom"),f.add(f.find(".ui-btn-inner")).find(".ui-li-link-alt").addClass("ui-corner-br").end().find(".ui-li-thumb").not(".ui-li-icon").addClass("ui-corner-bl"));
-a||this.element.trigger("updatelayout")},_findFirstElementByTagName:function(a,c,f,d){var g={};for(g[f]=g[d]=true;a;){if(g[a.nodeName])return a;a=a[c]}return null},_getChildrenByTagName:function(b,c,f){var d=[],g={};g[c]=g[f]=true;for(b=b.firstChild;b;)g[b.nodeName]&&d.push(b),b=b.nextSibling;return a(d)},_addThumbClasses:function(b){var c,f,d=b.length;for(c=0;c<d;c++)f=a(this._findFirstElementByTagName(b[c].firstChild,"nextSibling","img","IMG")),f.length&&(f.addClass("ui-li-thumb"),a(this._findFirstElementByTagName(f[0].parentNode,
-"parentNode","li","LI")).addClass(f.is(".ui-li-icon")?"ui-li-has-icon":"ui-li-has-thumb"))},refresh:function(b){this.parentPage=this.element.closest(".ui-page");this._createSubPages();var c=this.options,f=this.element,d=f.jqmData("dividertheme")||c.dividerTheme,g=f.jqmData("splittheme"),h=f.jqmData("spliticon"),j=this._getChildrenByTagName(f[0],"li","LI"),o=a.support.cssPseudoElement||!a.nodeName(f[0],"ol")?0:1,m={},p,l,r,n,q,t,x;o&&f.find(".ui-li-dec").remove();if(!c.theme)c.theme=a.mobile.getInheritedTheme(this.element,
-"c");for(var u=0,w=j.length;u<w;u++){p=j.eq(u);l="ui-li";if(b||!p.hasClass("ui-li"))r=p.jqmData("theme")||c.theme,n=this._getChildrenByTagName(p[0],"a","A"),n.length?(t=p.jqmData("icon"),p.buttonMarkup({wrapperEls:"div",shadow:false,corners:false,iconpos:"right",icon:n.length>1||t===false?false:t||"arrow-r",theme:r}),t!=false&&n.length==1&&p.addClass("ui-li-has-arrow"),n.first().removeClass("ui-link").addClass("ui-link-inherit"),n.length>1&&(l+=" ui-li-has-alt",n=n.last(),q=g||n.jqmData("theme")||
-c.splitTheme,x=n.jqmData("icon"),n.appendTo(p).attr("title",n.getEncodedText()).addClass("ui-li-link-alt").empty().buttonMarkup({shadow:false,corners:false,theme:r,icon:false,iconpos:false}).find(".ui-btn-inner").append(a(k.createElement("span")).buttonMarkup({shadow:true,corners:true,theme:q,iconpos:"notext",icon:x||t||h||c.splitIcon})))):p.jqmData("role")==="list-divider"?(l+=" ui-li-divider ui-bar-"+d,p.attr("role","heading"),o&&(o=1)):l+=" ui-li-static ui-body-"+r;o&&l.indexOf("ui-li-divider")<
-0&&(r=p.is(".ui-li-static:first")?p:p.find(".ui-link-inherit"),r.addClass("ui-li-jsnumbering").prepend("<span class='ui-li-dec'>"+o++ +". </span>"));m[l]||(m[l]=[]);m[l].push(p[0])}for(l in m)a(m[l]).addClass(l).children(".ui-btn-inner").addClass(l);f.find("h1, h2, h3, h4, h5, h6").addClass("ui-li-heading").end().find("p, dl").addClass("ui-li-desc").end().find(".ui-li-aside").each(function(){var b=a(this);b.prependTo(b.parent())}).end().find(".ui-li-count").each(function(){a(this).closest("li").addClass("ui-li-has-count")}).addClass("ui-btn-up-"+
-(f.jqmData("counttheme")||this.options.countTheme)+" ui-btn-corner-all");this._addThumbClasses(j);this._addThumbClasses(f.find(".ui-link-inherit"));this._refreshCorners(b)},_idStringEscape:function(a){return a.replace(/[^a-zA-Z0-9]/g,"-")},_createSubPages:function(){var b=this.element,e=b.closest(".ui-page"),f=e.jqmData("url"),d=f||e[0][a.expando],g=b.attr("id"),h=this.options,j="data-"+a.mobile.ns,k=this,m=e.find(":jqmData(role='footer')").jqmData("id"),p;typeof c[d]==="undefined"&&(c[d]=-1);g=g||
-++c[d];a(b.find("li>ul, li>ol").toArray().reverse()).each(function(c){var d=a(this),e=d.attr("id")||g+"-"+c,c=d.parent(),k=a(d.prevAll().toArray().reverse()),k=k.length?k:a("<span>"+a.trim(c.contents()[0].nodeValue)+"</span>"),o=k.first().getEncodedText(),e=(f||"")+"&"+a.mobile.subPageUrlKey+"="+e,x=d.jqmData("theme")||h.theme,u=d.jqmData("counttheme")||b.jqmData("counttheme")||h.countTheme;p=true;d.detach().wrap("<div "+j+"role='page' "+j+"url='"+e+"' "+j+"theme='"+x+"' "+j+"count-theme='"+u+"'><div "+
-j+"role='content'></div></div>").parent().before("<div "+j+"role='header' "+j+"theme='"+h.headerTheme+"'><div class='ui-title'>"+o+"</div></div>").after(m?a("<div "+j+"role='footer' "+j+"id='"+m+"'>"):"").parent().appendTo(a.mobile.pageContainer).page();d=c.find("a:first");d.length||(d=a("<a/>").html(k||o).prependTo(c.empty()));d.attr("href","#"+e)}).listview();p&&e.is(":jqmData(external-page='true')")&&e.data("page").options.domCache===false&&e.unbind("pagehide.remove").bind("pagehide.remove",function(b,
-c){var d=c.nextPage;c.nextPage&&(d=d.jqmData("url"),d.indexOf(f+"&"+a.mobile.subPageUrlKey)!==0&&(k.childPages().remove(),e.remove()))})},childPages:function(){var b=this.parentPage.jqmData("url");return a(":jqmData(url^='"+b+"&"+a.mobile.subPageUrlKey+"')")}});a(k).bind("pagecreate create",function(b){a.mobile.listview.prototype.enhanceWithin(b.target)})})(jQuery);(function(a,c){a.widget("mobile.checkboxradio",a.mobile.widget,{options:{theme:null,initSelector:"input[type='checkbox'],input[type='radio']"},
-_create:function(){var b=this,e=this.element,f=a(e).closest("label"),d=f.length?f:a(e).closest("form,fieldset,:jqmData(role='page'),:jqmData(role='dialog')").find("label").filter("[for='"+e[0].id+"']"),g=e[0].type,f=e.jqmData("mini")||e.closest("form,fieldset").jqmData("mini"),h=g+"-on",j=g+"-off",o=e.parents(":jqmData(type='horizontal')").length?c:j,m=e.jqmData("iconpos")||e.closest("form,fieldset").jqmData("iconpos");if(!(g!=="checkbox"&&g!=="radio")){a.extend(this,{label:d,inputtype:g,checkedClass:"ui-"+
-h+(o?"":" "+a.mobile.activeBtnClass),uncheckedClass:"ui-"+j,checkedicon:"ui-icon-"+h,uncheckedicon:"ui-icon-"+j});if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.element,"c");d.buttonMarkup({theme:this.options.theme,icon:o,shadow:false,mini:f,iconpos:m});f=k.createElement("div");f.className="ui-"+g;e.add(d).wrapAll(f);d.bind({vmouseover:function(b){a(this).parent().is(".ui-disabled")&&b.stopPropagation()},vclick:function(a){if(e.is(":disabled"))a.preventDefault();else return b._cacheVals(),
-e.prop("checked",g==="radio"&&true||!e.prop("checked")),e.triggerHandler("click"),b._getInputSet().not(e).prop("checked",false),b._updateAll(),false}});e.bind({vmousedown:function(){b._cacheVals()},vclick:function(){var c=a(this);c.is(":checked")?(c.prop("checked",true),b._getInputSet().not(c).prop("checked",false)):c.prop("checked",false);b._updateAll()},focus:function(){d.addClass(a.mobile.focusClass)},blur:function(){d.removeClass(a.mobile.focusClass)}});this.refresh()}},_cacheVals:function(){this._getInputSet().each(function(){a(this).jqmData("cacheVal",
-this.checked)})},_getInputSet:function(){return this.inputtype==="checkbox"?this.element:this.element.closest("form,fieldset,:jqmData(role='page')").find("input[name='"+this.element[0].name+"'][type='"+this.inputtype+"']")},_updateAll:function(){var b=this;this._getInputSet().each(function(){var c=a(this);(this.checked||b.inputtype==="checkbox")&&c.trigger("change")}).checkboxradio("refresh")},refresh:function(){var a=this.element[0],c=this.label,f=c.find(".ui-icon");a.checked?(c.addClass(this.checkedClass).removeClass(this.uncheckedClass),
-f.addClass(this.checkedicon).removeClass(this.uncheckedicon)):(c.removeClass(this.checkedClass).addClass(this.uncheckedClass),f.removeClass(this.checkedicon).addClass(this.uncheckedicon));a.disabled?this.disable():this.enable()},disable:function(){this.element.prop("disabled",true).parent().addClass("ui-disabled")},enable:function(){this.element.prop("disabled",false).parent().removeClass("ui-disabled")}});a(k).bind("pagecreate create",function(b){a.mobile.checkboxradio.prototype.enhanceWithin(b.target,
-true)})})(jQuery);(function(a,c){a.widget("mobile.button",a.mobile.widget,{options:{theme:null,icon:null,iconpos:null,inline:false,corners:true,shadow:true,iconshadow:true,initSelector:"button, [type='button'], [type='submit'], [type='reset'], [type='image']",mini:false},_create:function(){var b=this.element,e,f=this.options,d;d="";var g;if(b[0].tagName==="A")!b.hasClass("ui-btn")&&b.buttonMarkup();else{if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.element,"c");~b[0].className.indexOf("ui-btn-left")&&
-(d="ui-btn-left");~b[0].className.indexOf("ui-btn-right")&&(d="ui-btn-right");e=this.button=a("<div></div>").text(b.text()||b.val()).insertBefore(b).buttonMarkup({theme:f.theme,icon:f.icon,iconpos:f.iconpos,inline:f.inline,corners:f.corners,shadow:f.shadow,iconshadow:f.iconshadow,mini:f.mini}).addClass(d).append(b.addClass("ui-btn-hidden"));f=b.attr("type");d=b.attr("name");f!=="button"&&f!=="reset"&&d&&b.bind("vclick",function(){g===c&&(g=a("<input>",{type:"hidden",name:b.attr("name"),value:b.attr("value")}).insertBefore(b),
-a(k).one("submit",function(){g.remove();g=c}))});b.bind({focus:function(){e.addClass(a.mobile.focusClass)},blur:function(){e.removeClass(a.mobile.focusClass)}});this.refresh()}},enable:function(){this.element.attr("disabled",false);this.button.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.button.addClass("ui-disabled").attr("aria-disabled",true);return this._setOption("disabled",true)},refresh:function(){var b=
-this.element;b.prop("disabled")?this.disable():this.enable();a(this.button.data("buttonElements").text).text(b.text()||b.val())}});a(k).bind("pagecreate create",function(b){a.mobile.button.prototype.enhanceWithin(b.target,true)})})(jQuery);(function(a){a.fn.controlgroup=function(c){function b(a,b){a.removeClass("ui-btn-corner-all ui-shadow").eq(0).addClass(b[0]).end().last().addClass(b[1]).addClass("ui-controlgroup-last")}return this.each(function(){var e=a(this),f=a.extend({direction:e.jqmData("type")||
-"vertical",shadow:false,excludeInvisible:true,mini:e.jqmData("mini")},c),d=e.children("legend"),g=f.direction=="horizontal"?["ui-corner-left","ui-corner-right"]:["ui-corner-top","ui-corner-bottom"];e.find("input").first().attr("type");d.length&&(e.wrapInner("<div class='ui-controlgroup-controls'></div>"),a("<div role='heading' class='ui-controlgroup-label'>"+d.html()+"</div>").insertBefore(e.children(0)),d.remove());e.addClass("ui-corner-all ui-controlgroup ui-controlgroup-"+f.direction);b(e.find(".ui-btn"+
-(f.excludeInvisible?":visible":"")).not(".ui-slider-handle"),g);b(e.find(".ui-btn-inner"),g);f.shadow&&e.addClass("ui-shadow");f.mini&&e.addClass("ui-mini")})}})(jQuery);(function(a){a(k).bind("pagecreate create",function(c){a(c.target).find("a").jqmEnhanceable().not(".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')").addClass("ui-link")})})(jQuery);(function(a){var c=a("meta[name=viewport]"),b=c.attr("content"),e=b+",maximum-scale=1, user-scalable=no",f=b+",maximum-scale=10, user-scalable=yes",
-d=/(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test(b);a.mobile.zoom=a.extend({},{enabled:!d,locked:false,disable:function(b){if(!d&&!a.mobile.zoom.locked)c.attr("content",e),a.mobile.zoom.enabled=false,a.mobile.zoom.locked=b||false},enable:function(b){if(!d&&(!a.mobile.zoom.locked||b===true))c.attr("content",f),a.mobile.zoom.enabled=true,a.mobile.zoom.locked=false},restore:function(){if(!d)c.attr("content",b),a.mobile.zoom.enabled=true}})})(jQuery);(function(a){a.widget("mobile.textinput",
-a.mobile.widget,{options:{theme:null,preventFocusZoom:/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1,initSelector:"input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type])",
-clearSearchButtonText:"clear text"},_create:function(){var c=this.element,b=this.options,e=b.theme||a.mobile.getInheritedTheme(this.element,"c"),f=" ui-body-"+e,d=c.jqmData("mini")==true,g=d?" ui-mini":"",h,j;a("label[for='"+c.attr("id")+"']").addClass("ui-input-text");h=c.addClass("ui-input-text ui-body-"+e);typeof c[0].autocorrect!=="undefined"&&!a.support.touchOverflow&&(c[0].setAttribute("autocorrect","off"),c[0].setAttribute("autocomplete","off"));c.is("[type='search'],:jqmData(type='search')")?
-(h=c.wrap("<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield"+f+g+"'></div>").parent(),j=a("<a href='#' class='ui-input-clear' title='"+b.clearSearchButtonText+"'>"+b.clearSearchButtonText+"</a>").bind("click",function(a){c.val("").focus().trigger("change");j.addClass("ui-input-clear-hidden");a.preventDefault()}).appendTo(h).buttonMarkup({icon:"delete",iconpos:"notext",corners:true,shadow:true,mini:d}),e=function(){setTimeout(function(){j.toggleClass("ui-input-clear-hidden",
-!c.val())},0)},e(),c.bind("paste cut keyup focus change blur",e)):c.addClass("ui-corner-all ui-shadow-inset"+f+g);c.focus(function(){h.addClass(a.mobile.focusClass)}).blur(function(){h.removeClass(a.mobile.focusClass)}).bind("focus",function(){b.preventFocusZoom&&a.mobile.zoom.disable(true)}).bind("blur",function(){b.preventFocusZoom&&a.mobile.zoom.enable(true)});if(c.is("textarea")){var o=function(){var a=c[0].scrollHeight;c[0].clientHeight<a&&c.height(a+15)},m;c.keyup(function(){clearTimeout(m);
-m=setTimeout(o,100)});a(k).one("pagechange",o);a.trim(c.val())&&a(s).load(o)}},disable:function(){(this.element.attr("disabled",true).is("[type='search'],:jqmData(type='search')")?this.element.parent():this.element).addClass("ui-disabled")},enable:function(){(this.element.attr("disabled",false).is("[type='search'],:jqmData(type='search')")?this.element.parent():this.element).removeClass("ui-disabled")}});a(k).bind("pagecreate create",function(c){a.mobile.textinput.prototype.enhanceWithin(c.target,
-true)})})(jQuery);(function(a){a.mobile.listview.prototype.options.filter=false;a.mobile.listview.prototype.options.filterPlaceholder="Filter items...";a.mobile.listview.prototype.options.filterTheme="c";a.mobile.listview.prototype.options.filterCallback=function(a,b){return a.toLowerCase().indexOf(b)===-1};a(k).delegate(":jqmData(role='listview')","listviewcreate",function(){var c=a(this),b=c.data("listview");if(b.options.filter){var e=a("<form>",{"class":"ui-listview-filter ui-bar-"+b.options.filterTheme,
-role:"search"});a("<input>",{placeholder:b.options.filterPlaceholder}).attr("data-"+a.mobile.ns+"type","search").jqmData("lastval","").bind("keyup change",function(){var e=a(this),d=this.value.toLowerCase(),g=null,g=e.jqmData("lastval")+"",h=false,j="";e.jqmData("lastval",d);g=d.length<g.length||d.indexOf(g)!==0?c.children():c.children(":not(.ui-screen-hidden)");if(d){for(var k=g.length-1;k>=0;k--)e=a(g[k]),j=e.jqmData("filtertext")||e.text(),e.is("li:jqmData(role=list-divider)")?(e.toggleClass("ui-filter-hidequeue",
-!h),h=false):b.options.filterCallback(j,d)?e.toggleClass("ui-filter-hidequeue",true):h=true;g.filter(":not(.ui-filter-hidequeue)").toggleClass("ui-screen-hidden",false);g.filter(".ui-filter-hidequeue").toggleClass("ui-screen-hidden",true).toggleClass("ui-filter-hidequeue",false)}else g.toggleClass("ui-screen-hidden",false);b._refreshCorners()}).appendTo(e).textinput();b.options.inset&&e.addClass("ui-listview-filter-inset");e.bind("submit",function(){return false}).insertBefore(c)}})})(jQuery);(function(a,
-c){a.widget("mobile.slider",a.mobile.widget,{options:{theme:null,trackTheme:null,disabled:false,initSelector:"input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",mini:false},_create:function(){var b=this,e=this.element,f=a.mobile.getInheritedTheme(e,"c"),d=this.options.theme||f,f=this.options.trackTheme||f,g=e[0].nodeName.toLowerCase(),h=g=="select"?"ui-slider-switch":"",j=e.attr("id"),o=j+"-label",j=a("[for='"+j+"']").attr("id",o),m=function(){return g=="input"?parseFloat(e.val()):
-e[0].selectedIndex},p=g=="input"?parseFloat(e.attr("min")):0,l=g=="input"?parseFloat(e.attr("max")):e.find("option").length-1,r=s.parseFloat(e.attr("step")||1),n=this.options.inline||e.jqmData("inline")==true?" ui-slider-inline":"",q=this.options.mini||e.jqmData("mini")?" ui-slider-mini":"",t=k.createElement("a"),x=a(t),u=k.createElement("div"),w=a(u),v=e.jqmData("highlight")&&g!="select"?function(){var b=k.createElement("div");b.className="ui-slider-bg ui-btn-active ui-btn-corner-all";return a(b).prependTo(w)}():
-false;t.setAttribute("href","#");u.setAttribute("role","application");u.className=["ui-slider ",h," ui-btn-down-",f," ui-btn-corner-all",n,q].join("");t.className="ui-slider-handle";u.appendChild(t);x.buttonMarkup({corners:true,theme:d,shadow:true}).attr({role:"slider","aria-valuemin":p,"aria-valuemax":l,"aria-valuenow":m(),"aria-valuetext":m(),title:m(),"aria-labelledby":o});a.extend(this,{slider:w,handle:x,valuebg:v,dragging:false,beforeStart:null,userModified:false,mouseMoved:false});if(g=="select"){d=
-k.createElement("div");d.className="ui-slider-inneroffset";h=0;for(o=u.childNodes.length;h<o;h++)d.appendChild(u.childNodes[h]);u.appendChild(d);x.addClass("ui-slider-handle-snapping");u=e.find("option");d=0;for(h=u.length;d<h;d++)o=!d?"b":"a",n=!d?" ui-btn-down-"+f:" "+a.mobile.activeBtnClass,k.createElement("div"),q=k.createElement("span"),q.className=["ui-slider-label ui-slider-label-",o,n," ui-btn-corner-all"].join(""),q.setAttribute("role","img"),q.appendChild(k.createTextNode(u[d].innerHTML)),
-a(q).prependTo(w);b._labels=a(".ui-slider-label",w)}j.addClass("ui-slider");e.addClass(g==="input"?"ui-slider-input":"ui-slider-switch").change(function(){b.mouseMoved||b.refresh(m(),true)}).keyup(function(){b.refresh(m(),true,true)}).blur(function(){b.refresh(m(),true)});a(k).bind("vmousemove",function(a){if(b.dragging)return b.mouseMoved=true,g==="select"&&x.removeClass("ui-slider-handle-snapping"),b.refresh(a),b.userModified=b.beforeStart!==e[0].selectedIndex,false});w.bind("vmousedown",function(a){b.dragging=
-true;b.userModified=false;b.mouseMoved=false;if(g==="select")b.beforeStart=e[0].selectedIndex;b.refresh(a);return false}).bind("vclick",false);w.add(k).bind("vmouseup",function(){if(b.dragging)return b.dragging=false,g==="select"&&(x.addClass("ui-slider-handle-snapping"),b.mouseMoved?b.userModified?b.refresh(b.beforeStart==0?1:0):b.refresh(b.beforeStart):b.refresh(b.beforeStart==0?1:0)),b.mouseMoved=false});w.insertAfter(e);g=="select"&&this.handle.bind({focus:function(){w.addClass(a.mobile.focusClass)},
-blur:function(){w.removeClass(a.mobile.focusClass)}});this.handle.bind({vmousedown:function(){a(this).focus()},vclick:false,keydown:function(c){var d=m();if(!b.options.disabled){switch(c.keyCode){case a.mobile.keyCode.HOME:case a.mobile.keyCode.END:case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:if(c.preventDefault(),!b._keySliding)b._keySliding=true,a(this).addClass("ui-state-active")}switch(c.keyCode){case a.mobile.keyCode.HOME:b.refresh(p);
-break;case a.mobile.keyCode.END:b.refresh(l);break;case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:b.refresh(d+r);break;case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:b.refresh(d-r)}}},keyup:function(){if(b._keySliding)b._keySliding=false,a(this).removeClass("ui-state-active")}});this.refresh(c,c,true)},refresh:function(b,c,f){(this.options.disabled||this.element.attr("disabled"))&&this.disable();var d=this.element,g=d[0].nodeName.toLowerCase(),
-h=g==="input"?parseFloat(d.attr("min")):0,j=g==="input"?parseFloat(d.attr("max")):d.find("option").length-1,k=g==="input"&&parseFloat(d.attr("step"))>0?parseFloat(d.attr("step")):1;if(typeof b==="object"){if(!this.dragging||b.pageX<this.slider.offset().left-8||b.pageX>this.slider.offset().left+this.slider.width()+8)return;b=Math.round((b.pageX-this.slider.offset().left)/this.slider.width()*100)}else b==null&&(b=g==="input"?parseFloat(d.val()||0):d[0].selectedIndex),b=(parseFloat(b)-h)/(j-h)*100;if(!isNaN(b)){b<
-0&&(b=0);b>100&&(b=100);var m=b/100*(j-h)+h,p=(m-h)%k;m-=p;Math.abs(p)*2>=k&&(m+=p>0?k:-k);m=parseFloat(m.toFixed(5));m<h&&(m=h);m>j&&(m=j);this.handle.css("left",b+"%");this.handle.attr({"aria-valuenow":g==="input"?m:d.find("option").eq(m).attr("value"),"aria-valuetext":g==="input"?m:d.find("option").eq(m).getEncodedText(),title:g==="input"?m:d.find("option").eq(m).getEncodedText()});this.valuebg&&this.valuebg.css("width",b+"%");if(this._labels){var h=this.handle.width()/this.slider.width()*100,
-l=b&&h+(100-h)*b/100,r=b===100?0:Math.min(h+100-l,100);this._labels.each(function(){var b=a(this).is(".ui-slider-label-a");a(this).width((b?l:r)+"%")})}if(!f)f=false,g==="input"?(f=d.val()!==m,d.val(m)):(f=d[0].selectedIndex!==m,d[0].selectedIndex=m),!c&&f&&d.trigger("change")}},enable:function(){this.element.attr("disabled",false);this.slider.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.slider.addClass("ui-disabled").attr("aria-disabled",
-true);return this._setOption("disabled",true)}});a(k).bind("pagecreate create",function(b){a.mobile.slider.prototype.enhanceWithin(b.target,true)})})(jQuery);(function(a){a.widget("mobile.selectmenu",a.mobile.widget,{options:{theme:null,disabled:false,icon:"arrow-d",iconpos:"right",inline:false,corners:true,shadow:true,iconshadow:true,overlayTheme:"a",hidePlaceholderMenuItems:true,closeText:"Close",nativeMenu:true,preventFocusZoom:/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>
--1,initSelector:"select:not(:jqmData(role='slider'))",mini:false},_button:function(){return a("<div/>")},_setDisabled:function(a){this.element.attr("disabled",a);this.button.attr("aria-disabled",a);return this._setOption("disabled",a)},_focusButton:function(){var a=this;setTimeout(function(){a.button.focus()},40)},_selectOptions:function(){return this.select.find("option")},_preExtension:function(){var c="";~this.element[0].className.indexOf("ui-btn-left")&&(c=" ui-btn-left");~this.element[0].className.indexOf("ui-btn-right")&&
-(c=" ui-btn-right");this.select=this.element.wrap("<div class='ui-select"+c+"'>");this.selectID=this.select.attr("id");this.label=a("label[for='"+this.selectID+"']").addClass("ui-select");this.isMultiple=this.select[0].multiple;if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.select,"c")},_create:function(){this._preExtension();this._trigger("beforeCreate");this.button=this._button();var c=this,b=this.options,e=this.button.text(a(this.select[0].options.item(this.select[0].selectedIndex==
--1?0:this.select[0].selectedIndex)).text()).insertBefore(this.select).buttonMarkup({theme:b.theme,icon:b.icon,iconpos:b.iconpos,inline:b.inline,corners:b.corners,shadow:b.shadow,iconshadow:b.iconshadow,mini:b.mini});b.nativeMenu&&s.opera&&s.opera.version&&this.select.addClass("ui-select-nativeonly");if(this.isMultiple)this.buttonCount=a("<span>").addClass("ui-li-count ui-btn-up-c ui-btn-corner-all").hide().appendTo(e.addClass("ui-li-has-count"));(b.disabled||this.element.attr("disabled"))&&this.disable();
-this.select.change(function(){c.refresh()});this.build()},build:function(){var c=this;this.select.appendTo(c.button).bind("vmousedown",function(){c.button.addClass(a.mobile.activeBtnClass)}).bind("focus",function(){c.button.addClass(a.mobile.focusClass)}).bind("blur",function(){c.button.removeClass(a.mobile.focusClass)}).bind("focus vmouseover",function(){c.button.trigger("vmouseover")}).bind("vmousemove",function(){c.button.removeClass(a.mobile.activeBtnClass)}).bind("change blur vmouseout",function(){c.button.trigger("vmouseout").removeClass(a.mobile.activeBtnClass)}).bind("change blur",
-function(){c.button.removeClass("ui-btn-down-"+c.options.theme)});c.button.bind("vmousedown",function(){c.options.preventFocusZoom&&a.mobile.zoom.disable(true)}).bind("mouseup",function(){c.options.preventFocusZoom&&a.mobile.zoom.enable(true)})},selected:function(){return this._selectOptions().filter(":selected")},selectedIndices:function(){var a=this;return this.selected().map(function(){return a._selectOptions().index(this)}).get()},setButtonText:function(){var c=this,b=this.selected();this.button.find(".ui-btn-text").text(function(){return!c.isMultiple?
-b.text():b.length?b.map(function(){return a(this).text()}).get().join(", "):c.placeholder})},setButtonCount:function(){var a=this.selected();this.isMultiple&&this.buttonCount[a.length>1?"show":"hide"]().text(a.length)},refresh:function(){this.setButtonText();this.setButtonCount()},open:a.noop,close:a.noop,disable:function(){this._setDisabled(true);this.button.addClass("ui-disabled")},enable:function(){this._setDisabled(false);this.button.removeClass("ui-disabled")}});a(k).bind("pagecreate create",
-function(c){a.mobile.selectmenu.prototype.enhanceWithin(c.target,true)})})(jQuery);(function(a){var c=function(b){var c=b.selectID,f=b.label,d=b.select.closest(".ui-page"),g=a("<div>",{"class":"ui-selectmenu-screen ui-screen-hidden"}).appendTo(d),h=b._selectOptions(),j=b.isMultiple=b.select[0].multiple,o=c+"-button",m=c+"-menu",p=a("<div data-"+a.mobile.ns+"role='dialog' data-"+a.mobile.ns+"theme='"+b.options.theme+"' data-"+a.mobile.ns+"overlay-theme='"+b.options.overlayTheme+"'><div data-"+a.mobile.ns+
-"role='header'><div class='ui-title'>"+f.getEncodedText()+"</div></div><div data-"+a.mobile.ns+"role='content'></div></div>"),l=a("<div>",{"class":"ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-"+b.options.overlayTheme+" "+a.mobile.defaultDialogTransition}).insertAfter(g),r=a("<ul>",{"class":"ui-selectmenu-list",id:m,role:"listbox","aria-labelledby":o}).attr("data-"+a.mobile.ns+"theme",b.options.theme).appendTo(l),n=a("<div>",{"class":"ui-header ui-bar-"+b.options.theme}).prependTo(l),
-q=a("<h1>",{"class":"ui-title"}).appendTo(n),t;b.isMultiple&&(t=a("<a>",{text:b.options.closeText,href:"#","class":"ui-btn-left"}).attr("data-"+a.mobile.ns+"iconpos","notext").attr("data-"+a.mobile.ns+"icon","delete").appendTo(n).buttonMarkup());a.extend(b,{select:b.select,selectID:c,buttonId:o,menuId:m,thisPage:d,menuPage:p,label:f,screen:g,selectOptions:h,isMultiple:j,theme:b.options.theme,listbox:l,list:r,header:n,headerTitle:q,headerClose:t,menuPageContent:void 0,menuPageClose:void 0,placeholder:"",
-build:function(){var c=this;c.refresh();c.select.attr("tabindex","-1").focus(function(){a(this).blur();c.button.focus()});c.button.bind("vclick keydown",function(b){if(b.type=="vclick"||b.keyCode&&(b.keyCode===a.mobile.keyCode.ENTER||b.keyCode===a.mobile.keyCode.SPACE))c.open(),b.preventDefault()});c.list.attr("role","listbox").bind("focusin",function(b){a(b.target).attr("tabindex","0").trigger("vmouseover")}).bind("focusout",function(b){a(b.target).attr("tabindex","-1").trigger("vmouseout")}).delegate("li:not(.ui-disabled, .ui-li-divider)",
-"click",function(b){var d=c.select[0].selectedIndex,e=c.list.find("li:not(.ui-li-divider)").index(this),f=c._selectOptions().eq(e)[0];f.selected=c.isMultiple?!f.selected:true;c.isMultiple&&a(this).find(".ui-icon").toggleClass("ui-icon-checkbox-on",f.selected).toggleClass("ui-icon-checkbox-off",!f.selected);(c.isMultiple||d!==e)&&c.select.trigger("change");c.isMultiple||c.close();b.preventDefault()}).keydown(function(c){var d=a(c.target),e=d.closest("li");switch(c.keyCode){case 38:return c=e.prev().not(".ui-selectmenu-placeholder"),
-c.is(".ui-li-divider")&&(c=c.prev()),c.length&&(d.blur().attr("tabindex","-1"),c.addClass("ui-btn-down-"+b.options.theme).find("a").first().focus()),false;case 40:return c=e.next(),c.is(".ui-li-divider")&&(c=c.next()),c.length&&(d.blur().attr("tabindex","-1"),c.addClass("ui-btn-down-"+b.options.theme).find("a").first().focus()),false;case 13:case 32:return d.trigger("click"),false}});c.menuPage.bind("pagehide",function(){c.list.appendTo(c.listbox);c._focusButton();a.mobile._bindPageRemove.call(c.thisPage)});
-c.screen.bind("vclick",function(){c.close()});c.isMultiple&&c.headerClose.click(function(){if(c.menuType=="overlay")return c.close(),false});c.thisPage.addDependents(this.menuPage)},_isRebuildRequired:function(){var a=this.list.find("li");return this._selectOptions().text()!==a.text()},refresh:function(b){var c=this;this._selectOptions();this.selected();var d=this.selectedIndices();(b||this._isRebuildRequired())&&c._buildList();c.setButtonText();c.setButtonCount();c.list.find("li:not(.ui-li-divider)").removeClass(a.mobile.activeBtnClass).attr("aria-selected",
-false).each(function(b){a.inArray(b,d)>-1&&(b=a(this),b.attr("aria-selected",true),c.isMultiple?b.find(".ui-icon").removeClass("ui-icon-checkbox-off").addClass("ui-icon-checkbox-on"):b.is(".ui-selectmenu-placeholder")?b.next().addClass(a.mobile.activeBtnClass):b.addClass(a.mobile.activeBtnClass))})},close:function(){if(!this.options.disabled&&this.isOpen)this.menuType=="page"?s.history.back():(this.screen.addClass("ui-screen-hidden"),this.listbox.addClass("ui-selectmenu-hidden").removeAttr("style").removeClass("in"),
-this.list.appendTo(this.listbox),this._focusButton()),this.isOpen=false},open:function(){function b(){c.list.find("."+a.mobile.activeBtnClass+" a").focus()}if(!this.options.disabled){var c=this,d=a(s),e=c.list.parent(),f=e.outerHeight(),e=e.outerWidth();a(".ui-page-active");var g=d.scrollTop(),j=c.button.offset().top,h=d.height(),d=d.width();c.button.addClass(a.mobile.activeBtnClass);setTimeout(function(){c.button.removeClass(a.mobile.activeBtnClass)},300);if(f>h-80||!a.support.scrollTop){c.menuPage.appendTo(a.mobile.pageContainer).page();
-c.menuPageContent=p.find(".ui-content");c.menuPageClose=p.find(".ui-header a");c.thisPage.unbind("pagehide.remove");if(g==0&&j>h)c.thisPage.one("pagehide",function(){a(this).jqmData("lastScroll",j)});c.menuPage.one("pageshow",function(){b();c.isOpen=true});c.menuType="page";c.menuPageContent.append(c.list);c.menuPage.find("div .ui-title").text(c.label.text());a.mobile.changePage(c.menuPage,{transition:a.mobile.defaultDialogTransition})}else{c.menuType="overlay";c.screen.height(a(k).height()).removeClass("ui-screen-hidden");
-var l=j-g,m=g+h-j,n=f/2,o=parseFloat(c.list.parent().css("max-width")),f=l>f/2&&m>f/2?j+c.button.outerHeight()/2-n:l>m?g+h-f-30:g+30;e<o?g=(d-e)/2:(g=c.button.offset().left+c.button.outerWidth()/2-e/2,g<30?g=30:g+e>d&&(g=d-e-30));c.listbox.append(c.list).removeClass("ui-selectmenu-hidden").css({top:f,left:g}).addClass("in");b();c.isOpen=true}}},_buildList:function(){var b=this.options,c=this.placeholder,d=true,e=this.isMultiple?"checkbox-off":"false";this.list.empty().filter(".ui-listview").listview("destroy");
-var f=this.select.find("option"),g=f.length,j=this.select[0],h="data-"+a.mobile.ns,l=h+"option-index",m=h+"icon";h+="role";for(var n=k.createDocumentFragment(),o,p=0;p<g;p++){var r=f[p],q=a(r),s=r.parentNode,t=q.text(),D=k.createElement("a"),J=[];D.setAttribute("href","#");D.appendChild(k.createTextNode(t));s!==j&&s.nodeName.toLowerCase()==="optgroup"&&(s=s.getAttribute("label"),s!=o&&(o=k.createElement("li"),o.setAttribute(h,"list-divider"),o.setAttribute("role","option"),o.setAttribute("tabindex",
-"-1"),o.appendChild(k.createTextNode(s)),n.appendChild(o),o=s));if(d&&(!r.getAttribute("value")||t.length==0||q.jqmData("placeholder")))if(d=false,b.hidePlaceholderMenuItems&&J.push("ui-selectmenu-placeholder"),!c)c=this.placeholder=t;q=k.createElement("li");r.disabled&&(J.push("ui-disabled"),q.setAttribute("aria-disabled",true));q.setAttribute(l,p);q.setAttribute(m,e);q.className=J.join(" ");q.setAttribute("role","option");D.setAttribute("tabindex","-1");q.appendChild(D);n.appendChild(q)}this.list[0].appendChild(n);
-!this.isMultiple&&!c.length?this.header.hide():this.headerTitle.text(this.placeholder);this.list.listview()},_button:function(){return a("<a>",{href:"#",role:"button",id:this.buttonId,"aria-haspopup":"true","aria-owns":this.menuId})}})};a(k).bind("selectmenubeforecreate",function(b){b=a(b.target).data("selectmenu");b.options.nativeMenu||c(b)})})(jQuery);(function(a){a.widget("mobile.fixedtoolbar",a.mobile.widget,{options:{visibleOnPageShow:true,disablePageZoom:true,transition:"slide",fullscreen:false,
-tapToggle:true,tapToggleBlacklist:"a, input, select, textarea, .ui-header-fixed, .ui-footer-fixed",hideDuringFocus:"input, textarea, select",updatePagePadding:true,trackPersistentToolbars:true,supportBlacklist:function(){var a=s,b=navigator.userAgent,e=navigator.platform,f=b.match(/AppleWebKit\/([0-9]+)/),f=!!f&&f[1],d=b.match(/Fennec\/([0-9]+)/),d=!!d&&d[1],g=b.match(/Opera Mobi\/([0-9]+)/),h=!!g&&g[1];return(e.indexOf("iPhone")>-1||e.indexOf("iPad")>-1||e.indexOf("iPod")>-1)&&f&&f<534||a.operamini&&
-{}.toString.call(a.operamini)==="[object OperaMini]"||g&&h<7458||b.indexOf("Android")>-1&&f&&f<533||d&&d<6||"palmGetResource"in s&&f&&f<534||b.indexOf("MeeGo")>-1&&b.indexOf("NokiaBrowser/8.5.0")>-1?true:false},initSelector:":jqmData(position='fixed')"},_create:function(){var a=this.options,b=this.element,e=b.is(":jqmData(role='header')")?"header":"footer",f=b.closest(".ui-page");a.supportBlacklist()?this.destroy():(b.addClass("ui-"+e+"-fixed"),a.fullscreen?(b.addClass("ui-"+e+"-fullscreen"),f.addClass("ui-page-"+
-e+"-fullscreen")):f.addClass("ui-page-"+e+"-fixed"),this._addTransitionClass(),this._bindPageEvents(),this._bindToggleHandlers())},_addTransitionClass:function(){var a=this.options.transition;a&&a!=="none"&&(a==="slide"&&(a=this.element.is(".ui-header")?"slidedown":"slideup"),this.element.addClass(a))},_bindPageEvents:function(){var c=this,b=c.options;c.element.closest(".ui-page").bind("pagebeforeshow",function(){b.disablePageZoom&&a.mobile.zoom.disable(true);b.visibleOnPageShow||c.hide(true)}).bind("webkitAnimationStart animationstart updatelayout",
-function(){b.updatePagePadding&&c.updatePagePadding()}).bind("pageshow",function(){c.updatePagePadding();b.updatePagePadding&&a(s).bind("throttledresize."+c.widgetName,function(){c.updatePagePadding()})}).bind("pagebeforehide",function(e,f){b.disablePageZoom&&a.mobile.zoom.enable(true);b.updatePagePadding&&a(s).unbind("throttledresize."+c.widgetName);if(b.trackPersistentToolbars){var d=a(".ui-footer-fixed:jqmData(id)",this),g=a(".ui-header-fixed:jqmData(id)",this),h=d.length&&f.nextPage&&a(".ui-footer-fixed:jqmData(id='"+
-d.jqmData("id")+"')",f.nextPage),j=g.length&&f.nextPage&&a(".ui-header-fixed:jqmData(id='"+g.jqmData("id")+"')",f.nextPage),h=h||a();if(h.length||j.length)h.add(j).appendTo(a.mobile.pageContainer),f.nextPage.one("pageshow",function(){h.add(j).appendTo(this)})}})},_visible:true,updatePagePadding:function(){var a=this.element,b=a.is(".ui-header");this.options.fullscreen||a.closest(".ui-page").css("padding-"+(b?"top":"bottom"),a.outerHeight())},_useTransition:function(c){var b=this.element,e=a(s).scrollTop(),
-f=b.height(),d=b.closest(".ui-page").height(),g=a.mobile.getScreenHeight(),b=b.is(":jqmData(role='header')")?"header":"footer";return!c&&(this.options.transition&&this.options.transition!=="none"&&(b==="header"&&!this.options.fullscreen&&e>f||b==="footer"&&!this.options.fullscreen&&e+g<d-f)||this.options.fullscreen)},show:function(a){var b=this.element;this._useTransition(a)?b.removeClass("out ui-fixed-hidden").addClass("in"):b.removeClass("ui-fixed-hidden");this._visible=true},hide:function(a){var b=
-this.element,e="out"+(this.options.transition==="slide"?" reverse":"");this._useTransition(a)?b.addClass(e).removeClass("in").animationComplete(function(){b.addClass("ui-fixed-hidden").removeClass(e)}):b.addClass("ui-fixed-hidden").removeClass(e);this._visible=false},toggle:function(){this[this._visible?"hide":"show"]()},_bindToggleHandlers:function(){var c=this,b=c.options;c.element.closest(".ui-page").bind("vclick",function(e){b.tapToggle&&!a(e.target).closest(b.tapToggleBlacklist).length&&c.toggle()}).bind("focusin focusout",
-function(e){if(screen.width<500&&a(e.target).is(b.hideDuringFocus)&&!a(e.target).closest(".ui-header-fixed, .ui-footer-fixed").length)c[e.type==="focusin"&&c._visible?"hide":"show"]()})},destroy:function(){this.element.removeClass("ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden");this.element.closest(".ui-page").removeClass("ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen")}});a(k).bind("pagecreate create",
-function(c){a(c.target).jqmData("fullscreen")&&a(a.mobile.fixedtoolbar.prototype.options.initSelector,c.target).not(":jqmData(fullscreen)").jqmData("fullscreen",true);a.mobile.fixedtoolbar.prototype.enhanceWithin(c.target)})})(jQuery);(function(a,c){if(/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1){var b=a.mobile.zoom,e,f,d,g,h;a(c).bind("orientationchange.iosorientationfix",b.enable).bind("devicemotion.iosorientationfix",function(a){e=a.originalEvent;
-h=e.accelerationIncludingGravity;f=Math.abs(h.x);d=Math.abs(h.y);g=Math.abs(h.z);!c.orientation&&(f>7||(g>6&&d<8||g<8&&d>6)&&f>5)?b.enabled&&b.disable():b.enabled||b.enable()})}})(jQuery,this);(function(a,c){function b(){var b=a("."+a.mobile.activeBtnClass).first();h.css({top:a.support.scrollTop&&g.scrollTop()+g.height()/2||b.length&&b.offset().top||100})}function e(){var c=h.offset(),d=g.scrollTop(),f=a.mobile.getScreenHeight();if(c.top<d||c.top-d>f)h.addClass("ui-loader-fakefix"),b(),g.unbind("scroll",
-e).bind("scroll",b)}function f(){d.removeClass("ui-mobile-rendering")}var d=a("html");a("head");var g=a(c);a(c.document).trigger("mobileinit");if(a.mobile.gradeA()){if(a.mobile.ajaxBlacklist)a.mobile.ajaxEnabled=false;d.addClass("ui-mobile ui-mobile-rendering");setTimeout(f,5E3);var h=a("<div class='ui-loader'><span class='ui-icon ui-icon-loading'></span><h1></h1></div>");a.extend(a.mobile,{showPageLoadingMsg:function(b,c,f){d.addClass("ui-loading");if(a.mobile.loadingMessage){var k=f||a.mobile.loadingMessageTextVisible;
-b=b||a.mobile.loadingMessageTheme;h.attr("class","ui-loader ui-corner-all ui-body-"+(b||"a")+" ui-loader-"+(k?"verbose":"default")+(f?" ui-loader-textonly":"")).find("h1").text(c||a.mobile.loadingMessage).end().appendTo(a.mobile.pageContainer);e();g.bind("scroll",e)}},hidePageLoadingMsg:function(){d.removeClass("ui-loading");a.mobile.loadingMessage&&h.removeClass("ui-loader-fakefix");a(c).unbind("scroll",b);a(c).unbind("scroll",e)},initializePage:function(){var b=a(":jqmData(role='page'), :jqmData(role='dialog')");
-b.length||(b=a("body").wrapInner("<div data-"+a.mobile.ns+"role='page'></div>").children(0));b.each(function(){var b=a(this);b.jqmData("url")||b.attr("data-"+a.mobile.ns+"url",b.attr("id")||location.pathname+location.search)});a.mobile.firstPage=b.first();a.mobile.pageContainer=b.first().parent().addClass("ui-mobile-viewport");g.trigger("pagecontainercreate");a.mobile.showPageLoadingMsg();f();!a.mobile.hashListeningEnabled||!a.mobile.path.stripHash(location.hash)?a.mobile.changePage(a.mobile.firstPage,
-{transition:"none",reverse:true,changeHash:false,fromHashChange:true}):g.trigger("hashchange",[true])}});a.mobile._registerInternalEvents();a(function(){c.scrollTo(0,1);a.mobile.defaultHomeScroll=!a.support.scrollTop||a(c).scrollTop()===1?0:1;a.fn.controlgroup&&a(k).bind("pagecreate create",function(b){a(":jqmData(role='controlgroup')",b.target).jqmEnhanceable().controlgroup({excludeInvisible:false})});a.mobile.autoInitializePage&&a.mobile.initializePage();g.load(a.mobile.silentScroll)})}})(jQuery,
-this)});
--- a/OrthancExplorer/libs/jquery.mobile.simpledialog.min.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-/*
- * jQuery Mobile Framework : plugin to provide a simple Dialog widget.
- * Copyright (c) JTSage
- * CC 3.0 Attribution.  May be relicensed without permission/notifcation.
- * https://github.com/jtsage/jquery-mobile-simpledialog
- */
-
-.ui-simpledialog-header h4{margin-top:5px;margin-bottom:5px;text-align:center}.ui-simpledialog-container{border:5px solid #111!important;width:85%;max-width:500px}.ui-simpledialog-screen{position:absolute;top:0;left:0;width:100%;height:100%}.ui-simpledialog-hidden{display:none}.ui-simpledialog-input{width:85%!important;display:block!important;margin-left:auto;margin-right:auto}.ui-simpledialog-screen-modal{background-color:black;-moz-opacity:.8;opacity:.80;filter:alpha(opacity=80)}.ui-simpledialog-subtitle{text-align:center}.ui-simpledialog-controls .buttons-separator{min-height:.6em}.ui-simpledialog-controls .button-hidden{display:none}.ui-dialog .ui-simpledialog-container{border:none!important}.ui-dialog-simpledialog .ui-content{padding:5px!important}
\ No newline at end of file
--- a/OrthancExplorer/libs/jquery.mobile.simpledialog2.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,380 +0,0 @@
-/*
- * jQuery Mobile Framework : plugin to provide a dialogs Widget. ver2
- * Copyright (c) JTSage
- * CC 3.0 Attribution.  May be relicensed without permission/notifcation.
- * https://github.com/jtsage/jquery-mobile-simpledialog
- */
-
-(function($, undefined ) {
-  $.widget( "mobile.simpledialog2", $.mobile.widget, {
-    options: {
-      version: '1.0.1-2012061300', // jQueryMobile-YrMoDaySerial
-      mode: 'blank', // or 'button'
-      themeDialog: 'b',
-      themeInput: false,
-      themeButtonDefault: false,
-      themeHeader: 'a',
-      
-      fullScreen: false,
-      fullScreenForce: false,
-      dialogAllow: false,
-      dialogForce: false,
-      
-      headerText: false,
-      headerClose: false,
-      buttonPrompt: false,
-      buttonInput: false,
-      buttonInputDefault: false,
-      buttonPassword: false,
-      blankContent: false,
-      blankContentAdopt: false,
-      
-      resizeListener: true,
-      safeNuke: true,
-      forceInput: true,
-      showModal: true,
-      animate: true,
-      transition: 'pop',
-      clickEvent: 'click',
-      zindex: '500',
-      width: '280px',
-      left: false,
-      top: false,
-      
-      callbackOpen: false,
-      callbackOpenArgs: [],
-      callbackClose: false,
-      callbackCloseArgs: []
-    },
-    _eventHandler: function(e,p) {
-      // Handle the triggers
-      var self = e.data.widget,
-      o = e.data.widget.options;
-      
-      if ( ! e.isPropagationStopped() ) {
-	switch (p.method) {
-	case 'close':
-	  self.close();
-	  break;
-	case 'html':
-	  self.updateBlank(p.source);
-	  break;
-	}
-      }
-    },
-    _create: function () {
-      var self = this,
-      o = $.extend(this.options, this.element.jqmData('options')),
-      initDate = new Date(),
-      content = $("<div class='ui-simpledialog-container ui-overlay-shadow ui-corner-all ui-simpledialog-hidden " + 
-		  ((o.animate === true) ? o.transition : '') + " ui-body-" + o.themeDialog + "'></div>");
-      
-      if ( o.themeButtonDefault === false ) { o.themeButtonDefault = o.themeDialog; }
-      if ( o.themeInput === false ) { o.themeInput = o.themeDialog; }
-      $.mobile.sdCurrentDialog = self;
-      if ( typeof $.mobile.sdLastInput !== 'undefined' ) { delete $.mobile.sdLastInput; }
-      self.internalID = initDate.getTime();
-      self.displayAnchor = $.mobile.activePage.children('.ui-content').first();
-      if ( self.displayAnchor.length === 0 ) { self.displayAnchor = $.mobile.activePage; }
-      
-      self.dialogPage = $("<div data-role='dialog' data-theme='" + o.themeDialog + "'><div data-role='header'></div><div data-role='content'></div></div>");
-      self.sdAllContent = self.dialogPage.find('[data-role=content]');
-      
-      content.appendTo(self.sdAllContent);
-      
-      self.sdIntContent = self.sdAllContent.find('.ui-simpledialog-container');
-      self.sdIntContent.css('width', o.width);
-      
-      if ( o.headerText !== false || o.headerClose !== false ) {
-	self.sdHeader = $('<div style="margin-bottom: 4px;" class="ui-header ui-bar-'+o.themeHeader+'"></div>');
-	if ( o.headerClose === true ) {
-	  $("<a class='ui-btn-left' rel='close' href='#'>Close</a>").appendTo(self.sdHeader).buttonMarkup({ theme  : o.themeHeader, icon   : 'delete', iconpos: 'notext', corners: true, shadow : true });
-	}
-	$('<h1 class="ui-title">'+((o.headerText !== false)?o.headerText:'')+'</h1>').appendTo(self.sdHeader);
-	self.sdHeader.appendTo(self.sdIntContent);
-      }
-      
-      if ( o.mode === 'blank' ) {
-	if ( o.blankContent === true ) {
-	  if ( o.blankContentAdopt === true ) {
-	    o.blankContent = self.element.children();
-	  } else {
-	    o.blankContent = self.element.html();
-	  }
-	}
-	$(o.blankContent).appendTo(self.sdIntContent);
-      } else if ( o.mode === 'button' ) {
-	self._makeButtons().appendTo(self.sdIntContent);
-      }
-      
-      self.sdIntContent.appendTo(self.displayAnchor.parent());
-      
-      self.dialogPage.appendTo( $.mobile.pageContainer )
-	.page().css('minHeight', '0px').css('zIndex', o.zindex);
-      
-      if ( o.animate === true ) { self.dialogPage.addClass(o.transition); }
-      
-      self.screen = $("<div>", {'class':'ui-simpledialog-screen ui-simpledialog-hidden'})
-	.css('z-index', (o.zindex-1))
-	.appendTo(self.displayAnchor.parent())
-	.bind(o.clickEvent, function(event){
-	  if ( !o.forceInput ) {
-	    self.close();
-	  }
-	  event.preventDefault();
-	});
-
-      if ( o.showModal ) { self.screen.addClass('ui-simpledialog-screen-modal'); }
-      
-      $(document).bind('simpledialog.'+self.internalID, {widget:self}, function(e,p) { self._eventHandler(e,p); });
-    },
-    _makeButtons: function () {
-      var self = this,
-      o = self.options,
-      buttonHTML = $('<div></div>'),
-      pickerInput = $("<div class='ui-simpledialog-controls'><input class='ui-simpledialog-input ui-input-text ui-shadow-inset ui-corner-all ui-body-"+o.themeInput+"' type='"+((o.buttonPassword===true)?"password":"text")+"' value='"+((o.buttonInputDefault!==false)?o.buttonInputDefault.replace( '"', "&#34;" ).replace( "'", "&#39;" ):"")+"' name='pickin' /></div>"),
-      pickerChoice = $("<div>", { "class":'ui-simpledialog-controls' });
-      
-      
-      if ( o.buttonPrompt !== false ) {
-	self.buttonPromptText = $("<p class='ui-simpledialog-subtitle'>"+o.buttonPrompt+"</p>").appendTo(buttonHTML);
-      }
-      
-      if ( o.buttonInput !== false ) {
-	$.mobile.sdLastInput = "";
-	pickerInput.appendTo(buttonHTML);
-	pickerInput.find('input').bind('change', function () {
-	  $.mobile.sdLastInput = pickerInput.find('input').first().val();
-	  self.thisInput = pickerInput.find('input').first().val();
-	});
-      }
-      
-      pickerChoice.appendTo(buttonHTML);
-      
-      self.butObj = [];
-      
-      $.each(o.buttons, function(name, props) {
-	props = $.isFunction( props ) ? { click: props } : props;
-	props = $.extend({
-	  text   : name,
-	  id     : name + self.internalID,
-	  theme  : o.themeButtonDefault,
-	  icon   : 'check',
-	  iconpos: 'left',
-	  corners: 'true',
-	  shadow : 'true',
-	  args   : [],
-	  close  : true
-	}, props);
-	
-	self.butObj.push($("<a href='#'>"+name+"</a>")
-			 .appendTo(pickerChoice)
-			 .attr('id', props.id)
-			 .buttonMarkup({
-			   theme  : props.theme,
-			   icon   : props.icon,
-			   iconpos: props.iconpos,
-			   corners: props.corners,
-			   shadow : props.shadow
-			 }).unbind("vclick click")
-			 .bind(o.clickEvent, function() {
-			   if ( o.buttonInput ) { self.sdIntContent.find('input [name=pickin]').trigger('change'); }
-			   var returnValue = props.click.apply(self, $.merge(arguments, props.args));
-			   if ( returnValue !== false && props.close === true ) {
-			     self.close();
-			   }
-			 })
-			);
-      });
-      
-      return buttonHTML;
-    },
-    _getCoords: function(widget) {
-      var self = widget,
-      docWinWidth   = $.mobile.activePage.width(),
-      docWinHighOff = $(window).scrollTop(),
-      docWinHigh    = $(window).height(),
-      diaWinWidth   = widget.sdIntContent.innerWidth(),
-      diaWinHigh    = widget.sdIntContent.outerHeight(),
-      
-      coords        = {
-	'high'    : $(window).height(),
-	'width'   : $.mobile.activePage.width(),
-	'fullTop' : $(window).scrollTop(),
-	'fullLeft': $(window).scrollLeft(),
-	'winTop'  : docWinHighOff + ((widget.options.top !== false) ? widget.options.top : (( docWinHigh / 2 ) - ( diaWinHigh / 2 ) )),
-	'winLeft' : ((widget.options.left !== false) ? widget.options.left : (( docWinWidth / 2 ) - ( diaWinWidth / 2 ) ))
-      };
-      
-      if ( coords.winTop < 45 ) { coords.winTop = 45; }
-      
-      return coords;
-    },
-    _orientChange: function(e) {
-      var self = e.data.widget,
-      o = e.data.widget.options,
-      coords = e.data.widget._getCoords(e.data.widget);
-      
-      e.stopPropagation();
-      
-      if ( self.isDialog === true ) {
-	return true;
-      } else {
-	if ( o.fullScreen === true && ( coords.width < 400 || o.fullScreenForce === true ) ) {
-	  self.sdIntContent.css({'border': 'none', 'position': 'absolute', 'top': coords.fullTop, 'left': coords.fullLeft, 'height': coords.high, 'width': coords.width, 'maxWidth': coords.width }).removeClass('ui-simpledialog-hidden');
-	} else {
-	  self.sdIntContent.css({'position': 'absolute', 'top': coords.winTop, 'left': coords.winLeft}).removeClass('ui-simpledialog-hidden');
-	}
-      }
-    },
-    repos: function() {
-      var bsEvent = { data: {widget:this}, stopPropagation: function () { return true; }};
-      this._orientChange(bsEvent);
-    },
-    open: function() {
-      var self = this,
-      o = this.options,
-      coords = this._getCoords(this);
-      
-      self.sdAllContent.find('.ui-btn-active').removeClass('ui-btn-active');
-      self.sdIntContent.delegate('[rel=close]', o.clickEvent, function (e) { e.preventDefault(); self.close(); });
-      
-      if ( ( o.dialogAllow === true && coords.width < 400 ) || o.dialogForce ) {
-	self.isDialog = true;
-	
-	if ( o.mode === 'blank' ) { // Custom selects do not play well with dialog mode - so, we turn them off.
-	  self.sdIntContent.find('select').each(function () {
-	    $(this).jqmData('nativeMenu', true);
-	  });
-	}
-	
-	self.displayAnchor.parent().unbind("pagehide.remove");
-	self.sdAllContent.append(self.sdIntContent);
-	self.sdAllContent.trigger('create');
-	if ( o.headerText !== false ) {
-	  self.sdHeader.find('h1').appendTo(self.dialogPage.find('[data-role=header]'));
-	  self.sdIntContent.find('.ui-header').empty().removeClass();
-	}
-	if ( o.headerClose === true ) {
-	  self.dialogPage.find('.ui-header a').bind('click', function () {
-	    setTimeout("$.mobile.sdCurrentDialog.destroy();", 1000);
-	  });
-	} else {
-	  self.dialogPage.find('.ui-header a').remove();
-	}
-	
-	self.sdIntContent.removeClass().css({'top': 'auto', 'width': 'auto', 'left': 'auto', 'marginLeft': 'auto', 'marginRight': 'auto', 'zIndex': o.zindex});
-	$.mobile.changePage(self.dialogPage, {'transition': (o.animate === true) ? o.transition : 'none'});
-      } else {
-	self.isDialog = false;
-	self.selects = [];
-	
-	if ( o.fullScreen === false ) {
-	  if ( o.showModal === true && o.animate === true ) { self.screen.fadeIn('slow'); }
-	  else { self.screen.removeClass('ui-simpledialog-hidden'); }
-	}
-	
-	self.sdIntContent.addClass('ui-overlay-shadow in').css('zIndex', o.zindex).trigger('create');
-	
-	if ( o.fullScreen === true && ( coords.width < 400 || o.fullScreenForce === true ) ) {
-	  self.sdIntContent.removeClass('ui-simpledialog-container').css({'border': 'none', 'position': 'absolute', 'top': coords.fullTop, 'left': coords.fullLeft, 'height': coords.high, 'width': coords.width, 'maxWidth': coords.width }).removeClass('ui-simpledialog-hidden');
-	} else {
-	  self.sdIntContent.css({'position': 'absolute', 'top': coords.winTop, 'left': coords.winLeft}).removeClass('ui-simpledialog-hidden');
-	}
-	
-	$(document).bind('orientationchange.simpledialog', {widget:self}, function(e) { self._orientChange(e); });
-	if ( o.resizeListener === true ) {
-	  $(window).bind('resize.simpledialog', {widget:self}, function (e) { self._orientChange(e); });
-	}
-      }
-      if ( $.isFunction(o.callbackOpen) ) {
-	o.callbackOpen.apply(self, o.callbackOpenArgs);
-      }
-    },
-    close: function() {
-      var self = this, o = this.options, retty;
-      
-      if ( $.isFunction(self.options.callbackClose) ) {
-	retty = self.options.callbackClose.apply(self, self.options.callbackCloseArgs);
-	if ( retty === false ) { return false; }
-      }
-      
-      if ( self.isDialog ) {
-	$(self.dialogPage).dialog('close');
-	self.sdIntContent.addClass('ui-simpledialog-hidden');
-	self.sdIntContent.appendTo(self.displayAnchor.parent());
-	if ( $.mobile.activePage.jqmData("page").options.domCache != true && $.mobile.activePage.is(":jqmData(external-page='true')") ) {
-	  $.mobile.activePage.bind("pagehide.remove", function () {
-	    $(this).remove();
-	  });
-	}
-      } else {
-	if ( self.options.showModal === true && self.options.animate === true ) {
-	  self.screen.fadeOut('slow');
-	} else {
-	  self.screen.addClass('ui-simpledialog-hidden');
-	}
-	self.sdIntContent.addClass('ui-simpledialog-hidden').removeClass('in');
-	$(document).unbind('orientationchange.simpledialog');
-	if ( self.options.resizeListener === true ) { $(window).unbind('resize.simpledialog'); }
-      }
-      
-      if ( o.mode === 'blank' && o.blankContent !== false && o.blankContentAdopt === true ) {
-	self.element.append(o.blankContent);
-	o.blankContent = true;
-      }
-      
-      if ( self.isDialog === true || self.options.animate === true ) {
-	setTimeout(function(that) { return function () { that.destroy(); };}(self), 1000);
-      } else {
-	self.destroy();
-      }
-    },
-    destroy: function() {
-      var self = this,
-      ele = self.element;
-      
-      if ( self.options.mode === 'blank' ) {
-	$.mobile.sdCurrentDialog.sdIntContent.find('select').each(function() {
-	  if ( $(this).data('nativeMenu') == false ) {
-	    $(this).data('selectmenu').menuPage.remove();
-	    $(this).data('selectmenu').screen.remove();
-	    $(this).data('selectmenu').listbox.remove();
-	  }
-	});
-      }
-      
-      $(self.sdIntContent).remove();
-      $(self.dialogPage).remove();
-      $(self.screen).remove();
-      $(document).unbind('simpledialog.'+self.internalID);
-      delete $.mobile.sdCurrentDialog;
-      $.Widget.prototype.destroy.call(self);
-      if ( self.options.safeNuke === true && $(ele).parents().length === 0 && $(ele).contents().length === 0 ) {
-	ele.remove();
-      }
-    },
-    updateBlank: function (newHTML) {
-      var self = this,
-      o = this.options;
-      
-      self.sdIntContent.empty();
-      
-      if ( o.headerText !== false || o.headerClose !== false ) {
-	self.sdHeader = $('<div class="ui-header ui-bar-'+o.themeHeader+'"></div>');
-	if ( o.headerClose === true ) {
-	  $("<a class='ui-btn-left' rel='close' href='#'>Close</a>").appendTo(self.sdHeader).buttonMarkup({ theme  : o.themeHeader, icon   : 'delete', iconpos: 'notext', corners: true, shadow : true });
-	}
-	$('<h1 class="ui-title">'+((o.headerText !== false)?o.headerText:'')+'</h1>').appendTo(self.sdHeader);
-	self.sdHeader.appendTo(self.sdIntContent);
-      }
-      
-      $(newHTML).appendTo(self.sdIntContent);
-      self.sdIntContent.trigger('create');
-      $(document).trigger('orientationchange.simpledialog');
-    },
-    _init: function() {
-      this.open();
-    }
-  });
-})( jQuery );
--- a/OrthancExplorer/libs/slimbox2.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,244 +0,0 @@
-/*!
-  Slimbox v2.04 - The ultimate lightweight Lightbox clone for jQuery
-  (c) 2007-2010 Christophe Beyls <http://www.digitalia.be>
-  MIT-style license.
-*/
-
-(function($) {
-
-  // Global variables, accessible to Slimbox only
-  var win = $(window), options, images, activeImage = -1, activeURL, prevImage, nextImage, compatibleOverlay, middle, centerWidth, centerHeight,
-  ie6 = !window.XMLHttpRequest, hiddenElements = [], documentElement = document.documentElement,
-
-  // Preload images
-  preload = {}, preloadPrev = new Image(), preloadNext = new Image(),
-
-  // DOM elements
-  overlay, center, image, sizer, prevLink, nextLink, bottomContainer, bottom, caption, number;
-
-  /*
-    Initialization
-  */
-
-  $(function() {
-    // Append the Slimbox HTML code at the bottom of the document
-    $("body").append(
-      $([
-	overlay = $('<div id="lbOverlay" />')[0],
-	center = $('<div id="lbCenter" />')[0],
-	bottomContainer = $('<div id="lbBottomContainer" />')[0]
-      ]).css("display", "none")
-    );
-
-    image = $('<div id="lbImage" />').appendTo(center).append(
-      sizer = $('<div style="position: relative;" />').append([
-	prevLink = $('<a id="lbPrevLink" href="#" />').click(previous)[0],
-	nextLink = $('<a id="lbNextLink" href="#" />').click(next)[0]
-      ])[0]
-    )[0];
-
-    bottom = $('<div id="lbBottom" />').appendTo(bottomContainer).append([
-      $('<a id="lbCloseLink" href="#" />').add(overlay).click(close)[0],
-      caption = $('<div id="lbCaption" />')[0],
-      number = $('<div id="lbNumber" />')[0],
-      $('<div style="clear: both;" />')[0]
-    ])[0];
-  });
-
-
-  /*
-    API
-  */
-
-  // Open Slimbox with the specified parameters
-  $.slimbox = function(_images, startImage, _options) {
-    options = $.extend({
-      loop: false,				// Allows one to navigate between first and last images
-      overlayOpacity: 0.8,			// 1 is opaque, 0 is completely transparent (change the color in the CSS file)
-      overlayFadeDuration: 400,		// Duration of the overlay fade-in and fade-out animations (in milliseconds)
-      resizeDuration: 400,			// Duration of each of the box resize animations (in milliseconds)
-      resizeEasing: "swing",			// "swing" is jQuery's default easing
-      initialWidth: 250,			// Initial width of the box (in pixels)
-      initialHeight: 250,			// Initial height of the box (in pixels)
-      imageFadeDuration: 400,			// Duration of the image fade-in animation (in milliseconds)
-      captionAnimationDuration: 400,		// Duration of the caption animation (in milliseconds)
-      counterText: "Image {x} of {y}",	// Translate or change as you wish, or set it to false to disable counter text for image groups
-      closeKeys: [27, 88, 67],		// Array of keycodes to close Slimbox, default: Esc (27), 'x' (88), 'c' (67)
-      previousKeys: [37, 80],			// Array of keycodes to navigate to the previous image, default: Left arrow (37), 'p' (80)
-      nextKeys: [39, 78]			// Array of keycodes to navigate to the next image, default: Right arrow (39), 'n' (78)
-    }, _options);
-
-    // The function is called for a single image, with URL and Title as first two arguments
-    if (typeof _images == "string") {
-      _images = [[_images, startImage]];
-      startImage = 0;
-    }
-
-    middle = win.scrollTop() + (win.height() / 2);
-    centerWidth = options.initialWidth;
-    centerHeight = options.initialHeight;
-    $(center).css({top: Math.max(0, middle - (centerHeight / 2)), width: centerWidth, height: centerHeight, marginLeft: -centerWidth/2}).show();
-    compatibleOverlay = ie6 || (overlay.currentStyle && (overlay.currentStyle.position != "fixed"));
-    if (compatibleOverlay) overlay.style.position = "absolute";
-    $(overlay).css("opacity", options.overlayOpacity).fadeIn(options.overlayFadeDuration);
-    position();
-    setup(1);
-
-    images = _images;
-    options.loop = options.loop && (images.length > 1);
-    return changeImage(startImage);
-  };
-
-  /*
-    options:	Optional options object, see jQuery.slimbox()
-    linkMapper:	Optional function taking a link DOM element and an index as arguments and returning an array containing 2 elements:
-    the image URL and the image caption (may contain HTML)
-    linksFilter:	Optional function taking a link DOM element and an index as arguments and returning true if the element is part of
-    the image collection that will be shown on click, false if not. "this" refers to the element that was clicked.
-    This function must always return true when the DOM element argument is "this".
-  */
-  $.fn.slimbox = function(_options, linkMapper, linksFilter) {
-    linkMapper = linkMapper || function(el) {
-      return [el.href, el.title];
-    };
-
-    linksFilter = linksFilter || function() {
-      return true;
-    };
-
-    var links = this;
-
-    return links.unbind("click").click(function() {
-      // Build the list of images that will be displayed
-      var link = this, startIndex = 0, filteredLinks, i = 0, length;
-      filteredLinks = $.grep(links, function(el, i) {
-	return linksFilter.call(link, el, i);
-      });
-
-      // We cannot use jQuery.map() because it flattens the returned array
-      for (length = filteredLinks.length; i < length; ++i) {
-	if (filteredLinks[i] == link) startIndex = i;
-	filteredLinks[i] = linkMapper(filteredLinks[i], i);
-      }
-
-      return $.slimbox(filteredLinks, startIndex, _options);
-    });
-  };
-
-
-  /*
-    Internal functions
-  */
-
-  function position() {
-    var l = win.scrollLeft(), w = win.width();
-    $([center, bottomContainer]).css("left", l + (w / 2));
-    if (compatibleOverlay) $(overlay).css({left: l, top: win.scrollTop(), width: w, height: win.height()});
-  }
-
-  function setup(open) {
-    if (open) {
-      $("object").add(ie6 ? "select" : "embed").each(function(index, el) {
-	hiddenElements[index] = [el, el.style.visibility];
-	el.style.visibility = "hidden";
-      });
-    } else {
-      $.each(hiddenElements, function(index, el) {
-	el[0].style.visibility = el[1];
-      });
-      hiddenElements = [];
-    }
-    var fn = open ? "bind" : "unbind";
-    win[fn]("scroll resize", position);
-    $(document)[fn]("keydown", keyDown);
-  }
-
-  function keyDown(event) {
-    var code = event.keyCode, fn = $.inArray;
-    // Prevent default keyboard action (like navigating inside the page)
-    return (fn(code, options.closeKeys) >= 0) ? close()
-      : (fn(code, options.nextKeys) >= 0) ? next()
-      : (fn(code, options.previousKeys) >= 0) ? previous()
-      : false;
-  }
-
-  function previous() {
-    return changeImage(prevImage);
-  }
-
-  function next() {
-    return changeImage(nextImage);
-  }
-
-  function changeImage(imageIndex) {
-    if (imageIndex >= 0) {
-      activeImage = imageIndex;
-      activeURL = images[activeImage][0];
-      prevImage = (activeImage || (options.loop ? images.length : 0)) - 1;
-      nextImage = ((activeImage + 1) % images.length) || (options.loop ? 0 : -1);
-
-      stop();
-      center.className = "lbLoading";
-
-      preload = new Image();
-      preload.onload = animateBox;
-      preload.src = activeURL;
-    }
-
-    return false;
-  }
-
-  function animateBox() {
-    center.className = "";
-    $(image).css({backgroundImage: "url(" + activeURL + ")", visibility: "hidden", display: "" });
-    $(sizer).width(preload.width);
-    $([sizer, prevLink, nextLink]).height(preload.height);
-
-    $(caption).html(images[activeImage][1] || "");
-    $(number).html((((images.length > 1) && options.counterText) || "").replace(/{x}/, activeImage + 1).replace(/{y}/, images.length));
-
-    if (prevImage >= 0) preloadPrev.src = images[prevImage][0];
-    if (nextImage >= 0) preloadNext.src = images[nextImage][0];
-
-    centerWidth = image.offsetWidth;
-    centerHeight = image.offsetHeight;
-    var top = Math.max(0, middle - (centerHeight / 2));
-    if (center.offsetHeight != centerHeight) {
-      $(center).animate({height: centerHeight, top: top}, options.resizeDuration, options.resizeEasing);
-    }
-    if (center.offsetWidth != centerWidth) {
-      $(center).animate({width: centerWidth, marginLeft: -centerWidth/2}, options.resizeDuration, options.resizeEasing);
-    }
-    $(center).queue(function() {
-      $(bottomContainer).css({width: centerWidth, top: top + centerHeight, marginLeft: -centerWidth/2, visibility: "hidden", display: ""});
-      animateCaption();
-      $(image).css({display: "none", visibility: "", opacity: ""}).fadeIn(options.imageFadeDuration, animateCaption);
-    });
-  }
-
-  function animateCaption() {
-    if (prevImage >= 0) $(prevLink).show();
-    if (nextImage >= 0) $(nextLink).show();
-    $(bottom).css("marginTop", -bottom.offsetHeight).animate({marginTop: 0}, options.captionAnimationDuration);
-    bottomContainer.style.visibility = "";
-  }
-
-  function stop() {
-    preload.onload = null;
-    preload.src = preloadPrev.src = preloadNext.src = activeURL;
-    $([center, image, bottom]).stop(true);
-    $([prevLink, nextLink, image, bottomContainer]).hide();
-  }
-
-  function close() {
-    if (activeImage >= 0) {
-      stop();
-      activeImage = prevImage = nextImage = -1;
-      $(center).hide();
-      $(overlay).stop().fadeOut(options.overlayFadeDuration, setup);
-    }
-
-    return false;
-  }
-
-})(jQuery);
Binary file OrthancExplorer/libs/slimbox2/closelabel.gif has changed
Binary file OrthancExplorer/libs/slimbox2/loading.gif has changed
Binary file OrthancExplorer/libs/slimbox2/nextlabel.gif has changed
Binary file OrthancExplorer/libs/slimbox2/prevlabel.gif has changed
--- a/OrthancExplorer/libs/slimbox2/slimbox2-rtl.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/* SLIMBOX */
-
-#lbOverlay {
-	position: fixed;
-	z-index: 9999;
-	left: 0;
-	top: 0;
-	width: 100%;
-	height: 100%;
-	background-color: #000;
-	cursor: pointer;
-}
-
-#lbCenter, #lbBottomContainer {
-	position: absolute;
-	z-index: 9999;
-	overflow: hidden;
-	background-color: #fff;
-}
-
-.lbLoading {
-	background: #fff url(loading.gif) no-repeat center;
-}
-
-#lbImage {
-	position: absolute;
-	left: 0;
-	top: 0;
-	border: 10px solid #fff;
-	background-repeat: no-repeat;
-}
-
-#lbPrevLink, #lbNextLink {
-	display: block;
-	position: absolute;
-	top: 0;
-	width: 50%;
-	outline: none;
-}
-
-#lbPrevLink {
-	right: 0;
-}
-
-#lbPrevLink:hover {
-	background: transparent url(prevlabel.gif) no-repeat 100% 15%;
-}
-
-#lbNextLink {
-	left: 0;
-}
-
-#lbNextLink:hover {
-	background: transparent url(nextlabel.gif) no-repeat 0 15%;
-}
-
-#lbBottom {
-	font-family: Verdana, Arial, Geneva, Helvetica, sans-serif;
-	font-size: 10px;
-	color: #666;
-	line-height: 1.4em;
-	text-align: right;
-	border: 10px solid #fff;
-	border-top-style: none;
-	direction: rtl;
-}
-
-#lbCloseLink {
-	display: block;
-	float: left;
-	width: 66px;
-	height: 22px;
-	background: transparent url(closelabel.gif) no-repeat center;
-	margin: 5px 0;
-	outline: none;
-}
-
-#lbCaption, #lbNumber {
-	margin-left: 71px;
-}
-
-#lbCaption {
-	font-weight: bold;
-}
--- a/OrthancExplorer/libs/slimbox2/slimbox2.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/* SLIMBOX */
-
-#lbOverlay {
-	position: fixed;
-	z-index: 9999;
-	left: 0;
-	top: 0;
-	width: 100%;
-	height: 100%;
-	background-color: #000;
-	cursor: pointer;
-}
-
-#lbCenter, #lbBottomContainer {
-	position: absolute;
-	z-index: 9999;
-	overflow: hidden;
-	background-color: #fff;
-}
-
-.lbLoading {
-	background: #fff url(loading.gif) no-repeat center;
-}
-
-#lbImage {
-	position: absolute;
-	left: 0;
-	top: 0;
-	border: 10px solid #fff;
-	background-repeat: no-repeat;
-}
-
-#lbPrevLink, #lbNextLink {
-	display: block;
-	position: absolute;
-	top: 0;
-	width: 50%;
-	outline: none;
-}
-
-#lbPrevLink {
-	left: 0;
-}
-
-#lbPrevLink:hover {
-	background: transparent url(prevlabel.gif) no-repeat 0 15%;
-}
-
-#lbNextLink {
-	right: 0;
-}
-
-#lbNextLink:hover {
-	background: transparent url(nextlabel.gif) no-repeat 100% 15%;
-}
-
-#lbBottom {
-	font-family: Verdana, Arial, Geneva, Helvetica, sans-serif;
-	font-size: 10px;
-	color: #666;
-	line-height: 1.4em;
-	text-align: left;
-	border: 10px solid #fff;
-	border-top-style: none;
-}
-
-#lbCloseLink {
-	display: block;
-	float: right;
-	width: 66px;
-	height: 22px;
-	background: transparent url(closelabel.gif) no-repeat center;
-	margin: 5px 0;
-	outline: none;
-}
-
-#lbCaption, #lbNumber {
-	margin-right: 71px;
-}
-
-#lbCaption {
-	font-weight: bold;
-}
--- a/OrthancExplorer/libs/tree.jquery.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1837 +0,0 @@
-// Generated by CoffeeScript 1.3.3
-
-/*
-Copyright 2012 Marco Braak
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-
-(function() {
-  var $, BorderDropHint, DragAndDropHandler, DragElement, FolderElement, GhostDropHint, JqTreeWidget, Json, MouseWidget, Node, NodeElement, Position, SaveStateHandler, SelectNodeHandler, SimpleWidget, Tree, html_escape, indexOf, toJson,
-    __slice = [].slice,
-    __hasProp = {}.hasOwnProperty,
-    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
-
-  $ = this.jQuery;
-
-  SimpleWidget = (function() {
-
-    SimpleWidget.prototype.defaults = {};
-
-    function SimpleWidget(el, options) {
-      this.$el = $(el);
-      this.options = $.extend({}, this.defaults, options);
-      this._init();
-    }
-
-    SimpleWidget.prototype.destroy = function() {
-      return this._deinit();
-    };
-
-    SimpleWidget.prototype._init = function() {
-      return null;
-    };
-
-    SimpleWidget.prototype._deinit = function() {
-      return null;
-    };
-
-    SimpleWidget.register = function(widget_class, widget_name) {
-      var callFunction, createWidget, destroyWidget, getDataKey;
-      getDataKey = function() {
-        return "simple_widget_" + widget_name;
-      };
-      createWidget = function($el, options) {
-        var data_key;
-        data_key = getDataKey();
-        $el.each(function() {
-          var widget;
-          widget = new widget_class(this, options);
-          if (!$.data(this, data_key)) {
-            return $.data(this, data_key, widget);
-          }
-        });
-        return $el;
-      };
-      destroyWidget = function($el) {
-        var data_key;
-        data_key = getDataKey();
-        return $el.each(function() {
-          var widget;
-          widget = $.data(this, data_key);
-          if (widget && (widget instanceof SimpleWidget)) {
-            widget.destroy();
-          }
-          return $.removeData(this, data_key);
-        });
-      };
-      callFunction = function($el, function_name, args) {
-        var result;
-        result = null;
-        $el.each(function() {
-          var widget, widget_function;
-          widget = $.data(this, getDataKey());
-          if (widget && (widget instanceof SimpleWidget)) {
-            widget_function = widget[function_name];
-            if (widget_function && (typeof widget_function === 'function')) {
-              return result = widget_function.apply(widget, args);
-            }
-          }
-        });
-        return result;
-      };
-      return $.fn[widget_name] = function() {
-        var $el, args, argument1, function_name, options;
-        argument1 = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
-        $el = this;
-        if (argument1 === void 0 || typeof argument1 === 'object') {
-          options = argument1;
-          return createWidget($el, options);
-        } else if (typeof argument1 === 'string' && argument1[0] !== '_') {
-          function_name = argument1;
-          if (function_name === 'destroy') {
-            return destroyWidget($el);
-          } else {
-            return callFunction($el, function_name, args);
-          }
-        }
-      };
-    };
-
-    return SimpleWidget;
-
-  })();
-
-  this.SimpleWidget = SimpleWidget;
-
-  /*
-  This widget does the same a the mouse widget in jqueryui.
-  */
-
-
-  MouseWidget = (function(_super) {
-
-    __extends(MouseWidget, _super);
-
-    function MouseWidget() {
-      return MouseWidget.__super__.constructor.apply(this, arguments);
-    }
-
-    MouseWidget.is_mouse_handled = false;
-
-    MouseWidget.prototype._init = function() {
-      this.$el.bind('mousedown.mousewidget', $.proxy(this._mouseDown, this));
-      return this.is_mouse_started = false;
-    };
-
-    MouseWidget.prototype._deinit = function() {
-      var $document;
-      this.$el.unbind('mousedown.mousewidget');
-      $document = $(document);
-      $document.unbind('mousemove.mousewidget');
-      return $document.unbind('mouseup.mousewidget');
-    };
-
-    MouseWidget.prototype._mouseDown = function(e) {
-      var $document;
-      if (MouseWidget.is_mouse_handled) {
-        return;
-      }
-      if (!this.is_mouse_started) {
-        this._mouseUp(e);
-      }
-      if (e.which !== 1) {
-        return;
-      }
-      if (!this._mouseCapture(e)) {
-        return;
-      }
-      this.mouse_down_event = e;
-      $document = $(document);
-      $document.bind('mousemove.mousewidget', $.proxy(this._mouseMove, this));
-      $document.bind('mouseup.mousewidget', $.proxy(this._mouseUp, this));
-      e.preventDefault();
-      this.is_mouse_handled = true;
-      return true;
-    };
-
-    MouseWidget.prototype._mouseMove = function(e) {
-      if (this.is_mouse_started) {
-        this._mouseDrag(e);
-        return e.preventDefault();
-      }
-      this.is_mouse_started = this._mouseStart(this.mouse_down_event) !== false;
-      if (this.is_mouse_started) {
-        this._mouseDrag(e);
-      } else {
-        this._mouseUp(e);
-      }
-      return !this.is_mouse_started;
-    };
-
-    MouseWidget.prototype._mouseUp = function(e) {
-      var $document;
-      $document = $(document);
-      $document.unbind('mousemove.mousewidget');
-      $document.unbind('mouseup.mousewidget');
-      if (this.is_mouse_started) {
-        this.is_mouse_started = false;
-        this._mouseStop(e);
-      }
-      return false;
-    };
-
-    MouseWidget.prototype._mouseCapture = function(e) {
-      return true;
-    };
-
-    MouseWidget.prototype._mouseStart = function(e) {
-      return null;
-    };
-
-    MouseWidget.prototype._mouseDrag = function(e) {
-      return null;
-    };
-
-    MouseWidget.prototype._mouseStop = function(e) {
-      return null;
-    };
-
-    return MouseWidget;
-
-  })(SimpleWidget);
-
-  /*
-  Copyright 2012 Marco Braak
-  
-  Licensed under the Apache License, Version 2.0 (the "License");
-  you may not use this file except in compliance with the License.
-  You may obtain a copy of the License at
-  
-      http://www.apache.org/licenses/LICENSE-2.0
-  
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.
-  */
-
-
-  this.Tree = {};
-
-  $ = this.jQuery;
-
-  indexOf = function(array, item) {
-    var i, value, _i, _len;
-    if (array.indexOf) {
-      return array.indexOf(item);
-    } else {
-      for (i = _i = 0, _len = array.length; _i < _len; i = ++_i) {
-        value = array[i];
-        if (value === item) {
-          return i;
-        }
-      }
-      return -1;
-    }
-  };
-
-  this.Tree.indexOf = indexOf;
-
-  Json = {};
-
-  Json.escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
-
-  Json.meta = {
-    '\b': '\\b',
-    '\t': '\\t',
-    '\n': '\\n',
-    '\f': '\\f',
-    '\r': '\\r',
-    '"': '\\"',
-    '\\': '\\\\'
-  };
-
-  Json.quote = function(string) {
-    Json.escapable.lastIndex = 0;
-    if (Json.escapable.test(string)) {
-      return '"' + string.replace(Json.escapable, function(a) {
-        var c;
-        c = Json.meta[a];
-        return (typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4));
-      }) + '"';
-    } else {
-      return '"' + string + '"';
-    }
-  };
-
-  Json.str = function(key, holder) {
-    var i, k, partial, v, value, _i, _len;
-    value = holder[key];
-    switch (typeof value) {
-      case 'string':
-        return Json.quote(value);
-      case 'number':
-        if (isFinite(value)) {
-          return String(value);
-        } else {
-          return 'null';
-        }
-      case 'boolean':
-      case 'null':
-        return String(value);
-      case 'object':
-        if (!value) {
-          return 'null';
-        }
-        partial = [];
-        if (Object.prototype.toString.apply(value) === '[object Array]') {
-          for (i = _i = 0, _len = value.length; _i < _len; i = ++_i) {
-            v = value[i];
-            partial[i] = Json.str(i, value) || 'null';
-          }
-          return (partial.length === 0 ? '[]' : '[' + partial.join(',') + ']');
-        }
-        for (k in value) {
-          if (Object.prototype.hasOwnProperty.call(value, k)) {
-            v = Json.str(k, value);
-            if (v) {
-              partial.push(Json.quote(k) + ':' + v);
-            }
-          }
-        }
-        return (partial.length === 0 ? '{}' : '{' + partial.join(',') + '}');
-    }
-  };
-
-  toJson = function(value) {
-    return Json.str('', {
-      '': value
-    });
-  };
-
-  this.Tree.toJson = toJson;
-
-  html_escape = function(string) {
-    return ('' + string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;');
-  };
-
-  Position = {
-    getName: function(position) {
-      if (position === Position.BEFORE) {
-        return 'before';
-      } else if (position === Position.AFTER) {
-        return 'after';
-      } else if (position === Position.INSIDE) {
-        return 'inside';
-      } else {
-        return 'none';
-      }
-    }
-  };
-
-  Position.BEFORE = 1;
-
-  Position.AFTER = 2;
-
-  Position.INSIDE = 3;
-
-  Position.NONE = 4;
-
-  this.Tree.Position = Position;
-
-  Node = (function() {
-
-    function Node(o) {
-      this.setData(o);
-    }
-
-    Node.prototype.setData = function(o) {
-      var key, value;
-      if (typeof o !== 'object') {
-        this.name = o;
-      } else {
-        for (key in o) {
-          value = o[key];
-          if (key === 'label') {
-            this.name = value;
-          } else {
-            this[key] = value;
-          }
-        }
-      }
-      this.children = [];
-      return this.parent = null;
-    };
-
-    Node.prototype.initFromData = function(data) {
-      var addChildren, addNode,
-        _this = this;
-      addNode = function(node_data) {
-        _this.setData(node_data);
-        if (node_data.children) {
-          return addChildren(node_data.children);
-        }
-      };
-      addChildren = function(children_data) {
-        var child, node, _i, _len;
-        for (_i = 0, _len = children_data.length; _i < _len; _i++) {
-          child = children_data[_i];
-          node = new Node('');
-          node.initFromData(child);
-          _this.addChild(node);
-        }
-        return null;
-      };
-      addNode(data);
-      return null;
-    };
-
-    /*
-        Create tree from data.
-    
-        Structure of data is:
-        [
-            {
-                label: 'node1',
-                children: [
-                    { label: 'child1' },
-                    { label: 'child2' }
-                ]
-            },
-            {
-                label: 'node2'
-            }
-        ]
-    */
-
-
-    Node.prototype.loadFromData = function(data) {
-      var node, o, _i, _len;
-      this.children = [];
-      for (_i = 0, _len = data.length; _i < _len; _i++) {
-        o = data[_i];
-        node = new Node(o);
-        this.addChild(node);
-        if (typeof o === 'object' && o.children) {
-          node.loadFromData(o.children);
-        }
-      }
-      return null;
-    };
-
-    /*
-        Add child.
-    
-        tree.addChild(
-            new Node('child1')
-        );
-    */
-
-
-    Node.prototype.addChild = function(node) {
-      this.children.push(node);
-      return node._setParent(this);
-    };
-
-    /*
-        Add child at position. Index starts at 0.
-    
-        tree.addChildAtPosition(
-            new Node('abc'),
-            1
-        );
-    */
-
-
-    Node.prototype.addChildAtPosition = function(node, index) {
-      this.children.splice(index, 0, node);
-      return node._setParent(this);
-    };
-
-    Node.prototype._setParent = function(parent) {
-      this.parent = parent;
-      this.tree = parent.tree;
-      return this.tree.addNodeToIndex(this);
-    };
-
-    /*
-        Remove child.
-    
-        tree.removeChild(tree.children[0]);
-    */
-
-
-    Node.prototype.removeChild = function(node) {
-      this.children.splice(this.getChildIndex(node), 1);
-      return this.tree.removeNodeFromIndex(node);
-    };
-
-    /*
-        Get child index.
-    
-        var index = getChildIndex(node);
-    */
-
-
-    Node.prototype.getChildIndex = function(node) {
-      return $.inArray(node, this.children);
-    };
-
-    /*
-        Does the tree have children?
-    
-        if (tree.hasChildren()) {
-            //
-        }
-    */
-
-
-    Node.prototype.hasChildren = function() {
-      return this.children.length !== 0;
-    };
-
-    /*
-        Iterate over all the nodes in the tree.
-    
-        Calls callback with (node, level).
-    
-        The callback must return true to continue the iteration on current node.
-    
-        tree.iterate(
-            function(node, level) {
-               console.log(node.name);
-    
-               // stop iteration after level 2
-               return (level <= 2);
-            }
-        );
-    */
-
-
-    Node.prototype.iterate = function(callback) {
-      var _iterate,
-        _this = this;
-      _iterate = function(node, level) {
-        var child, result, _i, _len, _ref;
-        if (node.children) {
-          _ref = node.children;
-          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
-            child = _ref[_i];
-            result = callback(child, level);
-            if (_this.hasChildren() && result) {
-              _iterate(child, level + 1);
-            }
-          }
-          return null;
-        }
-      };
-      _iterate(this, 0);
-      return null;
-    };
-
-    /*
-        Move node relative to another node.
-    
-        Argument position: Position.BEFORE, Position.AFTER or Position.Inside
-    
-        // move node1 after node2
-        tree.moveNode(node1, node2, Position.AFTER);
-    */
-
-
-    Node.prototype.moveNode = function(moved_node, target_node, position) {
-      moved_node.parent.removeChild(moved_node);
-      if (position === Position.AFTER) {
-        return target_node.parent.addChildAtPosition(moved_node, target_node.parent.getChildIndex(target_node) + 1);
-      } else if (position === Position.BEFORE) {
-        return target_node.parent.addChildAtPosition(moved_node, target_node.parent.getChildIndex(target_node));
-      } else if (position === Position.INSIDE) {
-        return target_node.addChildAtPosition(moved_node, 0);
-      }
-    };
-
-    /*
-        Get the tree as data.
-    */
-
-
-    Node.prototype.getData = function() {
-      var getDataFromNodes,
-        _this = this;
-      getDataFromNodes = function(nodes) {
-        var data, k, node, tmp_node, v, _i, _len;
-        data = [];
-        for (_i = 0, _len = nodes.length; _i < _len; _i++) {
-          node = nodes[_i];
-          tmp_node = {};
-          for (k in node) {
-            v = node[k];
-            if ((k !== 'parent' && k !== 'children' && k !== 'element' && k !== 'tree') && Object.prototype.hasOwnProperty.call(node, k)) {
-              tmp_node[k] = v;
-            }
-          }
-          if (node.hasChildren()) {
-            tmp_node.children = getDataFromNodes(node.children);
-          }
-          data.push(tmp_node);
-        }
-        return data;
-      };
-      return getDataFromNodes(this.children);
-    };
-
-    Node.prototype.getNodeByName = function(name) {
-      var result;
-      result = null;
-      this.iterate(function(node) {
-        if (node.name === name) {
-          result = node;
-          return false;
-        } else {
-          return true;
-        }
-      });
-      return result;
-    };
-
-    Node.prototype.addAfter = function(node_info) {
-      var child_index, node;
-      if (!this.parent) {
-        return null;
-      } else {
-        node = new Node(node_info);
-        child_index = this.parent.getChildIndex(this);
-        this.parent.addChildAtPosition(node, child_index + 1);
-        return node;
-      }
-    };
-
-    Node.prototype.addBefore = function(node_info) {
-      var child_index, node;
-      if (!this.parent) {
-        return null;
-      } else {
-        node = new Node(node_info);
-        child_index = this.parent.getChildIndex(this);
-        return this.parent.addChildAtPosition(node, child_index);
-      }
-    };
-
-    Node.prototype.addParent = function(node_info) {
-      var child, new_parent, original_parent, _i, _len, _ref;
-      if (!this.parent) {
-        return null;
-      } else {
-        new_parent = new Node(node_info);
-        new_parent._setParent(this.tree);
-        original_parent = this.parent;
-        _ref = original_parent.children;
-        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
-          child = _ref[_i];
-          new_parent.addChild(child);
-        }
-        original_parent.children = [];
-        original_parent.addChild(new_parent);
-        return new_parent;
-      }
-    };
-
-    Node.prototype.remove = function() {
-      if (this.parent) {
-        this.parent.removeChild(this);
-        return this.parent = null;
-      }
-    };
-
-    Node.prototype.append = function(node_info) {
-      var node;
-      node = new Node(node_info);
-      this.addChild(node);
-      return node;
-    };
-
-    Node.prototype.prepend = function(node_info) {
-      var node;
-      node = new Node(node_info);
-      this.addChildAtPosition(node, 0);
-      return node;
-    };
-
-    return Node;
-
-  })();
-
-  Tree = (function(_super) {
-
-    __extends(Tree, _super);
-
-    function Tree(o) {
-      Tree.__super__.constructor.call(this, o, null, true);
-      this.id_mapping = {};
-      this.tree = this;
-    }
-
-    Tree.prototype.getNodeById = function(node_id) {
-      return this.id_mapping[node_id];
-    };
-
-    Tree.prototype.addNodeToIndex = function(node) {
-      if (node.id) {
-        return this.id_mapping[node.id] = node;
-      }
-    };
-
-    Tree.prototype.removeNodeFromIndex = function(node) {
-      if (node.id) {
-        return delete this.id_mapping[node.id];
-      }
-    };
-
-    return Tree;
-
-  })(Node);
-
-  this.Tree.Tree = Tree;
-
-  JqTreeWidget = (function(_super) {
-
-    __extends(JqTreeWidget, _super);
-
-    function JqTreeWidget() {
-      return JqTreeWidget.__super__.constructor.apply(this, arguments);
-    }
-
-    JqTreeWidget.prototype.defaults = {
-      autoOpen: false,
-      saveState: false,
-      dragAndDrop: false,
-      selectable: false,
-      onCanSelectNode: null,
-      onSetStateFromStorage: null,
-      onGetStateFromStorage: null,
-      onCreateLi: null,
-      onIsMoveHandle: null,
-      onCanMove: null,
-      onCanMoveTo: null,
-      autoEscape: true,
-      dataUrl: null
-    };
-
-    JqTreeWidget.prototype.toggle = function(node) {
-      if (node.hasChildren()) {
-        new FolderElement(node, this.element).toggle();
-      }
-      return this._saveState();
-    };
-
-    JqTreeWidget.prototype.getTree = function() {
-      return this.tree;
-    };
-
-    JqTreeWidget.prototype.selectNode = function(node, must_open_parents) {
-      return this.select_node_handler.selectNode(node, must_open_parents);
-    };
-
-    JqTreeWidget.prototype.getSelectedNode = function() {
-      return this.selected_node || false;
-    };
-
-    JqTreeWidget.prototype.toJson = function() {
-      return toJson(this.tree.getData());
-    };
-
-    JqTreeWidget.prototype.loadData = function(data, parent_node) {
-      var child, subtree, _i, _len, _ref;
-      if (!parent_node) {
-        this._initTree(data);
-      } else {
-        subtree = new Node('');
-        subtree._setParent(parent_node.tree);
-        subtree.loadFromData(data);
-        _ref = subtree.children;
-        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
-          child = _ref[_i];
-          parent_node.addChild(child);
-        }
-        this._refreshElements(parent_node.parent);
-      }
-      if (this.is_dragging) {
-        return this.dnd_handler.refreshHitAreas();
-      }
-    };
-
-    JqTreeWidget.prototype.getNodeById = function(node_id) {
-      return this.tree.getNodeById(node_id);
-    };
-
-    JqTreeWidget.prototype.getNodeByName = function(name) {
-      return this.tree.getNodeByName(name);
-    };
-
-    JqTreeWidget.prototype.openNode = function(node, skip_slide) {
-      if (node.hasChildren()) {
-        new FolderElement(node, this.element).open(null, skip_slide);
-        return this._saveState();
-      }
-    };
-
-    JqTreeWidget.prototype.closeNode = function(node, skip_slide) {
-      if (node.hasChildren()) {
-        new FolderElement(node, this.element).close(skip_slide);
-        return this._saveState();
-      }
-    };
-
-    JqTreeWidget.prototype.isDragging = function() {
-      return this.is_dragging;
-    };
-
-    JqTreeWidget.prototype.refreshHitAreas = function() {
-      return this.dnd_handler.refreshHitAreas();
-    };
-
-    JqTreeWidget.prototype.addNodeAfter = function(new_node_info, existing_node) {
-      var new_node;
-      new_node = existing_node.addAfter(new_node_info);
-      this._refreshElements(existing_node.parent);
-      return new_node;
-    };
-
-    JqTreeWidget.prototype.addNodeBefore = function(new_node_info, existing_node) {
-      var new_node;
-      new_node = existing_node.addBefore(new_node_info);
-      this._refreshElements(existing_node.parent);
-      return new_node;
-    };
-
-    JqTreeWidget.prototype.addParentNode = function(new_node_info, existing_node) {
-      var new_node;
-      new_node = existing_node.addParent(new_node_info);
-      this._refreshElements(new_node.parent);
-      return new_node;
-    };
-
-    JqTreeWidget.prototype.removeNode = function(node) {
-      var parent;
-      parent = node.parent;
-      if (parent) {
-        node.remove();
-        return this._refreshElements(parent.parent);
-      }
-    };
-
-    JqTreeWidget.prototype.appendNode = function(new_node_info, parent_node) {
-      var is_already_root_node, node;
-      if (!parent_node) {
-        parent_node = this.tree;
-      }
-      is_already_root_node = parent_node.hasChildren();
-      node = parent_node.append(new_node_info);
-      if (is_already_root_node) {
-        this._refreshElements(parent_node);
-      } else {
-        this._refreshElements(parent_node.parent);
-      }
-      return node;
-    };
-
-    JqTreeWidget.prototype.prependNode = function(new_node_info, parent_node) {
-      var node;
-      if (!parent_node) {
-        parent_node = this.tree;
-      }
-      node = parent_node.prepend(new_node_info);
-      this._refreshElements(parent_node);
-      return node;
-    };
-
-    JqTreeWidget.prototype._init = function() {
-      JqTreeWidget.__super__._init.call(this);
-      this.element = this.$el;
-      this._initData();
-      this.element.click($.proxy(this._click, this));
-      return this.element.bind('contextmenu', $.proxy(this._contextmenu, this));
-    };
-
-    JqTreeWidget.prototype._deinit = function() {
-      this.element.empty();
-      this.element.unbind();
-      this.tree = null;
-      return JqTreeWidget.__super__._deinit.call(this);
-    };
-
-    JqTreeWidget.prototype._initData = function() {
-      var data_url,
-        _this = this;
-      if (this.options.data) {
-        return this._initTree(this.options.data);
-      } else {
-        data_url = this.options.dataUrl || this.element.data('url');
-        if (data_url) {
-          return $.ajax({
-            url: data_url,
-            cache: false,
-            success: function(response) {
-              var data;
-              if ($.isArray(response) || typeof response === 'object') {
-                data = response;
-              } else {
-                data = $.parseJSON(response);
-              }
-              return _this._initTree(data);
-            }
-          });
-        }
-      }
-    };
-
-    JqTreeWidget.prototype._initTree = function(data) {
-      var event;
-      this.tree = new Tree();
-      this.tree.loadFromData(data);
-      this.selected_node = null;
-      this.save_state_handler = new SaveStateHandler(this);
-      this.select_node_handler = new SelectNodeHandler(this);
-      this.dnd_handler = new DragAndDropHandler(this);
-      this._openNodes();
-      this._refreshElements();
-      this.select_node_handler.selectCurrentNode();
-      event = $.Event('tree.init');
-      return this.element.trigger(event);
-    };
-
-    JqTreeWidget.prototype._openNodes = function() {
-      var max_level;
-      if (this.options.saveState) {
-        if (this.save_state_handler.restoreState()) {
-          return;
-        }
-      }
-      if (this.options.autoOpen === false) {
-        return;
-      } else if (this.options.autoOpen === true) {
-        max_level = -1;
-      } else {
-        max_level = parseInt(this.options.autoOpen);
-      }
-      return this.tree.iterate(function(node, level) {
-        node.is_open = true;
-        return level !== max_level;
-      });
-    };
-
-    JqTreeWidget.prototype._refreshElements = function(from_node) {
-      var $element, createFolderLi, createLi, createNodeLi, createUl, doCreateDomElements, escapeIfNecessary, is_root_node, node_element,
-        _this = this;
-      if (from_node == null) {
-        from_node = null;
-      }
-      escapeIfNecessary = function(value) {
-        if (_this.options.autoEscape) {
-          return html_escape(value);
-        } else {
-          return value;
-        }
-      };
-      createUl = function(is_root_node) {
-        var class_string;
-        if (is_root_node) {
-          class_string = ' class="tree"';
-        } else {
-          class_string = '';
-        }
-        return $("<ul" + class_string + "></ul>");
-      };
-      createLi = function(node) {
-        var $li;
-        if (node.hasChildren()) {
-          $li = createFolderLi(node);
-        } else {
-          $li = createNodeLi(node);
-        }
-        if (_this.options.onCreateLi) {
-          _this.options.onCreateLi(node, $li);
-        }
-        return $li;
-      };
-      createNodeLi = function(node) {
-        var escaped_name;
-        escaped_name = escapeIfNecessary(node.name);
-        return $("<li><div><span class=\"title\">" + escaped_name + "</span></div></li>");
-      };
-      createFolderLi = function(node) {
-        var button_class, escaped_name, folder_class, getButtonClass, getFolderClass;
-        getButtonClass = function() {
-          var classes;
-          classes = ['toggler'];
-          if (!node.is_open) {
-            classes.push('closed');
-          }
-          return classes.join(' ');
-        };
-        getFolderClass = function() {
-          var classes;
-          classes = ['folder'];
-          if (!node.is_open) {
-            classes.push('closed');
-          }
-          return classes.join(' ');
-        };
-        button_class = getButtonClass();
-        folder_class = getFolderClass();
-        escaped_name = escapeIfNecessary(node.name);
-        return $("<li class=\"" + folder_class + "\"><div><a class=\"" + button_class + "\">&raquo;</a><span class=\"title\">" + escaped_name + "</span></div></li>");
-      };
-      doCreateDomElements = function($element, children, is_root_node, is_open) {
-        var $li, $ul, child, _i, _len;
-        $ul = createUl(is_root_node);
-        $element.append($ul);
-        for (_i = 0, _len = children.length; _i < _len; _i++) {
-          child = children[_i];
-          $li = createLi(child);
-          $ul.append($li);
-          child.element = $li[0];
-          $li.data('node', child);
-          if (child.hasChildren()) {
-            doCreateDomElements($li, child.children, false, child.is_open);
-          }
-        }
-        return null;
-      };
-      if (from_node && from_node.parent) {
-        is_root_node = false;
-        node_element = this._getNodeElementForNode(from_node);
-        node_element.getUl().remove();
-        $element = node_element.$element;
-      } else {
-        from_node = this.tree;
-        $element = this.element;
-        $element.empty();
-        is_root_node = true;
-      }
-      return doCreateDomElements($element, from_node.children, is_root_node, is_root_node);
-    };
-
-    JqTreeWidget.prototype._click = function(e) {
-      var $target, event, node, node_element;
-      if (e.ctrlKey) {
-        return;
-      }
-      $target = $(e.target);
-      if ($target.is('.toggler')) {
-        node_element = this._getNodeElement($target);
-        if (node_element && node_element.node.hasChildren()) {
-          node_element.toggle();
-          this._saveState();
-          e.preventDefault();
-          return e.stopPropagation();
-        }
-      } else if ($target.is('div') || $target.is('span')) {
-        node = this._getNode($target);
-        if (node) {
-          if ((!this.options.onCanSelectNode) || this.options.onCanSelectNode(node)) {
-            this.selectNode(node);
-            event = $.Event('tree.click');
-            event.node = node;
-            return this.element.trigger(event);
-          }
-        }
-      }
-    };
-
-    JqTreeWidget.prototype._getNode = function($element) {
-      var $li;
-      $li = $element.closest('li');
-      if ($li.length === 0) {
-        return null;
-      } else {
-        return $li.data('node');
-      }
-    };
-
-    JqTreeWidget.prototype._getNodeElementForNode = function(node) {
-      if (node.hasChildren()) {
-        return new FolderElement(node, this.element);
-      } else {
-        return new NodeElement(node, this.element);
-      }
-    };
-
-    JqTreeWidget.prototype._getNodeElement = function($element) {
-      var node;
-      node = this._getNode($element);
-      if (node) {
-        return this._getNodeElementForNode(node);
-      } else {
-        return null;
-      }
-    };
-
-    JqTreeWidget.prototype._contextmenu = function(e) {
-      var $div, event, node;
-      $div = $(e.target).closest('ul.tree div');
-      if ($div.length) {
-        node = this._getNode($div);
-        if (node) {
-          e.preventDefault();
-          e.stopPropagation();
-          event = $.Event('tree.contextmenu');
-          event.node = node;
-          event.click_event = e;
-          this.element.trigger(event);
-          return false;
-        }
-      }
-    };
-
-    JqTreeWidget.prototype._saveState = function() {
-      if (this.options.saveState) {
-        return this.save_state_handler.saveState();
-      }
-    };
-
-    JqTreeWidget.prototype._mouseCapture = function(event) {
-      if (this.options.dragAndDrop) {
-        return this.dnd_handler.mouseCapture(event);
-      } else {
-        return false;
-      }
-    };
-
-    JqTreeWidget.prototype._mouseStart = function(event) {
-      if (this.options.dragAndDrop) {
-        return this.dnd_handler.mouseStart(event);
-      } else {
-        return false;
-      }
-    };
-
-    JqTreeWidget.prototype._mouseDrag = function(event) {
-      if (this.options.dragAndDrop) {
-        return this.dnd_handler.mouseDrag(event);
-      } else {
-        return false;
-      }
-    };
-
-    JqTreeWidget.prototype._mouseStop = function() {
-      if (this.options.dragAndDrop) {
-        return this.dnd_handler.mouseStop();
-      } else {
-        return false;
-      }
-    };
-
-    JqTreeWidget.prototype.testGenerateHitAreas = function(moving_node) {
-      this.dnd_handler.current_item = this._getNodeElementForNode(moving_node);
-      this.dnd_handler.generateHitAreas();
-      return this.dnd_handler.hit_areas;
-    };
-
-    return JqTreeWidget;
-
-  })(MouseWidget);
-
-  SimpleWidget.register(JqTreeWidget, 'tree');
-
-  GhostDropHint = (function() {
-
-    function GhostDropHint(node, $element, position) {
-      this.$element = $element;
-      this.node = node;
-      this.$ghost = $('<li class="ghost"><span class="circle"></span><span class="line"></span></li>');
-      if (position === Position.AFTER) {
-        this.moveAfter();
-      } else if (position === Position.BEFORE) {
-        this.moveBefore();
-      } else if (position === Position.INSIDE) {
-        if (node.hasChildren() && node.is_open) {
-          this.moveInsideOpenFolder();
-        } else {
-          this.moveInside();
-        }
-      }
-    }
-
-    GhostDropHint.prototype.remove = function() {
-      return this.$ghost.remove();
-    };
-
-    GhostDropHint.prototype.moveAfter = function() {
-      return this.$element.after(this.$ghost);
-    };
-
-    GhostDropHint.prototype.moveBefore = function() {
-      return this.$element.before(this.$ghost);
-    };
-
-    GhostDropHint.prototype.moveInsideOpenFolder = function() {
-      return $(this.node.children[0].element).before(this.$ghost);
-    };
-
-    GhostDropHint.prototype.moveInside = function() {
-      this.$element.after(this.$ghost);
-      return this.$ghost.addClass('inside');
-    };
-
-    return GhostDropHint;
-
-  })();
-
-  BorderDropHint = (function() {
-
-    function BorderDropHint($element) {
-      var $div, width;
-      $div = $element.children('div');
-      width = $element.width() - 4;
-      this.$hint = $('<span class="border"></span>');
-      $div.append(this.$hint);
-      this.$hint.css({
-        width: width,
-        height: $div.height() - 4
-      });
-    }
-
-    BorderDropHint.prototype.remove = function() {
-      return this.$hint.remove();
-    };
-
-    return BorderDropHint;
-
-  })();
-
-  NodeElement = (function() {
-
-    function NodeElement(node, tree_element) {
-      this.init(node, tree_element);
-    }
-
-    NodeElement.prototype.init = function(node, tree_element) {
-      this.node = node;
-      this.tree_element = tree_element;
-      return this.$element = $(node.element);
-    };
-
-    NodeElement.prototype.getUl = function() {
-      return this.$element.children('ul:first');
-    };
-
-    NodeElement.prototype.getSpan = function() {
-      return this.$element.children('div').find('span.title');
-    };
-
-    NodeElement.prototype.getLi = function() {
-      return this.$element;
-    };
-
-    NodeElement.prototype.addDropHint = function(position) {
-      if (position === Position.INSIDE) {
-        return new BorderDropHint(this.$element);
-      } else {
-        return new GhostDropHint(this.node, this.$element, position);
-      }
-    };
-
-    NodeElement.prototype.select = function() {
-      return this.getLi().addClass('selected');
-    };
-
-    NodeElement.prototype.deselect = function() {
-      return this.getLi().removeClass('selected');
-    };
-
-    return NodeElement;
-
-  })();
-
-  FolderElement = (function(_super) {
-
-    __extends(FolderElement, _super);
-
-    function FolderElement() {
-      return FolderElement.__super__.constructor.apply(this, arguments);
-    }
-
-    FolderElement.prototype.toggle = function() {
-      if (this.node.is_open) {
-        return this.close();
-      } else {
-        return this.open();
-      }
-    };
-
-    FolderElement.prototype.open = function(on_finished, skip_slide) {
-      var doOpen,
-        _this = this;
-      if (!this.node.is_open) {
-        this.node.is_open = true;
-        this.getButton().removeClass('closed');
-        doOpen = function() {
-          var event;
-          _this.getLi().removeClass('closed');
-          if (on_finished) {
-            on_finished();
-          }
-          event = $.Event('tree.open');
-          event.node = _this.node;
-          return _this.tree_element.trigger(event);
-        };
-        if (skip_slide) {
-          return doOpen();
-        } else {
-          return this.getUl().slideDown('fast', doOpen);
-        }
-      }
-    };
-
-    FolderElement.prototype.close = function(skip_slide) {
-      var doClose,
-        _this = this;
-      if (this.node.is_open) {
-        this.node.is_open = false;
-        this.getButton().addClass('closed');
-        doClose = function() {
-          var event;
-          _this.getLi().addClass('closed');
-          event = $.Event('tree.close');
-          event.node = _this.node;
-          return _this.tree_element.trigger(event);
-        };
-        if (skip_slide) {
-          return doClose();
-        } else {
-          return this.getUl().slideUp('fast', doClose);
-        }
-      }
-    };
-
-    FolderElement.prototype.getButton = function() {
-      return this.$element.children('div').find('a.toggler');
-    };
-
-    FolderElement.prototype.addDropHint = function(position) {
-      if (!this.node.is_open && position === Position.INSIDE) {
-        return new BorderDropHint(this.$element);
-      } else {
-        return new GhostDropHint(this.node, this.$element, position);
-      }
-    };
-
-    return FolderElement;
-
-  })(NodeElement);
-
-  DragElement = (function() {
-
-    function DragElement(node, offset_x, offset_y, $tree) {
-      this.offset_x = offset_x;
-      this.offset_y = offset_y;
-      this.$element = $("<span class=\"title tree-dragging\">" + node.name + "</span>");
-      this.$element.css("position", "absolute");
-      $tree.append(this.$element);
-    }
-
-    DragElement.prototype.move = function(page_x, page_y) {
-      return this.$element.offset({
-        left: page_x - this.offset_x,
-        top: page_y - this.offset_y
-      });
-    };
-
-    DragElement.prototype.remove = function() {
-      return this.$element.remove();
-    };
-
-    return DragElement;
-
-  })();
-
-  SaveStateHandler = (function() {
-
-    function SaveStateHandler(tree_widget) {
-      this.tree_widget = tree_widget;
-    }
-
-    SaveStateHandler.prototype.saveState = function() {
-      if (this.tree_widget.options.onSetStateFromStorage) {
-        return this.tree_widget.options.onSetStateFromStorage(this.getState());
-      } else if (typeof localStorage !== "undefined" && localStorage !== null) {
-        return localStorage.setItem(this.getCookieName(), this.getState());
-      } else if ($.cookie) {
-        return $.cookie(this.getCookieName(), this.getState(), {
-          path: '/'
-        });
-      }
-    };
-
-    SaveStateHandler.prototype.restoreState = function() {
-      var state;
-      if (this.tree_widget.options.onGetStateFromStorage) {
-        state = this.tree_widget.options.onGetStateFromStorage();
-      } else if (typeof localStorage !== "undefined" && localStorage !== null) {
-        state = localStorage.getItem(this.getCookieName());
-      } else if ($.cookie) {
-        state = $.cookie(this.getCookieName(), {
-          path: '/'
-        });
-      } else {
-        state = null;
-      }
-      if (!state) {
-        return false;
-      } else {
-        this.setState(state);
-        return true;
-      }
-    };
-
-    SaveStateHandler.prototype.getState = function() {
-      var open_nodes, selected_node,
-        _this = this;
-      open_nodes = [];
-      this.tree_widget.tree.iterate(function(node) {
-        if (node.is_open && node.id && node.hasChildren()) {
-          open_nodes.push(node.id);
-        }
-        return true;
-      });
-      selected_node = '';
-      if (this.tree_widget.selected_node) {
-        selected_node = this.tree_widget.selected_node.id;
-      }
-      return toJson({
-        open_nodes: open_nodes,
-        selected_node: selected_node
-      });
-    };
-
-    SaveStateHandler.prototype.setState = function(state) {
-      var data, open_nodes, selected_node_id,
-        _this = this;
-      data = $.parseJSON(state);
-      if (data) {
-        open_nodes = data.open_nodes;
-        selected_node_id = data.selected_node;
-        return this.tree_widget.tree.iterate(function(node) {
-          if (node.id && node.hasChildren() && (indexOf(open_nodes, node.id) >= 0)) {
-            node.is_open = true;
-          }
-          if (selected_node_id && (node.id === selected_node_id)) {
-            _this.tree_widget.selected_node = node;
-          }
-          return true;
-        });
-      }
-    };
-
-    SaveStateHandler.prototype.getCookieName = function() {
-      if (typeof this.tree_widget.options.saveState === 'string') {
-        return this.tree_widget.options.saveState;
-      } else {
-        return 'tree';
-      }
-    };
-
-    return SaveStateHandler;
-
-  })();
-
-  SelectNodeHandler = (function() {
-
-    function SelectNodeHandler(tree_widget) {
-      this.tree_widget = tree_widget;
-    }
-
-    SelectNodeHandler.prototype.selectNode = function(node, must_open_parents) {
-      var parent;
-      if (this.tree_widget.options.selectable) {
-        if (this.tree_widget.selected_node) {
-          this.tree_widget._getNodeElementForNode(this.tree_widget.selected_node).deselect();
-          this.tree_widget.selected_node = null;
-        }
-        if (node) {
-          this.tree_widget._getNodeElementForNode(node).select();
-          this.tree_widget.selected_node = node;
-          if (must_open_parents) {
-            parent = this.tree_widget.selected_node.parent;
-            while (parent) {
-              if (!parent.is_open) {
-                this.tree_widget.openNode(parent, true);
-              }
-              parent = parent.parent;
-            }
-          }
-        }
-        if (this.tree_widget.options.saveState) {
-          return this.tree_widget.save_state_handler.saveState();
-        }
-      }
-    };
-
-    SelectNodeHandler.prototype.selectCurrentNode = function() {
-      var node_element;
-      if (this.tree_widget.selected_node) {
-        node_element = this.tree_widget._getNodeElementForNode(this.tree_widget.selected_node);
-        if (node_element) {
-          return node_element.select();
-        }
-      }
-    };
-
-    return SelectNodeHandler;
-
-  })();
-
-  DragAndDropHandler = (function() {
-
-    function DragAndDropHandler(tree_widget) {
-      this.tree_widget = tree_widget;
-      this.hovered_area = null;
-      this.$ghost = null;
-      this.hit_areas = [];
-      this.is_dragging = false;
-    }
-
-    DragAndDropHandler.prototype.mouseCapture = function(event) {
-      var $element, node_element;
-      $element = $(event.target);
-      if (this.tree_widget.options.onIsMoveHandle && !this.tree_widget.options.onIsMoveHandle($element)) {
-        return null;
-      }
-      node_element = this.tree_widget._getNodeElement($element);
-      if (node_element && this.tree_widget.options.onCanMove) {
-        if (!this.tree_widget.options.onCanMove(node_element.node)) {
-          node_element = null;
-        }
-      }
-      this.current_item = node_element;
-      return this.current_item !== null;
-    };
-
-    DragAndDropHandler.prototype.mouseStart = function(event) {
-      var offsetX, offsetY, _ref;
-      this.refreshHitAreas();
-      _ref = this.getOffsetFromEvent(event), offsetX = _ref[0], offsetY = _ref[1];
-      this.drag_element = new DragElement(this.current_item.node, offsetX, offsetY, this.tree_widget.element);
-      this.is_dragging = true;
-      this.current_item.$element.addClass('moving');
-      return true;
-    };
-
-    DragAndDropHandler.prototype.mouseDrag = function(event) {
-      var area, position_name;
-      this.drag_element.move(event.pageX, event.pageY);
-      area = this.findHoveredArea(event.pageX, event.pageY);
-      if (area && this.tree_widget.options.onCanMoveTo) {
-        position_name = Position.getName(area.position);
-        if (!this.tree_widget.options.onCanMoveTo(this.current_item.node, area.node, position_name)) {
-          area = null;
-        }
-      }
-      if (!area) {
-        this.removeDropHint();
-        this.removeHover();
-        this.stopOpenFolderTimer();
-      } else {
-        if (this.hovered_area !== area) {
-          this.hovered_area = area;
-          this.updateDropHint();
-        }
-      }
-      return true;
-    };
-
-    DragAndDropHandler.prototype.mouseStop = function() {
-      this.moveItem();
-      this.clear();
-      this.removeHover();
-      this.removeDropHint();
-      this.removeHitAreas();
-      this.current_item.$element.removeClass('moving');
-      this.is_dragging = false;
-      return false;
-    };
-
-    DragAndDropHandler.prototype.getOffsetFromEvent = function(event) {
-      var element_offset;
-      element_offset = $(event.target).offset();
-      return [event.pageX - element_offset.left, event.pageY - element_offset.top];
-    };
-
-    DragAndDropHandler.prototype.refreshHitAreas = function() {
-      this.removeHitAreas();
-      return this.generateHitAreas();
-    };
-
-    DragAndDropHandler.prototype.removeHitAreas = function() {
-      return this.hit_areas = [];
-    };
-
-    DragAndDropHandler.prototype.clear = function() {
-      this.drag_element.remove();
-      return this.drag_element = null;
-    };
-
-    DragAndDropHandler.prototype.removeDropHint = function() {
-      if (this.previous_ghost) {
-        return this.previous_ghost.remove();
-      }
-    };
-
-    DragAndDropHandler.prototype.removeHover = function() {
-      return this.hovered_area = null;
-    };
-
-    DragAndDropHandler.prototype.generateHitAreas = function() {
-      var addPosition, getTop, groupPositions, handleAfterOpenFolder, handleClosedFolder, handleFirstNode, handleNode, handleOpenFolder, hit_areas, last_top, positions,
-        _this = this;
-      positions = [];
-      last_top = 0;
-      getTop = function($element) {
-        return $element.offset().top;
-      };
-      addPosition = function(node, position, top) {
-        positions.push({
-          top: top,
-          node: node,
-          position: position
-        });
-        return last_top = top;
-      };
-      groupPositions = function(handle_group) {
-        var group, position, previous_top, _i, _len;
-        previous_top = -1;
-        group = [];
-        for (_i = 0, _len = positions.length; _i < _len; _i++) {
-          position = positions[_i];
-          if (position.top !== previous_top) {
-            if (group.length) {
-              handle_group(group, previous_top, position.top);
-            }
-            previous_top = position.top;
-            group = [];
-          }
-          group.push(position);
-        }
-        return handle_group(group, previous_top, _this.tree_widget.element.offset().top + _this.tree_widget.element.height());
-      };
-      handleNode = function(node, next_node, $element) {
-        var top;
-        top = getTop($element);
-        if (node === _this.current_item.node) {
-          addPosition(node, Position.NONE, top);
-        } else {
-          addPosition(node, Position.INSIDE, top);
-        }
-        if (next_node === _this.current_item.node || node === _this.current_item.node) {
-          return addPosition(node, Position.NONE, top);
-        } else {
-          return addPosition(node, Position.AFTER, top);
-        }
-      };
-      handleOpenFolder = function(node, $element) {
-        if (node === _this.current_item.node) {
-          return false;
-        }
-        if (node.children[0] !== _this.current_item.node) {
-          addPosition(node, Position.INSIDE, getTop($element));
-        }
-        return true;
-      };
-      handleAfterOpenFolder = function(node, next_node, $element) {
-        if (node === _this.current_item.node || next_node === _this.current_item.node) {
-          return addPosition(node, Position.NONE, last_top);
-        } else {
-          return addPosition(node, Position.AFTER, last_top);
-        }
-      };
-      handleClosedFolder = function(node, next_node, $element) {
-        var top;
-        top = getTop($element);
-        if (node === _this.current_item.node) {
-          return addPosition(node, Position.NONE, top);
-        } else {
-          addPosition(node, Position.INSIDE, top);
-          if (next_node !== _this.current_item.node) {
-            return addPosition(node, Position.AFTER, top);
-          }
-        }
-      };
-      handleFirstNode = function(node, $element) {
-        if (node !== _this.current_item.node) {
-          return addPosition(node, Position.BEFORE, getTop($(node.element)));
-        }
-      };
-      this.iterateVisibleNodes(handleNode, handleOpenFolder, handleClosedFolder, handleAfterOpenFolder, handleFirstNode);
-      hit_areas = [];
-      groupPositions(function(positions_in_group, top, bottom) {
-        var area_height, area_top, position, _i, _len;
-        area_height = (bottom - top) / positions_in_group.length;
-        area_top = top;
-        for (_i = 0, _len = positions_in_group.length; _i < _len; _i++) {
-          position = positions_in_group[_i];
-          hit_areas.push({
-            top: area_top,
-            bottom: area_top + area_height,
-            node: position.node,
-            position: position.position
-          });
-          area_top += area_height;
-        }
-        return null;
-      });
-      return this.hit_areas = hit_areas;
-    };
-
-    DragAndDropHandler.prototype.iterateVisibleNodes = function(handle_node, handle_open_folder, handle_closed_folder, handle_after_open_folder, handle_first_node) {
-      var is_first_node, iterate,
-        _this = this;
-      is_first_node = true;
-      iterate = function(node, next_node) {
-        var $element, child, children_length, i, must_iterate_inside, _i, _len, _ref;
-        must_iterate_inside = (node.is_open || !node.element) && node.hasChildren();
-        if (node.element) {
-          $element = $(node.element);
-          if (!$element.is(':visible')) {
-            return;
-          }
-          if (is_first_node) {
-            handle_first_node(node, $element);
-            is_first_node = false;
-          }
-          if (!node.hasChildren()) {
-            handle_node(node, next_node, $element);
-          } else if (node.is_open) {
-            if (!handle_open_folder(node, $element)) {
-              must_iterate_inside = false;
-            }
-          } else {
-            handle_closed_folder(node, next_node, $element);
-          }
-        }
-        if (must_iterate_inside) {
-          children_length = node.children.length;
-          _ref = node.children;
-          for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
-            child = _ref[i];
-            if (i === (children_length - 1)) {
-              iterate(node.children[i], null);
-            } else {
-              iterate(node.children[i], node.children[i + 1]);
-            }
-          }
-          if (node.is_open) {
-            return handle_after_open_folder(node, next_node, $element);
-          }
-        }
-      };
-      return iterate(this.tree_widget.tree);
-    };
-
-    DragAndDropHandler.prototype.findHoveredArea = function(x, y) {
-      var area, high, low, mid, tree_offset;
-      tree_offset = this.tree_widget.element.offset();
-      if (x < tree_offset.left || y < tree_offset.top || x > (tree_offset.left + this.tree_widget.element.width()) || y > (tree_offset.top + this.tree_widget.element.height())) {
-        return null;
-      }
-      low = 0;
-      high = this.hit_areas.length;
-      while (low < high) {
-        mid = (low + high) >> 1;
-        area = this.hit_areas[mid];
-        if (y < area.top) {
-          high = mid;
-        } else if (y > area.bottom) {
-          low = mid + 1;
-        } else {
-          return area;
-        }
-      }
-      return null;
-    };
-
-    DragAndDropHandler.prototype.updateDropHint = function() {
-      var node, node_element;
-      this.stopOpenFolderTimer();
-      if (!this.hovered_area) {
-        return;
-      }
-      node = this.hovered_area.node;
-      if (node.hasChildren() && !node.is_open && this.hovered_area.position === Position.INSIDE) {
-        this.startOpenFolderTimer(node);
-      }
-      this.removeDropHint();
-      node_element = this.tree_widget._getNodeElementForNode(this.hovered_area.node);
-      return this.previous_ghost = node_element.addDropHint(this.hovered_area.position);
-    };
-
-    DragAndDropHandler.prototype.startOpenFolderTimer = function(folder) {
-      var openFolder,
-        _this = this;
-      openFolder = function() {
-        return _this.tree_widget._getNodeElementForNode(folder).open(function() {
-          _this.refreshHitAreas();
-          return _this.updateDropHint();
-        });
-      };
-      return this.open_folder_timer = setTimeout(openFolder, 500);
-    };
-
-    DragAndDropHandler.prototype.stopOpenFolderTimer = function() {
-      if (this.open_folder_timer) {
-        clearTimeout(this.open_folder_timer);
-        return this.open_folder_timer = null;
-      }
-    };
-
-    DragAndDropHandler.prototype.moveItem = function() {
-      var doMove, event, moved_node, position, previous_parent, target_node,
-        _this = this;
-      if (this.hovered_area && this.hovered_area.position !== Position.NONE) {
-        moved_node = this.current_item.node;
-        target_node = this.hovered_area.node;
-        position = this.hovered_area.position;
-        previous_parent = moved_node.parent;
-        if (position === Position.INSIDE) {
-          this.hovered_area.node.is_open = true;
-        }
-        doMove = function() {
-          _this.tree_widget.tree.moveNode(moved_node, target_node, position);
-          _this.tree_widget.element.empty();
-          return _this.tree_widget._refreshElements();
-        };
-        event = $.Event('tree.move');
-        event.move_info = {
-          moved_node: moved_node,
-          target_node: target_node,
-          position: Position.getName(position),
-          previous_parent: previous_parent,
-          do_move: doMove
-        };
-        this.tree_widget.element.trigger(event);
-        if (!event.isDefaultPrevented()) {
-          return doMove();
-        }
-      }
-    };
-
-    return DragAndDropHandler;
-
-  })();
-
-  this.Tree.Node = Node;
-
-}).call(this);
Binary file OrthancExplorer/orthanc-logo.png has changed
--- a/OrthancExplorer/query-retrieve.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,337 +0,0 @@
-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() {
-  var targetDate;
-
-  $.ajax({
-    url: '../modalities',
-    dataType: 'json',
-    async: false,
-    cache: false,
-    success: function(modalities) {
-      var targetServer = $('#qr-server');
-      var option;
-
-      $('option', targetServer).remove();
-
-      for (var i = 0; i < modalities.length; i++) {
-        option = $('<option>').text(modalities[i]);
-        targetServer.append(option);
-      }
-
-      targetServer.selectmenu('refresh');
-    }
-  });
-
-  targetDate = $('#qr-date');
-  $('option', targetDate).remove();
-  targetDate.append($('<option>').attr('value', '').text('Any date'));
-  targetDate.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
-  targetDate.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
-  targetDate.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
-  targetDate.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
-  targetDate.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
-  targetDate.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
-  targetDate.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, server, modalities, field;
-
-  query = {
-    'Level' : 'Study',
-    'Query' : {
-      'AccessionNumber' : '',
-      'PatientBirthDate' : '',
-      'PatientID' : '',
-      'PatientName' : '',
-      'PatientSex' : '',
-      'StudyDate' : $('#qr-date').val(),
-      'StudyDescription' : ''
-    }
-  };
-
-  modalities = '';
-
-  field = $('#qr-fields input:checked').val();
-  query['Query'][field] = $('#qr-value').val().toUpperCase();
-
-  $('#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;
-  }
-
-
-  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() {
-  var pageData, uri;
-  
-  if ($.mobile.pageData) {
-    pageData = DeepCopy($.mobile.pageData);
-
-    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() {
-  var pageData, query;
-
-  if ($.mobile.pageData) {
-    pageData = DeepCopy($.mobile.pageData);
-
-    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() {
-  var pageData, uri;
-  
-  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) {
-          var aet;
-
-          event.preventDefault();
-
-          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');
-            }
-          });
-        });
-      }
-    });
-  }
-});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/COPYING	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU 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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,58 @@
+set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/../EmbedResources.py"
+  CACHE INTERNAL "Path to the EmbedResources.py script from Orthanc")
+set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
+set(AUTOGENERATED_SOURCES)
+
+file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
+include_directories(${AUTOGENERATED_DIR})
+
+macro(EmbedResources)
+  # Convert a semicolon separated list to a whitespace separated string
+  set(SCRIPT_OPTIONS)
+  set(SCRIPT_ARGUMENTS)
+  set(DEPENDENCIES)
+  set(IS_PATH_NAME false)
+
+  set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
+
+  # Loop over the arguments of the function
+  foreach(arg ${ARGN})
+    # Extract the first character of the argument
+    string(SUBSTRING "${arg}" 0 1 FIRST_CHAR)
+    if (${FIRST_CHAR} STREQUAL "-")
+      # If the argument starts with a dash "-", this is an option to
+      # EmbedResources.py
+      if (${arg} MATCHES "--target=.*")
+        # Does the argument starts with "--target="?
+        string(SUBSTRING "${arg}" 9 -1 TARGET)  # 9 is the length of "--target="
+        set(TARGET_BASE "${AUTOGENERATED_DIR}/${TARGET}")
+      else()
+        list(APPEND SCRIPT_OPTIONS ${arg})
+      endif()
+    else()
+      if (${IS_PATH_NAME})
+        list(APPEND SCRIPT_ARGUMENTS "${arg}")
+        list(APPEND DEPENDENCIES "${arg}")
+        set(IS_PATH_NAME false)
+      else()
+        list(APPEND SCRIPT_ARGUMENTS "${arg}")
+        set(IS_PATH_NAME true)
+      endif()
+    endif()
+  endforeach()
+
+  add_custom_command(
+    OUTPUT
+    "${TARGET_BASE}.h"
+    "${TARGET_BASE}.cpp"
+    COMMAND ${PYTHON_EXECUTABLE} ${EMBED_RESOURCES_PYTHON}
+            ${SCRIPT_OPTIONS} "${TARGET_BASE}" ${SCRIPT_ARGUMENTS}
+    DEPENDS
+    ${EMBED_RESOURCES_PYTHON}
+    ${DEPENDENCIES}
+    )
+
+  list(APPEND AUTOGENERATED_SOURCES
+    "${TARGET_BASE}.cpp"
+    ) 
+endmacro()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,395 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_BOOST)
+  set(BOOST_STATIC 1)
+else()
+  include(FindBoost)
+
+  set(BOOST_STATIC 0)
+  #set(Boost_DEBUG 1)
+  #set(Boost_USE_STATIC_LIBS ON)
+
+  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()
+
+  
+  # Patch by xnox to fix issue #166 (CMake find_boost version is now
+  # broken with newer boost/cmake)
+  # https://bitbucket.org/sjodogne/orthanc/issues/166/
+  if (POLICY CMP0093)
+    set(BOOST144 1.44)
+  else()
+    set(BOOST144 104400)
+  endif()
+  
+  
+  # Boost releases 1.44 through 1.47 supply both V2 and V3 filesystem
+  # http://www.boost.org/doc/libs/1_46_1/libs/filesystem/v3/doc/index.htm
+  if (${Boost_VERSION} LESS ${BOOST144})
+    add_definitions(
+      -DBOOST_HAS_FILESYSTEM_V3=0
+      )
+  else()
+    add_definitions(
+      -DBOOST_HAS_FILESYSTEM_V3=1
+      -DBOOST_FILESYSTEM_VERSION=3
+      )
+  endif()
+
+  include_directories(${Boost_INCLUDE_DIRS})
+  link_libraries(${Boost_LIBRARIES})
+endif()
+
+
+if (BOOST_STATIC)
+  ##
+  ## Parameters for static compilation of Boost 
+  ##
+  
+  set(BOOST_NAME boost_1_69_0)
+  set(BOOST_VERSION 1.69.0)
+  set(BOOST_BCP_SUFFIX bcpdigest-1.5.6)
+  set(BOOST_MD5 "579bccc0ea4d1a261c1d0c5e27446c3d")
+  set(BOOST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/${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}")
+
+
+  ##
+  ## Patching boost
+  ## 
+
+  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
+    )
+
+  if (FirstRun AND Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+
+  ##
+  ## Generic configuration of Boost
+  ## 
+
+  if (CMAKE_COMPILER_IS_GNUCXX)
+    add_definitions(-isystem ${BOOST_SOURCES_DIR})
+  endif()
+
+  include_directories(
+    BEFORE ${BOOST_SOURCES_DIR}
+    )
+
+  if (ORTHANC_BUILDING_FRAMEWORK_LIBRARY)
+    add_definitions(
+      # Packaging Boost inside the Orthanc Framework DLL
+      -DBOOST_ALL_DYN_LINK
+      -DBOOST_THREAD_BUILD_DLL
+      #-DBOOST_REGEX_BUILD_DLL
+      )
+  else()
+    add_definitions(
+      # Static build of Boost (this was the only possibility in
+      # Orthanc <= 1.7.1)
+      -DBOOST_ALL_NO_LIB 
+      -DBOOST_ALL_NOLIB 
+      -DBOOST_DATE_TIME_NO_LIB 
+      -DBOOST_THREAD_BUILD_LIB
+      -DBOOST_PROGRAM_OPTIONS_NO_LIB
+      -DBOOST_REGEX_NO_LIB
+      -DBOOST_SYSTEM_NO_LIB
+      -DBOOST_LOCALE_NO_LIB
+      )
+  endif()
+
+  add_definitions(
+    # In static builds, explicitly prevent Boost from using the system
+    # locale in lexical casts. This is notably important if
+    # "boost::lexical_cast<double>()" is applied to strings containing
+    # "," instead of "." as decimal separators. Check out function
+    # "OrthancStone::LinearAlgebra::ParseVector()".
+    -DBOOST_LEXICAL_CAST_ASSUME_C_LOCALE
+    )
+
+  set(BOOST_SOURCES
+    ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp
+    )
+
+  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR
+      "${CMAKE_SYSTEM_NAME}" STREQUAL "Android")
+    add_definitions(
+      -DBOOST_SYSTEM_USE_STRERROR=1
+      )
+  endif()
+
+  
+  ##
+  ## 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()
+
+    # Fix for error: "boost_1_69_0/boost/chrono/detail/inlined/mac/thread_clock.hpp:54:28: 
+    # error: use of undeclared identifier 'pthread_mach_thread_np'"
+    # https://github.com/envoyproxy/envoy/pull/1785
+    if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+      add_definitions(-D_DARWIN_C_SOURCE=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
+    )
+
+
+  ##
+  ## 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()
+
+
+  ##
+  ## Configuration of boost::locale
+  ## 
+
+  if (NOT ENABLE_LOCALE)
+    message("boost::locale is disabled")
+  else()
+    set(BOOST_ICU_SOURCES
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/boundary.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/codecvt.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/collator.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/conversion.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/date_time.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/formatter.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/icu_backend.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/numeric.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/time_zone.cpp
+      )
+
+    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")
+      add_definitions(
+        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
+        )
+      
+      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
+        )
+
+      if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
+          BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
+        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
+      else()
+        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+      endif()
+
+    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
+      add_definitions(
+        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+        -DBOOST_LOCALE_NO_STD_BACKEND=1
+        )
+      
+      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
+        )
+
+      if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR
+          BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
+          BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+        # In WebAssembly or asm.js, we rely on the version of iconv
+        # that is shipped with the stdlib
+        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
+        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
+      else()
+        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+      endif()
+
+    elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+      add_definitions(
+        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
+        -DBOOST_LOCALE_NO_STD_BACKEND=1
+        )
+
+      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
+        )
+
+      # Starting with release 0.8.2, Orthanc statically links against
+      # libiconv on Windows. Indeed, the "WCONV" library of Windows XP
+      # seems not to support properly several codepages (notably
+      # "Latin3", "Hebrew", and "Arabic"). Set "BOOST_LOCALE_BACKEND"
+      # to "wconv" to use WCONV anyway.
+
+      if (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
+        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
+      elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv")
+        message("Using Window's wconv")
+        add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
+      else()
+        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND on Windows: ${BOOST_LOCALE_BACKEND}")
+      endif()
+
+    else()
+      message(FATAL_ERROR "Support your platform here")
+    endif()
+  endif()
+
+  
+  source_group(ThirdParty\\boost REGULAR_EXPRESSION ${BOOST_SOURCES_DIR}/.*)
+
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/BoostConfiguration.sh	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,67 @@
+#!/bin/bash
+
+set -e
+set -u
+
+## Starting with version 0.6.2, Orthanc is shipped with a subset of the
+## Boost libraries that is generated with the BCP tool:
+##
+## http://www.boost.org/doc/libs/1_54_0/tools/bcp/doc/html/index.html
+##
+## This script generates this subset.
+##
+## 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 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 between 1.4.0 and 1.4.2: Boost 1.67.0
+##   - Orthanc between 1.5.0 and 1.5.4: Boost 1.68.0
+##   - Orthanc >= 1.5.5: Boost 1.69.0
+
+BOOST_VERSION=1_69_0
+ORTHANC_VERSION=1.5.6
+
+rm -rf /tmp/boost_${BOOST_VERSION}
+rm -rf /tmp/bcp/boost_${BOOST_VERSION}
+
+cd /tmp
+echo "Uncompressing the sources of Boost ${BOOST_VERSION}..."
+tar xfz ./boost_${BOOST_VERSION}.tar.gz 
+
+echo "Generating the subset..."
+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 chrono /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..."
+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/OrthancFramework/Resources/CMake/CivetwebConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,91 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_CIVETWEB)
+
+  ## WARNING: "civetweb-1.12.tar.gz" comes with a subfolder
+  ## "civetweb-1.12/test/nonlatin" that cannot be removed by "hg purge
+  ## --all" on Windows hosts. We thus created a custom
+  ## "civetweb-1.12-fixed.tar.gz" as follows:
+  ##
+  ##  $ cd /tmp
+  ##  $ wget http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.12.tar.gz
+  ##  $ tar xvf civetweb-1.12.tar.gz
+  ##  $ rm -rf civetweb-1.12/src/third_party/ civetweb-1.12/test/
+  ##  $ tar cvfz civetweb-1.12-fixed.tar.gz civetweb-1.12
+  ##
+  
+  set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.12)
+  set(CIVETWEB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.12-fixed.tar.gz")
+  set(CIVETWEB_MD5 "016ed7cd26cbc46b5941f0cbfb2e4ac8")
+
+  if (IS_DIRECTORY "${CIVETWEB_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+
+  DownloadPackage(${CIVETWEB_MD5} ${CIVETWEB_URL} "${CIVETWEB_SOURCES_DIR}")
+
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${ORTHANC_ROOT}/Resources/Patches/civetweb-1.12.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (FirstRun AND Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+  
+  include_directories(
+    ${CIVETWEB_SOURCES_DIR}/include
+    )
+
+  set(CIVETWEB_SOURCES
+    ${CIVETWEB_SOURCES_DIR}/src/civetweb.c
+    )
+
+  # New in Orthanc 1.6.0: Enable support of compression in civetweb
+  set_source_files_properties(
+    ${CIVETWEB_SOURCES}
+    PROPERTIES COMPILE_DEFINITIONS
+    "USE_ZLIB=1")
+  
+  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()
+
+  source_group(ThirdParty\\Civetweb REGULAR_EXPRESSION ${CIVETWEB_SOURCES_DIR}/.*)
+
+  add_definitions(
+    -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1
+    )
+
+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)
+
+  add_definitions(
+    -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0
+    )
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/Compiler.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,263 @@
+# This file sets all the compiler-related flags
+
+include(CheckLibraryExists)
+
+if ((CMAKE_CROSSCOMPILING AND NOT
+      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") 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_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+  # Cache the environment variables "LSB_CC" and "LSB_CXX" for further
+  # use by "ExternalProject" in CMake
+  SET(CMAKE_LSB_CC $ENV{LSB_CC} CACHE STRING "")
+  SET(CMAKE_LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
+endif()
+
+
+if (CMAKE_COMPILER_IS_GNUCXX)
+  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 -Wno-long-long -Wno-variadic-macros")
+
+  if (CMAKE_CROSSCOMPILING)
+    # http://stackoverflow.com/a/3543845/881731
+    set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> -O coff -I<CMAKE_CURRENT_SOURCE_DIR> <SOURCE> <OBJECT>")
+  endif()
+
+elseif (MSVC)
+  # Use static runtime under Visual Studio
+  # http://www.cmake.org/Wiki/CMake_FAQ#Dynamic_Replace
+  # http://stackoverflow.com/a/6510446
+  foreach(flag_var
+    CMAKE_C_FLAGS_DEBUG
+    CMAKE_CXX_FLAGS_DEBUG
+    CMAKE_C_FLAGS_RELEASE 
+    CMAKE_CXX_FLAGS_RELEASE
+    CMAKE_C_FLAGS_MINSIZEREL 
+    CMAKE_CXX_FLAGS_MINSIZEREL 
+    CMAKE_C_FLAGS_RELWITHDEBINFO 
+    CMAKE_CXX_FLAGS_RELWITHDEBINFO) 
+    string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
+    string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}")
+  endforeach(flag_var)
+
+  # Add /Zm256 compiler option to Visual Studio to fix PCH errors
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256")
+
+  # New in Orthanc 1.5.5
+  if (MSVC_MULTIPLE_PROCESSES)
+    # "If you omit the processMax argument in the /MP option, the
+    # compiler obtains the number of effective processors from the
+    # operating system, and then creates one process per effective
+    # processor"
+    # https://blog.kitware.com/cmake-building-with-all-your-cores/
+    # https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes
+    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
+  endif()
+    
+  add_definitions(
+    -D_CRT_SECURE_NO_WARNINGS=1
+    -D_CRT_SECURE_NO_DEPRECATE=1
+    )
+
+  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" OR
+    ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
+      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+    # The "--no-undefined" linker flag makes the shared libraries
+    # (plugins ModalityWorklists and ServeFolders) fail to compile on
+    # OpenBSD, and make the PostgreSQL plugin complain about missing
+    # "environ" global variable in 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")
+  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(pthread)
+
+  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    link_libraries(rt)
+  endif()
+
+  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
+      )
+  endif()
+
+elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  if (MSVC)
+    message("MSVC compiler version = " ${MSVC_VERSION} "\n")
+    # Starting Visual Studio 2013 (version 1800), it is not possible
+    # to target Windows XP anymore
+    if (MSVC_VERSION LESS 1800)
+      add_definitions(
+        -DWINVER=0x0501
+        -D_WIN32_WINNT=0x0501
+        )
+    endif()
+  else()
+    add_definitions(
+      -DWINVER=0x0501
+      -D_WIN32_WINNT=0x0501
+      )
+  endif()
+
+  add_definitions(
+    -D_CRT_SECURE_NO_WARNINGS=1
+    )
+  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}")
+
+    if (DYNAMIC_MINGW_STDLIB)
+    else()
+      # This is a patch for MinGW64
+      SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_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++")
+    endif()
+
+    CHECK_LIBRARY_EXISTS(winpthread pthread_create "" HAVE_WIN_PTHREAD)
+    if (HAVE_WIN_PTHREAD)
+      if (DYNAMIC_MINGW_STDLIB)
+      else()
+        # This line is necessary to compile with recent versions of MinGW,
+        # otherwise "libwinpthread-1.dll" is not statically linked.
+        SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic")
+      endif()
+      add_definitions(-DHAVE_WIN_PTHREAD=1)
+    else()
+      add_definitions(-DHAVE_WIN_PTHREAD=0)
+    endif()
+  endif()
+
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -exported_symbols_list ${ORTHANC_ROOT}/Plugins/Samples/Common/ExportedSymbols.list")
+
+  add_definitions(
+    -D_XOPEN_SOURCE=1
+    )
+  link_libraries(iconv)
+
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  message("Building using Emscripten (for WebAssembly or asm.js targets)")
+
+  # 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.
+  # Setting this option to "ON" fixes error: "shared:ERROR:
+  # BINARYEN_TRAP_MODE is not supported by the LLVM wasm backend" if
+  # using the "upstream" backend of Emscripten.
+  if (NOT EMSCRIPTEN_SET_LLVM_WASM_BACKEND)
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s BINARYEN_TRAP_MODE='\"clamp\"'")
+  endif()
+
+  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
+  
+elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
+
+else()
+  message("Unknown target platform: ${CMAKE_SYSTEM_NAME}")
+  message(FATAL_ERROR "Support your platform here")
+endif()
+
+
+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_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()
+
+
+if (STATIC_BUILD)
+  add_definitions(-DORTHANC_STATIC=1)
+else()
+  add_definitions(-DORTHANC_STATIC=0)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,268 @@
+if (NOT DEFINED ENABLE_DCMTK_NETWORKING)
+  set(ENABLE_DCMTK_NETWORKING ON)
+endif()
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
+  if (DCMTK_STATIC_VERSION STREQUAL "3.6.0")
+    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.0.cmake)   
+  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.2")
+    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.2.cmake)
+  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.4")
+    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.4.cmake)
+  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.5")
+    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.5.cmake)
+  else()
+    message(FATAL_ERROR "Unsupported version of DCMTK: ${DCMTK_STATIC_VERSION}")
+  endif()
+
+
+  ##
+  ## Commands shared by all versions of DCMTK
+  ##
+
+  AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmdata/libsrc DCMTK_SOURCES)
+  AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/ofstd/libsrc DCMTK_SOURCES)
+
+  LIST(REMOVE_ITEM DCMTK_SOURCES 
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdictbi.cc
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdeftag.cc
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
+    )
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    message(${DCMTK_SOURCES_DIR})
+    list(REMOVE_ITEM DCMTK_SOURCES 
+      ${DCMTK_SOURCES_DIR}/ofstd/libsrc/offilsys.cc
+      )
+  endif()
+
+  if (ENABLE_DCMTK_NETWORKING)
+    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc DCMTK_SOURCES)
+    include_directories(
+      ${DCMTK_SOURCES_DIR}/dcmnet/include
+      )
+  endif()
+
+  if (ENABLE_DCMTK_TRANSCODING)
+    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimgle/libsrc DCMTK_SOURCES)
+    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimage/libsrc DCMTK_SOURCES)
+    include_directories(
+      ${DCMTK_SOURCES_DIR}/dcmimage/include
+      )
+  endif()
+  
+  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)
+    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg16 DCMTK_SOURCES)
+    include_directories(
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/include
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg16
+      ${DCMTK_SOURCES_DIR}/dcmimgle/include
+      )
+    list(REMOVE_ITEM DCMTK_SOURCES 
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/ddpiimpl.cc
+
+      # Solves linking problem in WebAssembly: "wasm-ld: error:
+      # duplicate symbol: jaritab" (modification in Orthanc 1.5.9)
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8/jaricom.c
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12/jaricom.c
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg24/jaricom.c
+      )
+
+    if (NOT ENABLE_DCMTK_TRANSCODING)
+      list(REMOVE_ITEM DCMTK_SOURCES 
+        # 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()
+  endif()
+
+
+  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(
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/include
+      ${DCMTK_SOURCES_DIR}/dcmjpls/include
+      ${DCMTK_SOURCES_DIR}/dcmjpls/libcharls
+      )
+    list(APPEND DCMTK_SOURCES 
+      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djrplol.cc
+      )
+
+    if (NOT ENABLE_DCMTK_TRANSCODING)
+      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
+        )
+    endif()
+  endif()
+
+  
+  # This fixes crashes related to the destruction of the DCMTK OFLogger
+  # http://support.dcmtk.org/docs-snapshot/file_macros.html
+  add_definitions(
+    -DLOG4CPLUS_DISABLE_FATAL=1
+    -DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER}
+    )
+
+
+  if (NOT ENABLE_DCMTK_LOG)
+    # Disable logging internal to DCMTK
+    # https://groups.google.com/d/msg/orthanc-users/v2SzzAmY948/VxT1QVGiBAAJ
+    add_definitions(
+      -DDCMTK_LOG4CPLUS_DISABLE_FATAL=1
+      -DDCMTK_LOG4CPLUS_DISABLE_ERROR=1
+      -DDCMTK_LOG4CPLUS_DISABLE_WARN=1
+      -DDCMTK_LOG4CPLUS_DISABLE_INFO=1
+      -DDCMTK_LOG4CPLUS_DISABLE_DEBUG=1
+      )
+  endif()
+
+  include_directories(
+    #${DCMTK_SOURCES_DIR}
+    ${DCMTK_SOURCES_DIR}/config/include
+    ${DCMTK_SOURCES_DIR}/ofstd/include
+    ${DCMTK_SOURCES_DIR}/oflog/include
+    ${DCMTK_SOURCES_DIR}/dcmdata/include
+    )
+
+  source_group(ThirdParty\\Dcmtk REGULAR_EXPRESSION ${DCMTK_SOURCES_DIR}/.*)
+
+  if (STANDALONE_BUILD)
+    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()
+    set(DCMTK_USE_EMBEDDED_DICTIONARIES 0)
+  endif()
+
+
+else()
+  if (CMAKE_CROSSCOMPILING AND
+      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+    CHECK_INCLUDE_FILE_CXX(dcmtk/dcmdata/dcfilefo.h HAVE_DCMTK_H)
+    if (NOT HAVE_DCMTK_H)
+      message(FATAL_ERROR "Please install the libdcmtk-dev package")
+    endif()
+
+    CHECK_LIBRARY_EXISTS(dcmdata "dcmDataDict" "" HAVE_DCMTK_LIB)
+    if (NOT HAVE_DCMTK_LIB)
+      message(FATAL_ERROR "Please install the libdcmtk package")
+    endif()  
+
+    find_path(DCMTK_INCLUDE_DIRS dcmtk/config/osconfig.h
+      /usr/include
+      )
+
+    link_libraries(dcmdata dcmnet dcmjpeg oflog ofstd)
+
+  else()
+    # The following line allows one to manually add libraries at the
+    # command-line, which is necessary for Ubuntu/Debian packages
+    set(tmp "${DCMTK_LIBRARIES}")
+    include(FindDCMTK)
+    list(APPEND DCMTK_LIBRARIES "${tmp}")
+
+    include_directories(${DCMTK_INCLUDE_DIRS})
+  endif()
+
+  add_definitions(
+    -DHAVE_CONFIG_H=1
+    )
+
+  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 libdcmtk*-dev")
+  endif()
+
+  message("DCMTK configuration file: ${DCMTK_CONFIGURATION_FILE}")
+  
+  # Autodetection of the version of DCMTK
+  file(STRINGS
+    "${DCMTK_CONFIGURATION_FILE}" 
+    DCMTK_VERSION_NUMBER1 REGEX
+    ".*PACKAGE_VERSION .*")    
+
+  string(REGEX REPLACE
+    ".*PACKAGE_VERSION.*\"([0-9]*)\\.([0-9]*)\\.([0-9]*)\"$"
+    "\\1\\2\\3" 
+    DCMTK_VERSION_NUMBER 
+    ${DCMTK_VERSION_NUMBER1})
+
+  set(DCMTK_USE_EMBEDDED_DICTIONARIES 0)
+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()
+
+    if (CMAKE_CROSSCOMPILING AND
+        "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+      # Remove the sysroot prefix
+      file(RELATIVE_PATH tmp ${CMAKE_FIND_ROOT_PATH} ${DCMTK_DICTIONARY_DIR_AUTO})
+      set(DCMTK_DICTIONARY_DIR_AUTO /${tmp} CACHE INTERNAL "")
+    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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.0.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,175 @@
+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://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.0.zip")
+SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4")
+
+if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
+
+
+if (FirstRun)
+  # 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()
+  message("The patches for DCMTK have already been applied")
+endif()
+
+
+# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
+IF (CMAKE_CROSSCOMPILING)
+  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_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})
+
+CONFIGURE_FILE(
+  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
+  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
+
+
+
+# Source for the logging facility of DCMTK
+AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
+    )
+
+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)
+    # This is a patch for DCMTK 3.6.0 and MinGW64
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.0-mingw64.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+
+    if (Failure AND FirstRun)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,194 @@
+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://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.2.tar.gz")
+SET(DCMTK_MD5 "d219a4152772985191c9b89d75302d12")
+
+macro(DCMTK_UNSET)
+endmacro()
+
+macro(DCMTK_UNSET_CACHE)
+endmacro()
+
+set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
+set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
+else()
+  set(DCMTK_WITH_THREADS ON)
+endif()
+
+add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
+
+if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
+
+
+if (FirstRun)
+  # "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.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+  configure_file(
+    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-dcdict_orthanc.cc
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
+    COPYONLY)
+else()
+  message("The patches for DCMTK have already been applied")
+endif()
+
+
+# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
+IF (CMAKE_CROSSCOMPILING)
+  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 "")
+
+  if (FirstRun)
+    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 (Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+  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_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})
+
+CONFIGURE_FILE(
+  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
+  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
+
+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()
+
+
+# Source for the logging facility of DCMTK
+AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
+    )
+
+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
+    )
+endif()
+
+
+#set_source_files_properties(${DCMTK_SOURCES}
+#  PROPERTIES COMPILE_DEFINITIONS
+#  "PACKAGE_VERSION=\"${DCMTK_PACKAGE_VERSION}\";PACKAGE_VERSION_NUMBER=\"${DCMTK_VERSION_NUMBER}\"")
+
+
+# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
+# default since this does not seem to be appropriate (anymore) for
+# most modern operating systems. In order to change this default, the
+# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
+# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
+# be defined to change this setting at compilation time (see
+# macros.txt for details).
+# https://forum.dcmtk.org/viewtopic.php?t=4632
+add_definitions(
+  -DDISABLE_NAGLE_ALGORITHM=1
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,179 @@
+SET(DCMTK_VERSION_NUMBER 364)
+SET(DCMTK_PACKAGE_VERSION "3.6.4")
+SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.4)
+SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.4.tar.gz")
+SET(DCMTK_MD5 "97597439a2ae7a39086066318db5f3bc")
+
+macro(DCMTK_UNSET)
+endmacro()
+
+macro(DCMTK_UNSET_CACHE)
+endmacro()
+
+set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
+set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
+else()
+  set(DCMTK_WITH_THREADS ON)
+endif()
+
+add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
+
+if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
+
+
+if (FirstRun)
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.4.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+  configure_file(
+    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-dcdict_orthanc.cc
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
+    COPYONLY)
+else()
+  message("The patches for DCMTK have already been applied")
+endif()
+
+
+include_directories(
+  ${DCMTK_SOURCES_DIR}/dcmiod/include
+  )
+
+
+# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
+IF (CMAKE_CROSSCOMPILING)
+  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 "")
+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_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})
+
+CONFIGURE_FILE(
+  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
+  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
+
+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()
+
+
+# Source for the logging facility of DCMTK
+AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
+    )
+
+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
+    )
+endif()
+
+
+# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
+# default since this does not seem to be appropriate (anymore) for
+# most modern operating systems. In order to change this default, the
+# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
+# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
+# be defined to change this setting at compilation time (see
+# macros.txt for details).
+# https://forum.dcmtk.org/viewtopic.php?t=4632
+add_definitions(
+  -DDISABLE_NAGLE_ALGORITHM=1
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,204 @@
+SET(DCMTK_VERSION_NUMBER 365)
+SET(DCMTK_PACKAGE_VERSION "3.6.5")
+SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.5)
+SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.5.tar.gz")
+SET(DCMTK_MD5 "e19707f64ee5695c496b9c1e48e39d07")
+
+macro(DCMTK_UNSET)
+endmacro()
+
+macro(DCMTK_UNSET_CACHE)
+endmacro()
+
+set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
+set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
+else()
+  set(DCMTK_WITH_THREADS ON)
+endif()
+
+add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
+
+if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
+
+
+if (FirstRun)
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.5.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+  configure_file(
+    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-dcdict_orthanc.cc
+    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
+    COPYONLY)
+else()
+  message("The patches for DCMTK have already been applied")
+endif()
+
+
+include_directories(
+  ${DCMTK_SOURCES_DIR}/dcmiod/include
+  )
+
+
+# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
+IF (CMAKE_CROSSCOMPILING)
+  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 "")
+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_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})
+
+CONFIGURE_FILE(
+  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
+  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
+
+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()
+
+
+# Source for the logging facility of DCMTK
+AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
+    )
+
+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
+    )
+endif()
+
+
+# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
+# default since this does not seem to be appropriate (anymore) for
+# most modern operating systems. In order to change this default, the
+# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
+# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
+# be defined to change this setting at compilation time (see
+# macros.txt for details).
+# https://forum.dcmtk.org/viewtopic.php?t=4632
+add_definitions(
+  -DDISABLE_NAGLE_ALGORITHM=1
+  )
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  # For compatibility with Windows XP, avoid using fiber-local-storage
+  # in log4cplus, but use thread-local-storage instead. Otherwise,
+  # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll
+  add_definitions(
+    -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS
+    )
+
+  if (CMAKE_COMPILER_IS_GNUCXX OR             # MinGW
+      "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")  # MSVC for 32bit (*)
+
+    # (*) With multithreaded logging enabled, Visual Studio 2008 fails
+    # with error: ".\dcmtk-3.6.5\oflog\libsrc\globinit.cc(422) : error
+    # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot
+    # convert parameter 1 from 'void (__stdcall *)(void *)' to
+    # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'"
+    #   None of the functions with this name in scope match the target type
+
+    add_definitions(
+      -DDCMTK_LOG4CPLUS_SINGLE_THREADED
+      )
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,497 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 "system" AND
+     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 \"system\", \"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")
+      set(ORTHANC_FRAMEWORK_MAJOR 999)
+      set(ORTHANC_FRAMEWORK_MINOR 999)
+      set(ORTHANC_FRAMEWORK_REVISION 999)
+
+    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.1")
+        set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
+        set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.0")
+        set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1")
+        set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
+        set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3")
+        set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4")
+        set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5")
+        set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6")
+        set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7")
+        set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8")
+        set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0")
+        set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1")
+        set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.0")
+        set(ORTHANC_FRAMEWORK_MD5 "ce5f689e852b01d3672bd3d2f952a5ef")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.1")
+        set(ORTHANC_FRAMEWORK_MD5 "3c171217f930abe80246997bdbcaf7cc")
+
+      # Below this point are development snapshots that were used to
+      # release some plugin, before an official release of the Orthanc
+      # framework was available. Here is the command to be used to
+      # generate a proper archive:
+      #
+      #   $ hg archive /tmp/Orthanc-`hg id -i | sed 's/\+//'`.tar.gz
+      #
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df")
+        # DICOMweb 1.1 (framework pre-1.6.0)
+        set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645")
+      endif()
+    endif()
+  endif()
+
+elseif (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
+  message("Using the Orthanc framework from a path of the filesystem. Assuming mainline version.")
+  set(ORTHANC_FRAMEWORK_MAJOR 999)
+  set(ORTHANC_FRAMEWORK_MINOR 999)
+  set(ORTHANC_FRAMEWORK_REVISION 999)
+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://hg.orthanc-server.com/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://orthanc.osimis.io/ThirdPartyDownloads/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()
+
+
+
+##
+## Case of the Orthanc framework installed as a shared library in a
+## GNU/Linux distribution (typically Debian)
+##
+
+if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
+  set(ORTHANC_FRAMEWORK_LIBDIR "" CACHE PATH "")
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND
+      CMAKE_COMPILER_IS_GNUCXX) # MinGW
+    set(DYNAMIC_MINGW_STDLIB ON)   # Disable static linking against libc (to throw exceptions)
+    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++")
+    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++")
+  endif()
+  
+  include(CheckIncludeFile)
+  include(CheckIncludeFileCXX)
+  include(FindPythonInterp)
+  include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
+  include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
+  include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
+  set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py)
+
+  if (NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows" AND
+      NOT ORTHANC_FRAMEWORK_STATIC)
+    # Look for mandatory dependency JsonCpp (cf. JsonCppConfiguration.cmake)
+    find_path(JSONCPP_INCLUDE_DIR json/reader.h
+      /usr/include/jsoncpp
+      /usr/local/include/jsoncpp
+      )
+
+    message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}")
+    include_directories(${JSONCPP_INCLUDE_DIR})
+    link_libraries(jsoncpp)
+
+    CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H)
+    if (NOT HAVE_JSONCPP_H)
+      message(FATAL_ERROR "Please install the libjsoncpp-dev package")
+    endif()
+
+    # Look for mandatory dependency Boost (cf. BoostConfiguration.cmake)
+    include(FindBoost)
+    find_package(Boost COMPONENTS filesystem thread system date_time regex)
+
+    if (NOT Boost_FOUND)
+      message(FATAL_ERROR "Unable to locate Boost on this system")
+    endif()
+    
+    include_directories(${Boost_INCLUDE_DIRS})
+    link_libraries(${Boost_LIBRARIES})
+
+    # Optional component - Lua
+    if (ENABLE_LUA)
+      include(FindLua)
+
+      if (NOT LUA_FOUND)
+        message(FATAL_ERROR "Please install the liblua-dev package")
+      endif()
+
+      include_directories(${LUA_INCLUDE_DIR})
+      link_libraries(${LUA_LIBRARIES})
+    endif()
+
+    # Optional component - SQLite
+    if (ENABLE_SQLITE)    
+      CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H)
+      if (NOT HAVE_SQLITE_H)
+        message(FATAL_ERROR "Please install the libsqlite3-dev package")
+      endif()
+      link_libraries(sqlite3)
+    endif()
+  endif()
+
+  # Optional component - Google Test
+  if (ENABLE_GOOGLE_TEST)
+    set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
+    set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
+    mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+    include(${CMAKE_CURRENT_LIST_DIR}/GoogleTestConfiguration.cmake)
+  endif()
+
+  # Look for Orthanc framework shared library
+  include(CheckCXXSymbolExists)
+
+  if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
+    set(ORTHANC_FRAMEWORK_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT})
+    include_directories(${ORTHANC_FRAMEWORK_ROOT}/..)
+  else()
+    find_path(ORTHANC_FRAMEWORK_INCLUDE_DIR OrthancFramework.h
+      /usr/include/orthanc-framework
+      /usr/local/include/orthanc-framework
+      ${ORTHANC_FRAMEWORK_ROOT}
+      )
+  endif()
+  
+  message("Orthanc framework include dir: ${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
+  include_directories(${ORTHANC_FRAMEWORK_INCLUDE_DIR})
+  
+  set(CMAKE_REQUIRED_INCLUDES "${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
+
+  if (NOT "${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "")
+    set(CMAKE_REQUIRED_LIBRARIES "-L${ORTHANC_FRAMEWORK_LIBDIR} -lOrthancFramework")
+  else()
+    set(CMAKE_REQUIRED_LIBRARIES "OrthancFramework")
+  endif()
+  
+  check_cxx_symbol_exists("Orthanc::InitializeFramework" "OrthancFramework.h" HAVE_ORTHANC_FRAMEWORK)
+  if(NOT HAVE_ORTHANC_FRAMEWORK)
+    message(FATAL_ERROR "Cannot find the Orthanc framework")
+  endif()
+
+  if (NOT "${ORTHANC_FRAMEWORK_ROOT}" STREQUAL "")
+    include_directories(${ORTHANC_FRAMEWORK_ROOT})
+  endif()
+
+  if (NOT "${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "")
+    link_directories(${ORTHANC_FRAMEWORK_LIBDIR})
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/DownloadPackage.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,258 @@
+macro(GetUrlFilename TargetVariable Url)
+  string(REGEX REPLACE "^.*/" "" ${TargetVariable} "${Url}")
+endmacro()
+
+
+macro(GetUrlExtension TargetVariable Url)
+  #string(REGEX REPLACE "^.*/[^.]*\\." "" TMP "${Url}")
+  string(REGEX REPLACE "^.*\\." "" TMP "${Url}")
+  string(TOLOWER "${TMP}" "${TargetVariable}")
+endmacro()
+
+
+
+##
+## Setup the patch 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()
+
+
+
+##
+## Check the existence of the required decompression tools
+##
+
+if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
+  find_program(ZIP_EXECUTABLE 7z 
+    PATHS 
+    "$ENV{ProgramFiles}/7-Zip"
+    "$ENV{ProgramW6432}/7-Zip"
+    )
+
+  if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND")
+    message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
+  endif()
+
+else()
+  find_program(UNZIP_EXECUTABLE unzip)
+  if (${UNZIP_EXECUTABLE} MATCHES "UNZIP_EXECUTABLE-NOTFOUND")
+    message(FATAL_ERROR "Please install the 'unzip' package")
+  endif()
+
+  find_program(TAR_EXECUTABLE tar)
+  if (${TAR_EXECUTABLE} MATCHES "TAR_EXECUTABLE-NOTFOUND")
+    message(FATAL_ERROR "Please install the 'tar' package")
+  endif()
+
+  find_program(GUNZIP_EXECUTABLE gunzip)
+  if (${GUNZIP_EXECUTABLE} MATCHES "GUNZIP_EXECUTABLE-NOTFOUND")
+    message(FATAL_ERROR "Please install the 'gzip' package")
+  endif()
+endif()
+
+
+macro(DownloadFile MD5 Url)
+  GetUrlFilename(TMP_FILENAME "${Url}")
+
+  set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}")
+  if (NOT EXISTS "${TMP_PATH}")
+    message("Downloading ${Url}")
+
+    # This fixes issue 6: "I think cmake shouldn't download the
+    # packages which are not in the system, it should stop and let
+    # user know."
+    # https://code.google.com/p/orthanc/issues/detail?id=6
+    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()
+
+    if ("${MD5}" STREQUAL "no-check")
+      message(WARNING "Not checking the MD5 of: ${Url}")
+      file(DOWNLOAD "${Url}" "${TMP_PATH}"
+        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
+        STATUS Failure)
+    else()
+      file(DOWNLOAD "${Url}" "${TMP_PATH}"
+        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
+        EXPECTED_MD5 "${MD5}" STATUS Failure)
+    endif()
+
+    list(GET Failure 0 Status)
+    if (NOT Status EQUAL 0)
+      message(FATAL_ERROR "Cannot download file: ${Url}")
+    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()
+endmacro()
+
+
+macro(DownloadPackage MD5 Url TargetDirectory)
+  if (NOT IS_DIRECTORY "${TargetDirectory}")
+    DownloadFile("${MD5}" "${Url}")
+    
+    GetUrlExtension(TMP_EXTENSION "${Url}")
+    #message(${TMP_EXTENSION})
+    message("Uncompressing ${TMP_FILENAME}")
+
+    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
+
+      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}
+          RESULT_VARIABLE Failure
+          OUTPUT_QUIET
+          )
+
+        if (Failure)
+          message(FATAL_ERROR "Error while running the uncompression tool")
+        endif()
+
+        if ("${TMP_EXTENSION}" STREQUAL "tgz")
+          string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}")
+        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(
+          COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_FILENAME2}
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          OUTPUT_QUIET
+          )
+      elseif ("${TMP_EXTENSION}" STREQUAL "zip")
+        execute_process(
+          COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_PATH}
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          OUTPUT_QUIET
+          )
+      else()
+        message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
+      endif()
+
+    else()
+      if ("${TMP_EXTENSION}" STREQUAL "zip")
+        execute_process(
+          COMMAND sh -c "${UNZIP_EXECUTABLE} -q ${TMP_PATH}"
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+        )
+      elseif (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz"))
+        #message("tar xvfz ${TMP_PATH}")
+        execute_process(
+          COMMAND sh -c "${TAR_EXECUTABLE} xfz ${TMP_PATH}"
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          )
+      elseif ("${TMP_EXTENSION}" STREQUAL "bz2")
+        execute_process(
+          COMMAND sh -c "${TAR_EXECUTABLE} xfj ${TMP_PATH}"
+          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 "Unsupported package extension: ${TMP_EXTENSION}")
+      endif()
+    endif()
+   
+    if (Failure)
+      message(FATAL_ERROR "Error while running the uncompression tool")
+    endif()
+
+    if (NOT IS_DIRECTORY "${TargetDirectory}")
+      message(FATAL_ERROR "The package was not uncompressed at the proper location. Check the CMake instructions.")
+    endif()
+  endif()
+endmacro()
+
+
+
+macro(DownloadCompressedFile MD5 Url TargetFile)
+  if (NOT EXISTS "${TargetFile}")
+    DownloadFile("${MD5}" "${Url}")
+    
+    GetUrlExtension(TMP_EXTENSION "${Url}")
+    #message(${TMP_EXTENSION})
+    message("Uncompressing ${TMP_FILENAME}")
+
+    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
+
+      if ("${TMP_EXTENSION}" STREQUAL "gz")
+        execute_process(
+          # "-so" writes uncompressed file to stdout
+          COMMAND ${ZIP_EXECUTABLE} e -so -y ${TMP_PATH}
+          OUTPUT_FILE "${TargetFile}"
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          OUTPUT_QUIET
+          )
+
+        if (Failure)
+          message(FATAL_ERROR "Error while running the uncompression tool")
+        endif()
+
+      else()
+        message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
+      endif()
+
+    else()
+      if ("${TMP_EXTENSION}" STREQUAL "gz")
+        execute_process(
+          COMMAND sh -c "${GUNZIP_EXECUTABLE} -c ${TMP_PATH}"
+          OUTPUT_FILE "${TargetFile}"
+          RESULT_VARIABLE Failure
+          )
+      else()
+        message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
+      endif()
+    endif()
+   
+    if (Failure)
+      message(FATAL_ERROR "Error while running the uncompression tool")
+    endif()
+
+    if (NOT EXISTS "${TargetFile}")
+      message(FATAL_ERROR "The file was not uncompressed at the proper location. Check the CMake instructions.")
+    endif()
+  endif()
+endmacro()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,69 @@
+if (USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+  find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR
+    NAMES src/gtest-all.cc
+    PATHS
+    ${CROSSTOOL_NG_IMAGE}/usr/src/gtest
+    ${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest
+    PATH_SUFFIXES src
+    )
+
+  find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR
+    NAMES gtest.h
+    PATHS
+    ${CROSSTOOL_NG_IMAGE}/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(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/googletest-release-1.8.1)
+  set(GOOGLE_TEST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/gtest-1.8.1.tar.gz")
+  set(GOOGLE_TEST_MD5 "2e6fbeb6a91310a16efe181886c59596")
+
+  DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}")
+
+  include_directories(
+    ${GOOGLE_TEST_SOURCES_DIR}/googletest
+    ${GOOGLE_TEST_SOURCES_DIR}/googletest/include
+    ${GOOGLE_TEST_SOURCES_DIR}
+    )
+
+  set(GOOGLE_TEST_SOURCES
+    ${GOOGLE_TEST_SOURCES_DIR}/googletest/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)
+  if (NOT GTEST_FOUND)
+    message(FATAL_ERROR "Unable to find GoogleTest")
+  endif()
+
+  include_directories(${GTEST_INCLUDE_DIRS})
+
+  # The variable GTEST_LIBRARIES contains the shared library of
+  # Google Test, create an alias for more uniformity
+  set(GOOGLE_TEST_LIBRARIES ${GTEST_LIBRARIES})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/JsonCppConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,88 @@
+set(JSONCPP_CXX11 OFF)
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP)
+  if (USE_LEGACY_JSONCPP)
+    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.7)
+    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/jsoncpp-0.10.7.tar.gz")
+    set(JSONCPP_MD5 "3a8072ca6a1fa9cbaf7715ae625f134f")
+    add_definitions(-DORTHANC_LEGACY_JSONCPP=1)
+  else()
+    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.8.4)
+    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/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}")
+
+  set(JSONCPP_SOURCES
+    ${JSONCPP_SOURCES_DIR}/src/lib_json/json_reader.cpp
+    ${JSONCPP_SOURCES_DIR}/src/lib_json/json_value.cpp
+    ${JSONCPP_SOURCES_DIR}/src/lib_json/json_writer.cpp
+    )
+
+  include_directories(
+    ${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()
+  find_path(JSONCPP_INCLUDE_DIR json/reader.h
+    /usr/include/jsoncpp
+    /usr/local/include/jsoncpp
+    )
+
+  message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}")
+  include_directories(${JSONCPP_INCLUDE_DIR})
+  link_libraries(jsoncpp)
+
+  CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H)
+  if (NOT HAVE_JSONCPP_H)
+    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)
+    message("Switching to C++11 standard in gcc, as version of JsonCpp is >= 1.0.0")
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wno-deprecated-declarations")
+  elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
+    message("Switching to C++11 standard in clang, as version of JsonCpp is >= 1.0.0")
+    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wno-deprecated-declarations")
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LibCurlConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,339 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
+  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-7.64.0)
+  SET(CURL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/curl-7.64.0.tar.gz")
+  SET(CURL_MD5 "a026740d599a32bcbbe6e70679397899")
+
+  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.64.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}/.*)
+
+  add_definitions(
+    -DBUILDING_LIBCURL=1
+    -DCURL_STATICLIB=1
+    -DCURL_DISABLE_LDAPS=1
+    -DCURL_DISABLE_LDAP=1
+    -DCURL_DISABLE_DICT=1
+    -DCURL_DISABLE_FILE=1
+    -DCURL_DISABLE_FTP=1
+    -DCURL_DISABLE_GOPHER=1
+    -DCURL_DISABLE_LDAP=1
+    -DCURL_DISABLE_LDAPS=1
+    -DCURL_DISABLE_POP3=1
+    #-DCURL_DISABLE_PROXY=1
+    -DCURL_DISABLE_RTSP=1
+    -DCURL_DISABLE_TELNET=1
+    -DCURL_DISABLE_TFTP=1
+    )
+
+  if (ENABLE_SSL)
+    add_definitions(
+      #-DHAVE_LIBSSL=1
+      -DUSE_OPENSSL=1
+      -DHAVE_OPENSSL_ENGINE_H=1
+      -DUSE_SSLEAY=1
+      )
+  endif()
+
+  if (NOT EXISTS "${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.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/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" OR
+      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+    if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
+      SET(TMP_OS "x86_64")
+    else()
+      SET(TMP_OS "x86")
+    endif()
+
+    set_property(
+      SOURCE ${CURL_SOURCES}
+      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)
+
+    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)
+
+    list(APPEND 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
+        )
+      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()
+
+elseif (CMAKE_CROSSCOMPILING AND
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+  CHECK_INCLUDE_FILE_CXX(curl/curl.h HAVE_CURL_H)
+  if (NOT HAVE_CURL_H)
+    message(FATAL_ERROR "Please install the libcurl-dev package")
+  endif()
+
+  CHECK_LIBRARY_EXISTS(curl "curl_easy_init" "" HAVE_CURL_LIB)
+  if (NOT HAVE_CURL_LIB)
+    message(FATAL_ERROR "Please install the libcurl package")
+  endif()  
+  
+  link_libraries(curl)
+
+else()
+  include(FindCURL)
+  include_directories(${CURL_INCLUDE_DIRS})
+  link_libraries(${CURL_LIBRARIES})
+
+  if (NOT ${CURL_FOUND})
+    message(FATAL_ERROR "Unable to find LibCurl")
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LibIconvConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,90 @@
+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://orthanc.osimis.io/ThirdPartyDownloads/libiconv-1.15.tar.gz")
+  set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
+
+  DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
+
+  # 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)
+
+  # https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
+  add_definitions(
+    -DBUILDING_LIBICONV=1
+    -DIN_LIBRARY=1
+    -DLIBDIR=""
+    -DICONV_CONST=
+    #-DENABLE_EXTRA=1
+    )
+
+  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)
+
+  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
+    )
+
+  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
+    )
+
+  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()
+
+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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LibIcuConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,80 @@
+
+# Check out: ../ThirdParty/icu/README.txt
+
+# http://userguide.icu-project.org/packaging
+# http://userguide.icu-project.org/howtouseicu
+
+message("Using libicu")
+
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICU)
+  include(${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/icu/Version.cmake)
+  DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
+
+  # Use the gzip-compressed data
+  DownloadFile(${LIBICU_DATA_COMPRESSED_MD5} ${LIBICU_DATA_URL})
+  set(LIBICU_RESOURCES
+    LIBICU_DATA  ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${LIBICU_DATA}
+    )
+
+  set_source_files_properties(
+    ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
+    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
+    )
+
+  include_directories(BEFORE
+    ${LIBICU_SOURCES_DIR}/source/common
+    ${LIBICU_SOURCES_DIR}/source/i18n
+    )
+
+  aux_source_directory(${LIBICU_SOURCES_DIR}/source/common LIBICU_SOURCES)
+  aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n LIBICU_SOURCES)
+
+  add_definitions(
+    #-DU_COMBINED_IMPLEMENTATION
+    #-DU_DEF_ICUDATA_ENTRY_POINT=icudt63l_dat
+    #-DU_LIB_SUFFIX_C_NAME=l
+
+    #-DUCONFIG_NO_SERVICE=1
+    -DU_COMMON_IMPLEMENTATION
+    -DU_STATIC_IMPLEMENTATION
+    -DU_ENABLE_DYLOAD=0
+    -DU_HAVE_STD_STRING=1
+    -DU_I18N_IMPLEMENTATION
+    -DU_IO_IMPLEMENTATION
+    -DU_STATIC_IMPLEMENTATION=1
+    #-DU_CHARSET_IS_UTF8
+    -DUNISTR_FROM_STRING_EXPLICIT=
+
+    -DORTHANC_STATIC_ICU=1
+    -DORTHANC_ICU_DATA_MD5="${LIBICU_DATA_UNCOMPRESSED_MD5}"
+    )
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
+    set_source_files_properties(
+      ${LIBICU_SOURCES_DIR}/source/common/locmap.c
+      PROPERTIES COMPILE_DEFINITIONS "LOCALE_SNAME=0x0000005c"
+      )
+  endif()
+
+  source_group(ThirdParty\\libicu REGULAR_EXPRESSION ${LIBICU_SOURCES_DIR}/.*)
+
+else() 
+  CHECK_INCLUDE_FILE_CXX(unicode/uvernum.h HAVE_ICU_H)
+  if (NOT HAVE_ICU_H)
+    message(FATAL_ERROR "Please install the libicu-dev package")
+  endif()
+
+  find_library(LIBICU_PATH_1 NAMES icuuc)
+  find_library(LIBICU_PATH_2 NAMES icui18n)
+
+  if (NOT LIBICU_PATH_1 OR 
+      NOT LIBICU_PATH_2)
+    message(FATAL_ERROR "Please install the libicu-dev package")
+  else()
+    link_libraries(${LIBICU_PATH_1} ${LIBICU_PATH_2})
+  endif()
+
+  add_definitions(
+    -DORTHANC_STATIC_ICU=0
+    )
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LibJpegConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,95 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBJPEG)
+  set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9c)
+  DownloadPackage(
+    "93c62597eeef81a84d988bccbda1e990"
+    "http://orthanc.osimis.io/ThirdPartyDownloads/jpegsrc.v9c.tar.gz"
+    "${LIBJPEG_SOURCES_DIR}")
+
+  include_directories(
+    ${LIBJPEG_SOURCES_DIR}
+    )
+
+  list(APPEND LIBJPEG_SOURCES 
+    ${LIBJPEG_SOURCES_DIR}/jaricom.c
+    ${LIBJPEG_SOURCES_DIR}/jcapimin.c
+    ${LIBJPEG_SOURCES_DIR}/jcapistd.c
+    ${LIBJPEG_SOURCES_DIR}/jcarith.c
+    ${LIBJPEG_SOURCES_DIR}/jccoefct.c
+    ${LIBJPEG_SOURCES_DIR}/jccolor.c
+    ${LIBJPEG_SOURCES_DIR}/jcdctmgr.c
+    ${LIBJPEG_SOURCES_DIR}/jchuff.c
+    ${LIBJPEG_SOURCES_DIR}/jcinit.c
+    ${LIBJPEG_SOURCES_DIR}/jcmarker.c
+    ${LIBJPEG_SOURCES_DIR}/jcmaster.c
+    ${LIBJPEG_SOURCES_DIR}/jcomapi.c
+    ${LIBJPEG_SOURCES_DIR}/jcparam.c
+    ${LIBJPEG_SOURCES_DIR}/jcprepct.c
+    ${LIBJPEG_SOURCES_DIR}/jcsample.c
+    ${LIBJPEG_SOURCES_DIR}/jctrans.c
+    ${LIBJPEG_SOURCES_DIR}/jdapimin.c
+    ${LIBJPEG_SOURCES_DIR}/jdapistd.c
+    ${LIBJPEG_SOURCES_DIR}/jdarith.c
+    ${LIBJPEG_SOURCES_DIR}/jdatadst.c
+    ${LIBJPEG_SOURCES_DIR}/jdatasrc.c
+    ${LIBJPEG_SOURCES_DIR}/jdcoefct.c
+    ${LIBJPEG_SOURCES_DIR}/jdcolor.c
+    ${LIBJPEG_SOURCES_DIR}/jddctmgr.c
+    ${LIBJPEG_SOURCES_DIR}/jdhuff.c
+    ${LIBJPEG_SOURCES_DIR}/jdinput.c
+    ${LIBJPEG_SOURCES_DIR}/jcmainct.c
+    ${LIBJPEG_SOURCES_DIR}/jdmainct.c
+    ${LIBJPEG_SOURCES_DIR}/jdmarker.c
+    ${LIBJPEG_SOURCES_DIR}/jdmaster.c
+    ${LIBJPEG_SOURCES_DIR}/jdmerge.c
+    ${LIBJPEG_SOURCES_DIR}/jdpostct.c
+    ${LIBJPEG_SOURCES_DIR}/jdsample.c
+    ${LIBJPEG_SOURCES_DIR}/jdtrans.c
+    ${LIBJPEG_SOURCES_DIR}/jerror.c
+    ${LIBJPEG_SOURCES_DIR}/jfdctflt.c
+    ${LIBJPEG_SOURCES_DIR}/jfdctfst.c
+    ${LIBJPEG_SOURCES_DIR}/jfdctint.c
+    ${LIBJPEG_SOURCES_DIR}/jidctflt.c
+    ${LIBJPEG_SOURCES_DIR}/jidctfst.c
+    ${LIBJPEG_SOURCES_DIR}/jidctint.c
+    #${LIBJPEG_SOURCES_DIR}/jmemansi.c
+    #${LIBJPEG_SOURCES_DIR}/jmemdos.c
+    #${LIBJPEG_SOURCES_DIR}/jmemmac.c
+    ${LIBJPEG_SOURCES_DIR}/jmemmgr.c
+    #${LIBJPEG_SOURCES_DIR}/jmemname.c
+    ${LIBJPEG_SOURCES_DIR}/jmemnobs.c
+    ${LIBJPEG_SOURCES_DIR}/jquant1.c
+    ${LIBJPEG_SOURCES_DIR}/jquant2.c
+    ${LIBJPEG_SOURCES_DIR}/jutils.c
+
+    # ${LIBJPEG_SOURCES_DIR}/rdbmp.c
+    # ${LIBJPEG_SOURCES_DIR}/rdcolmap.c
+    # ${LIBJPEG_SOURCES_DIR}/rdgif.c
+    # ${LIBJPEG_SOURCES_DIR}/rdppm.c
+    # ${LIBJPEG_SOURCES_DIR}/rdrle.c
+    # ${LIBJPEG_SOURCES_DIR}/rdswitch.c
+    # ${LIBJPEG_SOURCES_DIR}/rdtarga.c
+    # ${LIBJPEG_SOURCES_DIR}/transupp.c
+    # ${LIBJPEG_SOURCES_DIR}/wrbmp.c
+    # ${LIBJPEG_SOURCES_DIR}/wrgif.c
+    # ${LIBJPEG_SOURCES_DIR}/wrppm.c
+    # ${LIBJPEG_SOURCES_DIR}/wrrle.c
+    # ${LIBJPEG_SOURCES_DIR}/wrtarga.c
+    )
+
+  configure_file(
+    ${LIBJPEG_SOURCES_DIR}/jconfig.txt
+    ${LIBJPEG_SOURCES_DIR}/jconfig.h COPYONLY
+    )
+
+  source_group(ThirdParty\\libjpeg REGULAR_EXPRESSION ${LIBJPEG_SOURCES_DIR}/.*)
+
+else()
+  include(FindJPEG)
+
+  if (NOT ${JPEG_FOUND})
+    message(FATAL_ERROR "Unable to find libjpeg")
+  endif()
+
+  include_directories(${JPEG_INCLUDE_DIR})
+  link_libraries(${JPEG_LIBRARIES})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LibP11Configuration.cmake	Wed Jun 10 20:30:34 2020 +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://orthanc.osimis.io/ThirdPartyDownloads/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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LibPngConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,58 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG)
+  SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.6.36)
+  SET(LIBPNG_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libpng-1.6.36.tar.gz")
+  SET(LIBPNG_MD5 "65afdeaa05f5ec14e31d9276143012e9")
+
+  DownloadPackage(${LIBPNG_MD5} ${LIBPNG_URL} "${LIBPNG_SOURCES_DIR}")
+
+  include_directories(
+    ${LIBPNG_SOURCES_DIR}
+    )
+
+  configure_file(
+    ${LIBPNG_SOURCES_DIR}/scripts/pnglibconf.h.prebuilt
+    ${LIBPNG_SOURCES_DIR}/pnglibconf.h
+    )
+
+  set(LIBPNG_SOURCES
+    #${LIBPNG_SOURCES_DIR}/example.c
+    ${LIBPNG_SOURCES_DIR}/png.c
+    ${LIBPNG_SOURCES_DIR}/pngerror.c
+    ${LIBPNG_SOURCES_DIR}/pngget.c
+    ${LIBPNG_SOURCES_DIR}/pngmem.c
+    ${LIBPNG_SOURCES_DIR}/pngpread.c
+    ${LIBPNG_SOURCES_DIR}/pngread.c
+    ${LIBPNG_SOURCES_DIR}/pngrio.c
+    ${LIBPNG_SOURCES_DIR}/pngrtran.c
+    ${LIBPNG_SOURCES_DIR}/pngrutil.c
+    ${LIBPNG_SOURCES_DIR}/pngset.c
+    #${LIBPNG_SOURCES_DIR}/pngtest.c
+    ${LIBPNG_SOURCES_DIR}/pngtrans.c
+    ${LIBPNG_SOURCES_DIR}/pngwio.c
+    ${LIBPNG_SOURCES_DIR}/pngwrite.c
+    ${LIBPNG_SOURCES_DIR}/pngwtran.c
+    ${LIBPNG_SOURCES_DIR}/pngwutil.c
+    )
+
+  add_definitions(
+    -DPNG_NO_CONFIG_H=1
+    -DPNG_NO_CONSOLE_IO=1
+    -DPNG_NO_STDIO=1
+    # The following declaration avoids "__declspec(dllexport)" in
+    # libpng to prevent publicly exposing its symbols by the DLLs
+    -DPNG_IMPEXP=
+    )
+
+  source_group(ThirdParty\\libpng REGULAR_EXPRESSION ${LIBPNG_SOURCES_DIR}/.*)
+
+else()
+  include(FindPNG)
+
+  if (NOT ${PNG_FOUND})
+    message(FATAL_ERROR "Unable to find libpng")
+  endif()
+
+  include_directories(${PNG_INCLUDE_DIRS})
+  link_libraries(${PNG_LIBRARIES})
+  add_definitions(${PNG_DEFINITIONS})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/LuaConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,138 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_LUA)
+  SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.3.5)
+  SET(LUA_MD5 "4f4b4f323fd3514a68e0ab3da8ce3455")
+  SET(LUA_URL "http://orthanc.osimis.io/ThirdPartyDownloads/lua-5.3.5.tar.gz")
+
+  DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
+
+  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")
+
+      if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+          ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+        add_definitions(-DLUA_USE_LINUX=1)
+      elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+        add_definitions(
+          -DLUA_USE_LINUX=1
+          -DLUA_USE_READLINE=1
+          )
+      elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
+        add_definitions(-DLUA_USE_POSIX=1)
+      endif()
+
+    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(
+        -DLUA_USE_MACOSX=1
+        -DLUA_DL_DYLD=1       # Enable loading of shared libraries (for Apple OS X)
+        )
+      
+    else()
+      message(FATAL_ERROR "Support your platform here")
+    endif()
+  endif()
+
+  add_definitions(
+    -DLUA_COMPAT_5_2=1
+    )
+
+  include_directories(
+    ${LUA_SOURCES_DIR}/src
+    )
+
+  set(LUA_SOURCES
+    # Don't compile the Lua command-line
+    #${LUA_SOURCES_DIR}/src/lua.c
+    #${LUA_SOURCES_DIR}/src/luac.c
+
+    # Core Lua
+    ${LUA_SOURCES_DIR}/src/lapi.c
+    ${LUA_SOURCES_DIR}/src/lcode.c
+    ${LUA_SOURCES_DIR}/src/lctype.c
+    ${LUA_SOURCES_DIR}/src/ldebug.c
+    ${LUA_SOURCES_DIR}/src/ldo.c
+    ${LUA_SOURCES_DIR}/src/ldump.c
+    ${LUA_SOURCES_DIR}/src/lfunc.c
+    ${LUA_SOURCES_DIR}/src/lgc.c
+    ${LUA_SOURCES_DIR}/src/llex.c
+    ${LUA_SOURCES_DIR}/src/lmem.c
+    ${LUA_SOURCES_DIR}/src/lobject.c
+    ${LUA_SOURCES_DIR}/src/lopcodes.c
+    ${LUA_SOURCES_DIR}/src/lparser.c
+    ${LUA_SOURCES_DIR}/src/lstate.c
+    ${LUA_SOURCES_DIR}/src/lstring.c
+    ${LUA_SOURCES_DIR}/src/ltable.c
+    ${LUA_SOURCES_DIR}/src/ltm.c
+    ${LUA_SOURCES_DIR}/src/lundump.c
+    ${LUA_SOURCES_DIR}/src/lvm.c
+    ${LUA_SOURCES_DIR}/src/lzio.c
+
+    # Base Lua modules
+    ${LUA_SOURCES_DIR}/src/lauxlib.c
+    ${LUA_SOURCES_DIR}/src/lbaselib.c
+    ${LUA_SOURCES_DIR}/src/lbitlib.c
+    ${LUA_SOURCES_DIR}/src/lcorolib.c
+    ${LUA_SOURCES_DIR}/src/ldblib.c
+    ${LUA_SOURCES_DIR}/src/liolib.c
+    ${LUA_SOURCES_DIR}/src/lmathlib.c
+    ${LUA_SOURCES_DIR}/src/loadlib.c
+    ${LUA_SOURCES_DIR}/src/loslib.c
+    ${LUA_SOURCES_DIR}/src/lstrlib.c
+    ${LUA_SOURCES_DIR}/src/ltablib.c
+    ${LUA_SOURCES_DIR}/src/lutf8lib.c
+
+    ${LUA_SOURCES_DIR}/src/linit.c
+    )
+
+  source_group(ThirdParty\\Lua REGULAR_EXPRESSION ${LUA_SOURCES_DIR}/.*)
+
+elseif (CMAKE_CROSSCOMPILING AND
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+  set(LUA_VERSIONS 5.3 5.2 5.1)
+
+  unset(LUA_VERSION)
+  foreach(version IN ITEMS ${LUA_VERSIONS})
+    CHECK_INCLUDE_FILE(lua${version}/lua.h HAVE_LUA${version}_H)
+    if (HAVE_LUA${version}_H)
+      set(LUA_VERSION ${version})
+      break()
+    endif()
+  endforeach()
+
+  if (NOT LUA_VERSION)
+    message(FATAL_ERROR "Please install the liblua-dev package")
+  endif()
+  
+  CHECK_LIBRARY_EXISTS(lua${LUA_VERSION} "lua_call" "${LUA_LIB_DIR}" HAVE_LUA_LIB)
+  if (NOT HAVE_LUA_LIB)
+    message(FATAL_ERROR "Please install the liblua package")
+  endif()  
+
+  include_directories(${CROSSTOOL_NG_IMAGE}/usr/include/lua${LUA_VERSION})
+  link_libraries(lua${LUA_VERSION})
+
+else()
+  include(FindLua)
+
+  if (NOT LUA_FOUND)
+    message(FATAL_ERROR "Please install the liblua-dev package")
+  endif()
+
+  include_directories(${LUA_INCLUDE_DIR})
+  link_libraries(${LUA_LIBRARIES})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/MongooseConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,95 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_MONGOOSE)
+  SET(MONGOOSE_SOURCES_DIR ${CMAKE_BINARY_DIR}/mongoose)
+
+  if (IS_DIRECTORY "${MONGOOSE_SOURCES_DIR}")
+    set(FirstRun OFF)
+  else()
+    set(FirstRun ON)
+  endif()
+
+  if (0)
+    # Use Mongoose 3.1
+    DownloadPackage(
+      "e718fc287b4eb1bd523be3fa00942bb0"
+      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.1.tgz"
+      "${MONGOOSE_SOURCES_DIR}")
+    
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
+    set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.1-patch.diff)
+
+  else() 
+    # Use Mongoose 3.8
+    DownloadPackage(
+      "7e3296295072792cdc3c633f9404e0c3"
+      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.8.tgz"
+      "${MONGOOSE_SOURCES_DIR}")
+    
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
+    set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.8-patch.diff)
+  endif()
+
+  # Patch mongoose
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -N mongoose.c 
+    INPUT_FILE ${MONGOOSE_PATCH}
+    WORKING_DIRECTORY ${MONGOOSE_SOURCES_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure AND FirstRun)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+
+  include_directories(
+    ${MONGOOSE_SOURCES_DIR}
+    )
+
+  set(MONGOOSE_SOURCES
+    ${MONGOOSE_SOURCES_DIR}/mongoose.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 mongoose
+      )
+  endif()
+
+
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    if (CMAKE_COMPILER_IS_GNUCXX)
+      # This is a patch for MinGW64
+      add_definitions(-D_TIMESPEC_DEFINED=1)
+    endif()
+  endif()
+
+  source_group(ThirdParty\\Mongoose REGULAR_EXPRESSION ${MONGOOSE_SOURCES_DIR}/.*)
+
+else()
+  CHECK_INCLUDE_FILE_CXX(mongoose.h HAVE_MONGOOSE_H)
+  if (NOT HAVE_MONGOOSE_H)
+    message(FATAL_ERROR "Please install the mongoose-devel package")
+  endif()
+
+  CHECK_LIBRARY_EXISTS(mongoose mg_start "" HAVE_MONGOOSE_LIB)
+  if (NOT HAVE_MONGOOSE_LIB)
+    message(FATAL_ERROR "Please install the mongoose-devel package")
+  endif()
+
+  if (SYSTEM_MONGOOSE_USE_CALLBACKS)
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
+  else()
+    add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
+  endif()
+
+  link_libraries(mongoose)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/OpenSslConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,41 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL)
+  if (OPENSSL_STATIC_VERSION STREQUAL "1.0.2")
+    include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfigurationStatic-1.0.2.cmake)
+  elseif (OPENSSL_STATIC_VERSION STREQUAL "1.1.1")
+    include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfigurationStatic-1.1.1.cmake)
+  else()
+    message(FATAL_ERROR "Unsupported version of OpenSSL: ${OPENSSL_STATIC_VERSION}")
+  endif()
+
+  source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*)
+
+elseif (CMAKE_CROSSCOMPILING AND
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
+
+  CHECK_INCLUDE_FILE_CXX(openssl/opensslv.h HAVE_OPENSSL_H)
+  if (NOT HAVE_OPENSSL_H)
+    message(FATAL_ERROR "Please install the libopenssl-dev package")
+  endif()
+
+  CHECK_LIBRARY_EXISTS(crypto "OPENSSL_init" "" HAVE_OPENSSL_CRYPTO_LIB)
+  if (NOT HAVE_OPENSSL_CRYPTO_LIB)
+    message(FATAL_ERROR "Please install the libopenssl package")
+  endif()  
+  
+  CHECK_LIBRARY_EXISTS(ssl "SSL_library_init" "" HAVE_OPENSSL_SSL_LIB)
+  if (NOT HAVE_OPENSSL_SSL_LIB)
+    message(FATAL_ERROR "Please install the libopenssl package")
+  endif()  
+  
+  link_libraries(crypto ssl)
+
+else()
+  include(FindOpenSSL)
+
+  if (NOT ${OPENSSL_FOUND})
+    message(FATAL_ERROR "Unable to find OpenSSL")
+  endif()
+
+  include_directories(${OPENSSL_INCLUDE_DIR})
+  link_libraries(${OPENSSL_LIBRARIES})
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.0.2.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,332 @@
+SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.0.2p)
+SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.0.2p.tar.gz")
+SET(OPENSSL_MD5 "ac5eb30bf5798aa14b1ae6d0e7da58df")
+
+if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+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
+  -DOPENSSL_NO_ASM
+  -DOPENSSL_NO_DYNAMIC_ENGINE
+  -DNO_WINDOWS_BRAINDEATH
+
+  -DOPENSSL_NO_BF 
+  -DOPENSSL_NO_CAMELLIA
+  -DOPENSSL_NO_CAST 
+  -DOPENSSL_NO_EC_NISTP_64_GCC_128
+  -DOPENSSL_NO_GMP
+  -DOPENSSL_NO_GOST
+  -DOPENSSL_NO_HW
+  -DOPENSSL_NO_JPAKE
+  -DOPENSSL_NO_IDEA
+  -DOPENSSL_NO_KRB5 
+  -DOPENSSL_NO_MD2 
+  -DOPENSSL_NO_MDC2 
+  #-DOPENSSL_NO_MD4   # MD4 is necessary for MariaDB/MySQL client
+  -DOPENSSL_NO_RC2 
+  -DOPENSSL_NO_RC4 
+  -DOPENSSL_NO_RC5 
+  -DOPENSSL_NO_RFC3779
+  -DOPENSSL_NO_SCTP
+  -DOPENSSL_NO_STORE
+  -DOPENSSL_NO_SEED
+  -DOPENSSL_NO_WHIRLPOOL
+  -DOPENSSL_NO_RIPEMD
+  )
+
+include_directories(
+  ${OPENSSL_SOURCES_DIR}
+  ${OPENSSL_SOURCES_DIR}/crypto
+  ${OPENSSL_SOURCES_DIR}/crypto/asn1
+  ${OPENSSL_SOURCES_DIR}/crypto/modes
+  ${OPENSSL_SOURCES_DIR}/crypto/evp
+  ${OPENSSL_SOURCES_DIR}/include
+  )
+
+set(OPENSSL_SOURCES_SUBDIRS
+  ${OPENSSL_SOURCES_DIR}/crypto
+  ${OPENSSL_SOURCES_DIR}/crypto/aes
+  ${OPENSSL_SOURCES_DIR}/crypto/asn1
+  ${OPENSSL_SOURCES_DIR}/crypto/bio
+  ${OPENSSL_SOURCES_DIR}/crypto/bn
+  ${OPENSSL_SOURCES_DIR}/crypto/buffer
+  ${OPENSSL_SOURCES_DIR}/crypto/cmac
+  ${OPENSSL_SOURCES_DIR}/crypto/cms
+  ${OPENSSL_SOURCES_DIR}/crypto/comp
+  ${OPENSSL_SOURCES_DIR}/crypto/conf
+  ${OPENSSL_SOURCES_DIR}/crypto/des
+  ${OPENSSL_SOURCES_DIR}/crypto/dh
+  ${OPENSSL_SOURCES_DIR}/crypto/dsa
+  ${OPENSSL_SOURCES_DIR}/crypto/dso
+  ${OPENSSL_SOURCES_DIR}/crypto/engine
+  ${OPENSSL_SOURCES_DIR}/crypto/err
+  ${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
+  ${OPENSSL_SOURCES_DIR}/crypto/ocsp
+  ${OPENSSL_SOURCES_DIR}/crypto/pem
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs12
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7
+  ${OPENSSL_SOURCES_DIR}/crypto/pqueue
+  ${OPENSSL_SOURCES_DIR}/crypto/rand
+  ${OPENSSL_SOURCES_DIR}/crypto/rsa
+  ${OPENSSL_SOURCES_DIR}/crypto/sha
+  ${OPENSSL_SOURCES_DIR}/crypto/srp
+  ${OPENSSL_SOURCES_DIR}/crypto/stack
+  ${OPENSSL_SOURCES_DIR}/crypto/ts
+  ${OPENSSL_SOURCES_DIR}/crypto/txt_db
+  ${OPENSSL_SOURCES_DIR}/crypto/ui
+  ${OPENSSL_SOURCES_DIR}/crypto/x509
+  ${OPENSSL_SOURCES_DIR}/crypto/x509v3
+  ${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()
+
+list(REMOVE_ITEM OPENSSL_SOURCES
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_unix.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_vms.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c
+  ${OPENSSL_SOURCES_DIR}/crypto/armcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bf/bfs.cpp
+  ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_rtcp.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bn/exp.c
+  ${OPENSSL_SOURCES_DIR}/crypto/conf/cnf_save.c
+  ${OPENSSL_SOURCES_DIR}/crypto/conf/test.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/des.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/des3s.cpp
+  ${OPENSSL_SOURCES_DIR}/crypto/des/des_opts.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/dess.cpp
+  ${OPENSSL_SOURCES_DIR}/crypto/des/read_pwd.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/speed.c
+  ${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
+  ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/rand/randtest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/x509v3/tabtest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/x509v3/v3conf.c
+  ${OPENSSL_SOURCES_DIR}/ssl/ssl_task.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_nyi.c
+  ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_dgram.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bn/bntest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bn/expspeed.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bn/exptest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/engine/enginetest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/evp/evp_test.c
+  ${OPENSSL_SOURCES_DIR}/crypto/hmac/hmactest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/md5/md5.c
+  ${OPENSSL_SOURCES_DIR}/crypto/md5/md5test.c
+  ${OPENSSL_SOURCES_DIR}/crypto/o_dir_test.c
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/dec.c
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/enc.c
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/sign.c
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/verify.c
+  ${OPENSSL_SOURCES_DIR}/crypto/rsa/rsa_test.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha1.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha1t.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha1test.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha256t.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha512t.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sha/shatest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/srp/srptest.c
+
+  ${OPENSSL_SOURCES_DIR}/crypto/bn/divtest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bn/bnspeed.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/destest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/dh/p192.c
+  ${OPENSSL_SOURCES_DIR}/crypto/dh/p512.c
+  ${OPENSSL_SOURCES_DIR}/crypto/dh/p1024.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/rpw.c
+  ${OPENSSL_SOURCES_DIR}/ssl/ssltest.c
+  ${OPENSSL_SOURCES_DIR}/crypto/dsa/dsagen.c
+  ${OPENSSL_SOURCES_DIR}/crypto/dsa/dsatest.c
+  ${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")
+
+  if (ENABLE_OPENSSL_ENGINES)
+    link_libraries(crypt32)
+  endif()
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,258 @@
+SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.1.1g)
+SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.1.1g.tar.gz")
+SET(OPENSSL_MD5 "76766e98997660138cdaf13a187bd234")
+
+if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
+  set(FirstRun OFF)
+else()
+  set(FirstRun ON)
+endif()
+
+DownloadPackage(${OPENSSL_MD5} ${OPENSSL_URL} "${OPENSSL_SOURCES_DIR}")
+
+if (FirstRun)
+  file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/buildinf.h "
+#define DATE \"\"
+#define PLATFORM \"\"
+#define compiler_flags \"\"
+")
+  file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/bn_conf.h "")
+  file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/dso_conf.h "")
+
+  configure_file(
+    ${ORTHANC_ROOT}/Resources/Patches/openssl-1.1.1-conf.h.in
+    ${OPENSSL_SOURCES_DIR}/include/openssl/opensslconf.h
+    )
+
+  # Apply the patches
+  execute_process(
+    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+    ${ORTHANC_ROOT}/Resources/Patches/openssl-1.1.1g.patch
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    RESULT_VARIABLE Failure
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while patching a file")
+  endif()
+else()
+  message("The patches for OpenSSL have already been applied")
+endif()
+
+add_definitions(
+  -DOPENSSL_THREADS
+  -DOPENSSL_IA32_SSE2
+  -DOPENSSL_NO_ASM
+  -DOPENSSL_NO_DYNAMIC_ENGINE
+  -DOPENSSL_NO_DEVCRYPTOENG
+
+  -DOPENSSL_NO_BF 
+  -DOPENSSL_NO_CAMELLIA
+  -DOPENSSL_NO_CAST 
+  -DOPENSSL_NO_EC_NISTP_64_GCC_128
+  -DOPENSSL_NO_GMP
+  -DOPENSSL_NO_GOST
+  -DOPENSSL_NO_HW
+  -DOPENSSL_NO_JPAKE
+  -DOPENSSL_NO_IDEA
+  -DOPENSSL_NO_KRB5 
+  -DOPENSSL_NO_MD2 
+  -DOPENSSL_NO_MDC2 
+  #-DOPENSSL_NO_MD4   # MD4 is necessary for MariaDB/MySQL client
+  -DOPENSSL_NO_RC2 
+  -DOPENSSL_NO_RC4 
+  -DOPENSSL_NO_RC5 
+  -DOPENSSL_NO_RFC3779
+  -DOPENSSL_NO_SCTP
+  -DOPENSSL_NO_STORE
+  -DOPENSSL_NO_SEED
+  -DOPENSSL_NO_WHIRLPOOL
+  -DOPENSSL_NO_RIPEMD
+  -DOPENSSL_NO_AFALGENG
+
+  -DOPENSSLDIR="/usr/local/ssl"
+  )
+
+
+include_directories(
+  ${OPENSSL_SOURCES_DIR}
+  ${OPENSSL_SOURCES_DIR}/crypto
+  ${OPENSSL_SOURCES_DIR}/crypto/asn1
+  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448
+  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32
+  ${OPENSSL_SOURCES_DIR}/crypto/evp
+  ${OPENSSL_SOURCES_DIR}/crypto/include
+  ${OPENSSL_SOURCES_DIR}/crypto/modes
+  ${OPENSSL_SOURCES_DIR}/include
+  )
+
+
+set(OPENSSL_SOURCES_SUBDIRS
+  ${OPENSSL_SOURCES_DIR}/crypto
+  ${OPENSSL_SOURCES_DIR}/crypto/aes
+  ${OPENSSL_SOURCES_DIR}/crypto/aria
+  ${OPENSSL_SOURCES_DIR}/crypto/asn1
+  ${OPENSSL_SOURCES_DIR}/crypto/async
+  ${OPENSSL_SOURCES_DIR}/crypto/async/arch
+  ${OPENSSL_SOURCES_DIR}/crypto/bio
+  ${OPENSSL_SOURCES_DIR}/crypto/blake2
+  ${OPENSSL_SOURCES_DIR}/crypto/bn
+  ${OPENSSL_SOURCES_DIR}/crypto/buffer
+  ${OPENSSL_SOURCES_DIR}/crypto/chacha
+  ${OPENSSL_SOURCES_DIR}/crypto/cmac
+  ${OPENSSL_SOURCES_DIR}/crypto/cms
+  ${OPENSSL_SOURCES_DIR}/crypto/comp
+  ${OPENSSL_SOURCES_DIR}/crypto/conf
+  ${OPENSSL_SOURCES_DIR}/crypto/ct
+  ${OPENSSL_SOURCES_DIR}/crypto/des
+  ${OPENSSL_SOURCES_DIR}/crypto/dh
+  ${OPENSSL_SOURCES_DIR}/crypto/dsa
+  ${OPENSSL_SOURCES_DIR}/crypto/dso
+  ${OPENSSL_SOURCES_DIR}/crypto/ec
+  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448
+  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32
+  ${OPENSSL_SOURCES_DIR}/crypto/err
+  ${OPENSSL_SOURCES_DIR}/crypto/evp
+  ${OPENSSL_SOURCES_DIR}/crypto/hmac
+  ${OPENSSL_SOURCES_DIR}/crypto/kdf
+  ${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
+  ${OPENSSL_SOURCES_DIR}/crypto/ocsp
+  ${OPENSSL_SOURCES_DIR}/crypto/pem
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs12
+  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7
+  ${OPENSSL_SOURCES_DIR}/crypto/poly1305
+  ${OPENSSL_SOURCES_DIR}/crypto/pqueue
+  ${OPENSSL_SOURCES_DIR}/crypto/rand
+  ${OPENSSL_SOURCES_DIR}/crypto/ripemd
+  ${OPENSSL_SOURCES_DIR}/crypto/rsa
+  ${OPENSSL_SOURCES_DIR}/crypto/sha
+  ${OPENSSL_SOURCES_DIR}/crypto/siphash
+  ${OPENSSL_SOURCES_DIR}/crypto/sm2
+  ${OPENSSL_SOURCES_DIR}/crypto/sm3
+  ${OPENSSL_SOURCES_DIR}/crypto/sm4
+  ${OPENSSL_SOURCES_DIR}/crypto/srp
+  ${OPENSSL_SOURCES_DIR}/crypto/stack
+  ${OPENSSL_SOURCES_DIR}/crypto/store
+  ${OPENSSL_SOURCES_DIR}/crypto/ts
+  ${OPENSSL_SOURCES_DIR}/crypto/txt_db
+  ${OPENSSL_SOURCES_DIR}/crypto/ui
+  ${OPENSSL_SOURCES_DIR}/crypto/x509
+  ${OPENSSL_SOURCES_DIR}/crypto/x509v3
+  ${OPENSSL_SOURCES_DIR}/ssl
+  ${OPENSSL_SOURCES_DIR}/ssl/record
+  ${OPENSSL_SOURCES_DIR}/ssl/statem
+  )
+
+if (ENABLE_OPENSSL_ENGINES)
+  add_definitions(
+    #-DENGINESDIR="/usr/local/lib/engines-1.1"  # On GNU/Linux
+    -DENGINESDIR="."
+    )
+
+  list(APPEND OPENSSL_SOURCES_SUBDIRS
+    ${OPENSSL_SOURCES_DIR}/engines
+    ${OPENSSL_SOURCES_DIR}/crypto/engine
+    )
+else()
+  add_definitions(-DOPENSSL_NO_ENGINE)
+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()
+
+list(REMOVE_ITEM OPENSSL_SOURCES
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_nyi.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_unix.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_vms.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c
+  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c
+  ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c
+  ${OPENSSL_SOURCES_DIR}/crypto/armcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_dgram.c
+  ${OPENSSL_SOURCES_DIR}/crypto/des/ncbc_enc.c
+  ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256.c
+  ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256_table.c
+  ${OPENSSL_SOURCES_DIR}/crypto/engine/eng_devcrypto.c
+  ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_base2_44.c  # Cannot be compiled with MinGW
+  ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_ieee754.c  # Cannot be compiled with MinGW
+  ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c
+  ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c
+  ${OPENSSL_SOURCES_DIR}/engines/e_afalg.c  # Cannot be compiled with MinGW
+  )
+
+# Check out "${OPENSSL_SOURCES_DIR}/Configurations/README": "This is
+# default if no option is specified, it works on any supported
+# system." It is mandatory to define it as a macro, as it is used by
+# all the source files that include OpenSSL (e.g. "Core/Toolbox.cpp"
+# or curl)
+add_definitions(-DTHIRTY_TWO_BIT)
+
+
+if (NOT CMAKE_COMPILER_IS_GNUCXX OR
+    "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows" OR
+    "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+  # Disable the use of a gcc extension, that is neither available on
+  # MinGW, nor on LSB
+  add_definitions(
+    -DOPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE
+    )
+endif()
+
+
+if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
+  set(OPENSSL_DEFINITIONS
+    "${OPENSSL_DEFINITIONS};OPENSSL_SYSNAME_WIN32;SO_WIN32;WIN32_LEAN_AND_MEAN;L_ENDIAN;NO_WINDOWS_BRAINDEATH")
+  
+  if (ENABLE_OPENSSL_ENGINES)
+    link_libraries(crypt32)
+  endif()
+
+  add_definitions(
+    -DOPENSSL_RAND_SEED_OS  # ${OPENSSL_SOURCES_DIR}/crypto/rand/rand_win.c
+    )
+ 
+elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
+  add_definitions(
+    # In order for "crypto/mem_sec.c" to compile on LSB
+    -DOPENSSL_NO_SECURE_MEMORY
+
+    # The "OPENSSL_RAND_SEED_OS" value implies a syscall() to
+    # "__NR_getrandom" (i.e. system call "getentropy(2)") in
+    # "rand_unix.c", which is not available in LSB.
+    -DOPENSSL_RAND_SEED_DEVRANDOM
+    )
+
+else()
+  # Fixes error "OpenSSL error: error:2406C06E:random number
+  # generator:RAND_DRBG_instantiate:error retrieving entropy" that was
+  # present in Orthanc 1.6.0, if statically linking on Ubuntu 18.04
+  add_definitions(
+    -DOPENSSL_RAND_SEED_OS
+    )
+endif()
+
+
+set_source_files_properties(
+  ${OPENSSL_SOURCES}
+    PROPERTIES COMPILE_DEFINITIONS
+    "${OPENSSL_DEFINITIONS};DSO_NONE"
+    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,705 @@
+##
+## 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(OPENSSL_STATIC_VERSION 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(BOOST_LOCALE_BACKEND 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
+    -DORTHANC_ENABLE_DCMTK_TRANSCODING=0
+    )
+  unset(DCMTK_DICTIONARY_DIR CACHE)
+  unset(DCMTK_VERSION CACHE)
+  unset(USE_DCMTK_362_PRIVATE_DIC CACHE)
+  unset(USE_SYSTEM_DCMTK CACHE)
+  unset(ENABLE_DCMTK_JPEG CACHE)
+  unset(ENABLE_DCMTK_JPEG_LOSSLESS CACHE)
+  unset(DCMTK_STATIC_VERSION CACHE)
+  unset(ENABLE_DCMTK_LOG CACHE)
+endif()
+
+
+#####################################################################
+## List of source files
+#####################################################################
+
+set(ORTHANC_CORE_SOURCES_INTERNAL
+  ${ORTHANC_ROOT}/Core/Cache/MemoryCache.cpp
+  ${ORTHANC_ROOT}/Core/Cache/MemoryObjectCache.cpp
+  ${ORTHANC_ROOT}/Core/Cache/MemoryStringCache.cpp
+  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
+  ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
+  ${ORTHANC_ROOT}/Core/EnumerationDictionary.h
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp
+  ${ORTHANC_ROOT}/Core/HttpServer/MultipartStreamReader.cpp
+  ${ORTHANC_ROOT}/Core/HttpServer/StringMatcher.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/OrthancFramework.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
+      )
+    set(ORTHANC_ENABLE_CIVETWEB 1)
+  else()
+    include(${CMAKE_CURRENT_LIST_DIR}/MongooseConfiguration.cmake)
+  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/HttpServer.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpStreamTranscoder.cpp
+    ${ORTHANC_ROOT}/Core/HttpServer/HttpToolbox.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()
+
+if (ORTHANC_ENABLE_CIVETWEB)
+  add_definitions(-DORTHANC_ENABLE_CIVETWEB=1)
+else()
+  add_definitions(-DORTHANC_ENABLE_CIVETWEB=0)
+endif()
+
+if (ORTHANC_ENABLE_MONGOOSE)
+  add_definitions(-DORTHANC_ENABLE_MONGOOSE=1)
+else()
+  add_definitions(-DORTHANC_ENABLE_MONGOOSE=0)
+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
+##
+
+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(BOOST_LOCALE_BACKEND CACHE)
+  else()
+    if (BOOST_LOCALE_BACKEND STREQUAL "gcc")
+    elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
+      include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake)
+    elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
+      include(${CMAKE_CURRENT_LIST_DIR}/LibIcuConfiguration.cmake)
+    elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv")
+      message("Using Microsoft Window's wconv")
+    else()
+      message(FATAL_ERROR "Invalid value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
+    endif()
+  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 one 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/DicomWebJsonVisitor.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/FromDcmtkBridge.cpp
+    ${ORTHANC_ROOT}/Core/DicomParsing/ParsedDicomDir.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/DicomAssociation.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociationParameters.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomControlUserConnection.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.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/GetScp.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()
+
+  # New in Orthanc 1.6.0
+  if (ENABLE_DCMTK_TRANSCODING)
+    add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1)
+    list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL
+      ${ORTHANC_ROOT}/Core/DicomParsing/DcmtkTranscoder.cpp
+      ${ORTHANC_ROOT}/Core/DicomParsing/IDicomTranscoder.cpp
+      ${ORTHANC_ROOT}/Core/DicomParsing/MemoryBufferTranscoder.cpp
+      )
+  else()
+    add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=0)
+  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_BUILDING_FRAMEWORK_LIBRARY)
+  add_definitions(-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1)
+else()
+  add_definitions(-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0)
+endif()
+
+
+if (ORTHANC_SANDBOXED)
+  add_definitions(
+    -DORTHANC_SANDBOXED=1
+    )
+
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    set(ORTHANC_ENABLE_LOGGING ON)
+    set(ORTHANC_ENABLE_LOGGING_STDIO ON)
+  else()
+    set(ORTHANC_ENABLE_LOGGING OFF)
+  endif()
+  
+else()
+  set(ORTHANC_ENABLE_LOGGING ON)
+  set(ORTHANC_ENABLE_LOGGING_STDIO OFF)
+
+  add_definitions(
+    -DORTHANC_SANDBOXED=0
+    )
+
+  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
+    ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp
+    ${ORTHANC_ROOT}/Core/FileBuffer.cpp
+    ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp
+    ${ORTHANC_ROOT}/Core/MetricsRegistry.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 (ORTHANC_ENABLE_LOGGING)
+  add_definitions(-DORTHANC_ENABLE_LOGGING=1)
+else()
+  add_definitions(-DORTHANC_ENABLE_LOGGING=0)
+endif()
+
+if (ORTHANC_ENABLE_LOGGING_STDIO)
+  add_definitions(-DORTHANC_ENABLE_LOGGING_STDIO=1)
+else()
+  add_definitions(-DORTHANC_ENABLE_LOGGING_STDIO=0)
+endif()
+
+
+
+#####################################################################
+## Configuration of Orthanc versioning macros (new in Orthanc 1.5.0)
+#####################################################################
+
+if (ORTHANC_VERSION STREQUAL "mainline")
+  set(ORTHANC_VERSION_MAJOR "999")
+  set(ORTHANC_VERSION_MINOR "999")
+  set(ORTHANC_VERSION_REVISION "999")
+else()
+  string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\1" ORTHANC_VERSION_MAJOR    ${ORTHANC_VERSION})
+  string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\2" ORTHANC_VERSION_MINOR    ${ORTHANC_VERSION})
+  string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\3" ORTHANC_VERSION_REVISION ${ORTHANC_VERSION})
+
+  if (NOT ORTHANC_VERSION STREQUAL
+      "${ORTHANC_VERSION_MAJOR}.${ORTHANC_VERSION_MINOR}.${ORTHANC_VERSION_REVISION}")
+    message(FATAL_ERROR "Error in the (x.y.z) format of the Orthanc version: ${ORTHANC_VERSION}")
+  endif()
+endif()
+
+add_definitions(
+  -DORTHANC_VERSION_MAJOR=${ORTHANC_VERSION_MAJOR}
+  -DORTHANC_VERSION_MINOR=${ORTHANC_VERSION_MINOR}
+  -DORTHANC_VERSION_REVISION=${ORTHANC_VERSION_REVISION}
+  )
+
+
+
+#####################################################################
+## 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}
+  ${LIBICU_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()
+
+
+if (NOT "${LIBICU_RESOURCES}" STREQUAL "" OR
+    NOT "${DCMTK_DICTIONARIES}" STREQUAL "")
+  EmbedResources(
+    --namespace=Orthanc.FrameworkResources
+    --target=OrthancFrameworkResources
+    --framework-path=${ORTHANC_ROOT}/Core
+    ${LIBICU_RESOURCES}
+    ${DCMTK_DICTIONARIES}
+    )
+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/OrthancFramework/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,122 @@
+#####################################################################
+## 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 "7")
+
+
+#####################################################################
+## 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)")
+
+# Generic parameters of the build
+set(ENABLE_CIVETWEB ON CACHE BOOL "Use Civetweb instead of Mongoose (Mongoose was the default embedded HTTP server in Orthanc <= 1.5.1)")
+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_LIBICU ON CACHE BOOL "Use the system version of libicu")
+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(DCMTK_STATIC_VERSION "3.6.5" CACHE STRING "Version of DCMTK to be used in static builds (can be \"3.6.0\", \"3.6.2\", \"3.6.4\", or \"3.6.5\")")
+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_LOG ON CACHE BOOL "Enable logging internal to 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(BOOST_LOCALE_BACKEND "libiconv" CACHE STRING "Back-end for locales that is used by Boost (can be \"gcc\", \"libiconv\", \"icu\", or \"wconv\" on Windows)")
+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 LSB and old versions of Visual Studio)")
+set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
+set(MSVC_MULTIPLE_PROCESSES OFF CACHE BOOL "Add the /MP option to build with multiple processes if using Visual Studio")
+set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND OFF CACHE BOOL "Sets the compiler flags required to use the LLVM Web Assembly backend in emscripten")
+set(OPENSSL_STATIC_VERSION "1.1.1" CACHE STRING "Version of OpenSSL to be used in static builds (can be \"1.0.2\", or \"1.1.1\")")
+
+mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
+mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS)
+mark_as_advanced(USE_PUGIXML)
+mark_as_advanced(USE_DCMTK_362_PRIVATE_DIC)
+
+
+#####################################################################
+## 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_DCMTK_TRANSCODING OFF CACHE INTERNAL "Enable DICOM transcoding in DCMTK")
+set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL")
+
+set(ORTHANC_SANDBOXED OFF CACHE INTERNAL
+  "Whether Orthanc runs inside a sandboxed environment (such as Google NaCl or WebAssembly)")
+
+set(ORTHANC_BUILDING_FRAMEWORK_LIBRARY OFF CACHE INTERNAL
+  "Whether we are in the process of building the Orthanc Framework shared library")
+
+#
+# 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")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/PugixmlConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,26 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
+  set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.9)
+  set(PUGIXML_MD5 "7286ee2ed11376b6b780ced19fae0b64")
+  set(PUGIXML_URL "http://orthanc.osimis.io/ThirdPartyDownloads/pugixml-1.9.tar.gz")
+
+  DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}")
+
+  include_directories(
+    ${PUGIXML_SOURCES_DIR}/src
+    )
+
+  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)
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/SQLiteConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,77 @@
+if (APPLE)
+  # Under OS X, the binaries must always be linked against the
+  # system-wide version of SQLite. Otherwise, if some Orthanc plugin
+  # also uses its own version of SQLite (such as orthanc-webviewer),
+  # this results in a crash in "sqlite3_mutex_enter(db->mutex);" (the
+  # mutex is not initialized), probably because the EXE and the DYNLIB
+  # share the same memory location for this mutex.
+  set(SQLITE_STATIC OFF)
+
+elseif (STATIC_BUILD OR NOT USE_SYSTEM_SQLITE)
+  set(SQLITE_STATIC ON)
+else()
+  set(SQLITE_STATIC OFF)
+endif()
+
+
+if (SQLITE_STATIC)
+  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100)
+  SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da")
+  SET(SQLITE_URL "http://orthanc.osimis.io/ThirdPartyDownloads/sqlite-amalgamation-3270100.zip")
+
+  set(ORTHANC_SQLITE_VERSION 3027001)
+
+  DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
+
+  set(SQLITE_SOURCES
+    ${SQLITE_SOURCES_DIR}/sqlite3.c
+    )
+
+  add_definitions(
+    # For SQLite to run in the "Serialized" thread-safe mode
+    # http://www.sqlite.org/threadsafe.html
+    -DSQLITE_THREADSAFE=1  
+    -DSQLITE_OMIT_LOAD_EXTENSION  # Disable SQLite plugins
+    )
+
+  include_directories(
+    ${SQLITE_SOURCES_DIR}
+    )
+
+  source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*)
+
+else()
+  CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H)
+  if (NOT HAVE_SQLITE_H)
+    message(FATAL_ERROR "Please install the libsqlite3-dev package")
+  endif()
+
+  find_path(SQLITE_INCLUDE_DIR
+    NAMES sqlite3.h
+    PATHS
+    /usr/include
+    /usr/local/include
+    )
+  message("SQLite include dir: ${SQLITE_INCLUDE_DIR}")
+
+  # 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_NUMBER2 ${SQLITE_VERSION_NUMBER1})
+
+  # Remove the trailing spaces to convert the string to a proper integer
+  string(STRIP ${SQLITE_VERSION_NUMBER2} ORTHANC_SQLITE_VERSION)
+
+  message("Detected version of SQLite: ${ORTHANC_SQLITE_VERSION}")
+
+  IF (${ORTHANC_SQLITE_VERSION} LESS 3007000)
+    # "sqlite3_create_function_v2" is not defined in SQLite < 3.7.0
+    message(FATAL_ERROR "SQLite version must be above 3.7.0. Please set the CMake variable USE_SYSTEM_SQLITE to OFF.")
+  ENDIF()
+
+  link_libraries(sqlite3)
+endif()
+
+
+add_definitions(
+  -DORTHANC_SQLITE_VERSION=${ORTHANC_SQLITE_VERSION}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/Uninstall.cmake.in	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,25 @@
+# Code taken from the CMake FAQ
+# http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F
+
+if (NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
+  message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"")
+endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
+
+file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files)
+string(REGEX REPLACE "\n" ";" files "${files}")
+list(REVERSE files)
+foreach (file ${files})
+  message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"")
+  if (EXISTS "$ENV{DESTDIR}${file}")
+    execute_process(
+      COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}"
+      OUTPUT_VARIABLE rm_out
+      RESULT_VARIABLE rm_retval
+      )
+    if(NOT ${rm_retval} EQUAL 0)
+      message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"")
+    endif (NOT ${rm_retval} EQUAL 0)
+  else (EXISTS "$ENV{DESTDIR}${file}")
+    message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.")
+  endif (EXISTS "$ENV{DESTDIR}${file}")
+endforeach(file)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/UuidConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,131 @@
+if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+
+  if (STATIC_BUILD OR NOT USE_SYSTEM_UUID)
+    SET(E2FSPROGS_SOURCES_DIR ${CMAKE_BINARY_DIR}/e2fsprogs-1.44.5)
+    SET(E2FSPROGS_URL "http://orthanc.osimis.io/ThirdPartyDownloads/e2fsprogs-1.44.5.tar.gz")
+    SET(E2FSPROGS_MD5 "8d78b11d04d26c0b2dd149529441fa80")
+
+    if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}")
+      set(FirstRun OFF)
+    else()
+      set(FirstRun ON)
+    endif()
+
+    DownloadPackage(${E2FSPROGS_MD5} ${E2FSPROGS_URL} "${E2FSPROGS_SOURCES_DIR}")
+
+    
+    ##
+    ## Patch for OS X, in order to be compatible with Cocoa (used in Stone)
+    ## 
+
+    execute_process(
+      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
+      ${ORTHANC_ROOT}/Resources/Patches/e2fsprogs-1.44.5-apple.patch
+      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+      RESULT_VARIABLE Failure
+      )
+
+    if (FirstRun AND Failure)
+      message(FATAL_ERROR "Error while patching a file")
+    endif()
+
+
+    include_directories(
+      BEFORE ${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()
+
+    find_library(LIBUUID uuid
+      PATHS
+      /usr/lib
+      /usr/local/lib
+      )
+
+    check_library_exists(${LIBUUID} uuid_generate_random "" HAVE_LIBUUID)
+    if (NOT HAVE_LIBUUID)
+      message(FATAL_ERROR "Unable to find the uuid library")
+    endif()
+    
+    link_libraries(${LIBUUID})
+  endif()
+
+endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,14 @@
+macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources Target)
+  get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE)
+  set(PrecompiledBinary "${PrecompiledBasename}_${CMAKE_BUILD_TYPE}_${CMAKE_GENERATOR_PLATFORM}.pch")
+
+  set_source_files_properties(${PrecompiledSource}
+    PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
+    OBJECT_OUTPUTS "${PrecompiledBinary}")
+
+  set_source_files_properties(${${Sources}}
+    PROPERTIES COMPILE_FLAGS "/Yu\"${PrecompiledHeaders}\" /FI\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
+    OBJECT_DEPENDS "${PrecompiledBinary}")
+
+  set(${Target} ${PrecompiledSource})
+endmacro()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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://orthanc.osimis.io/ThirdPartyDownloads/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/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/Run2.cpp	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/app.js	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/arith.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/CMake/WebAssembly/ArithmeticTests/index.html	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/CMake/WebAssembly/arith.h	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/CMake/ZlibConfiguration.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,51 @@
+if (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB)
+  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.2.11)
+  SET(ZLIB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/zlib-1.2.11.tar.gz")
+  SET(ZLIB_MD5 "1c9f62f0778697a09d36121ead88e08e")
+
+  DownloadPackage(${ZLIB_MD5} ${ZLIB_URL} "${ZLIB_SOURCES_DIR}")
+
+  include_directories(
+    ${ZLIB_SOURCES_DIR}
+    )
+
+  list(APPEND ZLIB_SOURCES 
+    ${ZLIB_SOURCES_DIR}/adler32.c
+    ${ZLIB_SOURCES_DIR}/compress.c
+    ${ZLIB_SOURCES_DIR}/crc32.c 
+    ${ZLIB_SOURCES_DIR}/deflate.c 
+    ${ZLIB_SOURCES_DIR}/infback.c 
+    ${ZLIB_SOURCES_DIR}/inffast.c 
+    ${ZLIB_SOURCES_DIR}/inflate.c 
+    ${ZLIB_SOURCES_DIR}/inftrees.c 
+    ${ZLIB_SOURCES_DIR}/trees.c 
+    ${ZLIB_SOURCES_DIR}/uncompr.c 
+    ${ZLIB_SOURCES_DIR}/zutil.c
+    )
+
+  if (NOT ORTHANC_SANDBOXED)
+    # The source files below require access to the filesystem
+    list(APPEND ZLIB_SOURCES
+      ${ZLIB_SOURCES_DIR}/gzlib.c 
+      ${ZLIB_SOURCES_DIR}/gzclose.c 
+      ${ZLIB_SOURCES_DIR}/gzread.c 
+      ${ZLIB_SOURCES_DIR}/gzwrite.c 
+      )
+  endif()
+
+  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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMakeToolchains/CrossToolchain.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,56 @@
+#
+#  $ CROSSTOOL_NG_ARCH=mips CROSSTOOL_NG_BOARD=malta CROSSTOOL_NG_IMAGE=/tmp/mips cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/CrossToolchain.cmake -DBUILD_CONNECTIVITY_CHECKS=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON -DUSE_SYSTEM_JSONCPP=OFF -DUSE_SYSTEM_UUID=OFF -DENABLE_DCMTK_JPEG_LOSSLESS=OFF -G Ninja && ninja
+#
+
+INCLUDE(CMakeForceCompiler)
+
+SET(CROSSTOOL_NG_ROOT $ENV{CROSSTOOL_NG_ROOT} CACHE STRING "")
+SET(CROSSTOOL_NG_ARCH $ENV{CROSSTOOL_NG_ARCH} CACHE STRING "")
+SET(CROSSTOOL_NG_BOARD $ENV{CROSSTOOL_NG_BOARD} CACHE STRING "")
+SET(CROSSTOOL_NG_SUFFIX $ENV{CROSSTOOL_NG_SUFFIX} CACHE STRING "")
+SET(CROSSTOOL_NG_IMAGE $ENV{CROSSTOOL_NG_IMAGE} CACHE STRING "")
+
+IF ("${CROSSTOOL_NG_ROOT}" STREQUAL "")
+  SET(CROSSTOOL_NG_ROOT "/home/$ENV{USER}/x-tools")
+ENDIF()
+
+IF ("${CROSSTOOL_NG_SUFFIX}" STREQUAL "")
+  SET(CROSSTOOL_NG_SUFFIX "linux-gnu")
+ENDIF()
+
+SET(CROSSTOOL_NG_NAME ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_BOARD}-${CROSSTOOL_NG_SUFFIX})
+SET(CROSSTOOL_NG_BASE ${CROSSTOOL_NG_ROOT}/${CROSSTOOL_NG_NAME})
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Linux)
+SET(CMAKE_SYSTEM_VERSION CrossToolNg)
+SET(CMAKE_SYSTEM_PROCESSOR ${CROSSTOOL_NG_ARCH})
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-gcc)
+
+if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
+  CMAKE_FORCE_CXX_COMPILER(${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++ GNU)
+else()
+  SET(CMAKE_CXX_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++)
+endif()
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_IMAGE})
+#SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# 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_PACKAGE ONLY)
+
+SET(CMAKE_CROSSCOMPILING ON)
+#SET(CROSS_COMPILER_PREFIX ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX})
+
+SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
+SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMakeToolchains/LinuxStandardBaseToolchain.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,79 @@
+#
+# Full build, as used on the BuildBot CIS:
+#
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja
+#
+# Or, more lightweight version (without libp11 and ICU):
+#
+#   $ 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 -G Ninja
+#
+
+INCLUDE(CMakeForceCompiler)
+
+SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "")
+SET(LSB_CC $ENV{LSB_CC} CACHE STRING "")
+SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
+SET(LSB_TARGET_VERSION "4.0" CACHE STRING "")
+
+IF ("${LSB_PATH}" STREQUAL "")
+  SET(LSB_PATH "/opt/lsb")
+ENDIF()
+
+IF (EXISTS ${LSB_PATH}/lib64)
+  SET(LSB_TARGET_PROCESSOR "x86_64")
+  SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION})
+ELSEIF (EXISTS ${LSB_PATH}/lib)
+  SET(LSB_TARGET_PROCESSOR "x86")
+  SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION})
+ELSE()
+  MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.")
+ENDIF()
+
+SET(LSB_CPPPATH ${LSB_PATH}/include)
+SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/)
+
+# the name of the target operating system
+SET(CMAKE_SYSTEM_NAME Linux)
+SET(CMAKE_SYSTEM_VERSION LinuxStandardBase)
+SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR})
+
+# which compilers to use for C and C++
+SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc)
+
+if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
+  CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU)
+else()
+  SET(CMAKE_CXX_COMPILER ${LSB_PATH}/bin/lsbc++)
+endif()
+
+# here is the target environment located
+SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH})
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# 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 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()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMakeToolchains/MinGW-W64-Toolchain32.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,17 @@
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
+set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
+set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMakeToolchains/MinGW-W64-Toolchain64.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,17 @@
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
+set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
+set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CMakeToolchains/MinGWToolchain.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,20 @@
+# the name of the target operating system
+set(CMAKE_SYSTEM_NAME Windows)
+
+# which compilers to use for C and C++
+set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
+set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
+set(CMAKE_RC_COMPILER i586-mingw32msvc-windres)
+
+# here is the target environment located
+set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc)
+
+# adjust the default behaviour of the FIND_XXX() commands:
+# 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_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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/DicomTransferSyntaxes.json	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,376 @@
+[
+  {
+    "UID" : "1.2.840.10008.1.2",
+    "Name" : "Implicit VR Little Endian",
+    "Value" : "LittleEndianImplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_LittleEndianImplicit",
+    "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.1",
+    "Name" : "Explicit VR Little Endian",
+    "Value" : "LittleEndianExplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_LittleEndianExplicit",
+    "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.1.99",
+    "Name" : "Deflated Explicit VR Little Endian",
+    "Value" : "DeflatedLittleEndianExplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_DeflatedLittleEndianExplicit"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.2",
+    "Name" : "Explicit VR Big Endian",
+    "Value" : "BigEndianExplicit",
+    "Retired" : false,
+    "DCMTK" : "EXS_BigEndianExplicit"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.50",
+    "Name" : "JPEG Baseline (process 1, lossy)",
+    "Value" : "JPEGProcess1",
+    "Retired" : false,
+    "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression",
+    "DCMTK" : "EXS_JPEGProcess1",
+    "DCMTK360" : "EXS_JPEGProcess1TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.51",
+    "Name" : "JPEG Extended Sequential (processes 2 & 4)",
+    "Value" : "JPEGProcess2_4",
+    "Retired" : false,
+    "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)",
+    "DCMTK" : "EXS_JPEGProcess2_4",
+    "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.52",
+    "Name" : "JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess3_5",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess3_5",
+    "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.53",
+    "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess6_8",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess6_8",
+    "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.54",
+    "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess7_9",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess7_9",
+    "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.55",
+    "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess10_12",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess10_12",
+    "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.56",
+    "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess11_13",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess11_13",
+    "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.57",
+    "Name" : "JPEG Lossless, Nonhierarchical with any selection value (process 14)",
+    "Value" : "JPEGProcess14",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEGProcess14",
+    "DCMTK360" : "EXS_JPEGProcess14TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.58",
+    "Name" : "JPEG Lossless with any selection value, arithmetic coding",
+    "Value" : "JPEGProcess15",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess15",
+    "DCMTK360" : "EXS_JPEGProcess15TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.59",
+    "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess16_18",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess16_18",
+    "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.60",
+    "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess17_19",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess17_19",
+    "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.61",
+    "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess20_22",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess20_22",
+    "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.62",
+    "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess21_23",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess21_23",
+    "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.63",
+    "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit)",
+    "Value" : "JPEGProcess24_26",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess24_26",
+    "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.64",
+    "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding",
+    "Value" : "JPEGProcess25_27",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess25_27",
+    "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.65",
+    "Name" : "JPEG Lossless, Hierarchical",
+    "Value" : "JPEGProcess28",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess28",
+    "DCMTK360" : "EXS_JPEGProcess28TransferSyntax"
+  },
+  
+  {
+    "UID" : "1.2.840.10008.1.2.4.66",
+    "Name" : "JPEG Lossless, Hierarchical, arithmetic coding",
+    "Value" : "JPEGProcess29",
+    "Retired" : true,
+    "DCMTK" : "EXS_JPEGProcess29",
+    "DCMTK360" : "EXS_JPEGProcess29TransferSyntax"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.70",
+    "Name" : "JPEG Lossless, Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1])",
+    "Value" : "JPEGProcess14SV1",
+    "Retired" : false,
+    "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression",
+    "DCMTK" : "EXS_JPEGProcess14SV1",
+    "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.80",
+    "Name" : "JPEG-LS (lossless)",
+    "Value" : "JPEGLSLossless",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEGLSLossless",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.81",
+    "Name" : "JPEG-LS (lossy or near-lossless)",
+    "Value" : "JPEGLSLossy",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEGLSLossy",
+    "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.90",
+    "Name" : "JPEG 2000 (lossless)",
+    "Value" : "JPEG2000LosslessOnly",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000LosslessOnly",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.91",
+    "Name" : "JPEG 2000 (lossless or lossy)",
+    "Value" : "JPEG2000",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.92",
+    "Name" : "JPEG 2000 part 2 multicomponent extensions (lossless)",
+    "Value" : "JPEG2000MulticomponentLosslessOnly",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000MulticomponentLosslessOnly",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.93",
+    "Name" : "JPEG 2000 part 2 multicomponent extensions (lossless or lossy)",
+    "Value" : "JPEG2000Multicomponent",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPEG2000Multicomponent",
+    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.94",
+    "Name" : "JPIP Referenced",
+    "Value" : "JPIPReferenced",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPIPReferenced"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.95",
+    "Name" : "JPIP Referenced Deflate",
+    "Value" : "JPIPReferencedDeflate",
+    "Retired" : false,
+    "DCMTK" : "EXS_JPIPReferencedDeflate"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.100",
+    "Name" : "MPEG2 Main Profile / Main Level",
+    "Value" : "MPEG2MainProfileAtMainLevel",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.101",
+    "Name" : "MPEG2 Main Profile / High Level",
+    "Value" : "MPEG2MainProfileAtHighLevel",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.102",
+    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.1",
+    "Value" : "MPEG4HighProfileLevel4_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_1",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.103",
+    "Name" : "MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1",
+    "Value" : "MPEG4BDcompatibleHighProfileLevel4_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.104",
+    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video",
+    "Value" : "MPEG4HighProfileLevel4_2_For2DVideo",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.105",
+    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video",
+    "Value" : "MPEG4HighProfileLevel4_2_For3DVideo",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.106",
+    "Name" : "MPEG4 AVC/H.264 Stereo High Profile / Level 4.2",
+    "Value" : "MPEG4StereoHighProfileLevel4_2",
+    "Retired" : false,
+    "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2",
+    "SinceDCMTK" : "361"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.107",
+    "Name" : "HEVC/H.265 Main Profile / Level 5.1",
+    "Value" : "HEVCMainProfileLevel5_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_HEVCMainProfileLevel5_1",
+    "SinceDCMTK" : "362"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.4.108",
+    "Name" : "HEVC/H.265 Main 10 Profile / Level 5.1",
+    "Value" : "HEVCMain10ProfileLevel5_1",
+    "Retired" : false,
+    "DCMTK" : "EXS_HEVCMain10ProfileLevel5_1",
+    "SinceDCMTK" : "362"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.5",
+    "Name" : "RLE - Run Length Encoding (lossless)",
+    "Value" : "RLELossless",
+    "Retired" : false,
+    "DCMTK" : "EXS_RLELossless",
+    "GDCM" : "gdcm::TransferSyntax::RLELossless"
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.6.1",
+    "Name" : "RFC 2557 MIME Encapsulation",
+    "Value" : "RFC2557MimeEncapsulation",
+    "Retired" : true
+  },
+
+  {
+    "UID" : "1.2.840.10008.1.2.6.2",
+    "Name" : "XML Encoding",
+    "Value" : "XML",
+    "Retired" : true
+  }
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/EncodingTests.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,63 @@
+static const unsigned int testEncodingsCount = 18;
+static const ::Orthanc::Encoding testEncodings[] = {
+  ::Orthanc::Encoding_Latin5,
+  ::Orthanc::Encoding_Hebrew,
+  ::Orthanc::Encoding_Greek,
+  ::Orthanc::Encoding_Arabic,
+  ::Orthanc::Encoding_Cyrillic,
+  ::Orthanc::Encoding_Latin4,
+  ::Orthanc::Encoding_Latin3,
+  ::Orthanc::Encoding_Latin2,
+  ::Orthanc::Encoding_Latin1,
+  ::Orthanc::Encoding_Utf8,
+  ::Orthanc::Encoding_Thai,
+  ::Orthanc::Encoding_Japanese,
+  ::Orthanc::Encoding_Ascii,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Chinese,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Windows1251,
+  ::Orthanc::Encoding_Windows1251
+};
+static const char *testEncodingsEncoded[18] = {
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xdd",
+  "\x54\x65\x73\x74\xe3",
+  "\x54\x65\x73\x74\xc8",
+  "\x54\x65\x73\x74\xd5",
+  "\x54\x65\x73\x74\xb4\xfb",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf3",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xf8\xa9",
+  "\x54\x65\x73\x74\xe9\xe4\xf6",
+  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2",
+  "\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",
+  "\x54\x65\x73\x74\xfb",
+  "\x54\x65\x73\x74\x84\x44\x83\xa6\xc8",
+  "\x54\x65\x73\x74",
+  "\x54\x65\x73\x74\xc4\x9e",
+  "\x81\x30\x89\x37\x81\x30\x89\x38\xA8\xA4\xA8\xA2\x81\x30\x89\x39\x81\x30\x8A\x30",
+  "\xd0\xe5\xed\xf2\xe3\xe5\xed\xee\xe3\xf0\xe0\xf4\xe8\xff",
+  "\xD2\xE0\xE7",
+  "\xcf\xf0\xff\xec\xe0\xff"
+};
+static const char *testEncodingsExpected[18] = {
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\xb0",
+  "\x54\x65\x73\x74\xd7\x93",
+  "\x54\x65\x73\x74\xce\x98",
+  "\x54\x65\x73\x74\xd8\xb5",
+  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc4\xb7",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\x9d\xc4\xb0",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6",
+  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2",
+  "\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",
+  "\x54\x65\x73\x74\xe0\xb9\x9b",
+  "\x54\x65\x73\x74\xd0\x94\xce\x98\xef\xbe\x88",
+  "\x54\x65\x73\x74",
+  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
+  "\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3",
+  "\xd0\xa0\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb3\xd0\xb5\xd0\xbd\xd0\xbe\xd0\xb3\xd1\x80\xd0\xb0\xd1\x84\xd0\xb8\xd1\x8f",
+  "\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";
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/EncodingTests.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,80 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+source = u'TestéäöòДΘĝדصķћ๛ネİ'
+
+encodings = {
+    'UTF-8' : 'Utf8',
+    'ASCII' : 'Ascii',
+    'ISO-8859-1' : 'Latin1',
+    'ISO-8859-2' : 'Latin2',
+    'ISO-8859-3' : 'Latin3',
+    'ISO-8859-4' : 'Latin4',
+    'ISO-8859-9' : 'Latin5',
+    'ISO-8859-5' : 'Cyrillic',
+    'WINDOWS-1251' : 'Windows1251',
+    'ISO-8859-6' : 'Arabic',
+    'ISO-8859-7' : 'Greek',
+    'ISO-8859-8' : 'Hebrew',
+    'TIS-620' : 'Thai',
+    'SHIFT-JIS' : 'Japanese',
+    #'GB18030' : 'Chinese',  # Done manually below (*)
+}
+
+#from encodings.aliases import aliases
+#for a, b in aliases.iteritems():
+#    print '%s : %s' % (a, b)
+
+
+# "63" corresponds to "?"
+l = []
+encoded = []
+expected = []
+
+def ToArray(source):
+    result = ''
+    for byte in bytearray(source):
+        result += '\\x%02x' % byte
+    return '"%s"' % result
+    
+
+for encoding, orthancEnumeration in encodings.iteritems():
+    l.append('::Orthanc::Encoding_%s' % orthancEnumeration)
+    s = source.encode(encoding, 'ignore')
+    encoded.append(ToArray(s))
+    expected.append(ToArray(s.decode(encoding).encode('utf-8')))
+
+
+# https://en.wikipedia.org/wiki/GB_18030#Technical_details  (*)
+l.append('::Orthanc::Encoding_Chinese')
+expected.append(ToArray('Þßàáâã'))
+encoded.append('"\\x81\\x30\\x89\\x37\\x81\\x30\\x89\\x38\\xA8\\xA4\\xA8\\xA2\\x81\\x30\\x89\\x39\\x81\\x30\\x8A\\x30"')
+
+# Issue 32
+# "encoded" is the copy/paste from "dcm2xml +Ca cyrillic Issue32.dcm"
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xd0\\xe5\\xed\\xf2\\xe3\\xe5\\xed\\xee\\xe3\\xf0\\xe0\\xf4\\xe8\\xff"')
+expected.append(ToArray('Рентгенография'))
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xD2\\xE0\\xE7"')
+expected.append(ToArray('Таз'))
+l.append('::Orthanc::Encoding_Windows1251')
+encoded.append('"\\xcf\\xf0\\xff\\xec\\xe0\\xff"')
+expected.append(ToArray('Прямая'))
+
+
+if True:
+    print 'static const unsigned int testEncodingsCount = %d;' % len(l)
+    print 'static const ::Orthanc::Encoding testEncodings[] = {\n  %s\n};' % (',\n  '.join(l))
+    print 'static const char *testEncodingsEncoded[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(encoded))
+    print 'static const char *testEncodingsExpected[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(expected))
+else:
+    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'))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateErrorCodes.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,160 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 json
+import os
+import re
+import sys
+
+START_PLUGINS = 1000000
+BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+
+
+## 
+## Read all the available error codes and HTTP status
+##
+
+with open(os.path.join(BASE, 'Resources', 'ErrorCodes.json'), 'r') as f:
+    ERRORS = json.loads(re.sub('/\*.*?\*/', '', f.read()))
+
+for error in ERRORS:
+    if error['Code'] >= START_PLUGINS:
+        print('ERROR: Error code must be below %d, but "%s" is set to %d' % (START_PLUGINS, error['Name'], error['Code']))
+        sys.exit(-1)
+
+with open(os.path.join(BASE, 'Core', 'Enumerations.h'), 'r') as f:
+    a = f.read()
+
+HTTP = {}
+for i in re.findall('(HttpStatus_([0-9]+)_\w+)', a):
+    HTTP[int(i[1])] = i[0]
+
+
+
+##
+## Generate the "ErrorCode" enumeration in "Enumerations.h"
+##
+
+path = os.path.join(BASE, 'Core', 'Enumerations.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = ',\n'.join(map(lambda x: '    ErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
+
+s += ',\n    ErrorCode_START_PLUGINS = %d' % START_PLUGINS
+a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "OrthancPluginErrorCode" enumeration in "OrthancCPlugin.h"
+##
+
+path = os.path.join(BASE, 'Plugins', 'Include', 'orthanc', 'OrthancCPlugin.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = ',\n'.join(map(lambda x: '    OrthancPluginErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
+s += ',\n\n    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff\n  '
+a = re.sub('(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "EnumerationToString(ErrorCode)" and
+## "ConvertErrorCodeToHttpStatus(ErrorCode)" functions in
+## "Enumerations.cpp"
+##
+
+path = os.path.join(BASE, 'Core', 'Enumerations.cpp')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = '\n\n'.join(map(lambda x: '      case ErrorCode_%s:\n        return "%s";' % (x['Name'], x['Description']), ERRORS))
+a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+           r'\1\n%s\2' % s, a, re.DOTALL)
+
+def GetHttpStatus(x):
+    s = HTTP[x['HttpStatus']]
+    return '      case ErrorCode_%s:\n        return %s;' % (x['Name'], s)
+
+s = '\n\n'.join(map(GetHttpStatus, filter(lambda x: 'HttpStatus' in x, ERRORS)))
+a = re.sub('(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+           r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "ErrorCode" enumeration in "OrthancSQLiteException.h"
+##
+
+path = os.path.join(BASE, 'Core', 'SQLite', 'OrthancSQLiteException.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+e = filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS)
+s = ',\n'.join(map(lambda x: '      ErrorCode_%s' % x['Name'], e))
+a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+s = '\n\n'.join(map(lambda x: '          case ErrorCode_%s:\n            return "%s";' % (x['Name'], x['Description']), e))
+a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
+           r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the "PrintErrors" function in "main.cpp"
+##
+
+path = os.path.join(BASE, 'OrthancServer', 'main.cpp')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = '\n'.join(map(lambda x: '    PrintErrorCode(ErrorCode_%s, "%s");' % (x['Name'], x['Description']), ERRORS))
+a = re.sub('(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n  }' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxes.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,84 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 json
+import os
+import re
+import sys
+import pystache
+
+BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+
+
+## https://www.dicomlibrary.com/dicom/transfer-syntax/
+## https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=EDICOM_transfer_syntax
+
+
+with open(os.path.join(BASE, 'Resources', 'DicomTransferSyntaxes.json'), 'r') as f:
+    SYNTAXES = json.loads(f.read())
+
+
+
+##
+## Generate the "DicomTransferSyntax" enumeration in "Enumerations.h"
+##
+
+path = os.path.join(BASE, 'Core', 'Enumerations.h')
+with open(path, 'r') as f:
+    a = f.read()
+
+s = ',\n'.join(map(lambda x: '    DicomTransferSyntax_%s    /*!< %s */' % (x['Value'], x['Name']), SYNTAXES))
+
+a = re.sub('(enum DicomTransferSyntax\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
+
+with open(path, 'w') as f:
+    f.write(a)
+
+
+
+##
+## Generate the implementations
+##
+
+with open(os.path.join(BASE, 'Core', 'Enumerations_TransferSyntaxes.impl.h'), 'w') as b:
+    with open(os.path.join(BASE, 'Resources', 'GenerateTransferSyntaxesEnumerations.mustache'), 'r') as a:
+        b.write(pystache.render(a.read(), {
+            'Syntaxes' : SYNTAXES
+        }))
+
+with open(os.path.join(BASE, 'Core', 'DicomParsing', 'FromDcmtkBridge_TransferSyntaxes.impl.h'), 'w') as b:
+    with open(os.path.join(BASE, 'Resources', 'GenerateTransferSyntaxesDcmtk.mustache'), 'r') as a:
+        b.write(pystache.render(a.read(), {
+            'Syntaxes' : SYNTAXES
+        }))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesDcmtk.mustache	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                                  DicomTransferSyntax source)
+  {
+    switch (source)
+    {
+      {{#Syntaxes}}
+      {{#DCMTK}}
+      {{#SinceDCMTK}}
+#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
+      {{/SinceDCMTK}}
+      case DicomTransferSyntax_{{Value}}:
+        {{#DCMTK360}}
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = {{DCMTK360}};
+#  else
+        target = {{DCMTK}};
+#  endif
+        {{/DCMTK360}}
+        {{^DCMTK360}}
+        target = {{DCMTK}};
+        {{/DCMTK360}}
+        return true;
+      {{#SinceDCMTK}}
+#endif
+      {{/SinceDCMTK}}
+
+      {{/DCMTK}}
+      {{/Syntaxes}}
+      default:
+        return false;
+    }
+  }
+  
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    E_TransferSyntax source)
+  {
+    switch (source)
+    {
+      {{#Syntaxes}}
+      {{#DCMTK}}
+      {{#SinceDCMTK}}
+#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
+      {{/SinceDCMTK}}
+      {{#DCMTK360}}
+#  if DCMTK_VERSION_NUMBER <= 360
+      case {{DCMTK360}}:
+#  else
+      case {{DCMTK}}:
+#  endif
+      {{/DCMTK360}}
+      {{^DCMTK360}}
+      case {{DCMTK}}:
+      {{/DCMTK360}}
+        target = DicomTransferSyntax_{{Value}};
+        return true;
+      {{#SinceDCMTK}}
+#endif
+      {{/SinceDCMTK}}
+
+      {{/DCMTK}}
+      {{/Syntaxes}}
+      default:
+        return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/CodeGeneration/GenerateTransferSyntaxesEnumerations.mustache	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      {{#Syntaxes}}
+      case DicomTransferSyntax_{{Value}}:
+        return "{{UID}}";
+
+      {{/Syntaxes}}
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      {{#Syntaxes}}
+      case DicomTransferSyntax_{{Value}}:
+        {{#Retired}}
+        return true;
+        {{/Retired}}
+        {{^Retired}}
+        return false;
+        {{/Retired}}
+
+      {{/Syntaxes}}
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool LookupTransferSyntax(DicomTransferSyntax& target,
+                            const std::string& uid)
+  {
+    {{#Syntaxes}}
+    if (uid == "{{UID}}")
+    {
+      target = DicomTransferSyntax_{{Value}};
+      return true;
+    }
+    
+    {{/Syntaxes}}
+    return false;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/DcmtkTools/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,31 @@
+#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../Resources/LinuxStandardBaseToolchain.cmake -G Ninja
+
+cmake_minimum_required(VERSION 2.8)
+
+project(DcmtkTools)
+
+set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../)
+include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+
+set(STATIC_BUILD ON CACHE BOOL "")
+set(ALLOW_DOWNLOADS ON CACHE BOOL "")
+
+set(DCMTK_STATIC_VERSION "3.6.5" CACHE STRING "")
+set(ENABLE_DCMTK_JPEG ON CACHE BOOL "")
+set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "")
+set(ENABLE_DCMTK_LOG ON CACHE BOOL "")
+set(ENABLE_DCMTK_NETWORKING ON CACHE BOOL "")
+set(ENABLE_DCMTK_TRANSCODING ON CACHE BOOL "")
+
+include(${ORTHANC_ROOT}/Resources/CMake/DcmtkConfiguration.cmake)
+
+add_library(dcmtk STATIC
+  ${CMAKE_SOURCE_DIR}/dummy.cpp
+  ${DCMTK_SOURCES}
+  )
+
+add_executable(getscu
+  ${DCMTK_SOURCES_DIR}/dcmnet/apps/getscu.cc
+  )
+target_link_libraries(getscu dcmtk)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/DcmtkTools/dummy.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,22 @@
+#include <string>
+
+struct OrthancLinesIterator;
+
+OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content)
+{
+  return NULL;
+}
+
+bool OrthancLinesIterator_GetLine(std::string& target,
+                                  const OrthancLinesIterator* iterator)
+{
+  return false;
+}
+
+void OrthancLinesIterator_Next(OrthancLinesIterator* iterator)
+{
+}
+
+void OrthancLinesIterator_Free(OrthancLinesIterator* iterator)
+{
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/EmbedResources.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,455 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 sys
+import os
+import os.path
+import pprint
+import re
+
+UPCASE_CHECK = True
+USE_SYSTEM_EXCEPTION = False
+EXCEPTION_CLASS = 'OrthancException'
+OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)'
+INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)'
+NAMESPACE = 'Orthanc.EmbeddedResources'
+FRAMEWORK_PATH = None
+
+ARGS = []
+for i in range(len(sys.argv)):
+    if not sys.argv[i].startswith('--'):
+        ARGS.append(sys.argv[i])
+    elif sys.argv[i].lower() == '--no-upcase-check':
+        UPCASE_CHECK = False
+    elif sys.argv[i].lower() == '--system-exception':
+        USE_SYSTEM_EXCEPTION = True
+        EXCEPTION_CLASS = '::std::runtime_error'
+        OUT_OF_RANGE_EXCEPTION = '%s("Parameter out of range")' % EXCEPTION_CLASS
+        INEXISTENT_PATH_EXCEPTION = '%s("Unknown path in a directory resource")' % EXCEPTION_CLASS
+    elif sys.argv[i].startswith('--namespace='):
+        NAMESPACE = sys.argv[i][sys.argv[i].find('=') + 1 : ]
+    elif sys.argv[i].startswith('--framework-path='):
+        FRAMEWORK_PATH = sys.argv[i][sys.argv[i].find('=') + 1 : ]
+
+if len(ARGS) < 2 or len(ARGS) % 2 != 0:
+    print ('Usage:')
+    print ('python %s [--no-upcase-check] [--system-exception] [--namespace=<Namespace>] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
+    exit(-1)
+
+TARGET_BASE_FILENAME = ARGS[1]
+SOURCES = ARGS[2:]
+
+try:
+    # Make sure the destination directory exists
+    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
+except:
+    pass
+
+
+#####################################################################
+## Read each resource file
+#####################################################################
+
+def CheckNoUpcase(s):
+    global UPCASE_CHECK
+    if (UPCASE_CHECK and
+        re.search('[A-Z]', s) != None):
+        raise Exception("Path in a directory with an upcase letter: %s" % s)
+
+resources = {}
+
+counter = 0
+i = 0
+while i < len(SOURCES):
+    resourceName = SOURCES[i].upper()
+    pathName = SOURCES[i + 1]
+
+    if not os.path.exists(pathName):
+        raise Exception("Non existing path: %s" % pathName)
+
+    if resourceName in resources:
+        raise Exception("Twice the same resource: " + resourceName)
+    
+    if os.path.isdir(pathName):
+        # The resource is a directory: Recursively explore its files
+        content = {}
+        for root, dirs, files in os.walk(pathName):
+            dirs.sort()
+            files.sort()
+            base = os.path.relpath(root, pathName)
+
+            # Fix issue #24 (Build fails on OSX when directory has .DS_Store files):
+            # Ignore folders whose name starts with a dot (".")
+            if base.find('/.') != -1:
+                print('Ignoring folder: %s' % root)
+                continue
+
+            for f in files:
+                if f.find('~') == -1:  # Ignore Emacs backup files
+                    if base == '.':
+                        r = f
+                    else:
+                        r = os.path.join(base, f)
+
+                    CheckNoUpcase(r)
+                    r = '/' + r.replace('\\', '/')
+                    if r in content:
+                        raise Exception("Twice the same filename (check case): " + r)
+
+                    content[r] = {
+                        'Filename' : os.path.join(root, f),
+                        'Index' : counter
+                        }
+                    counter += 1
+
+        resources[resourceName] = {
+            'Type' : 'Directory',
+            'Files' : content
+            }
+
+    elif os.path.isfile(pathName):
+        resources[resourceName] = {
+            'Type' : 'File',
+            'Index' : counter,
+            'Filename' : pathName
+            }
+        counter += 1
+
+    else:
+        raise Exception("Not a regular file, nor a directory: " + pathName)
+
+    i += 2
+
+#pprint.pprint(resources)
+
+
+#####################################################################
+## Write .h header
+#####################################################################
+
+header = open(TARGET_BASE_FILENAME + '.h', 'w')
+
+header.write("""
+#pragma once
+
+#include <string>
+#include <list>
+
+#if defined(_MSC_VER)
+#  pragma warning(disable: 4065)  // "Switch statement contains 'default' but no 'case' labels"
+#endif
+
+""")
+
+
+for ns in NAMESPACE.split('.'):
+    header.write('namespace %s {\n' % ns)
+    
+
+header.write("""
+    enum FileResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    enum DirectoryResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    const void* GetFileResourceBuffer(FileResourceId id);
+    size_t GetFileResourceSize(FileResourceId id);
+    void GetFileResource(std::string& result, FileResourceId id);
+
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
+
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id);
+
+""")
+
+
+for ns in NAMESPACE.split('.'):
+    header.write('}\n')
+
+header.close()
+
+
+
+#####################################################################
+## Write the resource content in the .cpp source
+#####################################################################
+
+PYTHON_MAJOR_VERSION = sys.version_info[0]
+
+def WriteResource(cpp, item):
+    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
+
+    f = open(item['Filename'], "rb")
+    content = f.read()
+    f.close()
+
+    # http://stackoverflow.com/a/1035360
+    pos = 0
+    buffer = []  # instead of appending a few bytes at a time to the cpp file, 
+                 # we first append each chunk to a list, join it and write it 
+                 # to the file.  We've measured that it was 2-3 times faster in python3.
+                 # Note that speed is important since if generation is too slow,
+                 # cmake might try to compile the EmbeddedResources.cpp file while it is
+                 # still being generated !
+    for b in content:
+        if PYTHON_MAJOR_VERSION == 2:
+            c = ord(b[0])
+        else:
+            c = b
+
+        if pos > 0:
+            buffer.append(",")
+
+        if (pos % 16) == 0:
+            buffer.append("\n")
+
+        if c < 0:
+            raise Exception("Internal error")
+
+        buffer.append("0x%02x" % c)
+        pos += 1
+
+    cpp.write("".join(buffer))
+    # Zero-size array are disallowed, so we put one single void character in it.
+    if pos == 0:
+        cpp.write('  0')
+
+    cpp.write('  };\n')
+    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
+
+
+cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
+
+cpp.write('#include "%s.h"\n' % os.path.basename(TARGET_BASE_FILENAME))
+
+if USE_SYSTEM_EXCEPTION:
+    cpp.write('#include <stdexcept>')
+elif FRAMEWORK_PATH != None:
+    cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH)
+else:
+    cpp.write('#include <OrthancException.h>')
+
+cpp.write("""
+#include <stdint.h>
+#include <string.h>
+
+""")
+
+for ns in NAMESPACE.split('.'):
+    cpp.write('namespace %s {\n' % ns)
+
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        WriteResource(cpp, resources[name])
+    else:
+        for f in resources[name]['Files']:
+            WriteResource(cpp, resources[name]['Files'][f])
+
+
+
+#####################################################################
+## Write the accessors to the file resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetFileResourceBuffer(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw %s;
+      }
+    }
+
+    size_t GetFileResourceSize(FileResourceId id)
+    {
+      switch (id)
+      {
+""" % OUT_OF_RANGE_EXCEPTION)
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw %s;
+      }
+    }
+""" % OUT_OF_RANGE_EXCEPTION)
+
+
+
+#####################################################################
+## Write the accessors to the directory resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
+
+cpp.write("""      default:
+        throw %s;
+      }
+    }
+
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""" % OUT_OF_RANGE_EXCEPTION)
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
+
+cpp.write("""      default:
+        throw %s;
+      }
+    }
+""" % OUT_OF_RANGE_EXCEPTION)
+
+
+
+
+#####################################################################
+## List the resources in a directory
+#####################################################################
+
+cpp.write("""
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id)
+    {
+      result.clear();
+
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        for path in sorted(resources[name]['Files']):
+            cpp.write('        result.push_back("%s");\n' % path)
+        cpp.write('        break;\n\n')
+
+cpp.write("""      default:
+        throw %s;
+      }
+    }
+""" % OUT_OF_RANGE_EXCEPTION)
+
+
+
+
+#####################################################################
+## Write the convenience wrappers in .cpp
+#####################################################################
+
+cpp.write("""
+    void GetFileResource(std::string& result, FileResourceId id)
+    {
+      size_t size = GetFileResourceSize(id);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetFileResourceBuffer(id), size);
+    }
+
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
+    {
+      size_t size = GetDirectoryResourceSize(id, path);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
+    }
+""")
+
+
+for ns in NAMESPACE.split('.'):
+    cpp.write('}\n')
+
+cpp.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Fonts/GenerateFont.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,114 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+
+
+# sudo pip install freetype-py
+
+
+
+import freetype
+import json
+import os
+import sys
+import unicodedata
+
+
+if len(sys.argv) != 3:
+    print('Usage: %s <Font> <Size>\n' % sys.argv[0])
+    print('Example: %s /usr/share/fonts/truetype/ubuntu-font-family/UbuntuMono-B.ttf 16\n' % sys.argv[0])
+    sys.exit(-1)
+
+
+
+FONT = sys.argv[1]
+PIXEL_SIZE = int(sys.argv[2])
+CHARSET = 'latin-1'
+
+
+# Load the font
+face = freetype.Face(FONT)
+face.set_char_size(PIXEL_SIZE * 64)
+
+# Generate all the characters between 0 and 255
+characters = ''.join(map(chr, range(0, 256)))
+
+# Interpret the string using the required charset
+characters = characters.decode(CHARSET, 'ignore')
+
+# Keep only non-control characters
+characters = filter(lambda c: unicodedata.category(c)[0] != 'C', characters)
+
+font = {
+    'Name' : os.path.basename(FONT),
+    'Size' : PIXEL_SIZE,
+    'Characters' : {}
+}
+
+
+def PrintCharacter(c):
+    pos = 0
+    for i in range(c['Height']):
+        s = ''
+        for j in range(c['Width']):
+            if c['Bitmap'][pos] > 127:
+                s += '*'
+            else:
+                s += ' '
+            pos += 1
+        print s
+
+
+for c in characters:
+    face.load_char(c)
+
+    info = {
+        'Width' : face.glyph.bitmap.width,
+        'Height' : face.glyph.bitmap.rows,
+        'Advance' : face.glyph.metrics.horiAdvance / 64,
+        'Top' : -face.glyph.metrics.horiBearingY / 64,
+        'Bitmap' : face.glyph.bitmap.buffer,
+    }
+
+    font['Characters'][ord(c)] = info
+
+    #PrintCharacter(info)
+
+minTop = min(map(lambda (k, v): v['Top'], font['Characters'].iteritems()))
+for c in font['Characters']:
+    font['Characters'][c]['Top'] -= minTop
+
+font['MaxAdvance'] = max(map(lambda (k, v): v['Advance'], font['Characters'].iteritems()))
+font['MaxHeight'] = max(map(lambda (k, v): v['Height'], font['Characters'].iteritems()))
+
+print json.dumps(font)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Fonts/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,6 @@
+This file contains a precompiled version of several open-source fonts,
+that are used to draw text on images within Orthanc. These fonts are
+encoded as JSON files through the "./GenerateFont.py" script.
+
+- UbuntuMonoBold-16.json is the Ubuntu Mono Bold, size 16, licensed
+  under the Ubuntu Font Licence.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Fonts/UbuntuMonoBold-16.json	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1 @@
+{"MaxAdvance": 9, "MaxHeight": 15, "Name": "UbuntuMono-B.ttf", "Characters": {"32": {"Width": 0, "Advance": 8, "Bitmap": [], "Top": 14, "Height": 0}, "33": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 254, 251, 245, 242, 224, 223, 196, 199, 0, 0, 199, 198, 199, 198], "Top": 4, "Height": 10}, "34": {"Width": 5, "Advance": 8, "Bitmap": [255, 255, 0, 255, 255, 248, 247, 0, 248, 247, 226, 226, 0, 226, 226, 197, 196, 0, 197, 196, 160, 160, 0, 160, 162], "Top": 3, "Height": 5}, "35": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 31, 255, 254, 255, 225, 0, 0, 0, 93, 255, 250, 255, 163, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 234, 255, 255, 255, 21, 0, 0, 25, 255, 253, 255, 231, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 163, 243, 161, 255, 93, 0, 0, 0, 225, 94, 105, 255, 31, 0, 0], "Top": 4, "Height": 10}, "36": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 8, 0, 33, 198, 255, 255, 248, 169, 190, 255, 255, 255, 255, 202, 246, 255, 46, 8, 44, 66, 216, 255, 231, 148, 27, 0, 71, 243, 255, 255, 239, 59, 0, 23, 115, 205, 255, 205, 82, 56, 10, 42, 255, 246, 211, 255, 255, 255, 255, 209, 120, 227, 255, 255, 245, 72, 0, 0, 255, 255, 15, 0, 0, 0, 255, 255, 0, 0], "Top": 3, "Height": 13}, "37": {"Width": 7, "Advance": 8, "Bitmap": [127, 242, 123, 0, 0, 77, 179, 240, 57, 239, 0, 8, 213, 33, 240, 55, 239, 0, 128, 127, 0, 126, 243, 123, 34, 213, 8, 0, 0, 0, 0, 179, 76, 0, 0, 0, 0, 77, 179, 0, 0, 0, 0, 8, 213, 33, 126, 59, 124, 0, 128, 127, 0, 240, 6, 239, 34, 213, 8, 0, 240, 68, 240, 179, 76, 0, 0, 128, 243, 126], "Top": 4, "Height": 10}, "38": {"Width": 9, "Advance": 8, "Bitmap": [0, 39, 190, 245, 206, 57, 0, 0, 0, 0, 201, 255, 255, 255, 219, 0, 0, 0, 0, 242, 255, 55, 255, 233, 0, 0, 0, 0, 184, 255, 116, 255, 110, 0, 0, 0, 0, 128, 255, 255, 130, 6, 188, 52, 0, 102, 255, 103, 202, 235, 70, 255, 70, 0, 225, 255, 11, 26, 231, 249, 255, 45, 0, 240, 255, 82, 17, 174, 255, 255, 17, 0, 162, 255, 255, 255, 255, 255, 255, 125, 0, 14, 152, 229, 240, 188, 114, 255, 235, 7], "Top": 4, "Height": 10}, "39": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 251, 250, 230, 230, 200, 199, 161, 163], "Top": 3, "Height": 5}, "40": {"Width": 5, "Advance": 8, "Bitmap": [0, 0, 9, 152, 142, 0, 12, 199, 253, 110, 0, 166, 255, 118, 0, 46, 255, 207, 2, 0, 146, 255, 106, 0, 0, 209, 255, 40, 0, 0, 239, 255, 12, 0, 0, 242, 255, 14, 0, 0, 213, 255, 43, 0, 0, 157, 255, 117, 0, 0, 58, 255, 219, 8, 0, 0, 180, 255, 153, 0, 0, 17, 207, 255, 160, 0, 0, 12, 157, 119], "Top": 3, "Height": 14}, "41": {"Width": 6, "Advance": 8, "Bitmap": [120, 194, 49, 0, 0, 0, 82, 230, 250, 103, 0, 0, 0, 23, 219, 255, 93, 0, 0, 0, 32, 242, 243, 24, 0, 0, 0, 129, 255, 131, 0, 0, 0, 45, 255, 208, 0, 0, 0, 8, 255, 245, 0, 0, 0, 9, 255, 247, 0, 0, 0, 50, 255, 216, 0, 0, 0, 140, 255, 146, 0, 0, 50, 247, 250, 37, 0, 47, 235, 255, 117, 0, 121, 250, 253, 123, 0, 0, 80, 201, 60, 0, 0, 0], "Top": 3, "Height": 14}, "42": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 136, 189, 68, 255, 66, 183, 136, 213, 255, 255, 255, 255, 255, 214, 0, 7, 161, 248, 162, 8, 0, 0, 174, 255, 132, 255, 176, 0, 0, 67, 187, 4, 185, 63, 0], "Top": 4, "Height": 7}, "43": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 6, "Height": 8}, "44": {"Width": 4, "Advance": 8, "Bitmap": [0, 140, 243, 125, 0, 246, 255, 240, 0, 166, 255, 234, 0, 40, 255, 177, 49, 203, 240, 42, 204, 174, 39, 0], "Top": 11, "Height": 6}, "45": {"Width": 4, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255], "Top": 9, "Height": 2}, "46": {"Width": 3, "Advance": 9, "Bitmap": [91, 243, 85, 234, 255, 212, 91, 244, 85], "Top": 11, "Height": 3}, "47": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 47, 255, 217, 0, 0, 0, 0, 124, 255, 139, 0, 0, 0, 0, 201, 255, 62, 0, 0, 0, 24, 254, 237, 3, 0, 0, 0, 99, 255, 164, 0, 0, 0, 0, 176, 255, 87, 0, 0, 0, 8, 245, 250, 15, 0, 0, 0, 75, 255, 189, 0, 0, 0, 0, 152, 255, 111, 0, 0, 0, 0, 228, 255, 34, 0, 0, 0, 50, 255, 213, 0, 0, 0, 0, 127, 255, 136, 0, 0, 0, 0, 204, 255, 59, 0, 0, 0, 26, 254, 235, 2, 0, 0, 0], "Top": 3, "Height": 14}, "48": {"Width": 6, "Advance": 8, "Bitmap": [0, 125, 237, 238, 130, 0, 85, 255, 255, 255, 255, 89, 183, 255, 112, 114, 255, 184, 231, 255, 28, 30, 255, 231, 251, 255, 205, 204, 255, 250, 251, 255, 205, 204, 255, 249, 232, 255, 28, 30, 255, 230, 185, 255, 112, 114, 255, 184, 90, 255, 255, 255, 255, 89, 0, 131, 238, 238, 131, 0], "Top": 4, "Height": 10}, "49": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 48, 216, 255, 0, 0, 49, 161, 252, 255, 255, 0, 0, 196, 255, 221, 255, 255, 0, 0, 56, 80, 3, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "50": {"Width": 7, "Advance": 8, "Bitmap": [0, 31, 166, 239, 236, 162, 19, 0, 206, 255, 255, 255, 255, 177, 0, 93, 125, 21, 76, 255, 247, 0, 0, 0, 0, 22, 255, 200, 0, 0, 0, 5, 182, 247, 54, 0, 0, 9, 184, 244, 73, 0, 0, 3, 184, 233, 49, 0, 0, 0, 117, 255, 60, 0, 0, 0, 0, 227, 255, 255, 255, 255, 255, 0, 253, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "51": {"Width": 6, "Advance": 8, "Bitmap": [78, 198, 247, 226, 148, 13, 201, 255, 255, 255, 255, 155, 66, 71, 13, 62, 255, 236, 0, 0, 12, 99, 255, 226, 0, 255, 255, 255, 250, 88, 0, 255, 255, 255, 251, 95, 0, 1, 16, 94, 255, 228, 69, 33, 7, 76, 255, 239, 215, 255, 255, 255, 255, 162, 145, 222, 251, 233, 144, 13], "Top": 4, "Height": 10}, "52": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 64, 249, 255, 0, 0, 0, 34, 238, 255, 255, 0, 0, 3, 198, 241, 255, 255, 0, 0, 119, 255, 91, 255, 255, 0, 33, 246, 179, 0, 255, 255, 0, 165, 252, 37, 0, 255, 255, 0, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0], "Top": 4, "Height": 10}, "53": {"Width": 6, "Advance": 8, "Bitmap": [0, 171, 255, 255, 255, 255, 0, 184, 255, 255, 255, 255, 0, 198, 236, 0, 0, 0, 0, 221, 255, 215, 118, 2, 0, 244, 255, 255, 255, 118, 0, 4, 34, 154, 255, 218, 0, 0, 0, 12, 255, 246, 60, 25, 8, 96, 255, 216, 213, 255, 255, 255, 255, 108, 156, 231, 251, 222, 106, 1], "Top": 4, "Height": 10}, "54": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 42, 155, 221, 227, 0, 91, 249, 255, 255, 249, 42, 249, 237, 115, 42, 11, 156, 255, 140, 100, 31, 0, 223, 255, 255, 255, 244, 68, 249, 254, 158, 193, 255, 204, 247, 252, 1, 13, 255, 246, 211, 255, 78, 70, 255, 222, 121, 255, 255, 255, 255, 126, 4, 144, 237, 244, 147, 4], "Top": 4, "Height": 10}, "55": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 77, 255, 161, 0, 0, 3, 218, 241, 18, 0, 0, 89, 255, 135, 0, 0, 0, 205, 253, 26, 0, 0, 45, 255, 193, 0, 0, 0, 132, 255, 111, 0, 0, 0, 201, 255, 52, 0, 0, 0, 242, 255, 14, 0, 0], "Top": 4, "Height": 10}, "56": {"Width": 6, "Advance": 8, "Bitmap": [7, 142, 237, 243, 186, 38, 123, 255, 255, 255, 255, 203, 184, 255, 52, 50, 255, 244, 137, 255, 104, 72, 255, 153, 16, 237, 255, 255, 213, 7, 136, 255, 86, 183, 255, 130, 231, 255, 9, 19, 255, 233, 241, 255, 68, 67, 255, 239, 174, 255, 255, 255, 255, 159, 21, 168, 238, 242, 158, 14], "Top": 4, "Height": 10}, "57": {"Width": 6, "Advance": 8, "Bitmap": [3, 141, 242, 235, 131, 1, 125, 255, 255, 255, 255, 93, 224, 255, 60, 85, 255, 198, 246, 255, 12, 2, 253, 236, 205, 255, 198, 166, 255, 249, 69, 244, 255, 255, 255, 228, 0, 29, 93, 127, 255, 174, 0, 15, 80, 227, 255, 74, 0, 253, 255, 255, 154, 0, 0, 236, 203, 101, 1, 0], "Top": 4, "Height": 10}, "58": {"Width": 3, "Advance": 9, "Bitmap": [91, 243, 85, 234, 255, 212, 91, 244, 85, 0, 0, 0, 0, 0, 0, 91, 243, 85, 234, 255, 212, 91, 244, 85], "Top": 6, "Height": 8}, "59": {"Width": 4, "Advance": 8, "Bitmap": [0, 91, 243, 85, 0, 234, 255, 212, 0, 91, 244, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 243, 125, 0, 246, 255, 240, 0, 166, 255, 234, 0, 40, 255, 177, 49, 203, 240, 42, 204, 174, 39, 0], "Top": 6, "Height": 11}, "60": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 94, 131, 0, 0, 0, 32, 185, 255, 203, 0, 2, 112, 244, 255, 255, 192, 27, 201, 255, 255, 234, 98, 1, 72, 255, 252, 148, 16, 0, 0, 72, 255, 252, 147, 16, 0, 0, 25, 195, 255, 255, 234, 97, 1, 0, 1, 108, 243, 255, 255, 192, 0, 0, 0, 30, 183, 255, 203, 0, 0, 0, 0, 0, 93, 131], "Top": 3, "Height": 10}, "61": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 8, "Height": 5}, "62": {"Width": 6, "Advance": 8, "Bitmap": [132, 83, 0, 0, 0, 0, 206, 255, 166, 18, 0, 0, 195, 255, 255, 231, 78, 0, 2, 108, 241, 255, 255, 161, 0, 0, 26, 172, 255, 255, 0, 0, 26, 171, 255, 255, 2, 107, 241, 255, 255, 155, 195, 255, 255, 229, 73, 0, 206, 255, 165, 17, 0, 0, 132, 82, 0, 0, 0, 0], "Top": 3, "Height": 10}, "63": {"Width": 6, "Advance": 8, "Bitmap": [98, 200, 241, 241, 170, 22, 200, 255, 255, 255, 255, 174, 68, 61, 11, 67, 255, 243, 0, 0, 0, 65, 255, 196, 0, 1, 119, 250, 232, 40, 0, 132, 255, 220, 35, 0, 0, 245, 255, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 197, 195, 0, 0, 0, 0, 198, 198, 0, 0, 0], "Top": 4, "Height": 10}, "64": {"Width": 7, "Advance": 8, "Bitmap": [0, 30, 175, 244, 229, 135, 6, 10, 211, 81, 14, 124, 255, 132, 111, 139, 0, 0, 10, 255, 224, 190, 59, 31, 209, 247, 255, 252, 232, 20, 172, 255, 247, 255, 255, 249, 5, 235, 255, 47, 255, 255, 248, 4, 251, 255, 2, 255, 255, 229, 21, 231, 255, 45, 255, 255, 179, 70, 152, 255, 253, 255, 255, 88, 176, 14, 159, 237, 229, 129, 1, 181, 163, 45, 7, 0, 0, 0, 5, 120, 209, 246, 254, 216], "Top": 4, "Height": 12}, "65": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 4, "Height": 10}, "66": {"Width": 6, "Advance": 8, "Bitmap": [212, 240, 251, 232, 160, 21, 255, 255, 255, 255, 255, 182, 255, 255, 5, 53, 255, 244, 255, 255, 1, 70, 255, 222, 255, 255, 255, 255, 247, 70, 255, 255, 255, 255, 251, 125, 255, 255, 1, 66, 255, 237, 255, 255, 2, 62, 255, 246, 255, 255, 255, 255, 255, 169, 206, 242, 251, 228, 151, 14], "Top": 4, "Height": 10}, "67": {"Width": 7, "Advance": 8, "Bitmap": [0, 15, 140, 225, 248, 209, 91, 10, 208, 255, 255, 255, 255, 223, 124, 255, 216, 58, 9, 72, 133, 210, 255, 64, 0, 0, 0, 0, 247, 255, 8, 0, 0, 0, 0, 249, 255, 15, 0, 0, 0, 0, 219, 255, 67, 0, 0, 0, 0, 148, 255, 211, 51, 8, 64, 134, 27, 235, 255, 255, 255, 255, 225, 0, 37, 171, 238, 249, 205, 87], "Top": 4, "Height": 10}, "68": {"Width": 7, "Advance": 8, "Bitmap": [214, 244, 252, 229, 159, 29, 0, 255, 255, 255, 255, 255, 229, 23, 255, 255, 5, 42, 212, 255, 143, 255, 255, 0, 0, 65, 255, 218, 255, 255, 0, 0, 12, 255, 247, 255, 255, 0, 0, 15, 255, 246, 255, 255, 0, 0, 72, 255, 214, 255, 255, 8, 61, 219, 255, 135, 255, 255, 255, 255, 255, 219, 17, 217, 248, 250, 221, 145, 19, 0], "Top": 4, "Height": 10}, "69": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "70": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 4, "Height": 10}, "71": {"Width": 7, "Advance": 8, "Bitmap": [0, 20, 151, 231, 254, 255, 234, 13, 215, 255, 255, 255, 255, 190, 130, 255, 219, 61, 11, 72, 122, 212, 255, 68, 0, 0, 0, 0, 247, 255, 13, 0, 0, 0, 0, 248, 255, 13, 0, 0, 255, 255, 219, 255, 58, 0, 0, 255, 255, 148, 255, 200, 39, 5, 255, 255, 28, 236, 255, 255, 255, 255, 255, 0, 40, 178, 242, 255, 255, 255], "Top": 4, "Height": 10}, "72": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 4, "Height": 10}, "73": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "74": {"Width": 6, "Advance": 8, "Bitmap": [0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 8, 255, 252, 115, 93, 9, 109, 255, 223, 216, 255, 255, 255, 255, 128, 72, 209, 249, 232, 131, 4], "Top": 4, "Height": 10}, "75": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 74, 255, 182, 255, 255, 0, 12, 223, 245, 35, 255, 255, 0, 163, 255, 107, 0, 255, 255, 114, 255, 172, 0, 0, 255, 255, 253, 236, 11, 0, 0, 255, 255, 216, 255, 127, 0, 0, 255, 255, 30, 235, 253, 62, 0, 255, 255, 0, 77, 255, 217, 3, 255, 255, 0, 0, 179, 255, 97, 255, 255, 0, 0, 56, 255, 205], "Top": 4, "Height": 10}, "76": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "77": {"Width": 7, "Advance": 8, "Bitmap": [119, 255, 43, 16, 255, 255, 17, 142, 255, 131, 44, 255, 255, 53, 163, 228, 217, 72, 255, 227, 88, 180, 191, 230, 149, 236, 183, 120, 195, 200, 158, 246, 164, 193, 151, 209, 210, 85, 255, 89, 204, 177, 221, 220, 0, 0, 0, 216, 199, 232, 230, 0, 0, 0, 227, 219, 242, 240, 0, 0, 0, 239, 234, 251, 250, 0, 0, 0, 250, 248], "Top": 4, "Height": 10}, "78": {"Width": 7, "Advance": 8, "Bitmap": [255, 246, 31, 0, 0, 255, 255, 255, 255, 171, 0, 0, 255, 255, 255, 255, 255, 53, 0, 255, 255, 255, 255, 229, 185, 0, 255, 255, 255, 255, 106, 255, 57, 255, 255, 255, 255, 6, 223, 178, 255, 255, 255, 255, 0, 97, 254, 255, 255, 255, 255, 0, 5, 229, 255, 255, 255, 255, 0, 0, 120, 255, 255, 255, 255, 0, 0, 17, 244, 255], "Top": 4, "Height": 10}, "79": {"Width": 7, "Advance": 8, "Bitmap": [0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 4, "Height": 10}, "80": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 255, 236, 199, 95, 1, 255, 255, 255, 255, 255, 255, 124, 255, 255, 5, 12, 120, 255, 226, 255, 255, 0, 0, 12, 255, 250, 255, 255, 0, 15, 122, 255, 223, 255, 255, 255, 255, 255, 255, 118, 255, 255, 255, 234, 195, 89, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0], "Top": 4, "Height": 10}, "81": {"Width": 7, "Advance": 8, "Bitmap": [0, 76, 212, 249, 214, 81, 0, 53, 251, 255, 255, 255, 252, 55, 166, 255, 146, 19, 145, 255, 166, 225, 255, 32, 0, 34, 255, 224, 250, 255, 5, 0, 7, 255, 248, 246, 255, 5, 0, 7, 255, 245, 223, 255, 31, 0, 36, 255, 221, 168, 255, 143, 19, 150, 255, 165, 63, 253, 255, 255, 255, 252, 60, 0, 94, 231, 255, 227, 92, 0, 0, 0, 52, 255, 181, 70, 18, 0, 0, 0, 175, 255, 255, 210, 0, 0, 0, 4, 101, 192, 149], "Top": 4, "Height": 13}, "82": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 249, 214, 121, 2, 255, 255, 255, 255, 255, 122, 255, 255, 7, 106, 255, 222, 255, 255, 0, 10, 255, 249, 255, 255, 6, 105, 255, 212, 255, 255, 255, 255, 252, 77, 255, 255, 255, 247, 51, 0, 255, 255, 142, 255, 133, 0, 255, 255, 7, 214, 252, 47, 255, 255, 0, 73, 255, 189], "Top": 4, "Height": 10}, "83": {"Width": 6, "Advance": 8, "Bitmap": [11, 142, 225, 248, 211, 92, 153, 255, 255, 255, 255, 216, 234, 255, 58, 7, 74, 128, 241, 255, 144, 31, 0, 0, 139, 255, 255, 253, 157, 10, 3, 118, 233, 255, 255, 149, 0, 0, 5, 111, 255, 240, 135, 69, 7, 61, 255, 238, 223, 255, 255, 255, 255, 167, 97, 212, 249, 231, 160, 19], "Top": 4, "Height": 10}, "84": {"Width": 8, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 4, "Height": 10}, "85": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 4, "Height": 10}, "86": {"Width": 7, "Advance": 8, "Bitmap": [232, 250, 3, 0, 3, 249, 230, 182, 255, 35, 0, 39, 255, 178, 123, 255, 79, 0, 84, 255, 123, 62, 255, 124, 0, 131, 255, 63, 7, 247, 175, 0, 182, 249, 9, 0, 188, 228, 0, 233, 190, 0, 0, 120, 255, 65, 255, 120, 0, 0, 50, 255, 194, 255, 48, 0, 0, 1, 232, 255, 225, 0, 0, 0, 0, 156, 255, 147, 0, 0], "Top": 4, "Height": 10}, "87": {"Width": 7, "Advance": 8, "Bitmap": [252, 248, 0, 0, 0, 253, 251, 245, 234, 0, 0, 0, 246, 241, 236, 221, 0, 0, 0, 237, 231, 225, 209, 32, 120, 29, 223, 221, 215, 196, 120, 255, 109, 210, 210, 201, 182, 189, 241, 177, 197, 197, 186, 182, 248, 108, 241, 191, 181, 170, 237, 227, 2, 230, 237, 165, 148, 255, 149, 0, 153, 255, 146, 124, 255, 71, 0, 72, 255, 126], "Top": 4, "Height": 10}, "88": {"Width": 8, "Advance": 8, "Bitmap": [178, 255, 160, 0, 0, 137, 255, 178, 32, 246, 252, 37, 29, 247, 246, 32, 0, 124, 255, 164, 159, 255, 124, 0, 0, 7, 218, 253, 253, 218, 7, 0, 0, 0, 82, 255, 255, 87, 0, 0, 0, 0, 152, 255, 255, 173, 0, 0, 0, 45, 253, 212, 213, 255, 68, 0, 0, 184, 255, 89, 92, 255, 206, 1, 68, 255, 221, 3, 3, 225, 255, 81, 197, 255, 109, 0, 0, 119, 255, 200], "Top": 4, "Height": 10}, "89": {"Width": 8, "Advance": 8, "Bitmap": [201, 255, 76, 0, 0, 57, 255, 203, 92, 255, 172, 0, 0, 158, 255, 98, 6, 232, 249, 22, 17, 245, 233, 8, 0, 118, 255, 130, 120, 255, 120, 0, 0, 9, 227, 236, 234, 233, 11, 0, 0, 0, 98, 255, 255, 101, 0, 0, 0, 0, 2, 255, 255, 2, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 4, "Height": 10}, "90": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 177, 0, 0, 0, 148, 245, 31, 0, 0, 39, 251, 123, 0, 0, 0, 177, 222, 8, 0, 0, 62, 255, 86, 0, 0, 0, 200, 201, 0, 0, 0, 79, 255, 67, 0, 0, 0, 204, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "91": {"Width": 4, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "92": {"Width": 6, "Advance": 8, "Bitmap": [220, 255, 44, 0, 0, 0, 147, 255, 116, 0, 0, 0, 75, 255, 189, 0, 0, 0, 10, 248, 249, 12, 0, 0, 0, 186, 255, 78, 0, 0, 0, 113, 255, 150, 0, 0, 0, 40, 255, 223, 0, 0, 0, 0, 223, 255, 40, 0, 0, 0, 151, 255, 112, 0, 0, 0, 79, 255, 185, 0, 0, 0, 12, 249, 247, 9, 0, 0, 0, 190, 255, 74, 0, 0, 0, 117, 255, 146, 0, 0, 0, 44, 255, 219], "Top": 3, "Height": 14}, "93": {"Width": 4, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "94": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 49, 250, 251, 54, 0, 0, 0, 7, 211, 255, 255, 218, 11, 0, 0, 139, 255, 171, 169, 255, 155, 0, 60, 253, 235, 22, 19, 231, 255, 80, 74, 201, 88, 0, 0, 76, 208, 90], "Top": 4, "Height": 5}, "95": {"Width": 8, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 15, "Height": 2}, "96": {"Width": 4, "Advance": 8, "Bitmap": [64, 187, 34, 0, 175, 255, 245, 113, 0, 65, 179, 136], "Top": 3, "Height": 3}, "97": {"Width": 7, "Advance": 8, "Bitmap": [0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 6, "Height": 8}, "98": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 250, 172, 10, 255, 255, 255, 255, 255, 124, 255, 255, 0, 94, 255, 210, 255, 255, 0, 10, 255, 242, 255, 255, 0, 12, 255, 238, 255, 255, 0, 95, 255, 198, 255, 255, 243, 255, 255, 87, 183, 232, 250, 226, 101, 0], "Top": 3, "Height": 11}, "99": {"Width": 6, "Advance": 8, "Bitmap": [0, 71, 198, 246, 240, 172, 60, 253, 255, 255, 255, 201, 187, 255, 160, 26, 10, 32, 236, 255, 19, 0, 0, 0, 239, 255, 20, 0, 0, 0, 197, 255, 157, 23, 7, 34, 84, 255, 255, 255, 255, 218, 0, 86, 206, 248, 240, 182], "Top": 6, "Height": 8}, "100": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 118, 226, 254, 255, 255, 255, 90, 255, 255, 255, 255, 255, 255, 199, 255, 128, 8, 0, 255, 255, 239, 255, 14, 0, 0, 255, 255, 238, 255, 21, 0, 0, 255, 255, 194, 255, 154, 17, 6, 255, 255, 70, 255, 255, 255, 255, 255, 255, 0, 86, 206, 248, 246, 226, 185], "Top": 3, "Height": 11}, "101": {"Width": 8, "Advance": 8, "Bitmap": [0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 6, "Height": 8}, "102": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 19, 159, 232, 248, 223, 147, 0, 0, 186, 255, 255, 255, 255, 208, 0, 0, 250, 255, 50, 6, 34, 54, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 3, "Height": 11}, "103": {"Width": 7, "Advance": 8, "Bitmap": [0, 67, 191, 243, 247, 224, 174, 72, 253, 255, 255, 255, 255, 255, 195, 255, 162, 23, 6, 255, 255, 245, 255, 23, 0, 0, 255, 255, 242, 255, 13, 0, 0, 255, 255, 206, 255, 129, 10, 34, 255, 255, 101, 255, 255, 255, 255, 255, 255, 1, 129, 230, 249, 210, 255, 244, 0, 0, 0, 6, 77, 255, 209, 27, 255, 255, 255, 255, 255, 106, 45, 198, 239, 250, 220, 122, 2], "Top": 6, "Height": 11}, "104": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 228, 246, 171, 16, 255, 255, 255, 255, 255, 153, 255, 255, 16, 82, 255, 228, 255, 255, 0, 8, 255, 251, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255], "Top": 3, "Height": 11}, "105": {"Width": 7, "Advance": 8, "Bitmap": [0, 201, 199, 0, 0, 0, 0, 0, 201, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 1, 0, 0, 0, 0, 238, 255, 63, 0, 0, 0, 0, 180, 255, 255, 255, 173, 0, 0, 33, 189, 244, 234, 136], "Top": 3, "Height": 11}, "106": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 0, 0, 201, 199, 0, 0, 0, 0, 201, 199, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 2, 255, 254, 48, 156, 25, 64, 255, 233, 122, 255, 255, 255, 255, 157, 48, 175, 234, 238, 163, 16], "Top": 3, "Height": 14}, "107": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 1, 182, 255, 141, 255, 255, 0, 129, 255, 158, 1, 255, 255, 94, 255, 161, 2, 0, 255, 255, 252, 216, 2, 0, 0, 255, 255, 179, 255, 136, 0, 0, 255, 255, 9, 205, 255, 100, 0, 255, 255, 0, 34, 241, 248, 49, 255, 255, 0, 0, 97, 196, 148], "Top": 3, "Height": 11}, "108": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 243, 255, 40, 0, 0, 0, 199, 255, 255, 223, 0, 0, 59, 220, 247, 187], "Top": 3, "Height": 11}, "109": {"Width": 7, "Advance": 8, "Bitmap": [185, 234, 244, 179, 232, 228, 76, 255, 255, 255, 255, 255, 255, 208, 255, 255, 35, 244, 37, 255, 245, 255, 255, 0, 255, 1, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 6, "Height": 8}, "110": {"Width": 7, "Advance": 8, "Bitmap": [175, 217, 238, 251, 229, 144, 8, 255, 255, 255, 255, 255, 255, 143, 255, 255, 7, 9, 115, 255, 222, 255, 255, 0, 0, 12, 255, 250, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 6, "Height": 8}, "111": {"Width": 7, "Advance": 8, "Bitmap": [0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 6, "Height": 8}, "112": {"Width": 6, "Advance": 8, "Bitmap": [178, 228, 249, 227, 118, 0, 255, 255, 255, 255, 255, 90, 255, 255, 8, 119, 255, 200, 255, 255, 0, 16, 255, 239, 255, 255, 0, 9, 255, 241, 255, 255, 20, 92, 255, 209, 255, 255, 255, 255, 255, 122, 255, 255, 210, 247, 164, 6, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 6, "Height": 11}, "113": {"Width": 6, "Advance": 8, "Bitmap": [0, 98, 225, 251, 232, 181, 85, 255, 255, 255, 255, 255, 197, 255, 117, 8, 255, 255, 239, 255, 15, 0, 255, 255, 243, 255, 9, 0, 255, 255, 212, 255, 92, 0, 255, 255, 126, 255, 255, 255, 255, 255, 7, 168, 250, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255], "Top": 6, "Height": 11}, "114": {"Width": 6, "Advance": 8, "Bitmap": [122, 195, 239, 253, 239, 187, 255, 255, 255, 255, 255, 200, 255, 255, 28, 3, 17, 34, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 6, "Height": 8}, "115": {"Width": 6, "Advance": 8, "Bitmap": [28, 163, 237, 249, 227, 158, 196, 255, 255, 255, 255, 213, 239, 255, 54, 8, 40, 65, 144, 255, 239, 158, 58, 0, 0, 79, 171, 244, 255, 125, 94, 48, 7, 49, 255, 236, 227, 255, 255, 255, 255, 209, 123, 217, 250, 241, 185, 42], "Top": 6, "Height": 8}, "116": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 250, 255, 48, 7, 48, 0, 0, 209, 255, 255, 255, 221, 0, 0, 62, 210, 249, 237, 170], "Top": 4, "Height": 10}, "117": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 6, "Height": 8}, "118": {"Width": 7, "Advance": 8, "Bitmap": [166, 255, 49, 0, 1, 236, 220, 97, 255, 110, 0, 53, 255, 149, 26, 254, 182, 0, 133, 255, 75, 0, 203, 247, 11, 218, 241, 6, 0, 123, 255, 131, 255, 162, 0, 0, 35, 255, 251, 255, 67, 0, 0, 0, 198, 255, 224, 2, 0, 0, 0, 98, 255, 123, 0, 0], "Top": 6, "Height": 8}, "119": {"Width": 8, "Advance": 8, "Bitmap": [245, 255, 0, 0, 0, 0, 255, 244, 224, 255, 0, 0, 0, 0, 255, 221, 201, 255, 64, 255, 116, 0, 255, 195, 174, 255, 113, 255, 217, 1, 255, 164, 143, 255, 166, 235, 255, 75, 255, 130, 105, 255, 227, 84, 242, 199, 255, 89, 63, 255, 245, 8, 123, 255, 255, 45, 13, 253, 179, 0, 8, 222, 246, 3], "Top": 6, "Height": 8}, "120": {"Width": 8, "Advance": 8, "Bitmap": [154, 255, 212, 9, 0, 132, 255, 168, 6, 199, 255, 153, 55, 252, 230, 18, 0, 26, 231, 255, 234, 255, 74, 0, 0, 0, 58, 253, 255, 160, 0, 0, 0, 0, 109, 255, 255, 210, 8, 0, 0, 64, 251, 238, 204, 255, 138, 0, 22, 231, 255, 83, 32, 243, 252, 46, 174, 255, 180, 0, 0, 112, 255, 188], "Top": 6, "Height": 8}, "121": {"Width": 8, "Advance": 8, "Bitmap": [0, 222, 255, 34, 0, 30, 255, 222, 0, 153, 255, 105, 0, 89, 255, 156, 0, 80, 255, 180, 0, 152, 255, 88, 0, 9, 244, 249, 16, 219, 253, 19, 0, 0, 165, 255, 141, 255, 200, 0, 0, 0, 66, 255, 254, 255, 120, 0, 0, 0, 1, 214, 255, 255, 37, 0, 0, 0, 0, 123, 255, 196, 0, 0, 0, 0, 44, 228, 255, 86, 0, 0, 174, 255, 255, 255, 185, 0, 0, 0, 198, 249, 234, 156, 13, 0, 0, 0], "Top": 6, "Height": 11}, "122": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 153, 0, 0, 29, 237, 201, 6, 0, 3, 195, 240, 31, 0, 0, 126, 255, 90, 0, 0, 49, 250, 175, 0, 0, 0, 206, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 6, "Height": 8}, "123": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 77, 220, 252, 255, 0, 0, 221, 255, 255, 255, 0, 0, 253, 255, 36, 0, 0, 0, 255, 255, 0, 0, 0, 1, 255, 255, 0, 0, 0, 59, 255, 228, 0, 0, 255, 255, 218, 76, 0, 0, 255, 255, 219, 76, 0, 0, 0, 61, 255, 228, 0, 0, 0, 1, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 253, 255, 36, 0, 0, 0, 221, 255, 255, 255, 0, 0, 77, 221, 253, 255], "Top": 3, "Height": 14}, "124": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "125": {"Width": 6, "Advance": 8, "Bitmap": [255, 252, 218, 74, 0, 0, 255, 255, 255, 221, 0, 0, 0, 38, 255, 253, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 1, 0, 0, 0, 229, 255, 59, 0, 0, 0, 78, 220, 255, 255, 0, 0, 74, 217, 255, 255, 0, 0, 228, 255, 61, 0, 0, 0, 255, 255, 1, 0, 0, 0, 255, 255, 0, 0, 0, 37, 255, 253, 0, 0, 255, 255, 255, 222, 0, 0, 255, 253, 219, 74, 0, 0], "Top": 3, "Height": 14}, "126": {"Width": 7, "Advance": 8, "Bitmap": [31, 211, 234, 134, 22, 146, 178, 174, 255, 255, 255, 255, 255, 168, 180, 143, 24, 137, 235, 212, 29], "Top": 8, "Height": 3}, "160": {"Width": 0, "Advance": 8, "Bitmap": [], "Top": 14, "Height": 0}, "161": {"Width": 2, "Advance": 8, "Bitmap": [199, 198, 199, 198, 0, 0, 202, 193, 226, 219, 245, 240, 254, 251, 255, 255, 255, 255, 255, 255], "Top": 7, "Height": 10}, "162": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 2, 0, 78, 206, 255, 255, 217, 70, 252, 255, 255, 255, 188, 191, 255, 151, 21, 0, 0, 240, 255, 18, 0, 0, 0, 240, 255, 19, 0, 0, 0, 193, 255, 154, 22, 7, 33, 73, 253, 255, 255, 255, 219, 0, 80, 207, 255, 255, 227, 0, 0, 0, 255, 255, 3, 0, 0, 0, 255, 255, 0], "Top": 4, "Height": 12}, "163": {"Width": 6, "Advance": 8, "Bitmap": [0, 24, 171, 238, 236, 148, 0, 172, 255, 255, 255, 162, 0, 240, 255, 56, 81, 54, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 0, 255, 249, 0, 0, 0, 0, 255, 224, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "164": {"Width": 8, "Advance": 8, "Bitmap": [52, 190, 17, 0, 0, 16, 189, 54, 156, 255, 226, 242, 242, 225, 255, 153, 0, 217, 255, 255, 255, 255, 216, 0, 0, 249, 255, 60, 60, 255, 248, 0, 0, 216, 255, 255, 255, 255, 216, 0, 153, 255, 226, 245, 245, 227, 255, 155, 55, 189, 16, 0, 0, 17, 190, 52], "Top": 6, "Height": 7}, "165": {"Width": 8, "Advance": 8, "Bitmap": [185, 255, 52, 0, 0, 52, 255, 182, 46, 253, 156, 0, 0, 155, 254, 44, 0, 161, 246, 24, 24, 245, 172, 0, 0, 38, 252, 146, 147, 255, 55, 0, 0, 0, 162, 248, 248, 188, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 4, "Height": 10}, "166": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "167": {"Width": 6, "Advance": 8, "Bitmap": [27, 167, 232, 247, 218, 148, 190, 255, 255, 255, 255, 154, 241, 255, 74, 1, 0, 0, 162, 255, 255, 224, 113, 2, 183, 255, 255, 255, 255, 148, 250, 255, 47, 133, 255, 241, 240, 255, 125, 44, 255, 228, 149, 255, 255, 255, 255, 121, 4, 119, 231, 255, 255, 175, 133, 91, 14, 82, 255, 243, 218, 255, 255, 255, 255, 202, 105, 207, 247, 241, 188, 41], "Top": 4, "Height": 12}, "168": {"Width": 5, "Advance": 8, "Bitmap": [199, 198, 0, 199, 199, 201, 199, 0, 201, 201], "Top": 3, "Height": 2}, "169": {"Width": 8, "Advance": 8, "Bitmap": [0, 58, 185, 244, 243, 184, 56, 0, 57, 241, 113, 23, 23, 113, 240, 55, 189, 108, 78, 227, 245, 38, 112, 186, 244, 14, 229, 58, 10, 0, 15, 244, 244, 14, 232, 61, 11, 0, 15, 244, 188, 105, 84, 230, 242, 42, 109, 184, 43, 236, 107, 21, 21, 108, 235, 39, 0, 43, 181, 244, 244, 179, 42, 0], "Top": 6, "Height": 8}, "170": {"Width": 5, "Advance": 8, "Bitmap": [0, 213, 249, 225, 83, 0, 21, 53, 255, 226, 7, 72, 90, 255, 254, 191, 255, 184, 255, 255, 240, 255, 22, 255, 255, 100, 224, 250, 238, 205], "Top": 4, "Height": 6}, "171": {"Width": 8, "Advance": 8, "Bitmap": [0, 41, 193, 16, 0, 46, 191, 14, 1, 194, 218, 1, 1, 198, 216, 1, 99, 255, 110, 0, 101, 255, 108, 0, 216, 255, 27, 0, 216, 255, 27, 0, 93, 255, 111, 0, 96, 255, 110, 0, 0, 188, 220, 2, 0, 193, 218, 1, 0, 37, 191, 15, 0, 41, 190, 14], "Top": 6, "Height": 7}, "172": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255], "Top": 8, "Height": 6}, "174": {"Width": 8, "Advance": 8, "Bitmap": [0, 58, 185, 244, 243, 184, 56, 0, 57, 241, 113, 23, 23, 113, 240, 55, 189, 108, 0, 239, 245, 142, 112, 186, 244, 14, 0, 255, 45, 234, 15, 244, 244, 14, 0, 255, 255, 82, 15, 244, 188, 105, 0, 255, 126, 198, 114, 184, 43, 236, 107, 21, 21, 108, 235, 39, 0, 43, 181, 244, 244, 179, 42, 0], "Top": 6, "Height": 8}, "175": {"Width": 5, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255], "Top": 4, "Height": 1}, "176": {"Width": 4, "Advance": 8, "Bitmap": [88, 232, 230, 81, 233, 53, 51, 229, 234, 54, 52, 230, 94, 234, 231, 83], "Top": 3, "Height": 4}, "177": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 5, "Height": 9}, "178": {"Width": 4, "Advance": 8, "Bitmap": [115, 232, 239, 110, 139, 41, 255, 241, 0, 90, 255, 211, 42, 240, 238, 42, 193, 255, 83, 0, 250, 255, 255, 255], "Top": 4, "Height": 6}, "179": {"Width": 5, "Advance": 8, "Bitmap": [146, 233, 237, 128, 0, 133, 47, 255, 241, 0, 0, 255, 255, 174, 0, 0, 1, 62, 254, 16, 136, 32, 52, 252, 15, 179, 244, 230, 108, 0], "Top": 4, "Height": 6}, "180": {"Width": 4, "Advance": 8, "Bitmap": [0, 35, 187, 63, 113, 245, 255, 174, 136, 179, 64, 0], "Top": 3, "Height": 3}, "181": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 8, 0, 255, 255, 255, 255, 84, 6, 255, 255, 255, 255, 255, 255, 255, 255, 255, 251, 230, 248, 225, 172, 255, 246, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 6, "Height": 11}, "182": {"Width": 7, "Advance": 8, "Bitmap": [1, 104, 203, 240, 248, 230, 190, 129, 255, 255, 255, 254, 255, 255, 238, 255, 255, 255, 1, 255, 255, 243, 255, 255, 255, 0, 255, 255, 188, 255, 255, 255, 0, 255, 255, 36, 200, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255], "Top": 4, "Height": 13}, "183": {"Width": 3, "Advance": 9, "Bitmap": [91, 243, 85, 234, 255, 212, 91, 244, 85], "Top": 8, "Height": 3}, "184": {"Width": 3, "Advance": 8, "Bitmap": [0, 176, 122, 0, 61, 235, 207, 247, 150], "Top": 14, "Height": 3}, "185": {"Width": 5, "Advance": 8, "Bitmap": [27, 122, 236, 255, 0, 208, 224, 255, 255, 0, 26, 2, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255, 255, 255, 255], "Top": 4, "Height": 5}, "186": {"Width": 6, "Advance": 8, "Bitmap": [5, 136, 238, 239, 137, 5, 122, 255, 255, 255, 255, 122, 217, 255, 75, 75, 255, 216, 246, 255, 6, 7, 255, 246, 218, 255, 73, 73, 255, 218, 125, 255, 255, 255, 255, 126, 5, 140, 231, 231, 140, 6], "Top": 4, "Height": 7}, "187": {"Width": 7, "Advance": 8, "Bitmap": [165, 77, 0, 159, 90, 0, 0, 153, 233, 21, 145, 237, 25, 0, 32, 250, 176, 22, 243, 181, 0, 0, 187, 255, 55, 162, 255, 56, 31, 250, 179, 21, 242, 183, 0, 152, 235, 24, 144, 240, 28, 0, 164, 84, 0, 159, 97, 0, 0], "Top": 6, "Height": 7}, "188": {"Width": 8, "Advance": 8, "Bitmap": [85, 247, 0, 0, 0, 60, 191, 0, 236, 255, 0, 0, 0, 188, 63, 0, 50, 255, 0, 0, 61, 190, 0, 0, 0, 255, 0, 0, 189, 62, 0, 0, 0, 0, 0, 62, 189, 0, 0, 0, 0, 0, 0, 190, 61, 0, 0, 0, 0, 0, 63, 188, 1, 186, 255, 0, 0, 0, 191, 60, 110, 149, 255, 0, 0, 64, 188, 0, 241, 255, 255, 255, 0, 191, 60, 0, 0, 0, 255, 0], "Top": 4, "Height": 10}, "189": {"Width": 8, "Advance": 8, "Bitmap": [69, 246, 0, 0, 0, 35, 213, 6, 198, 255, 0, 0, 0, 174, 81, 0, 0, 255, 0, 0, 63, 191, 0, 0, 0, 255, 0, 2, 204, 49, 0, 0, 0, 0, 0, 97, 158, 0, 0, 0, 0, 0, 12, 218, 25, 0, 0, 0, 0, 0, 132, 123, 0, 143, 245, 176, 0, 30, 216, 9, 0, 0, 40, 234, 0, 166, 89, 0, 0, 75, 205, 65, 56, 198, 1, 0, 0, 229, 255, 255], "Top": 4, "Height": 10}, "190": {"Width": 8, "Advance": 8, "Bitmap": [156, 246, 126, 0, 0, 3, 206, 46, 11, 91, 199, 0, 0, 102, 153, 0, 25, 255, 236, 0, 14, 219, 22, 0, 209, 250, 169, 0, 136, 119, 0, 0, 0, 0, 0, 33, 215, 7, 0, 0, 0, 0, 0, 171, 85, 0, 0, 0, 0, 0, 60, 194, 0, 171, 255, 88, 0, 1, 201, 52, 94, 164, 240, 88, 0, 94, 161, 0, 225, 255, 255, 219, 10, 217, 27, 0, 0, 0, 240, 88], "Top": 4, "Height": 10}, "191": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 199, 193, 0, 0, 0, 0, 200, 196, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 184, 255, 48, 0, 0, 67, 254, 244, 10, 0, 29, 236, 255, 115, 0, 0, 170, 255, 173, 2, 0, 0, 240, 255, 19, 0, 0, 0, 233, 255, 79, 11, 103, 129, 148, 255, 255, 255, 255, 217, 13, 160, 240, 247, 196, 73], "Top": 6, "Height": 11}, "192": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 64, 187, 34, 0, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 0, "Height": 14}, "193": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 0, "Height": 14}, "194": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 0, 4, 162, 255, 255, 161, 4, 0, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 0, "Height": 14}, "195": {"Width": 8, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 1, "Height": 13}, "196": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 199, 198, 0, 199, 199, 0, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 1, "Height": 13}, "197": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 27, 133, 134, 30, 0, 0, 0, 0, 197, 137, 137, 209, 0, 0, 0, 0, 213, 114, 113, 221, 0, 0, 0, 0, 134, 255, 255, 149, 0, 0, 0, 0, 195, 255, 255, 211, 0, 0, 0, 16, 252, 206, 208, 255, 28, 0, 0, 85, 255, 113, 115, 255, 100, 0, 0, 158, 255, 31, 33, 255, 172, 0, 0, 227, 214, 0, 0, 218, 237, 1, 39, 255, 255, 255, 255, 255, 255, 47, 104, 255, 255, 255, 255, 255, 255, 111, 165, 255, 70, 0, 0, 76, 255, 169, 226, 255, 18, 0, 0, 20, 255, 227], "Top": 1, "Height": 13}, "198": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 45, 255, 255, 255, 255, 255, 0, 0, 135, 243, 255, 255, 255, 255, 0, 0, 222, 183, 255, 255, 0, 0, 0, 52, 255, 118, 255, 255, 0, 0, 0, 134, 255, 57, 255, 255, 255, 0, 0, 210, 247, 6, 255, 255, 255, 0, 28, 255, 255, 255, 255, 255, 0, 0, 98, 255, 255, 255, 255, 255, 0, 0, 163, 255, 104, 0, 255, 255, 255, 255, 225, 255, 48, 0, 255, 255, 255, 255], "Top": 4, "Height": 10}, "199": {"Width": 8, "Advance": 8, "Bitmap": [0, 14, 140, 225, 248, 209, 91, 0, 9, 206, 255, 255, 255, 255, 223, 0, 122, 255, 216, 58, 9, 72, 133, 0, 209, 255, 64, 0, 0, 0, 0, 0, 246, 255, 8, 0, 0, 0, 0, 0, 248, 255, 16, 0, 0, 0, 0, 0, 213, 255, 74, 0, 0, 0, 0, 0, 126, 255, 221, 65, 11, 29, 155, 2, 9, 191, 255, 255, 255, 255, 255, 1, 0, 4, 116, 205, 255, 240, 123, 0, 0, 0, 0, 10, 236, 121, 0, 0, 0, 0, 0, 16, 46, 239, 0, 0, 0, 0, 0, 209, 244, 133, 0, 0], "Top": 4, "Height": 13}, "200": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "201": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "202": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "203": {"Width": 6, "Advance": 8, "Bitmap": [199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 1, "Height": 13}, "204": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "205": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "206": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "207": {"Width": 6, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 1, "Height": 13}, "208": {"Width": 8, "Advance": 8, "Bitmap": [0, 213, 243, 252, 231, 161, 31, 0, 0, 255, 255, 255, 255, 255, 231, 25, 0, 255, 255, 4, 42, 212, 255, 144, 0, 255, 255, 0, 0, 65, 255, 218, 255, 255, 255, 255, 0, 12, 255, 247, 255, 255, 255, 255, 0, 15, 255, 246, 0, 255, 255, 0, 0, 72, 255, 214, 0, 255, 255, 9, 62, 220, 255, 135, 0, 255, 255, 255, 255, 255, 219, 17, 0, 219, 247, 250, 223, 146, 19, 0], "Top": 4, "Height": 10}, "209": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 255, 246, 31, 0, 0, 255, 255, 255, 255, 171, 0, 0, 255, 255, 255, 255, 255, 53, 0, 255, 255, 255, 255, 229, 185, 0, 255, 255, 255, 255, 106, 255, 57, 255, 255, 255, 255, 6, 223, 178, 255, 255, 255, 255, 0, 97, 254, 255, 255, 255, 255, 0, 5, 229, 255, 255, 255, 255, 0, 0, 120, 255, 255, 255, 255, 0, 0, 17, 244, 255], "Top": 1, "Height": 13}, "210": {"Width": 7, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 0, "Height": 14}, "211": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 0, "Height": 14}, "212": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 0, "Height": 14}, "213": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 1, "Height": 13}, "214": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 1, "Height": 13}, "215": {"Width": 6, "Advance": 8, "Bitmap": [102, 139, 0, 0, 141, 99, 156, 255, 132, 133, 255, 154, 1, 144, 255, 255, 140, 0, 0, 129, 255, 255, 132, 0, 145, 255, 124, 131, 255, 148, 98, 123, 0, 0, 129, 92], "Top": 7, "Height": 6}, "216": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 0, 150, 18, 0, 34, 169, 237, 249, 214, 253, 52, 25, 233, 255, 255, 236, 255, 255, 0, 145, 255, 181, 20, 143, 255, 255, 0, 216, 255, 44, 57, 245, 255, 255, 0, 247, 255, 14, 216, 146, 255, 255, 0, 250, 255, 137, 247, 31, 255, 248, 0, 226, 255, 252, 143, 30, 255, 223, 0, 165, 255, 254, 46, 136, 255, 165, 0, 49, 254, 255, 255, 255, 252, 54, 0, 46, 246, 213, 249, 215, 81, 0, 0, 17, 139, 0, 0, 0, 0, 0, 0], "Top": 3, "Height": 12}, "217": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 0, "Height": 14}, "218": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 0, "Height": 14}, "219": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 0, "Height": 14}, "220": {"Width": 6, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 1, "Height": 13}, "221": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 201, 255, 76, 0, 0, 57, 255, 203, 92, 255, 172, 0, 0, 158, 255, 98, 6, 232, 249, 22, 17, 245, 233, 8, 0, 118, 255, 130, 120, 255, 120, 0, 0, 9, 227, 236, 234, 233, 11, 0, 0, 0, 98, 255, 255, 101, 0, 0, 0, 0, 2, 255, 255, 2, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 0, "Height": 14}, "222": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 2, 0, 0, 0, 255, 255, 254, 226, 136, 8, 255, 255, 249, 255, 255, 133, 255, 255, 0, 84, 255, 220, 255, 255, 0, 9, 255, 246, 255, 255, 3, 100, 255, 217, 255, 255, 255, 255, 255, 121, 255, 255, 252, 221, 125, 4, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 4, "Height": 10}, "223": {"Width": 7, "Advance": 8, "Bitmap": [9, 146, 238, 249, 193, 44, 0, 140, 255, 255, 255, 255, 206, 0, 225, 255, 88, 48, 255, 249, 0, 252, 255, 5, 44, 255, 194, 0, 255, 255, 0, 176, 255, 60, 0, 255, 255, 0, 244, 255, 20, 0, 255, 255, 0, 184, 255, 191, 17, 255, 255, 0, 7, 141, 255, 181, 255, 255, 0, 115, 40, 255, 245, 255, 255, 0, 231, 255, 255, 215, 255, 255, 0, 177, 248, 219, 70], "Top": 3, "Height": 11}, "224": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "225": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "226": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "227": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 3, "Height": 11}, "228": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 3, "Height": 11}, "229": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 141, 244, 141, 0, 0, 0, 0, 244, 72, 244, 0, 0, 0, 0, 143, 245, 143, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "230": {"Width": 9, "Advance": 8, "Bitmap": [0, 182, 243, 205, 99, 226, 207, 37, 0, 0, 218, 255, 255, 255, 255, 255, 170, 0, 0, 36, 14, 183, 255, 144, 81, 234, 0, 47, 203, 249, 247, 255, 255, 255, 255, 2, 206, 255, 255, 255, 255, 255, 255, 255, 6, 245, 255, 55, 123, 255, 150, 7, 26, 0, 189, 255, 255, 255, 255, 255, 255, 214, 0, 34, 194, 244, 196, 102, 214, 246, 186, 0], "Top": 6, "Height": 8}, "231": {"Width": 6, "Advance": 8, "Bitmap": [0, 70, 198, 246, 242, 179, 57, 253, 255, 255, 255, 190, 183, 255, 160, 26, 0, 0, 235, 255, 19, 0, 0, 0, 241, 255, 20, 0, 0, 0, 192, 255, 157, 23, 0, 0, 64, 249, 255, 255, 255, 211, 0, 59, 185, 252, 253, 206, 0, 0, 0, 211, 114, 0, 0, 0, 0, 44, 238, 0, 0, 0, 207, 245, 136, 0], "Top": 6, "Height": 11}, "232": {"Width": 8, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 2, "Height": 12}, "233": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 2, "Height": 12}, "234": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 0, 4, 162, 255, 255, 161, 4, 0, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 2, "Height": 12}, "235": {"Width": 8, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 3, "Height": 11}, "236": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 2, "Height": 12}, "237": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 2, "Height": 12}, "238": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 2, "Height": 12}, "239": {"Width": 6, "Advance": 8, "Bitmap": [199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 3, "Height": 11}, "240": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 109, 207, 59, 38, 74, 0, 0, 113, 255, 253, 255, 195, 0, 0, 196, 197, 217, 255, 53, 0, 0, 1, 0, 58, 255, 146, 0, 85, 203, 237, 247, 255, 208, 97, 255, 255, 255, 255, 255, 244, 213, 255, 116, 21, 13, 255, 247, 245, 255, 10, 0, 32, 255, 222, 212, 255, 108, 15, 149, 255, 164, 104, 255, 255, 255, 255, 254, 59, 0, 106, 223, 249, 215, 88, 0], "Top": 3, "Height": 11}, "241": {"Width": 7, "Advance": 8, "Bitmap": [75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 175, 217, 238, 251, 229, 144, 8, 255, 255, 255, 255, 255, 255, 143, 255, 255, 7, 9, 115, 255, 222, 255, 255, 0, 0, 12, 255, 250, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 3, "Height": 11}, "242": {"Width": 7, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 2, "Height": 12}, "243": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 2, "Height": 12}, "244": {"Width": 7, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 2, "Height": 12}, "245": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 3, "Height": 11}, "246": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 3, "Height": 11}, "247": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 199, 200, 0, 0, 0, 0, 201, 202, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 199, 200, 0, 0, 0, 0, 199, 200, 0, 0], "Top": 6, "Height": 8}, "248": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 21, 9, 0, 86, 214, 248, 212, 206, 158, 64, 255, 255, 255, 255, 255, 86, 195, 255, 127, 45, 246, 255, 187, 246, 252, 11, 195, 131, 240, 237, 226, 239, 129, 195, 12, 253, 237, 86, 254, 245, 44, 128, 255, 196, 0, 146, 255, 255, 255, 255, 87, 0, 200, 189, 238, 235, 113, 0, 0, 29, 1, 0, 0, 0, 0], "Top": 5, "Height": 10}, "249": {"Width": 7, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 2, "Height": 12}, "250": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 2, "Height": 12}, "251": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 2, "Height": 12}, "252": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 3, "Height": 11}, "253": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 222, 255, 34, 0, 30, 255, 222, 0, 153, 255, 105, 0, 89, 255, 156, 0, 80, 255, 180, 0, 152, 255, 88, 0, 9, 244, 249, 16, 219, 253, 19, 0, 0, 165, 255, 141, 255, 200, 0, 0, 0, 66, 255, 254, 255, 120, 0, 0, 0, 1, 214, 255, 255, 37, 0, 0, 0, 0, 123, 255, 196, 0, 0, 0, 0, 44, 228, 255, 86, 0, 0, 174, 255, 255, 255, 185, 0, 0, 0, 198, 249, 234, 156, 13, 0, 0, 0], "Top": 2, "Height": 15}, "254": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 240, 246, 198, 73, 0, 255, 255, 255, 255, 255, 254, 71, 255, 255, 8, 19, 163, 255, 196, 255, 255, 0, 0, 24, 255, 246, 255, 255, 0, 0, 14, 255, 238, 255, 255, 28, 10, 131, 255, 197, 255, 255, 255, 255, 255, 255, 90, 255, 255, 203, 248, 228, 106, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0], "Top": 3, "Height": 14}, "255": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 199, 198, 0, 199, 199, 0, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 222, 255, 34, 0, 30, 255, 222, 0, 153, 255, 105, 0, 89, 255, 156, 0, 80, 255, 180, 0, 152, 255, 88, 0, 9, 244, 249, 16, 219, 253, 19, 0, 0, 165, 255, 141, 255, 200, 0, 0, 0, 66, 255, 254, 255, 120, 0, 0, 0, 1, 214, 255, 255, 37, 0, 0, 0, 0, 123, 255, 196, 0, 0, 0, 0, 44, 228, 255, 86, 0, 0, 174, 255, 255, 255, 185, 0, 0, 0, 198, 249, 234, 156, 13, 0, 0, 0], "Top": 3, "Height": 14}}, "Size": 16}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Graveyard/EclipseCodingStyle.xml	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<profiles version="1">
+<profile kind="CodeFormatterProfile" name="Orthanc" version="1">
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.lineSplit" value="80"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_member_access" value="0"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_base_types" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_constructor_initializer_list" value="0"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_exception_specification" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_base_types" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_access_specifier" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_exception_specification" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_arguments" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.comment.min_distance_between_code_and_line_comment" value="1"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_expressions_in_array_initializer" value="18"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_declarator_list" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_bracket" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.tabulation.size" value="2"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_else_in_if_statement" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_enumerator_list" value="49"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_declarator_list" value="16"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_method_declaration" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_arguments" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_base_clause" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.join_wrapped_lines" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_declarator_list" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_arguments_in_method_invocation" value="18"/>
+<setting id="org.eclipse.cdt.core.formatter.comment.never_indent_line_comments_on_first_column" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_brackets" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_bracket" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_parameters_in_method_declaration" value="18"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_type_declaration" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_arguments" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_expression_list" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_parameters" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.continuation_indentation" value="2"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_expression_list" value="0"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_parameters" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression" value="34"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_extra_spaces" value="0"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_compare_to_type_header" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_namespace_header" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_assignment" value="16"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression_chain" value="18"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_parameters" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_expression_list" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_exception_specification" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_identifier_in_function_declaration" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_base_clause_in_type_declaration" value="80"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_exception_specification" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_declaration_compare_to_template_header" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_binary_expression" value="18"/>
+<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_arguments" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_parameters" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_parameters" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_colon_in_constructor_initializer_list" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block_in_case" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_template_declaration" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_base_clause" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_switch" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.alignment_for_overloaded_left_shift_chain" value="18"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.indentation.size" value="2"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_namespace_declaration" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_arguments" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.brace_position_for_array_initializer" value="next_line"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_namespace_declaration" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_bracket" value="do not insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_while_in_do_statement" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_parameters" value="insert"/>
+<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_arguments" value="do not insert"/>
+</profile>
+</profiles>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Graveyard/FromDcmtkBridge.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,168 @@
+  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
+  {
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+
+    if (tag.IsPrivate())
+    {
+      // This raises BitBucket issue 140 (Modifying private tags with
+      // REST API changes VR from LO to UN)
+      // https://bitbucket.org/sjodogne/orthanc/issues/140
+      LOG(WARNING) << "You are using DCMTK < 3.6.1: All the private tags "
+        "are considered as having a binary value representation";
+      return new DcmOtherByteOtherWord(key);
+    }
+    else if (IsBinaryTag(key))
+    {
+      return new DcmOtherByteOtherWord(key);
+    }
+
+    switch (key.getEVR())
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+
+      /**
+       * Binary types, handled above
+       **/
+    
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_OD:
+#endif            
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EVR_OL:
+#endif            
+
+      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);
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UC:  // unlimited characters
+        return new DcmUnlimitedCharacters(key);
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UR:  // URI/URL
+        return new DcmUniversalResourceIdentifierOrLocator(key);
+#endif
+          
+        
+      /**
+       * 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);
+  }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasks.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/ICommand.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/ILockable.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/Locker.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/Mutex.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Resources/Graveyard/TestTranscoding.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,967 @@
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmFileFormat& dicom,
+                                           DicomTransferSyntax syntax)
+  {
+    E_TransferSyntax xfer;
+    if (!LookupDcmtkTransferSyntax(xfer, syntax))
+    {
+      return false;
+    }
+    else if (!dicom.validateMetaInfo(xfer).good())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot setup the transfer syntax to write a DICOM instance");
+    }
+    else
+    {
+      return SaveToMemoryBufferInternal(buffer, dicom, xfer);
+    }
+  }
+
+
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmFileFormat& dicom)
+  {
+    E_TransferSyntax xfer = dicom.getDataset()->getCurrentXfer();
+    if (xfer == EXS_Unknown)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot write a DICOM instance with unknown transfer syntax");
+    }
+    else if (!dicom.validateMetaInfo(xfer).good())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot setup the transfer syntax to write a DICOM instance");
+    }
+    else
+    {
+      return SaveToMemoryBufferInternal(buffer, dicom, xfer);
+    }
+  }
+
+
+
+
+
+#include <dcmtk/dcmdata/dcostrmb.h>
+#include <dcmtk/dcmdata/dcpixel.h>
+#include <dcmtk/dcmdata/dcpxitem.h>
+
+#include "../Core/DicomParsing/Internals/DicomFrameIndex.h"
+
+namespace Orthanc
+{
+  class IParsedDicomImage : public boost::noncopyable
+  {
+  public:
+    virtual ~IParsedDicomImage()
+    {
+    }
+
+    virtual DicomTransferSyntax GetTransferSyntax() = 0;
+
+    virtual std::string GetSopClassUid() = 0;
+
+    virtual std::string GetSopInstanceUid() = 0;
+
+    virtual unsigned int GetFramesCount() = 0;
+
+    // Can return NULL, for compressed transfer syntaxes
+    virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) = 0;
+    
+    virtual void GetCompressedFrame(std::string& target,
+                                    unsigned int frame) = 0;
+    
+    virtual void WriteToMemoryBuffer(std::string& target) = 0;
+  };
+
+
+  class IDicomImageReader : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomImageReader()
+    {
+    }
+
+    virtual IParsedDicomImage* Read(const void* data,
+                                    size_t size) = 0;
+
+    virtual IParsedDicomImage* Transcode(const void* data,
+                                         size_t size,
+                                         DicomTransferSyntax syntax,
+                                         bool allowNewSopInstanceUid) = 0;
+  };
+
+
+  class DcmtkImageReader : public IDicomImageReader
+  {
+  private:
+    class Image : public IParsedDicomImage
+    {
+    private:
+      std::unique_ptr<DcmFileFormat>    dicom_;
+      std::unique_ptr<DicomFrameIndex>  index_;
+      DicomTransferSyntax               transferSyntax_;
+      std::string                       sopClassUid_;
+      std::string                       sopInstanceUid_;
+
+      static std::string GetStringTag(DcmDataset& dataset,
+                                      const DcmTagKey& tag)
+      {
+        const char* value = NULL;
+
+        if (!dataset.findAndGetString(tag, value).good() ||
+            value == NULL)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Missing SOP class/instance UID in DICOM instance");
+        }
+        else
+        {
+          return std::string(value);
+        }
+      }
+
+    public:
+      Image(DcmFileFormat* dicom,
+            DicomTransferSyntax syntax) :
+        dicom_(dicom),
+        transferSyntax_(syntax)
+      {
+        if (dicom == NULL ||
+            dicom_->getDataset() == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        DcmDataset& dataset = *dicom_->getDataset();
+        index_.reset(new DicomFrameIndex(dataset));
+
+        sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID);
+        sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID);
+      }
+
+      virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
+      {
+        return transferSyntax_;
+      }
+
+      virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
+      {
+        return sopClassUid_;
+      }
+    
+      virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
+      {
+        return sopInstanceUid_;
+      }
+
+      virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
+      {
+        return index_->GetFramesCount();
+      }
+
+      virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE
+      {
+        assert(dicom_.get() != NULL);
+        if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, transferSyntax_))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot write the DICOM instance to a memory buffer");
+        }
+      }
+
+      virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) ORTHANC_OVERRIDE
+      {
+        assert(dicom_.get() != NULL &&
+               dicom_->getDataset() != NULL);
+        return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
+      }
+
+      virtual void GetCompressedFrame(std::string& target,
+                                      unsigned int frame) ORTHANC_OVERRIDE
+      {
+        assert(index_.get() != NULL);
+        index_->GetRawFrame(target, frame);
+      }
+    };
+
+    unsigned int lossyQuality_;
+
+    static DicomTransferSyntax DetectTransferSyntax(DcmFileFormat& dicom)
+    {
+      if (dicom.getDataset() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+        
+      DcmDataset& dataset = *dicom.getDataset();
+
+      E_TransferSyntax xfer = dataset.getCurrentXfer();
+      if (xfer == EXS_Unknown)
+      {
+        dataset.updateOriginalXfer();
+        xfer = dataset.getCurrentXfer();
+        if (xfer == EXS_Unknown)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Cannot determine the transfer syntax of the DICOM instance");
+        }
+      }
+
+      DicomTransferSyntax syntax;
+      if (FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, xfer))
+      {
+        return syntax;
+      }
+      else
+      {
+        throw OrthancException(
+          ErrorCode_BadFileFormat,
+          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
+      }
+    }
+
+
+    static uint16_t GetBitsStored(DcmFileFormat& dicom)
+    {
+      if (dicom.getDataset() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      uint16_t bitsStored;
+      if (dicom.getDataset()->findAndGetUint16(DCM_BitsStored, bitsStored).good())
+      {
+        return bitsStored;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing \"Bits Stored\" tag in DICOM instance");
+      }      
+    }
+    
+      
+  public:
+    DcmtkImageReader() :
+      lossyQuality_(90)
+    {
+    }
+
+    void SetLossyQuality(unsigned int quality)
+    {
+      if (quality <= 0 ||
+          quality > 100)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        lossyQuality_ = quality;
+      }
+    }
+
+    unsigned int GetLossyQuality() const
+    {
+      return lossyQuality_;
+    }
+
+    virtual IParsedDicomImage* Read(const void* data,
+                                    size_t size)
+    {
+      std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size));
+      if (dicom.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      DicomTransferSyntax transferSyntax = DetectTransferSyntax(*dicom);
+
+      return new Image(dicom.release(), transferSyntax);
+    }
+
+    virtual IParsedDicomImage* Transcode(const void* data,
+                                         size_t size,
+                                         DicomTransferSyntax syntax,
+                                         bool allowNewSopInstanceUid)
+    {
+      std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size));
+      if (dicom.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const uint16_t bitsStored = GetBitsStored(*dicom);
+
+      if (syntax == DetectTransferSyntax(*dicom))
+      {
+        // No transcoding is needed
+        return new Image(dicom.release(), syntax);
+      }
+      
+      if (syntax == DicomTransferSyntax_LittleEndianImplicit &&
+          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
+      {
+        return new Image(dicom.release(), syntax);
+      }
+
+      if (syntax == DicomTransferSyntax_LittleEndianExplicit &&
+          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
+      {
+        return new Image(dicom.release(), syntax);
+      }
+      
+      if (syntax == DicomTransferSyntax_BigEndianExplicit &&
+          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
+      {
+        return new Image(dicom.release(), syntax);
+      }
+
+      if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit &&
+          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
+      {
+        return new Image(dicom.release(), syntax);
+      }
+
+#if ORTHANC_ENABLE_JPEG == 1
+      if (syntax == DicomTransferSyntax_JPEGProcess1 &&
+          allowNewSopInstanceUid &&
+          bitsStored == 8)
+      {
+        DJ_RPLossy rpLossy(lossyQuality_);
+        
+        if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess1, &rpLossy))
+        {
+          return new Image(dicom.release(), syntax);
+        }
+      }
+#endif
+      
+#if ORTHANC_ENABLE_JPEG == 1
+      if (syntax == DicomTransferSyntax_JPEGProcess2_4 &&
+          allowNewSopInstanceUid &&
+          bitsStored <= 12)
+      {
+        DJ_RPLossy rpLossy(lossyQuality_);
+        if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess2_4, &rpLossy))
+        {
+          return new Image(dicom.release(), syntax);
+        }
+      }
+#endif
+
+      //LOG(INFO) << "Unable to transcode DICOM image using the built-in reader";
+      return NULL;
+    }
+  };
+  
+
+  
+  class IDicomTranscoder1 : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomTranscoder1()
+    {
+    }
+
+    virtual DcmFileFormat& GetDicom() = 0;
+
+    virtual DicomTransferSyntax GetTransferSyntax() = 0;
+
+    virtual std::string GetSopClassUid() = 0;
+
+    virtual std::string GetSopInstanceUid() = 0;
+
+    virtual unsigned int GetFramesCount() = 0;
+
+    virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0;
+
+    virtual void GetCompressedFrame(std::string& target,
+                                    unsigned int frame) = 0;
+
+    // NB: Transcoding can change the value of "GetSopInstanceUid()"
+    // and "GetTransferSyntax()" if lossy compression is applied
+    virtual bool Transcode(std::string& target,
+                           DicomTransferSyntax syntax,
+                           bool allowNewSopInstanceUid) = 0;
+
+    virtual void WriteToMemoryBuffer(std::string& target) = 0;
+  };
+
+
+  class DcmtkTranscoder2 : public IDicomTranscoder1
+  {
+  private:
+    std::unique_ptr<DcmFileFormat>    dicom_;
+    std::unique_ptr<DicomFrameIndex>  index_;
+    DicomTransferSyntax               transferSyntax_;
+    std::string                       sopClassUid_;
+    std::string                       sopInstanceUid_;
+    uint16_t                          bitsStored_;
+    unsigned int                      lossyQuality_;
+
+    static std::string GetStringTag(DcmDataset& dataset,
+                                    const DcmTagKey& tag)
+    {
+      const char* value = NULL;
+
+      if (!dataset.findAndGetString(tag, value).good() ||
+          value == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing SOP class/instance UID in DICOM instance");
+      }
+      else
+      {
+        return std::string(value);
+      }
+    }
+
+    void Setup(DcmFileFormat* dicom)
+    {
+      lossyQuality_ = 90;
+      
+      dicom_.reset(dicom);
+      
+      if (dicom == NULL ||
+          dicom_->getDataset() == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      DcmDataset& dataset = *dicom_->getDataset();
+      index_.reset(new DicomFrameIndex(dataset));
+
+      E_TransferSyntax xfer = dataset.getCurrentXfer();
+      if (xfer == EXS_Unknown)
+      {
+        dataset.updateOriginalXfer();
+        xfer = dataset.getCurrentXfer();
+        if (xfer == EXS_Unknown)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "Cannot determine the transfer syntax of the DICOM instance");
+        }
+      }
+
+      if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer))
+      {
+        throw OrthancException(
+          ErrorCode_BadFileFormat,
+          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
+      }
+
+      if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored_).good())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing \"Bits Stored\" tag in DICOM instance");
+      }      
+
+      sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID);
+      sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID);
+    }
+    
+  public:
+    DcmtkTranscoder2(DcmFileFormat* dicom)  // Takes ownership
+    {
+      Setup(dicom);
+    }
+
+    DcmtkTranscoder2(const void* dicom,
+                    size_t size)
+    {
+      Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size));
+    }
+
+    void SetLossyQuality(unsigned int quality)
+    {
+      if (quality <= 0 ||
+          quality > 100)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        lossyQuality_ = quality;
+      }
+    }
+
+    unsigned int GetLossyQuality() const
+    {
+      return lossyQuality_;
+    }
+
+    unsigned int GetBitsStored() const
+    {
+      return bitsStored_;
+    }
+
+    virtual DcmFileFormat& GetDicom()
+    {
+      assert(dicom_ != NULL);
+      return *dicom_;
+    }
+
+    virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
+    {
+      return transferSyntax_;
+    }
+
+    virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
+    {
+      return sopClassUid_;
+    }
+    
+    virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
+    {
+      return sopInstanceUid_;
+    }
+
+    virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
+    {
+      return index_->GetFramesCount();
+    }
+
+    virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE
+    {
+      if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_))
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot write the DICOM instance to a memory buffer");
+      }
+    }
+
+    virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE
+    {
+      assert(dicom_->getDataset() != NULL);
+      return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
+    }
+
+    virtual void GetCompressedFrame(std::string& target,
+                                    unsigned int frame) ORTHANC_OVERRIDE
+    {
+      index_->GetRawFrame(target, frame);
+    }
+
+    virtual bool Transcode(std::string& target,
+                           DicomTransferSyntax syntax,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE
+    {
+      assert(dicom_ != NULL &&
+             dicom_->getDataset() != NULL);
+      
+      if (syntax == GetTransferSyntax())
+      {
+        printf("NO TRANSCODING\n");
+        
+        // No change in the transfer syntax => simply serialize the current dataset
+        WriteToMemoryBuffer(target);
+        return true;
+      }
+      
+      printf(">> %d\n", bitsStored_);
+
+      if (syntax == DicomTransferSyntax_LittleEndianImplicit &&
+          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
+          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
+      {
+        transferSyntax_ = DicomTransferSyntax_LittleEndianImplicit;
+        return true;
+      }
+
+      if (syntax == DicomTransferSyntax_LittleEndianExplicit &&
+          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
+          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
+      {
+        transferSyntax_ = DicomTransferSyntax_LittleEndianExplicit;
+        return true;
+      }
+      
+      if (syntax == DicomTransferSyntax_BigEndianExplicit &&
+          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
+          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
+      {
+        transferSyntax_ = DicomTransferSyntax_BigEndianExplicit;
+        return true;
+      }
+
+      if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit &&
+          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
+          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
+      {
+        transferSyntax_ = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+        return true;
+      }
+
+#if ORTHANC_ENABLE_JPEG == 1
+      if (syntax == DicomTransferSyntax_JPEGProcess1 &&
+          allowNewSopInstanceUid &&
+          GetBitsStored() == 8)
+      {
+        DJ_RPLossy rpLossy(lossyQuality_);
+        
+        if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) &&
+            FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
+        {
+          transferSyntax_ = DicomTransferSyntax_JPEGProcess1;
+          sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID);
+          return true;
+        }
+      }
+#endif
+      
+#if ORTHANC_ENABLE_JPEG == 1
+      if (syntax == DicomTransferSyntax_JPEGProcess2_4 &&
+          allowNewSopInstanceUid &&
+          GetBitsStored() <= 12)
+      {
+        DJ_RPLossy rpLossy(lossyQuality_);
+        if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) &&
+            FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
+        {
+          transferSyntax_ = DicomTransferSyntax_JPEGProcess2_4;
+          sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID);
+          return true;
+        }
+      }
+#endif
+
+      return false;
+    }
+  };
+}
+
+
+
+
+#include <boost/filesystem.hpp>
+
+
+static void TestFile(const std::string& path)
+{
+  static unsigned int count = 0;
+  count++;
+  
+
+  printf("** %s\n", path.c_str());
+
+  std::string s;
+  SystemToolbox::ReadFile(s, path);
+
+  Orthanc::DcmtkTranscoder2 transcoder(s.c_str(), s.size());
+
+  /*if (transcoder.GetBitsStored() != 8)  // TODO
+    return; */
+
+  {
+    char buf[1024];
+    sprintf(buf, "/tmp/source-%06d.dcm", count);
+    printf(">> %s\n", buf);
+    Orthanc::SystemToolbox::WriteFile(s, buf);
+  }
+
+  printf("[%s] [%s] [%s] %d %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()),
+         transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(),
+         transcoder.GetFramesCount(), transcoder.GetTransferSyntax());
+
+  for (size_t i = 0; i < transcoder.GetFramesCount(); i++)
+  {
+    std::string f;
+    transcoder.GetCompressedFrame(f, i);
+
+    if (i == 0)
+    {
+      char buf[1024];
+      sprintf(buf, "/tmp/frame-%06d.raw", count);
+      printf(">> %s\n", buf);
+      Orthanc::SystemToolbox::WriteFile(f, buf);
+    }
+  }
+
+  {
+    std::string t;
+    transcoder.WriteToMemoryBuffer(t);
+
+    Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size());
+    printf(">> %d %d ; %lu bytes\n", transcoder.GetTransferSyntax(), transcoder2.GetTransferSyntax(), t.size());
+  }
+
+  {
+    std::string a = transcoder.GetSopInstanceUid();
+    DicomTransferSyntax b = transcoder.GetTransferSyntax();
+    
+    DicomTransferSyntax syntax = DicomTransferSyntax_JPEGProcess2_4;
+    //DicomTransferSyntax syntax = DicomTransferSyntax_LittleEndianExplicit;
+
+    std::string t;
+    bool ok = transcoder.Transcode(t, syntax, true);
+    printf("Transcoding: %d\n", ok);
+
+    if (ok)
+    {
+      printf("[%s] => [%s]\n", a.c_str(), transcoder.GetSopInstanceUid().c_str());
+      printf("[%s] => [%s]\n", GetTransferSyntaxUid(b),
+             GetTransferSyntaxUid(transcoder.GetTransferSyntax()));
+      
+      {
+        char buf[1024];
+        sprintf(buf, "/tmp/transcoded-%06d.dcm", count);
+        printf(">> %s\n", buf);
+        Orthanc::SystemToolbox::WriteFile(t, buf);
+      }
+
+      Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size());
+      printf("  => transcoded transfer syntax %d ; %lu bytes\n", transcoder2.GetTransferSyntax(), t.size());
+    }
+  }
+  
+  printf("\n");
+}
+
+TEST(Toto, DISABLED_Transcode)
+{
+  //OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
+
+  if (1)
+  {
+    const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes";
+    
+    for (boost::filesystem::directory_iterator it(PATH);
+         it != boost::filesystem::directory_iterator(); ++it)
+    {
+      if (boost::filesystem::is_regular_file(it->status()))
+      {
+        TestFile(it->path().string());
+      }
+    }
+  }
+
+  if (0)
+  {
+    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm");
+    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm");
+  }
+
+  if (0)
+  {
+    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm");
+  }
+}
+
+
+TEST(Toto, DISABLED_Transcode2)
+{
+  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
+  {
+    DicomTransferSyntax a = (DicomTransferSyntax) i;
+
+    std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
+                        std::string(GetTransferSyntaxUid(a)) + ".dcm");
+    if (Orthanc::SystemToolbox::IsRegularFile(path))
+    {
+      printf("\n======= %s\n", GetTransferSyntaxUid(a));
+
+      std::string source;
+      Orthanc::SystemToolbox::ReadFile(source, path);
+
+      DcmtkImageReader reader;
+
+      {
+        std::unique_ptr<IParsedDicomImage> image(
+          reader.Read(source.c_str(), source.size()));
+        ASSERT_TRUE(image.get() != NULL);
+        ASSERT_EQ(a, image->GetTransferSyntax());
+
+        std::string target;
+        image->WriteToMemoryBuffer(target);
+      }
+
+      for (int j = 0; j <= DicomTransferSyntax_XML; j++)
+      {
+        DicomTransferSyntax b = (DicomTransferSyntax) j;
+        //if (a == b) continue;
+
+        std::unique_ptr<IParsedDicomImage> image(
+          reader.Transcode(source.c_str(), source.size(), b, true));
+        if (image.get() != NULL)
+        {
+          printf("[%s] -> [%s]\n", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b));
+
+          std::string target;
+          image->WriteToMemoryBuffer(target);
+
+          char buf[1024];
+          sprintf(buf, "/tmp/%s-%s.dcm", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b));
+          
+          SystemToolbox::WriteFile(target, buf);
+        }
+        else if (a != DicomTransferSyntax_JPEG2000 &&
+                 a != DicomTransferSyntax_JPEG2000LosslessOnly)
+        {
+          ASSERT_TRUE(b != DicomTransferSyntax_LittleEndianImplicit &&
+                      b != DicomTransferSyntax_LittleEndianExplicit &&
+                      b != DicomTransferSyntax_BigEndianExplicit &&
+                      b != DicomTransferSyntax_DeflatedLittleEndianExplicit);
+        }
+      }
+    }
+  }
+}
+
+
+#include "../Core/DicomNetworking/DicomAssociation.h"
+#include "../Core/DicomNetworking/DicomControlUserConnection.h"
+#include "../Core/DicomNetworking/DicomStoreUserConnection.h"
+
+TEST(Toto, DISABLED_DicomAssociation)
+{
+  DicomAssociationParameters params;
+  params.SetLocalApplicationEntityTitle("ORTHANC");
+  params.SetRemoteApplicationEntityTitle("PACS");
+  params.SetRemotePort(2001);
+
+#if 0
+  DicomAssociation assoc;
+  assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass);
+  assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass);
+  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
+                                   DicomTransferSyntax_JPEGProcess1);
+  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
+                                   DicomTransferSyntax_JPEGProcess2_4);
+  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
+                                   DicomTransferSyntax_JPEG2000);
+  
+  assoc.Open(params);
+
+  int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage);
+  printf(">> %d\n", presID);
+    
+  std::map<DicomTransferSyntax, uint8_t> pc;
+  printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage));
+  
+  for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
+         it = pc.begin(); it != pc.end(); ++it)
+  {
+    printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second);
+  }
+#else
+  {
+    DicomControlUserConnection assoc(params);
+
+    try
+    {
+      printf(">> %d\n", assoc.Echo());
+    }
+    catch (OrthancException&)
+    {
+    }
+  }
+    
+  params.SetRemoteApplicationEntityTitle("PACS");
+  params.SetRemotePort(2000);
+
+  {
+    DicomControlUserConnection assoc(params);
+    printf(">> %d\n", assoc.Echo());
+  }
+
+#endif
+}
+
+static void TestTranscode(DicomStoreUserConnection& scu,
+                          const std::string& sopClassUid,
+                          DicomTransferSyntax transferSyntax)
+{
+  std::set<DicomTransferSyntax> accepted;
+
+  scu.LookupTranscoding(accepted, sopClassUid, transferSyntax);
+  if (accepted.empty())
+  {
+    throw OrthancException(ErrorCode_NetworkProtocol,
+                           "The SOP class is not supported by the remote modality");
+  }
+
+  {
+    unsigned int count = 0;
+    for (std::set<DicomTransferSyntax>::const_iterator
+           it = accepted.begin(); it != accepted.end(); ++it)
+    {
+      LOG(INFO) << "available for transcoding " << (count++) << ": " << sopClassUid
+                << " / " << GetTransferSyntaxUid(*it);
+    }
+  }
+  
+  if (accepted.find(transferSyntax) != accepted.end())
+  {
+    printf("**** OK, without transcoding !! [%s]\n", GetTransferSyntaxUid(transferSyntax));
+  }
+  else
+  {
+    // Transcoding - only in Orthanc >= 1.7.0
+
+    const DicomTransferSyntax uncompressed[] = {
+      DicomTransferSyntax_LittleEndianImplicit,  // Default transfer syntax
+      DicomTransferSyntax_LittleEndianExplicit,
+      DicomTransferSyntax_BigEndianExplicit
+    };
+
+    bool found = false;
+    for (size_t i = 0; i < 3; i++)
+    {
+      if (accepted.find(uncompressed[i]) != accepted.end())
+      {
+        printf("**** TRANSCODING to %s\n", GetTransferSyntaxUid(uncompressed[i]));
+        found = true;
+        break;
+      }
+    }
+
+    if (!found)
+    {
+      printf("**** KO KO KO\n");
+    }
+  }
+}
+
+
+TEST(Toto, DISABLED_Store)
+{
+  DicomAssociationParameters params;
+  params.SetLocalApplicationEntityTitle("ORTHANC");
+  params.SetRemoteApplicationEntityTitle("STORESCP");
+  params.SetRemotePort(2000);
+
+  DicomStoreUserConnection assoc(params);
+  assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess1);
+  assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4);
+  //assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
+
+  //assoc.SetUncompressedSyntaxesProposed(false);  // Necessary for transcoding
+  assoc.SetCommonClassesProposed(false);
+  assoc.SetRetiredBigEndianProposed(true);
+  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
+  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
+  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
+}
+
+
+TEST(Toto, DISABLED_Store2)
+{
+  DicomAssociationParameters params;
+  params.SetLocalApplicationEntityTitle("ORTHANC");
+  params.SetRemoteApplicationEntityTitle("STORESCP");
+  params.SetRemotePort(2000);
+
+  DicomStoreUserConnection assoc(params);
+  //assoc.SetCommonClassesProposed(false);
+  assoc.SetRetiredBigEndianProposed(true);
+
+  std::string s;
+  Orthanc::SystemToolbox::ReadFile(s, "/tmp/i/" + std::string(GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit)) +".dcm");
+
+  std::string c, i;
+  assoc.Store(c, i, s.c_str(), s.size());
+  printf("[%s] [%s]\n", c.c_str(), i.c_str());
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/boost-1.65.1-linux-standard-base.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/boost-1.66.0-linux-standard-base.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/boost-1.67.0-linux-standard-base.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/boost-1.68.0-linux-standard-base.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,76 @@
+diff -urEb boost_1_68_0.orig/boost/move/adl_move_swap.hpp boost_1_68_0/boost/move/adl_move_swap.hpp
+--- boost_1_68_0.orig/boost/move/adl_move_swap.hpp	2018-11-13 16:08:32.214434915 +0100
++++ boost_1_68_0/boost/move/adl_move_swap.hpp	2018-11-13 16:09:03.558399048 +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
+diff -urEb boost_1_68_0.orig/boost/thread/detail/config.hpp boost_1_68_0/boost/thread/detail/config.hpp
+--- boost_1_68_0.orig/boost/thread/detail/config.hpp	2018-11-13 16:08:32.210434920 +0100
++++ boost_1_68_0/boost/thread/detail/config.hpp	2018-11-13 16:10:03.386329911 +0100
+@@ -417,7 +417,7 @@
+   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+ #elif defined(BOOST_THREAD_CHRONO_MAC_API)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+-#elif defined(__ANDROID__)
++#elif defined(__LSB_VERSION__) || defined(__ANDROID__)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
+     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp
+--- boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
++++ boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:11:08.374253901 +0100
+@@ -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_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp
+--- boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
++++ boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:14:30.278012856 +0100
+@@ -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,10 @@
+    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
+Only in boost_1_68_0/boost/type_traits/detail: has_prefix_operator.hpp~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/boost-1.69.0-linux-standard-base.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,123 @@
+diff -urEb boost_1_69_0.orig/boost/move/adl_move_swap.hpp boost_1_69_0/boost/move/adl_move_swap.hpp
+--- boost_1_69_0.orig/boost/move/adl_move_swap.hpp	2019-02-22 15:05:32.682359994 +0100
++++ boost_1_69_0/boost/move/adl_move_swap.hpp	2019-02-22 15:05:48.426358034 +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
+diff -urEb boost_1_69_0.orig/boost/system/detail/system_category_win32.hpp boost_1_69_0/boost/system/detail/system_category_win32.hpp
+--- boost_1_69_0.orig/boost/system/detail/system_category_win32.hpp	2019-02-22 15:05:32.722359989 +0100
++++ boost_1_69_0/boost/system/detail/system_category_win32.hpp	2019-02-22 15:06:31.922352713 +0100
+@@ -26,7 +26,7 @@
+ namespace detail
+ {
+ 
+-#if ( defined(_MSC_VER) && _MSC_VER < 1900 ) || ( defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) )
++#if ( defined(_MSC_VER) && _MSC_VER < 1900 ) || ( defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) ) || 1  /* std::snprintf() does not seem to exist on Visual Studio 2015 */
+ 
+ inline char const * unknown_message_win32( int ev, char * buffer, std::size_t len )
+ {
+diff -urEb boost_1_69_0.orig/boost/thread/detail/config.hpp boost_1_69_0/boost/thread/detail/config.hpp
+--- boost_1_69_0.orig/boost/thread/detail/config.hpp	2019-02-22 15:05:32.598360004 +0100
++++ boost_1_69_0/boost/thread/detail/config.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -418,7 +418,7 @@
+   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+ #elif defined(BOOST_THREAD_CHRONO_MAC_API)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+-#elif defined(__ANDROID__)
++#elif defined(__ANDROID__) || defined(__LSB_VERSION__)
+   #define BOOST_THREAD_HAS_MONO_CLOCK
+   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
+     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
+diff -urEb boost_1_69_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_69_0/boost/type_traits/detail/has_postfix_operator.hpp
+--- boost_1_69_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2019-02-22 15:05:32.650359998 +0100
++++ boost_1_69_0/boost/type_traits/detail/has_postfix_operator.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -85,8 +85,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
+@@ -234,7 +237,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_69_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_69_0/boost/type_traits/detail/has_prefix_operator.hpp
+--- boost_1_69_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2019-02-22 15:05:32.650359998 +0100
++++ boost_1_69_0/boost/type_traits/detail/has_prefix_operator.hpp	2019-02-22 15:05:48.426358034 +0100
+@@ -114,8 +114,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
+@@ -263,7 +266,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
+diff -urEb boost_1_69_0.orig/libs/filesystem/src/operations.cpp boost_1_69_0/libs/filesystem/src/operations.cpp
+--- boost_1_69_0.orig/libs/filesystem/src/operations.cpp	2019-02-22 15:05:32.566360008 +0100
++++ boost_1_69_0/libs/filesystem/src/operations.cpp	2019-02-22 18:04:17.346573047 +0100
+@@ -2111,9 +2111,16 @@
+     std::size_t path_size (0);  // initialization quiets gcc warning (ticket #3509)
+     error_code ec = path_max(path_size);
+     if (ec)return ec;
+-    dirent de;
+-    buffer = std::malloc((sizeof(dirent) - sizeof(de.d_name))
+-      +  path_size + 1); // + 1 for "/0"
++
++    // Fixed possible use of uninitialized dirent::d_type in dir_iterator
++    // https://github.com/boostorg/filesystem/commit/bbe9d1771e5d679b3f10c42a58fc81f7e8c024a9
++    const std::size_t buffer_size = (sizeof(dirent) - sizeof(dirent().d_name))
++      +  path_size + 1; // + 1 for "\0"
++    buffer = std::malloc(buffer_size);
++    if (BOOST_UNLIKELY(!buffer))
++      return make_error_code(boost::system::errc::not_enough_memory);
++    std::memset(buffer, 0, buffer_size);
++    
+     return ok;
+   }  
+ 
+@@ -2142,6 +2149,13 @@
+     *result = 0;
+     if ((p = ::readdir(dirp))== 0)
+       return errno;
++
++    // Fixed possible use of uninitialized dirent::d_type in dir_iterator
++    // https://github.com/boostorg/filesystem/commit/bbe9d1771e5d679b3f10c42a58fc81f7e8c024a9    
++#   ifdef BOOST_FILESYSTEM_STATUS_CACHE
++    entry->d_type = p->d_type;
++#   endif
++
+     std::strcpy(entry->d_name, p->d_name);
+     *result = entry;
+     return 0;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/civetweb-1.11.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,98 @@
+diff -urEb civetweb-1.11.orig/include/civetweb.h civetweb-1.11/include/civetweb.h
+--- civetweb-1.11.orig/include/civetweb.h	2019-01-17 21:09:41.844888908 +0100
++++ civetweb-1.11/include/civetweb.h	2019-01-21 12:05:08.138998659 +0100
+@@ -1507,6 +1507,10 @@
+ #endif
+ 
+ 
++// Added by SJ
++CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
++
++
+ #ifdef __cplusplus
+ }
+ #endif /* __cplusplus */
+diff -urEb civetweb-1.11.orig/src/civetweb.c civetweb-1.11/src/civetweb.c
+--- civetweb-1.11.orig/src/civetweb.c	2019-01-17 21:09:41.852888857 +0100
++++ civetweb-1.11/src/civetweb.c	2019-01-21 12:06:35.826868284 +0100
+@@ -59,6 +59,9 @@
+ #if defined(__linux__) && !defined(_XOPEN_SOURCE)
+ #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */
+ #endif
++#if defined(__LSB_VERSION__)
++#define NEED_TIMEGM
++#endif
+ #if !defined(_LARGEFILE_SOURCE)
+ #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */
+ #endif
+@@ -129,6 +132,12 @@
+ 
+ 
+ /* Alternative queue is well tested and should be the new default */
++#if defined(__LSB_VERSION__)
++/* Function "eventfd()" is not available in Linux Standard Base, can't
++ * use the alternative queue */
++#define NO_ALTERNATIVE_QUEUE
++#endif
++
+ #if defined(NO_ALTERNATIVE_QUEUE)
+ #if defined(ALTERNATIVE_QUEUE)
+ #error "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE or none, but not both"
+@@ -536,6 +545,10 @@
+ #if !defined(EWOULDBLOCK)
+ #define EWOULDBLOCK WSAEWOULDBLOCK
+ #endif /* !EWOULDBLOCK */
++#if !defined(ECONNRESET)
++/* This macro is not defined e.g. in Visual Studio 2008 */
++#define ECONNRESET WSAECONNRESET
++#endif /* !ECONNRESET */
+ #define _POSIX_
+ #define INT64_FMT "I64d"
+ #define UINT64_FMT "I64u"
+@@ -2939,6 +2952,13 @@
+ #endif
+ 
+ 
++#if defined(__LSB_VERSION__)
++static void
++mg_set_thread_name(const char *threadName)
++{
++  /* prctl() does not seem to be available in Linux Standard Base */
++}
++#else
+ static void
+ mg_set_thread_name(const char *name)
+ {
+@@ -2980,6 +3000,7 @@
+ 	(void)prctl(PR_SET_NAME, threadName, 0, 0, 0);
+ #endif
+ }
++#endif
+ #else /* !defined(NO_THREAD_NAME) */
+ void
+ mg_set_thread_name(const char *threadName)
+@@ -16919,6 +16940,10 @@
+ 	/* Message is a valid request */
+ 
+ 	/* Is there a "host" ? */
++        /* https://github.com/civetweb/civetweb/pull/675/commits/96e3e8c50acb4b8e0c946d02b5f880a3e62986e1 */
++	if (conn->host!=NULL) {
++		mg_free((void *)conn->host);
++	}
+ 	conn->host = alloc_get_host(conn);
+ 	if (!conn->host) {
+ 		mg_snprintf(conn,
+@@ -19857,4 +19882,13 @@
+ }
+ 
+ 
++// Added by SJ
++void mg_disable_keep_alive(struct mg_connection *conn)
++{
++  if (conn != NULL) {
++    conn->must_close = 1;
++  }
++}
++
++
+ /* End of civetweb.c */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/civetweb-1.12.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,29 @@
+diff -urEb civetweb-1.12.orig/include/civetweb.h civetweb-1.12/include/civetweb.h
+--- civetweb-1.12.orig/include/civetweb.h	2020-04-02 12:07:20.727054140 +0200
++++ civetweb-1.12/include/civetweb.h	2020-04-02 12:07:42.734996559 +0200
+@@ -1614,6 +1614,9 @@
+                                   struct mg_error_data *error);
+ #endif
+ 
++// Added by SJ
++CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
++
+ #ifdef __cplusplus
+ }
+ #endif /* __cplusplus */
+diff -urEb civetweb-1.12.orig/src/civetweb.c civetweb-1.12/src/civetweb.c
+--- civetweb-1.12.orig/src/civetweb.c	2020-04-02 12:07:20.731054129 +0200
++++ civetweb-1.12/src/civetweb.c	2020-04-02 12:07:52.250971600 +0200
+@@ -20704,5 +20704,12 @@
+ 	return 1;
+ }
+ 
++// Added by SJ
++void mg_disable_keep_alive(struct mg_connection *conn)
++{
++  if (conn != NULL) {
++    conn->must_close = 1;
++  }
++}
+ 
+ /* End of civetweb.c */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/curl-7.57.0-cmake.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/curl-7.64.0-cmake.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,12 @@
+diff -urEb curl-7.64.0.orig/CMake/Macros.cmake curl-7.64.0/CMake/Macros.cmake
+--- curl-7.64.0.orig/CMake/Macros.cmake	2019-02-21 20:35:26.403471603 +0100
++++ curl-7.64.0/CMake/Macros.cmake	2019-02-21 20:36:19.987272782 +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/OrthancFramework/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/dcmtk-3.6.0-mingw64.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/dcmtk-3.6.0-speed.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Patches/dcmtk-3.6.2-private.dic	Wed Jun 10 20:30:34 2020 +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
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.2.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,48 @@
+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	2020-01-06 17:42:52.299540389 +0100
++++ dcmtk-3.6.2/CMake/GenerateDCMTKConfigure.cmake	2020-01-06 17:43:56.707520036 +0100
+@@ -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)
+diff -urEb dcmtk-3.6.2.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.2/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.2.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 17:42:52.283540394 +0100
++++ dcmtk-3.6.2/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 17:46:21.711473976 +0100
+@@ -152,6 +152,12 @@
+     /// 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
+diff -urEb dcmtk-3.6.2.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.2/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.2.orig/dcmdata/libsrc/dcdict.cc	2020-01-06 17:42:52.287540392 +0100
++++ dcmtk-3.6.2/dcmdata/libsrc/dcdict.cc	2020-01-06 17:47:18.335299472 +0100
+@@ -876,3 +876,6 @@
+   wrlock().clear();
+   unlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/dcmtk-3.6.4.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,99 @@
+diff -urEb dcmtk-3.6.4.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.4.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 19:55:12.887153062 +0100
++++ dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 19:55:28.156447233 +0100
+@@ -152,6 +152,12 @@
+     /// 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
+diff -urEb dcmtk-3.6.4.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.4/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.4.orig/dcmdata/libsrc/dcdict.cc	2020-01-06 19:55:12.899154075 +0100
++++ dcmtk-3.6.4/dcmdata/libsrc/dcdict.cc	2020-01-06 19:55:28.156447233 +0100
+@@ -899,3 +899,6 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.4.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.4/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.4.orig/dcmdata/libsrc/dcpxitem.cc	2020-01-06 19:55:12.899154075 +0100
++++ dcmtk-3.6.4/dcmdata/libsrc/dcpxitem.cc	2020-01-06 19:55:28.156447233 +0100
+@@ -36,6 +36,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.4.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.4/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.4.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-01-06 19:55:12.911155088 +0100
++++ dcmtk-3.6.4/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-01-06 19:56:26.991372656 +0100
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -148,7 +148,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ FairMutex::FairMutex ()
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.4.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.4/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.4.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-01-06 19:55:12.951158464 +0100
++++ dcmtk-3.6.4/ofstd/include/dcmtk/ofstd/offile.h	2020-01-06 19:55:28.156447233 +0100
+@@ -575,7 +575,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/OrthancFramework/Resources/Patches/dcmtk-3.6.5.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,133 @@
+diff -urEb dcmtk-3.6.5.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.5/CMake/GenerateDCMTKConfigure.cmake
+--- dcmtk-3.6.5.orig/CMake/GenerateDCMTKConfigure.cmake	2020-06-08 22:19:03.265799573 +0200
++++ dcmtk-3.6.5/CMake/GenerateDCMTKConfigure.cmake	2020-06-08 22:21:22.670025141 +0200
+@@ -169,6 +169,8 @@
+ endif()
+ 
+ # Check the sizes of various types
++if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
++  # This doesn't work for wasm, Orthanc defines the macros manually
+ include (CheckTypeSize)
+ CHECK_TYPE_SIZE("char" SIZEOF_CHAR)
+ CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
+@@ -177,6 +179,7 @@
+ CHECK_TYPE_SIZE("long" SIZEOF_LONG)
+ CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
+ CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
++endif()
+ 
+ # Check for include files, libraries, and functions
+ include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
+diff -urEb dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h
+--- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-06-08 22:19:03.229799498 +0200
++++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-06-08 22:19:35.317862998 +0200
+@@ -152,6 +152,12 @@
+     /// 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
+diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc
+--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc	2020-06-08 22:19:03.245799531 +0200
++++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc	2020-06-08 22:19:35.317862998 +0200
+@@ -900,3 +900,6 @@
+   wrlock().clear();
+   wrunlock();
+ }
++
++
++#include "dcdict_orthanc.cc"
+diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc
+--- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc	2020-06-08 22:19:03.245799531 +0200
++++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc	2020-06-08 22:19:35.317862998 +0200
+@@ -36,6 +36,9 @@
+ #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
+ #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
+ 
++#undef max
++#include "dcmtk/ofstd/oflimits.h"
++
+ 
+ // ********************************
+ 
+diff -urEb dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h
+--- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-06-08 22:19:03.261799565 +0200
++++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-06-08 22:19:35.317862998 +0200
+@@ -63,7 +63,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Mutex::Mutex (Mutex::Type t)
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
+ { }
+ 
+ 
+@@ -106,7 +106,7 @@
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
+     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
+-    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
++    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
+ { }
+ 
+ 
+@@ -148,7 +148,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ FairMutex::FairMutex ()
+-    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
++    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
+ { }
+ 
+ 
+@@ -190,7 +190,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
+-    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
++    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
+ { }
+ 
+ 
+@@ -252,7 +252,7 @@
+ 
+ DCMTK_LOG4CPLUS_INLINE_EXPORT
+ SharedMutex::SharedMutex ()
+-    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
++    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
+ { }
+ 
+ 
+diff -urEb dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc dcmtk-3.6.5/oflog/libsrc/oflog.cc
+--- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc	2020-06-08 22:19:03.261799565 +0200
++++ dcmtk-3.6.5/oflog/libsrc/oflog.cc	2020-06-08 22:19:35.317862998 +0200
+@@ -19,6 +19,10 @@
+  *
+  */
+ 
++#if defined(_WIN32)
++#  include <winsock2.h>
++#endif
++
+ #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+ #include "dcmtk/oflog/oflog.h"
+ 
+diff -urEb dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h
+--- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-06-08 22:19:03.293799632 +0200
++++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h	2020-06-08 22:19:35.317862998 +0200
+@@ -575,7 +575,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/OrthancFramework/Resources/Patches/dcmtk-dcdict_orthanc.cc	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,205 @@
+// 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 <string>
+#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/OrthancFramework/Resources/Patches/dcmtk.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,12 @@
+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
+diff -urEb dcmtk-3.6.4.orig/ dcmtk-3.6.4
+diff -urEb dcmtk-3.6.5.orig/ dcmtk-3.6.5
+
+For "dcmtk-3.6.2-private.dic"
+=============================
+
+# cp ../../ThirdPartyDownloads/dcmtk-3.6.2/dcmdata/data/private.dic dcmtk-3.6.2-private.dic
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/e2fsprogs-1.43.8-apple.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,24 @@
+diff -urEb e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in e2fsprogs-1.43.8/lib/uuid/uuid.h.in
+--- e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in	2018-01-02 05:52:58.000000000 +0100
++++ e2fsprogs-1.43.8/lib/uuid/uuid.h.in	2018-11-05 12:18:29.962235770 +0100
+@@ -35,6 +35,20 @@
+ #ifndef _UUID_UUID_H
+ #define _UUID_UUID_H
+ 
++
++#if defined(__APPLE__)
++// This patch defines the "uuid_string_t" type on OS X, which is
++// required if linking against Cocoa (this occurs in Stone of Orthanc)
++#include <sys/_types.h>
++#include <sys/_types/_uuid_t.h>
++
++#ifndef _UUID_STRING_T
++#define _UUID_STRING_T
++typedef __darwin_uuid_string_t  uuid_string_t;
++#endif /* _UUID_STRING_T */
++#endif
++
++
+ #include <sys/types.h>
+ #ifndef _WIN32
+ #include <sys/time.h>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/e2fsprogs-1.44.5-apple.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,24 @@
+diff -urEb e2fsprogs-1.44.5.orig/lib/uuid/uuid.h.in e2fsprogs-1.44.5/lib/uuid/uuid.h.in
+--- e2fsprogs-1.44.5.orig/lib/uuid/uuid.h.in	2019-02-21 20:17:23.461402522 +0100
++++ e2fsprogs-1.44.5/lib/uuid/uuid.h.in	2019-02-21 20:25:05.664540445 +0100
+@@ -35,6 +35,20 @@
+ #ifndef _UUID_UUID_H
+ #define _UUID_UUID_H
+ 
++
++#if defined(__APPLE__)
++// This patch defines the "uuid_string_t" type on OS X, which is
++// required if linking against Cocoa (this occurs in Stone of Orthanc)
++#include <sys/_types.h>
++#include <sys/_types/_uuid_t.h>
++
++#ifndef _UUID_STRING_T
++#define _UUID_STRING_T
++typedef __darwin_uuid_string_t  uuid_string_t;
++#endif /* _UUID_STRING_T */
++#endif
++
++
+ #include <sys/types.h>
+ #ifndef _WIN32
+ #include <sys/time.h>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/libp11-0.4.0.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,68 @@
+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	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/atfork.c	2020-04-02 17:04:10.152619121 +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	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/engine.h	2020-04-02 17:04:10.152619121 +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	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/libp11-int.h	2020-04-02 17:04:10.152619121 +0200
+@@ -20,7 +20,7 @@
+ #define _LIBP11_INT_H
+ 
+ #ifndef _WIN32
+-#include "config.h"
++//#include "config.h"
+ #endif
+ 
+ #include "libp11.h"
+diff -urEb libp11-0.4.0.orig/src/p11_key.c libp11-0.4.0/src/p11_key.c
+--- libp11-0.4.0.orig/src/p11_key.c	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/p11_key.c	2020-04-02 17:05:39.892516032 +0200
+@@ -21,6 +21,12 @@
+ #include <string.h>
+ #include <openssl/bn.h>
+ 
++#if OPENSSL_VERSION_NUMBER >= 0x10100105L // File renamed in OpenSSL 1.1.1e
++#  include <crypto/rsa/rsa_local.h>
++#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.0.2
++#  include <crypto/rsa/rsa_locl.h>
++#endif
++
+ #ifdef _WIN32
+ #define strncasecmp strnicmp
+ #endif
+diff -urEb libp11-0.4.0.orig/src/p11_rsa.c libp11-0.4.0/src/p11_rsa.c
+--- libp11-0.4.0.orig/src/p11_rsa.c	2020-04-02 17:03:55.340634019 +0200
++++ libp11-0.4.0/src/p11_rsa.c	2020-04-02 17:05:49.176504198 +0200
+@@ -27,6 +27,12 @@
+ #include <openssl/evp.h>
+ #include <openssl/rsa.h>
+ 
++#if OPENSSL_VERSION_NUMBER >= 0x10100105L // File renamed in OpenSSL 1.1.1e
++#  include <crypto/rsa/rsa_local.h>
++#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.0.2
++#  include <crypto/rsa/rsa_locl.h>
++#endif
++
+ static int rsa_ex_index = 0;
+ 
+ #if OPENSSL_VERSION_NUMBER < 0x10100003L
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/mongoose-3.1-patch.diff	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,54 @@
+--- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c	2012-03-11 23:41:35.000000000 +0100
++++ mongoose.c	2013-03-07 10:07:00.566266153 +0100
+@@ -92,8 +92,9 @@
+ #define strtoll(x, y, z) strtol(x, y, z)
+ #else
+ #define __func__  __FUNCTION__
+-#define strtoull(x, y, z) _strtoui64(x, y, z)
+-#define strtoll(x, y, z) _strtoi64(x, y, z)
++#include <stdlib.h>
++//#define strtoull(x, y, z) _strtoui64(x, y, z)
++//#define strtoll(x, y, z) _strtoi64(x, y, z)
+ #endif // _MSC_VER
+ 
+ #define ERRNO   GetLastError()
+@@ -253,6 +254,14 @@
+ #define MSG_NOSIGNAL 0
+ #endif
+ 
++#if __gnu_hurd__ == 1
++/**
++ * There is no limit on the length on a path under GNU Hurd, so we set
++ * it to an arbitrary constant.
++ **/
++#define PATH_MAX 4096
++#endif
++
+ typedef void * (*mg_thread_func_t)(void *);
+ 
+ static const char *http_500_error = "Internal Server Error";
+@@ -3844,10 +3853,8 @@
+ }
+ 
+ static void discard_current_request_from_buffer(struct mg_connection *conn) {
+-  char *buffered;
+   int buffered_len, body_len;
+ 
+-  buffered = conn->buf + conn->request_len;
+   buffered_len = conn->data_len - conn->request_len;
+   assert(buffered_len >= 0);
+ 
+@@ -4148,7 +4155,13 @@
+ 
+   // Wait until mg_fini() stops
+   while (ctx->stop_flag != 2) {
++#if defined(__linux__)
++    usleep(100000);
++#elif defined(_WIN32)
++    Sleep(100);
++#else
+     (void) sleep(0);
++#endif
+   }
+   free_context(ctx);
+ 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/mongoose-3.8-patch.diff	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,106 @@
+--- mongoose.c.orig	2019-07-31 14:06:36.043726677 +0200
++++ mongoose.c	2019-07-31 14:10:06.767727652 +0200
+@@ -50,6 +50,14 @@
+ #define PATH_MAX FILENAME_MAX
+ #endif // __SYMBIAN32__
+ 
++#if __gnu_hurd__ == 1
++/**
++ * There is no limit on the length on a path under GNU Hurd, so we set
++ * it to an arbitrary constant.
++ **/
++#define PATH_MAX 4096
++#endif
++
+ #ifndef _WIN32_WCE // Some ANSI #includes are not available on Windows CE
+ #include <sys/types.h>
+ #include <sys/stat.h>
+@@ -108,8 +116,9 @@
+ #define strtoll(x, y, z) _atoi64(x)
+ #else
+ #define __func__  __FUNCTION__
+-#define strtoull(x, y, z) _strtoui64(x, y, z)
+-#define strtoll(x, y, z) _strtoi64(x, y, z)
++#include <stdlib.h>
++//#define strtoull(x, y, z) _strtoui64(x, y, z)
++//#define strtoll(x, y, z) _strtoi64(x, y, z)
+ #endif // _MSC_VER
+ 
+ #define ERRNO   GetLastError()
+@@ -2997,19 +3006,19 @@
+   }
+ }
+ 
+-static int is_valid_http_method(const char *method) {
+-  return !strcmp(method, "GET") || !strcmp(method, "POST") ||
++static int is_valid_http_method(const char *method, int *isValidHttpMethod) {
++  *isValidHttpMethod = !strcmp(method, "GET") || !strcmp(method, "POST") ||
+     !strcmp(method, "HEAD") || !strcmp(method, "CONNECT") ||
+     !strcmp(method, "PUT") || !strcmp(method, "DELETE") ||
+     !strcmp(method, "OPTIONS") || !strcmp(method, "PROPFIND")
+-    || !strcmp(method, "MKCOL")
+-          ;
++    || !strcmp(method, "MKCOL");
++  return *isValidHttpMethod;
+ }
+ 
+ // Parse HTTP request, fill in mg_request_info structure.
+ // This function modifies the buffer by NUL-terminating
+ // HTTP request components, header names and header values.
+-static int parse_http_message(char *buf, int len, struct mg_request_info *ri) {
++static int parse_http_message(char *buf, int len, struct mg_request_info *ri, int *isValidHttpMethod) {
+   int is_request, request_length = get_request_len(buf, len);
+   if (request_length > 0) {
+     // Reset attributes. DO NOT TOUCH is_ssl, remote_ip, remote_port
+@@ -3025,7 +3034,7 @@
+     ri->request_method = skip(&buf, " ");
+     ri->uri = skip(&buf, " ");
+     ri->http_version = skip(&buf, "\r\n");
+-    if (((is_request = is_valid_http_method(ri->request_method)) &&
++    if (((is_request = is_valid_http_method(ri->request_method, isValidHttpMethod)) &&
+          memcmp(ri->http_version, "HTTP/", 5) != 0) ||
+         (!is_request && memcmp(ri->request_method, "HTTP/", 5)) != 0) {
+       request_length = -1;
+@@ -4930,7 +4939,7 @@
+   return uri[0] == '/' || (uri[0] == '*' && uri[1] == '\0');
+ }
+ 
+-static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len) {
++static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *isValidHttpMethod) {
+   const char *cl;
+ 
+   ebuf[0] = '\0';
+@@ -4944,7 +4953,7 @@
+   } else if (conn->request_len <= 0) {
+     snprintf(ebuf, ebuf_len, "%s", "Client closed connection");
+   } else if (parse_http_message(conn->buf, conn->buf_size,
+-                                &conn->request_info) <= 0) {
++                                &conn->request_info, isValidHttpMethod) <= 0) {
+     snprintf(ebuf, ebuf_len, "Bad request: [%.*s]", conn->data_len, conn->buf);
+   } else {
+     // Request is valid
+@@ -4973,7 +4982,8 @@
+   } else if (mg_vprintf(conn, fmt, ap) <= 0) {
+     snprintf(ebuf, ebuf_len, "%s", "Error sending request");
+   } else {
+-    getreq(conn, ebuf, ebuf_len);
++    int isValidHttpMethod = 1; /* unused in this case */
++    getreq(conn, ebuf, ebuf_len, &isValidHttpMethod);
+   }
+   if (ebuf[0] != '\0' && conn != NULL) {
+     mg_close_connection(conn);
+@@ -4995,8 +5005,13 @@
+   // to crule42.
+   conn->data_len = 0;
+   do {
+-    if (!getreq(conn, ebuf, sizeof(ebuf))) {
++    int isValidHttpMethod = 1;
++    if (!getreq(conn, ebuf, sizeof(ebuf), &isValidHttpMethod)) {
++      if (isValidHttpMethod) {
+       send_http_error(conn, 500, "Server Error", "%s", ebuf);
++      } else {
++        send_http_error(conn, 400, "Bad Request", "%s", ebuf);
++      }
+       conn->must_close = 1;
+     } else if (!is_valid_uri(conn->request_info.uri)) {
+       snprintf(ebuf, sizeof(ebuf), "Invalid URI: [%s]", ri->uri);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/openssl-1.1.1-conf.h.in	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,122 @@
+/*
+ * {- join("\n * ", @autowarntext) -}
+ *
+ * Copyright 2016-2018 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <openssl/opensslv.h>
+
+#ifdef  __cplusplus
+extern "C" {
+#endif
+
+#ifdef OPENSSL_ALGORITHM_DEFINES
+# error OPENSSL_ALGORITHM_DEFINES no longer supported
+#endif
+
+
+/*
+ * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers
+ * don't like that.  This will hopefully silence them.
+ */
+#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy;
+
+/*
+ * Applications should use -DOPENSSL_API_COMPAT=<version> to suppress the
+ * declarations of functions deprecated in or before <version>. Otherwise, they
+ * still won't see them if the library has been built to disable deprecated
+ * functions.
+ */
+#ifndef DECLARE_DEPRECATED
+# define DECLARE_DEPRECATED(f)   f;
+# ifdef __GNUC__
+#  if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0)
+#   undef DECLARE_DEPRECATED
+#   define DECLARE_DEPRECATED(f)    f __attribute__ ((deprecated));
+#  endif
+# endif
+#endif
+
+#ifndef OPENSSL_FILE
+# ifdef OPENSSL_NO_FILENAMES
+#  define OPENSSL_FILE ""
+#  define OPENSSL_LINE 0
+# else
+#  define OPENSSL_FILE __FILE__
+#  define OPENSSL_LINE __LINE__
+# endif
+#endif
+
+#ifndef OPENSSL_MIN_API
+# define OPENSSL_MIN_API 0
+#endif
+
+#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API
+# undef OPENSSL_API_COMPAT
+# define OPENSSL_API_COMPAT OPENSSL_MIN_API
+#endif
+
+/*
+ * Do not deprecate things to be deprecated in version 1.2.0 before the
+ * OpenSSL version number matches.
+ */
+#if OPENSSL_VERSION_NUMBER < 0x10200000L
+# define DEPRECATEDIN_1_2_0(f)   f;
+#elif OPENSSL_API_COMPAT < 0x10200000L
+# define DEPRECATEDIN_1_2_0(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_1_2_0(f)
+#endif
+
+#if OPENSSL_API_COMPAT < 0x10100000L
+# define DEPRECATEDIN_1_1_0(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_1_1_0(f)
+#endif
+
+#if OPENSSL_API_COMPAT < 0x10000000L
+# define DEPRECATEDIN_1_0_0(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_1_0_0(f)
+#endif
+
+#if OPENSSL_API_COMPAT < 0x00908000L
+# define DEPRECATEDIN_0_9_8(f)   DECLARE_DEPRECATED(f)
+#else
+# define DEPRECATEDIN_0_9_8(f)
+#endif
+
+
+#define OPENSSL_UNISTD <unistd.h>
+
+#if 0
+/* Generate 80386 code? */
+{- ${processor} eq "386" ? "#define" : "#undef" -} I386_ONLY
+
+#undef OPENSSL_UNISTD
+#define OPENSSL_UNISTD {- ${unistd} -}
+
+{- ${export_var_as_fn} ? "#define" : "#undef" -} OPENSSL_EXPORT_VAR_AS_FUNCTION
+
+/*
+ * The following are cipher-specific, but are part of the public API.
+ */
+#if !defined(OPENSSL_SYS_UEFI)
+{- ${bn_ll} ? "# define" : "# undef" -} BN_LLONG
+/* Only one for the following should be defined */
+{- ${b64l} ? "# define" : "# undef" -} SIXTY_FOUR_BIT_LONG
+{- ${b64}  ? "# define" : "# undef" -} SIXTY_FOUR_BIT
+{- ${b32}  ? "# define" : "# undef" -} THIRTY_TWO_BIT
+#endif
+
+#define RC4_INT {- ${rc4_int} -}
+#endif
+
+#ifdef  __cplusplus
+}
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Patches/openssl-1.1.1g.patch	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,19 @@
+diff -urEb openssl-1.1.1g.orig/crypto/rand/rand_unix.c openssl-1.1.1g/crypto/rand/rand_unix.c
+--- openssl-1.1.1g.orig/crypto/rand/rand_unix.c	2020-05-05 17:58:08.785998440 +0200
++++ openssl-1.1.1g/crypto/rand/rand_unix.c	2020-05-05 17:58:55.201881117 +0200
+@@ -445,6 +445,7 @@
+              * system call and this should always succeed which renders
+              * this alternative but essentially identical source moot.
+              */
++#if !defined(__LSB_VERSION__)  // "syscall()" is not available in LSB
+             if (uname(&un) == 0) {
+                 kernel[0] = atoi(un.release);
+                 p = strchr(un.release, '.');
+@@ -455,6 +456,7 @@
+                     return 0;
+                 }
+             }
++#endif
+             /* Open /dev/random and wait for it to be readable */
+             if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) {
+                 if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/RetrieveCACertificates.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,71 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 subprocess
+import urllib2
+
+
+if len(sys.argv) <= 2:
+    print('Download a set of CA certificates, convert them to PEM, then format them as a C macro')
+    print('Usage: %s [Macro] [Certificate1] <Certificate2>...' % sys.argv[0])
+    print('')
+    print('Example: %s BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt' % sys.argv[0])
+    print('')
+    sys.exit(-1)
+
+MACRO = sys.argv[1]
+
+sys.stdout.write('#define %s ' % MACRO)
+
+for url in sys.argv[2:]:
+    # Download the certificate from the CA authority, in the DES format
+    des = urllib2.urlopen(url).read()
+
+    # Convert DES to PEM
+    p = subprocess.Popen([ 'openssl', 'x509', '-inform', 'DES', '-outform', 'PEM' ],
+                         stdin = subprocess.PIPE,
+                         stdout = subprocess.PIPE)
+    pem = p.communicate(input = des)[0]
+    pem = re.sub(r'\r', '', pem)       # Remove any carriage return
+    pem = re.sub(r'\\', r'\\\\', pem)  # Escape any backslash
+    pem = re.sub(r'"', r'\\"', pem)    # Escape any quote
+
+    # Write the PEM data into the macro
+    for line in pem.split('\n'):
+        sys.stdout.write(' \\\n')
+        sys.stdout.write('"%s\\n" ' % line)
+
+sys.stdout.write('\n')
+sys.stderr.write('Done!\n')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/Samples/MicroService/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Samples/MicroService/README.txt	Wed Jun 10 20:30:34 2020 +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/OrthancFramework/Resources/Samples/MicroService/Sample.cpp	Wed Jun 10 20:30:34 2020 +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;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/VisualStudio/stdint.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +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-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_ ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/base64/base64.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,179 @@
+/* 
+   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
+
+   ------------------------------
+   This version has been modified (changed the interface + use another decoding algorithm
+   inspired from https://stackoverflow.com/a/34571089 which was faster)
+*/
+
+#include "base64.h"
+#include <string.h>
+#include <vector>
+
+static const std::string base64_chars = 
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+    "abcdefghijklmnopqrstuvwxyz"
+    "0123456789+/";
+
+static inline bool is_base64(unsigned char c) {
+  return (isalnum(c) || (c == '+') || (c == '/'));
+}
+
+void base64_encode(std::string& result, const std::string& stringToEncode)
+{
+  const unsigned char* bytes_to_encode = reinterpret_cast<const unsigned char*>
+      (stringToEncode.size() > 0 ? &stringToEncode[0] : NULL);
+  size_t in_len = stringToEncode.size();
+  
+  result.reserve(result.size() + in_len * 4 / 3 + 10);
+
+  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++)
+        result += 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++)
+      result += base64_chars[char_array_4[j]];
+
+    while((i++ < 3))
+      result += '=';
+
+  }
+}
+
+// old code from Ren Nyffenegger.  This code is slower
+void base64_decode_old(std::string& result, const std::string& encoded_string) {
+  size_t in_len = encoded_string.size();
+  int i = 0;
+  int j = 0;
+  int in_ = 0;
+  unsigned char char_array_4[4], char_array_3[3];
+
+  result.reserve(result.size() + in_len * 3 / 4 + 10);
+
+  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++)
+        result += 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++)
+      result += char_array_3[j];
+  }
+}
+
+
+// new code from https://stackoverflow.com/a/34571089
+// note that the encoding algorithm from this page was slower (and bugged !)
+// this code is not using std::vector::find
+
+// static init equivalent to:
+// decode_indexes.assign(256, -1);
+// for (int i=0; i<64; ++i)
+//   decode_indexes[base64_chars[i]] = i;
+
+static const int decode_indexes[] = {
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+  52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
+  -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
+  15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+  -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+  41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
+};
+
+
+void base64_decode(std::string& result, const std::string &stringToDecode) {
+
+  result.reserve(result.size() + stringToDecode.size() * 3 / 4 + 10);
+
+  int val=0, valb=-8;
+  for (std::string::const_iterator c = stringToDecode.begin(); c != stringToDecode.end(); ++c)
+  {
+    size_t index = static_cast<size_t>(*c);
+    if (decode_indexes[index] == -1)
+      break;
+    val = (val<<6) + decode_indexes[index];
+    valb += 6;
+    if (valb>=0) {
+      result.push_back(char((val>>valb)&0xFF));
+      valb-=8;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/base64/base64.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,4 @@
+#include <string>
+
+void base64_encode(std::string& result, const std::string& stringToEncode);
+void base64_decode(std::string& result, const std::string& s);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/icu/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,126 @@
+cmake_minimum_required(VERSION 2.8)
+project(IcuCodeGeneration)
+
+set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
+
+if (NOT USE_LEGACY_LIBICU)
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
+endif()
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/Compiler.cmake)
+include(${CMAKE_SOURCE_DIR}/../../CMake/DownloadPackage.cmake)
+include(Version.cmake)
+
+set(SOURCE_DATA
+  "${LIBICU_SOURCES_DIR}/source/data/in/${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat")
+
+set(ALLOW_DOWNLOADS ON)
+DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
+
+include_directories(
+  ${LIBICU_SOURCES_DIR}/source/common
+  ${LIBICU_SOURCES_DIR}/source/i18n
+  ${LIBICU_SOURCES_DIR}/source/tools/toolutil/
+  )
+
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/common         LIBICU_SOURCES)
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n           LIBICU_SOURCES)
+aux_source_directory(${LIBICU_SOURCES_DIR}/source/tools/toolutil LIBICU_SOURCES)
+
+if (USE_LEGACY_LIBICU)
+  list(APPEND LIBICU_SOURCES
+    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.c
+    )
+else()
+  list(APPEND LIBICU_SOURCES
+    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.cpp
+    )
+  set_source_files_properties(
+    ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
+    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
+    )
+endif()
+
+
+
+add_executable(IcuCodeGeneration
+  ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
+  ${LIBICU_SOURCES}
+  )
+
+configure_file(${SOURCE_DATA}
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
+  COPYONLY)
+
+add_custom_command(
+  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
+  COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
+  DEPENDS  IcuCodeGeneration
+  )
+
+# "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
+add_custom_command(
+  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  COMMAND  gzip ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.c --no-name -c > ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  DEPENDS  ${LIBICU_DATA}
+  )
+
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
+  # Generate a precompiled version for Visual Studio 64bit
+  set(TMP_ASSEMBLER      ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.S)
+  set(TMP_OBJECT         ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.o)
+  set(TMP_LIBRARY        ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.lib)
+  set(PRECOMPILED_WIN64  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.lib.gz)
+  
+  add_custom_command(
+    OUTPUT   ${TMP_ASSEMBLER}
+    COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat --assembly gcc-mingw64
+    DEPENDS  IcuCodeGeneration
+    )
+
+  add_custom_command(
+    OUTPUT   ${TMP_OBJECT}
+    COMMAND  x86_64-w64-mingw32-gcc -c ${TMP_ASSEMBLER} -o ${TMP_OBJECT}
+    DEPENDS  ${TMP_ASSEMBLER}
+    )
+
+  add_custom_command(
+    OUTPUT   ${TMP_LIBRARY}
+    COMMAND  x86_64-w64-mingw32-ar qc ${TMP_LIBRARY} ${TMP_OBJECT}
+    COMMAND  x86_64-w64-mingw32-ranlib ${TMP_LIBRARY}
+    DEPENDS  ${TMP_OBJECT}
+    )
+
+  # "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
+  add_custom_command(
+    OUTPUT   ${PRECOMPILED_WIN64}
+    COMMAND  gzip ${TMP_LIBRARY} --no-name -c > ${PRECOMPILED_WIN64}
+    DEPENDS  ${TMP_LIBRARY}
+    )
+endif()
+
+
+add_custom_target(Final ALL DEPENDS
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  ${PRECOMPILED_WIN64}
+  )
+
+install(
+  FILES
+  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
+  ${PRECOMPILED_WIN64}
+  DESTINATION ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads
+  )
+
+add_definitions(
+  #-DU_COMBINED_IMPLEMENTATION
+  -DUCONFIG_NO_SERVICE=1
+  -DU_COMMON_IMPLEMENTATION
+  -DU_ENABLE_DYLOAD=0
+  -DU_HAVE_STD_STRING=1
+  -DU_I18N_IMPLEMENTATION
+  -DU_IO_IMPLEMENTATION
+  -DU_STATIC_IMPLEMENTATION=1
+  -DU_TOOLUTIL_IMPLEMENTATION
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/icu/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,36 @@
+Generating ICU data file
+========================
+
+This folder generates the "icudtXXX_dat.c" file that contains the
+resources internal to ICU.
+
+IMPORTANT: Since ICU 59, C++11 is mandatory, making it incompatible
+with Linux Standard Base (LSB) SDK. The option
+"-DUSE_LEGACY_LIBICU=ON" will use the latest version of ICU that does
+not use C++11 (58-2).
+
+
+Usage
+-----
+
+Newest release of icu:
+
+$ cmake .. -G Ninja && ninja install
+
+Legacy version suitable for LSB:
+
+$ cmake .. -G Ninja -DUSE_LEGACY_LIBICU=ON && ninja install
+
+Legacy version, compiled using LSB:
+
+$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -G Ninja \
+  -DCMAKE_TOOLCHAIN_FILE=../../../LinuxStandardBaseToolchain.cmake \
+  -DUSE_LEGACY_LIBICU=ON
+$ ninja install
+
+
+Result
+------
+
+The resulting files are placed in the "ThirdPartyDownloads" folder at
+the root of the Orthanc repository (next to the main "CMakeLists.txt").
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/icu/Version.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,34 @@
+# NB: Orthanc assume that the platform is of ASCII-family, not of
+# EBCDIC-family. A "e" suffix would be needed on EBCDIC. Look for
+# macro "U_ICUDATA_TYPE_LETTER" in the source code of icu for more
+# information.
+
+include(TestBigEndian)
+TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
+if(IS_BIG_ENDIAN)
+  set(LIBICU_SUFFIX "b")
+else()
+  set(LIBICU_SUFFIX "l")
+endif()
+
+set(LIBICU_BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
+
+if (USE_LEGACY_LIBICU)
+  # This is the latest version of icu that compiles without C++11
+  # support. It is used for Linux Standard Base and Visual Studio 2008.
+  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-58_2-src.tgz")
+  set(LIBICU_MD5 "fac212b32b7ec7ab007a12dff1f3aea1")
+  set(LIBICU_DATA_VERSION "icudt58")
+  set(LIBICU_DATA_COMPRESSED_MD5 "a39b07b38195158c6c3070332cef2173")
+  set(LIBICU_DATA_UNCOMPRESSED_MD5 "54d2593cec5c6a4469373231658153ce")
+else()
+  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-63_1-src.tgz")
+  set(LIBICU_MD5 "9e40f6055294284df958200e308bce50")
+  set(LIBICU_DATA_VERSION "icudt63")
+  set(LIBICU_DATA_COMPRESSED_MD5 "be495c0830de5f377fdfa8301a5faf3d")
+  set(LIBICU_DATA_UNCOMPRESSED_MD5 "99613c3f2ca9426c45dc554ad28cfb79")
+endif()
+
+set(LIBICU_SOURCES_DIR ${CMAKE_BINARY_DIR}/icu)
+set(LIBICU_DATA "${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat.gz")
+set(LIBICU_DATA_URL "${LIBICU_BASE_URL}/${LIBICU_DATA}")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/md5/md5.c	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,381 @@
+/*
+  Copyright (C) 1999, 2000, 2002 Aladdin Enterprises.  All rights reserved.
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  L. Peter Deutsch
+  ghost@aladdin.com
+
+ */
+/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */
+/*
+  Independent implementation of MD5 (RFC 1321).
+
+  This code implements the MD5 Algorithm defined in RFC 1321, whose
+  text is available at
+	http://www.ietf.org/rfc/rfc1321.txt
+  The code is derived from the text of the RFC, including the test suite
+  (section A.5) but excluding the rest of Appendix A.  It does not include
+  any code or documentation that is identified in the RFC as being
+  copyrighted.
+
+  The original and principal author of md5.c is L. Peter Deutsch
+  <ghost@aladdin.com>.  Other authors are noted in the change history
+  that follows (in reverse chronological order):
+
+  2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order
+	either statically or dynamically; added missing #include <string.h>
+	in library.
+  2002-03-11 lpd Corrected argument list for main(), and added int return
+	type, in test program and T value program.
+  2002-02-21 lpd Added missing #include <stdio.h> in test program.
+  2000-07-03 lpd Patched to eliminate warnings about "constant is
+	unsigned in ANSI C, signed in traditional"; made test program
+	self-checking.
+  1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
+  1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5).
+  1999-05-03 lpd Original version.
+ */
+
+#include "md5.h"
+#include <string.h>
+
+#undef BYTE_ORDER	/* 1 = big-endian, -1 = little-endian, 0 = unknown */
+#ifdef ARCH_IS_BIG_ENDIAN
+#  define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1)
+#else
+#  define BYTE_ORDER 0
+#endif
+
+#define T_MASK ((md5_word_t)~0)
+#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87)
+#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9)
+#define T3    0x242070db
+#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111)
+#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050)
+#define T6    0x4787c62a
+#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec)
+#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe)
+#define T9    0x698098d8
+#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850)
+#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e)
+#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841)
+#define T13    0x6b901122
+#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c)
+#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71)
+#define T16    0x49b40821
+#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d)
+#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf)
+#define T19    0x265e5a51
+#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855)
+#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2)
+#define T22    0x02441453
+#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e)
+#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437)
+#define T25    0x21e1cde6
+#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829)
+#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278)
+#define T28    0x455a14ed
+#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa)
+#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07)
+#define T31    0x676f02d9
+#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375)
+#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd)
+#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e)
+#define T35    0x6d9d6122
+#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3)
+#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb)
+#define T38    0x4bdecfa9
+#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f)
+#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f)
+#define T41    0x289b7ec6
+#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805)
+#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a)
+#define T44    0x04881d05
+#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6)
+#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a)
+#define T47    0x1fa27cf8
+#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a)
+#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb)
+#define T50    0x432aff97
+#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58)
+#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6)
+#define T53    0x655b59c3
+#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d)
+#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82)
+#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e)
+#define T57    0x6fa87e4f
+#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f)
+#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb)
+#define T60    0x4e0811a1
+#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d)
+#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca)
+#define T63    0x2ad7d2bb
+#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e)
+
+
+static void
+md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/)
+{
+    md5_word_t
+	a = pms->abcd[0], b = pms->abcd[1],
+	c = pms->abcd[2], d = pms->abcd[3];
+    md5_word_t t;
+#if BYTE_ORDER > 0
+    /* Define storage only for big-endian CPUs. */
+    md5_word_t X[16];
+#else
+    /* Define storage for little-endian or both types of CPUs. */
+    md5_word_t xbuf[16];
+    const md5_word_t *X;
+#endif
+
+    {
+#if BYTE_ORDER == 0
+	/*
+	 * Determine dynamically whether this is a big-endian or
+	 * little-endian machine, since we can use a more efficient
+	 * algorithm on the latter.
+	 */
+	static const int w = 1;
+
+	if (*((const md5_byte_t *)&w)) /* dynamic little-endian */
+#endif
+#if BYTE_ORDER <= 0		/* little-endian */
+	{
+	    /*
+	     * On little-endian machines, we can process properly aligned
+	     * data without copying it.
+	     */
+	    if (!((data - (const md5_byte_t *)0) & 3)) {
+		/* data are properly aligned */
+		X = (const md5_word_t *)data;
+	    } else {
+		/* not aligned */
+		memcpy(xbuf, data, 64);
+		X = xbuf;
+	    }
+	}
+#endif
+#if BYTE_ORDER == 0
+	else			/* dynamic big-endian */
+#endif
+#if BYTE_ORDER >= 0		/* big-endian */
+	{
+	    /*
+	     * On big-endian machines, we must arrange the bytes in the
+	     * right order.
+	     */
+	    const md5_byte_t *xp = data;
+	    int i;
+
+#  if BYTE_ORDER == 0
+	    X = xbuf;		/* (dynamic only) */
+#  else
+#    define xbuf X		/* (static only) */
+#  endif
+	    for (i = 0; i < 16; ++i, xp += 4)
+		xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24);
+	}
+#endif
+    }
+
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
+
+    /* Round 1. */
+    /* Let [abcd k s i] denote the operation
+       a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */
+#define F(x, y, z) (((x) & (y)) | (~(x) & (z)))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + F(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+    /* Do the following 16 operations. */
+    SET(a, b, c, d,  0,  7,  T1);
+    SET(d, a, b, c,  1, 12,  T2);
+    SET(c, d, a, b,  2, 17,  T3);
+    SET(b, c, d, a,  3, 22,  T4);
+    SET(a, b, c, d,  4,  7,  T5);
+    SET(d, a, b, c,  5, 12,  T6);
+    SET(c, d, a, b,  6, 17,  T7);
+    SET(b, c, d, a,  7, 22,  T8);
+    SET(a, b, c, d,  8,  7,  T9);
+    SET(d, a, b, c,  9, 12, T10);
+    SET(c, d, a, b, 10, 17, T11);
+    SET(b, c, d, a, 11, 22, T12);
+    SET(a, b, c, d, 12,  7, T13);
+    SET(d, a, b, c, 13, 12, T14);
+    SET(c, d, a, b, 14, 17, T15);
+    SET(b, c, d, a, 15, 22, T16);
+#undef SET
+
+     /* Round 2. */
+     /* Let [abcd k s i] denote the operation
+          a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */
+#define G(x, y, z) (((x) & (z)) | ((y) & ~(z)))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + G(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+     /* Do the following 16 operations. */
+    SET(a, b, c, d,  1,  5, T17);
+    SET(d, a, b, c,  6,  9, T18);
+    SET(c, d, a, b, 11, 14, T19);
+    SET(b, c, d, a,  0, 20, T20);
+    SET(a, b, c, d,  5,  5, T21);
+    SET(d, a, b, c, 10,  9, T22);
+    SET(c, d, a, b, 15, 14, T23);
+    SET(b, c, d, a,  4, 20, T24);
+    SET(a, b, c, d,  9,  5, T25);
+    SET(d, a, b, c, 14,  9, T26);
+    SET(c, d, a, b,  3, 14, T27);
+    SET(b, c, d, a,  8, 20, T28);
+    SET(a, b, c, d, 13,  5, T29);
+    SET(d, a, b, c,  2,  9, T30);
+    SET(c, d, a, b,  7, 14, T31);
+    SET(b, c, d, a, 12, 20, T32);
+#undef SET
+
+     /* Round 3. */
+     /* Let [abcd k s t] denote the operation
+          a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + H(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+     /* Do the following 16 operations. */
+    SET(a, b, c, d,  5,  4, T33);
+    SET(d, a, b, c,  8, 11, T34);
+    SET(c, d, a, b, 11, 16, T35);
+    SET(b, c, d, a, 14, 23, T36);
+    SET(a, b, c, d,  1,  4, T37);
+    SET(d, a, b, c,  4, 11, T38);
+    SET(c, d, a, b,  7, 16, T39);
+    SET(b, c, d, a, 10, 23, T40);
+    SET(a, b, c, d, 13,  4, T41);
+    SET(d, a, b, c,  0, 11, T42);
+    SET(c, d, a, b,  3, 16, T43);
+    SET(b, c, d, a,  6, 23, T44);
+    SET(a, b, c, d,  9,  4, T45);
+    SET(d, a, b, c, 12, 11, T46);
+    SET(c, d, a, b, 15, 16, T47);
+    SET(b, c, d, a,  2, 23, T48);
+#undef SET
+
+     /* Round 4. */
+     /* Let [abcd k s t] denote the operation
+          a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */
+#define I(x, y, z) ((y) ^ ((x) | ~(z)))
+#define SET(a, b, c, d, k, s, Ti)\
+  t = a + I(b,c,d) + X[k] + Ti;\
+  a = ROTATE_LEFT(t, s) + b
+     /* Do the following 16 operations. */
+    SET(a, b, c, d,  0,  6, T49);
+    SET(d, a, b, c,  7, 10, T50);
+    SET(c, d, a, b, 14, 15, T51);
+    SET(b, c, d, a,  5, 21, T52);
+    SET(a, b, c, d, 12,  6, T53);
+    SET(d, a, b, c,  3, 10, T54);
+    SET(c, d, a, b, 10, 15, T55);
+    SET(b, c, d, a,  1, 21, T56);
+    SET(a, b, c, d,  8,  6, T57);
+    SET(d, a, b, c, 15, 10, T58);
+    SET(c, d, a, b,  6, 15, T59);
+    SET(b, c, d, a, 13, 21, T60);
+    SET(a, b, c, d,  4,  6, T61);
+    SET(d, a, b, c, 11, 10, T62);
+    SET(c, d, a, b,  2, 15, T63);
+    SET(b, c, d, a,  9, 21, T64);
+#undef SET
+
+     /* Then perform the following additions. (That is increment each
+        of the four registers by the value it had before this block
+        was started.) */
+    pms->abcd[0] += a;
+    pms->abcd[1] += b;
+    pms->abcd[2] += c;
+    pms->abcd[3] += d;
+}
+
+void
+md5_init(md5_state_t *pms)
+{
+    pms->count[0] = pms->count[1] = 0;
+    pms->abcd[0] = 0x67452301;
+    pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476;
+    pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301;
+    pms->abcd[3] = 0x10325476;
+}
+
+void
+md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes)
+{
+    const md5_byte_t *p = data;
+    int left = nbytes;
+    int offset = (pms->count[0] >> 3) & 63;
+    md5_word_t nbits = (md5_word_t)(nbytes << 3);
+
+    if (nbytes <= 0)
+	return;
+
+    /* Update the message length. */
+    pms->count[1] += nbytes >> 29;
+    pms->count[0] += nbits;
+    if (pms->count[0] < nbits)
+	pms->count[1]++;
+
+    /* Process an initial partial block. */
+    if (offset) {
+	int copy = (offset + nbytes > 64 ? 64 - offset : nbytes);
+
+	memcpy(pms->buf + offset, p, copy);
+	if (offset + copy < 64)
+	    return;
+	p += copy;
+	left -= copy;
+	md5_process(pms, pms->buf);
+    }
+
+    /* Process full blocks. */
+    for (; left >= 64; p += 64, left -= 64)
+	md5_process(pms, p);
+
+    /* Process a final partial block. */
+    if (left)
+	memcpy(pms->buf, p, left);
+}
+
+void
+md5_finish(md5_state_t *pms, md5_byte_t digest[16])
+{
+    static const md5_byte_t pad[64] = {
+	0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+    };
+    md5_byte_t data[8];
+    int i;
+
+    /* Save the length before padding. */
+    for (i = 0; i < 8; ++i)
+	data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3));
+    /* Pad to 56 bytes mod 64. */
+    md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1);
+    /* Append the length. */
+    md5_append(pms, data, 8);
+    for (i = 0; i < 16; ++i)
+	digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/md5/md5.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,91 @@
+/*
+  Copyright (C) 1999, 2002 Aladdin Enterprises.  All rights reserved.
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+  L. Peter Deutsch
+  ghost@aladdin.com
+
+ */
+/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */
+/*
+  Independent implementation of MD5 (RFC 1321).
+
+  This code implements the MD5 Algorithm defined in RFC 1321, whose
+  text is available at
+	http://www.ietf.org/rfc/rfc1321.txt
+  The code is derived from the text of the RFC, including the test suite
+  (section A.5) but excluding the rest of Appendix A.  It does not include
+  any code or documentation that is identified in the RFC as being
+  copyrighted.
+
+  The original and principal author of md5.h is L. Peter Deutsch
+  <ghost@aladdin.com>.  Other authors are noted in the change history
+  that follows (in reverse chronological order):
+
+  2002-04-13 lpd Removed support for non-ANSI compilers; removed
+	references to Ghostscript; clarified derivation from RFC 1321;
+	now handles byte order either statically or dynamically.
+  1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
+  1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5);
+	added conditionalization for C++ compilation from Martin
+	Purschke <purschke@bnl.gov>.
+  1999-05-03 lpd Original version.
+ */
+
+#ifndef md5_INCLUDED
+#  define md5_INCLUDED
+
+/*
+ * This package supports both compile-time and run-time determination of CPU
+ * byte order.  If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be
+ * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is
+ * defined as non-zero, the code will be compiled to run only on big-endian
+ * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to
+ * run on either big- or little-endian CPUs, but will run slightly less
+ * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined.
+ */
+
+typedef unsigned char md5_byte_t; /* 8-bit byte */
+typedef unsigned int md5_word_t; /* 32-bit word */
+
+/* Define the state of the MD5 Algorithm. */
+typedef struct md5_state_s {
+    md5_word_t count[2];	/* message length in bits, lsw first */
+    md5_word_t abcd[4];		/* digest buffer */
+    md5_byte_t buf[64];		/* accumulate block */
+} md5_state_t;
+
+#ifdef __cplusplus
+extern "C" 
+{
+#endif
+
+/* Initialize the algorithm. */
+void md5_init(md5_state_t *pms);
+
+/* Append a string to the message. */
+void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes);
+
+/* Finish the message and return the digest. */
+void md5_finish(md5_state_t *pms, md5_byte_t digest[16]);
+
+#ifdef __cplusplus
+}  /* end extern "C" */
+#endif
+
+#endif /* md5_INCLUDED */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/minizip/NOTES	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1 @@
+These files come from the "contrib/minizip" directory in zlib 1.2.11.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/minizip/crypt.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,131 @@
+/* crypt.h -- base code for crypt/uncrypt ZIPfile
+
+
+   Version 1.01e, February 12th, 2005
+
+   Copyright (C) 1998-2005 Gilles Vollant
+
+   This code is a modified version of crypting code in Infozip distribution
+
+   The encryption/decryption parts of this source code (as opposed to the
+   non-echoing password parts) were originally written in Europe.  The
+   whole source package can be freely distributed, including from the USA.
+   (Prior to January 2000, re-export from the US was a violation of US law.)
+
+   This encryption code is a direct transcription of the algorithm from
+   Roger Schlafly, described by Phil Katz in the file appnote.txt.  This
+   file (appnote.txt) is distributed with the PKZIP program (even in the
+   version without encryption capabilities).
+
+   If you don't need crypting in your application, just define symbols
+   NOCRYPT and NOUNCRYPT.
+
+   This code support the "Traditional PKWARE Encryption".
+
+   The new AES encryption added on Zip format by Winzip (see the page
+   http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong
+   Encryption is not supported.
+*/
+
+#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8))
+
+/***********************************************************************
+ * Return the next byte in the pseudo-random sequence
+ */
+static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab)
+{
+    unsigned temp;  /* POTENTIAL BUG:  temp*(temp^1) may overflow in an
+                     * unpredictable manner on 16-bit systems; not a problem
+                     * with any known compiler so far, though */
+
+    temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2;
+    return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
+}
+
+/***********************************************************************
+ * Update the encryption keys with the next byte of plain text
+ */
+static int update_keys(unsigned long* pkeys,const z_crc_t* pcrc_32_tab,int c)
+{
+    (*(pkeys+0)) = CRC32((*(pkeys+0)), c);
+    (*(pkeys+1)) += (*(pkeys+0)) & 0xff;
+    (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1;
+    {
+      register int keyshift = (int)((*(pkeys+1)) >> 24);
+      (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift);
+    }
+    return c;
+}
+
+
+/***********************************************************************
+ * Initialize the encryption keys and the random header according to
+ * the given password.
+ */
+static void init_keys(const char* passwd,unsigned long* pkeys,const z_crc_t* pcrc_32_tab)
+{
+    *(pkeys+0) = 305419896L;
+    *(pkeys+1) = 591751049L;
+    *(pkeys+2) = 878082192L;
+    while (*passwd != '\0') {
+        update_keys(pkeys,pcrc_32_tab,(int)*passwd);
+        passwd++;
+    }
+}
+
+#define zdecode(pkeys,pcrc_32_tab,c) \
+    (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab)))
+
+#define zencode(pkeys,pcrc_32_tab,c,t) \
+    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c))
+
+#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED
+
+#define RAND_HEAD_LEN  12
+   /* "last resort" source for second part of crypt seed pattern */
+#  ifndef ZCR_SEED2
+#    define ZCR_SEED2 3141592654UL     /* use PI as default pattern */
+#  endif
+
+static int crypthead(const char* passwd,      /* password string */
+                     unsigned char* buf,      /* where to write header */
+                     int bufSize,
+                     unsigned long* pkeys,
+                     const z_crc_t* pcrc_32_tab,
+                     unsigned long crcForCrypting)
+{
+    int n;                       /* index in random header */
+    int t;                       /* temporary */
+    int c;                       /* random byte */
+    unsigned char header[RAND_HEAD_LEN-2]; /* random header */
+    static unsigned calls = 0;   /* ensure different random header each time */
+
+    if (bufSize<RAND_HEAD_LEN)
+      return 0;
+
+    /* First generate RAND_HEAD_LEN-2 random bytes. We encrypt the
+     * output of rand() to get less predictability, since rand() is
+     * often poorly implemented.
+     */
+    if (++calls == 1)
+    {
+        srand((unsigned)(time(NULL) ^ ZCR_SEED2));
+    }
+    init_keys(passwd, pkeys, pcrc_32_tab);
+    for (n = 0; n < RAND_HEAD_LEN-2; n++)
+    {
+        c = (rand() >> 7) & 0xff;
+        header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t);
+    }
+    /* Encrypt random header (last two bytes is high word of crc) */
+    init_keys(passwd, pkeys, pcrc_32_tab);
+    for (n = 0; n < RAND_HEAD_LEN-2; n++)
+    {
+        buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t);
+    }
+    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t);
+    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t);
+    return n;
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.c	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,247 @@
+/* ioapi.h -- IO base function header for compress/uncompress .zip
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+*/
+
+#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS)))
+        #define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#if defined(__APPLE__) || defined(IOAPI_NO_64)
+// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions
+#define FOPEN_FUNC(filename, mode) fopen(filename, mode)
+#define FTELLO_FUNC(stream) ftello(stream)
+#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin)
+#else
+#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)
+#define FTELLO_FUNC(stream) ftello64(stream)
+#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)
+#endif
+
+
+#include "ioapi.h"
+
+voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)
+{
+    if (pfilefunc->zfile_func64.zopen64_file != NULL)
+        return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);
+    else
+    {
+        return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode);
+    }
+}
+
+long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)
+{
+    if (pfilefunc->zfile_func64.zseek64_file != NULL)
+        return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);
+    else
+    {
+        uLong offsetTruncated = (uLong)offset;
+        if (offsetTruncated != offset)
+            return -1;
+        else
+            return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin);
+    }
+}
+
+ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)
+{
+    if (pfilefunc->zfile_func64.zseek64_file != NULL)
+        return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);
+    else
+    {
+        uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
+        if ((tell_uLong) == MAXU32)
+            return (ZPOS64_T)-1;
+        else
+            return tell_uLong;
+    }
+}
+
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32)
+{
+    p_filefunc64_32->zfile_func64.zopen64_file = NULL;
+    p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;
+    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
+    p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;
+    p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;
+    p_filefunc64_32->zfile_func64.ztell64_file = NULL;
+    p_filefunc64_32->zfile_func64.zseek64_file = NULL;
+    p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file;
+    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
+    p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque;
+    p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file;
+    p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file;
+}
+
+
+
+static voidpf  ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode));
+static uLong   ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size));
+static uLong   ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size));
+static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream));
+static long    ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
+static int     ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream));
+static int     ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream));
+
+static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode)
+{
+    FILE* file = NULL;
+    const char* mode_fopen = NULL;
+    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
+        mode_fopen = "rb";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
+        mode_fopen = "r+b";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
+        mode_fopen = "wb";
+
+    if ((filename!=NULL) && (mode_fopen != NULL))
+        file = fopen(filename, mode_fopen);
+    return file;
+}
+
+static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode)
+{
+    FILE* file = NULL;
+    const char* mode_fopen = NULL;
+    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
+        mode_fopen = "rb";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
+        mode_fopen = "r+b";
+    else
+    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
+        mode_fopen = "wb";
+
+    if ((filename!=NULL) && (mode_fopen != NULL))
+        file = FOPEN_FUNC((const char*)filename, mode_fopen);
+    return file;
+}
+
+
+static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size)
+{
+    uLong ret;
+    ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);
+    return ret;
+}
+
+static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size)
+{
+    uLong ret;
+    ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);
+    return ret;
+}
+
+static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream)
+{
+    long ret;
+    ret = ftell((FILE *)stream);
+    return ret;
+}
+
+
+static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream)
+{
+    ZPOS64_T ret;
+    ret = FTELLO_FUNC((FILE *)stream);
+    return ret;
+}
+
+static long ZCALLBACK fseek_file_func (voidpf  opaque, voidpf stream, uLong offset, int origin)
+{
+    int fseek_origin=0;
+    long ret;
+    switch (origin)
+    {
+    case ZLIB_FILEFUNC_SEEK_CUR :
+        fseek_origin = SEEK_CUR;
+        break;
+    case ZLIB_FILEFUNC_SEEK_END :
+        fseek_origin = SEEK_END;
+        break;
+    case ZLIB_FILEFUNC_SEEK_SET :
+        fseek_origin = SEEK_SET;
+        break;
+    default: return -1;
+    }
+    ret = 0;
+    if (fseek((FILE *)stream, offset, fseek_origin) != 0)
+        ret = -1;
+    return ret;
+}
+
+static long ZCALLBACK fseek64_file_func (voidpf  opaque, voidpf stream, ZPOS64_T offset, int origin)
+{
+    int fseek_origin=0;
+    long ret;
+    switch (origin)
+    {
+    case ZLIB_FILEFUNC_SEEK_CUR :
+        fseek_origin = SEEK_CUR;
+        break;
+    case ZLIB_FILEFUNC_SEEK_END :
+        fseek_origin = SEEK_END;
+        break;
+    case ZLIB_FILEFUNC_SEEK_SET :
+        fseek_origin = SEEK_SET;
+        break;
+    default: return -1;
+    }
+    ret = 0;
+
+    if(FSEEKO_FUNC((FILE *)stream, offset, fseek_origin) != 0)
+                        ret = -1;
+
+    return ret;
+}
+
+
+static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream)
+{
+    int ret;
+    ret = fclose((FILE *)stream);
+    return ret;
+}
+
+static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream)
+{
+    int ret;
+    ret = ferror((FILE *)stream);
+    return ret;
+}
+
+void fill_fopen_filefunc (pzlib_filefunc_def)
+  zlib_filefunc_def* pzlib_filefunc_def;
+{
+    pzlib_filefunc_def->zopen_file = fopen_file_func;
+    pzlib_filefunc_def->zread_file = fread_file_func;
+    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
+    pzlib_filefunc_def->ztell_file = ftell_file_func;
+    pzlib_filefunc_def->zseek_file = fseek_file_func;
+    pzlib_filefunc_def->zclose_file = fclose_file_func;
+    pzlib_filefunc_def->zerror_file = ferror_file_func;
+    pzlib_filefunc_def->opaque = NULL;
+}
+
+void fill_fopen64_filefunc (zlib_filefunc64_def*  pzlib_filefunc_def)
+{
+    pzlib_filefunc_def->zopen64_file = fopen64_file_func;
+    pzlib_filefunc_def->zread_file = fread_file_func;
+    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
+    pzlib_filefunc_def->ztell64_file = ftell64_file_func;
+    pzlib_filefunc_def->zseek64_file = fseek64_file_func;
+    pzlib_filefunc_def->zclose_file = fclose_file_func;
+    pzlib_filefunc_def->zerror_file = ferror_file_func;
+    pzlib_filefunc_def->opaque = NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/minizip/ioapi.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,208 @@
+/* ioapi.h -- IO base function header for compress/uncompress .zip
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         Changes
+
+    Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this)
+    Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux.
+               More if/def section may be needed to support other platforms
+    Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows.
+                          (but you should use iowin32.c for windows instead)
+
+*/
+
+#ifndef _ZLIBIOAPI64_H
+#define _ZLIBIOAPI64_H
+
+#if (!defined(_WIN32)) && (!defined(WIN32)) && (!defined(__APPLE__))
+
+  // Linux needs this to support file operation on files larger then 4+GB
+  // But might need better if/def to select just the platforms that needs them.
+
+        #ifndef __USE_FILE_OFFSET64
+                #define __USE_FILE_OFFSET64
+        #endif
+        #ifndef __USE_LARGEFILE64
+                #define __USE_LARGEFILE64
+        #endif
+        #ifndef _LARGEFILE64_SOURCE
+                #define _LARGEFILE64_SOURCE
+        #endif
+        #ifndef _FILE_OFFSET_BIT
+                #define _FILE_OFFSET_BIT 64
+        #endif
+
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include "zlib.h"
+
+#if defined(USE_FILE32API)
+#define fopen64 fopen
+#define ftello64 ftell
+#define fseeko64 fseek
+#else
+#ifdef __FreeBSD__
+#define fopen64 fopen
+#define ftello64 ftello
+#define fseeko64 fseeko
+#endif
+#ifdef _MSC_VER
+ #define fopen64 fopen
+ #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC)))
+  #define ftello64 _ftelli64
+  #define fseeko64 _fseeki64
+ #else // old MSC
+  #define ftello64 ftell
+  #define fseeko64 fseek
+ #endif
+#endif
+#endif
+
+/*
+#ifndef ZPOS64_T
+  #ifdef _WIN32
+                #define ZPOS64_T fpos_t
+  #else
+    #include <stdint.h>
+    #define ZPOS64_T uint64_t
+  #endif
+#endif
+*/
+
+#ifdef HAVE_MINIZIP64_CONF_H
+#include "mz64conf.h"
+#endif
+
+/* a type choosen by DEFINE */
+#ifdef HAVE_64BIT_INT_CUSTOM
+typedef  64BIT_INT_CUSTOM_TYPE ZPOS64_T;
+#else
+#ifdef HAS_STDINT_H
+#include "stdint.h"
+typedef uint64_t ZPOS64_T;
+#else
+
+/* Maximum unsigned 32-bit value used as placeholder for zip64 */
+#define MAXU32 0xffffffff
+
+#if defined(_MSC_VER) || defined(__BORLANDC__)
+typedef unsigned __int64 ZPOS64_T;
+#else
+typedef unsigned long long int ZPOS64_T;
+#endif
+#endif
+#endif
+
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+#define ZLIB_FILEFUNC_SEEK_CUR (1)
+#define ZLIB_FILEFUNC_SEEK_END (2)
+#define ZLIB_FILEFUNC_SEEK_SET (0)
+
+#define ZLIB_FILEFUNC_MODE_READ      (1)
+#define ZLIB_FILEFUNC_MODE_WRITE     (2)
+#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3)
+
+#define ZLIB_FILEFUNC_MODE_EXISTING (4)
+#define ZLIB_FILEFUNC_MODE_CREATE   (8)
+
+
+#ifndef ZCALLBACK
+ #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK)
+   #define ZCALLBACK CALLBACK
+ #else
+   #define ZCALLBACK
+ #endif
+#endif
+
+
+
+
+typedef voidpf   (ZCALLBACK *open_file_func)      OF((voidpf opaque, const char* filename, int mode));
+typedef uLong    (ZCALLBACK *read_file_func)      OF((voidpf opaque, voidpf stream, void* buf, uLong size));
+typedef uLong    (ZCALLBACK *write_file_func)     OF((voidpf opaque, voidpf stream, const void* buf, uLong size));
+typedef int      (ZCALLBACK *close_file_func)     OF((voidpf opaque, voidpf stream));
+typedef int      (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream));
+
+typedef long     (ZCALLBACK *tell_file_func)      OF((voidpf opaque, voidpf stream));
+typedef long     (ZCALLBACK *seek_file_func)      OF((voidpf opaque, voidpf stream, uLong offset, int origin));
+
+
+/* here is the "old" 32 bits structure structure */
+typedef struct zlib_filefunc_def_s
+{
+    open_file_func      zopen_file;
+    read_file_func      zread_file;
+    write_file_func     zwrite_file;
+    tell_file_func      ztell_file;
+    seek_file_func      zseek_file;
+    close_file_func     zclose_file;
+    testerror_file_func zerror_file;
+    voidpf              opaque;
+} zlib_filefunc_def;
+
+typedef ZPOS64_T (ZCALLBACK *tell64_file_func)    OF((voidpf opaque, voidpf stream));
+typedef long     (ZCALLBACK *seek64_file_func)    OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
+typedef voidpf   (ZCALLBACK *open64_file_func)    OF((voidpf opaque, const void* filename, int mode));
+
+typedef struct zlib_filefunc64_def_s
+{
+    open64_file_func    zopen64_file;
+    read_file_func      zread_file;
+    write_file_func     zwrite_file;
+    tell64_file_func    ztell64_file;
+    seek64_file_func    zseek64_file;
+    close_file_func     zclose_file;
+    testerror_file_func zerror_file;
+    voidpf              opaque;
+} zlib_filefunc64_def;
+
+void fill_fopen64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def));
+void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def));
+
+/* now internal definition, only for zip.c and unzip.h */
+typedef struct zlib_filefunc64_32_def_s
+{
+    zlib_filefunc64_def zfile_func64;
+    open_file_func      zopen32_file;
+    tell_file_func      ztell32_file;
+    seek_file_func      zseek32_file;
+} zlib_filefunc64_32_def;
+
+
+#define ZREAD64(filefunc,filestream,buf,size)     ((*((filefunc).zfile_func64.zread_file))   ((filefunc).zfile_func64.opaque,filestream,buf,size))
+#define ZWRITE64(filefunc,filestream,buf,size)    ((*((filefunc).zfile_func64.zwrite_file))  ((filefunc).zfile_func64.opaque,filestream,buf,size))
+//#define ZTELL64(filefunc,filestream)            ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream))
+//#define ZSEEK64(filefunc,filestream,pos,mode)   ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode))
+#define ZCLOSE64(filefunc,filestream)             ((*((filefunc).zfile_func64.zclose_file))  ((filefunc).zfile_func64.opaque,filestream))
+#define ZERROR64(filefunc,filestream)             ((*((filefunc).zfile_func64.zerror_file))  ((filefunc).zfile_func64.opaque,filestream))
+
+voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode));
+long    call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin));
+ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream));
+
+void    fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);
+
+#define ZOPEN64(filefunc,filename,mode)         (call_zopen64((&(filefunc)),(filename),(mode)))
+#define ZTELL64(filefunc,filestream)            (call_ztell64((&(filefunc)),(filestream)))
+#define ZSEEK64(filefunc,filestream,pos,mode)   (call_zseek64((&(filefunc)),(filestream),(pos),(mode)))
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/minizip/zip.c	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2007 @@
+/* zip.c -- IO on .zip files using zlib
+   Version 1.1, February 14h, 2010
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         Changes
+   Oct-2009 - Mathias Svensson - Remove old C style function prototypes
+   Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives
+   Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions.
+   Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data
+                                 It is used when recreting zip archive with RAW when deleting items from a zip.
+                                 ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed.
+   Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required)
+   Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
+
+*/
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "zlib.h"
+#include "zip.h"
+
+#ifdef STDC
+#  include <stddef.h>
+#  include <string.h>
+#  include <stdlib.h>
+#endif
+#ifdef NO_ERRNO_H
+    extern int errno;
+#else
+#   include <errno.h>
+#endif
+
+
+#ifndef local
+#  define local static
+#endif
+/* compile with -Dlocal if your debugger can't find static symbols */
+
+#ifndef VERSIONMADEBY
+# define VERSIONMADEBY   (0x0) /* platform depedent */
+#endif
+
+#ifndef Z_BUFSIZE
+#define Z_BUFSIZE (64*1024) //(16384)
+#endif
+
+#ifndef Z_MAXFILENAMEINZIP
+#define Z_MAXFILENAMEINZIP (256)
+#endif
+
+#ifndef ALLOC
+# define ALLOC(size) (malloc(size))
+#endif
+#ifndef TRYFREE
+# define TRYFREE(p) {if (p) free(p);}
+#endif
+
+/*
+#define SIZECENTRALDIRITEM (0x2e)
+#define SIZEZIPLOCALHEADER (0x1e)
+*/
+
+/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */
+
+
+// NOT sure that this work on ALL platform
+#define MAKEULONG64(a, b) ((ZPOS64_T)(((unsigned long)(a)) | ((ZPOS64_T)((unsigned long)(b))) << 32))
+
+#ifndef SEEK_CUR
+#define SEEK_CUR    1
+#endif
+
+#ifndef SEEK_END
+#define SEEK_END    2
+#endif
+
+#ifndef SEEK_SET
+#define SEEK_SET    0
+#endif
+
+#ifndef DEF_MEM_LEVEL
+#if MAX_MEM_LEVEL >= 8
+#  define DEF_MEM_LEVEL 8
+#else
+#  define DEF_MEM_LEVEL  MAX_MEM_LEVEL
+#endif
+#endif
+const char zip_copyright[] =" zip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll";
+
+
+#define SIZEDATA_INDATABLOCK (4096-(4*4))
+
+#define LOCALHEADERMAGIC    (0x04034b50)
+#define CENTRALHEADERMAGIC  (0x02014b50)
+#define ENDHEADERMAGIC      (0x06054b50)
+#define ZIP64ENDHEADERMAGIC      (0x6064b50)
+#define ZIP64ENDLOCHEADERMAGIC   (0x7064b50)
+
+#define FLAG_LOCALHEADER_OFFSET (0x06)
+#define CRC_LOCALHEADER_OFFSET  (0x0e)
+
+#define SIZECENTRALHEADER (0x2e) /* 46 */
+
+typedef struct linkedlist_datablock_internal_s
+{
+  struct linkedlist_datablock_internal_s* next_datablock;
+  uLong  avail_in_this_block;
+  uLong  filled_in_this_block;
+  uLong  unused; /* for future use and alignment */
+  unsigned char data[SIZEDATA_INDATABLOCK];
+} linkedlist_datablock_internal;
+
+typedef struct linkedlist_data_s
+{
+    linkedlist_datablock_internal* first_block;
+    linkedlist_datablock_internal* last_block;
+} linkedlist_data;
+
+
+typedef struct
+{
+    z_stream stream;            /* zLib stream structure for inflate */
+#ifdef HAVE_BZIP2
+    bz_stream bstream;          /* bzLib stream structure for bziped */
+#endif
+
+    int  stream_initialised;    /* 1 is stream is initialised */
+    uInt pos_in_buffered_data;  /* last written byte in buffered_data */
+
+    ZPOS64_T pos_local_header;     /* offset of the local header of the file
+                                     currenty writing */
+    char* central_header;       /* central header data for the current file */
+    uLong size_centralExtra;
+    uLong size_centralheader;   /* size of the central header for cur file */
+    uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */
+    uLong flag;                 /* flag of the file currently writing */
+
+    int  method;                /* compression method of file currenty wr.*/
+    int  raw;                   /* 1 for directly writing raw data */
+    Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/
+    uLong dosDate;
+    uLong crc32;
+    int  encrypt;
+    int  zip64;               /* Add ZIP64 extened information in the extra field */
+    ZPOS64_T pos_zip64extrainfo;
+    ZPOS64_T totalCompressedData;
+    ZPOS64_T totalUncompressedData;
+#ifndef NOCRYPT
+    unsigned long keys[3];     /* keys defining the pseudo-random sequence */
+    const z_crc_t* pcrc_32_tab;
+    int crypt_header_size;
+#endif
+} curfile64_info;
+
+typedef struct
+{
+    zlib_filefunc64_32_def z_filefunc;
+    voidpf filestream;        /* io structore of the zipfile */
+    linkedlist_data central_dir;/* datablock with central dir in construction*/
+    int  in_opened_file_inzip;  /* 1 if a file in the zip is currently writ.*/
+    curfile64_info ci;            /* info on the file curretly writing */
+
+    ZPOS64_T begin_pos;            /* position of the beginning of the zipfile */
+    ZPOS64_T add_position_when_writing_offset;
+    ZPOS64_T number_entry;
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+    char *globalcomment;
+#endif
+
+} zip64_internal;
+
+
+#ifndef NOCRYPT
+#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED
+#include "crypt.h"
+#endif
+
+local linkedlist_datablock_internal* allocate_new_datablock()
+{
+    linkedlist_datablock_internal* ldi;
+    ldi = (linkedlist_datablock_internal*)
+                 ALLOC(sizeof(linkedlist_datablock_internal));
+    if (ldi!=NULL)
+    {
+        ldi->next_datablock = NULL ;
+        ldi->filled_in_this_block = 0 ;
+        ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ;
+    }
+    return ldi;
+}
+
+local void free_datablock(linkedlist_datablock_internal* ldi)
+{
+    while (ldi!=NULL)
+    {
+        linkedlist_datablock_internal* ldinext = ldi->next_datablock;
+        TRYFREE(ldi);
+        ldi = ldinext;
+    }
+}
+
+local void init_linkedlist(linkedlist_data* ll)
+{
+    ll->first_block = ll->last_block = NULL;
+}
+
+local void free_linkedlist(linkedlist_data* ll)
+{
+    free_datablock(ll->first_block);
+    ll->first_block = ll->last_block = NULL;
+}
+
+
+local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len)
+{
+    linkedlist_datablock_internal* ldi;
+    const unsigned char* from_copy;
+
+    if (ll==NULL)
+        return ZIP_INTERNALERROR;
+
+    if (ll->last_block == NULL)
+    {
+        ll->first_block = ll->last_block = allocate_new_datablock();
+        if (ll->first_block == NULL)
+            return ZIP_INTERNALERROR;
+    }
+
+    ldi = ll->last_block;
+    from_copy = (unsigned char*)buf;
+
+    while (len>0)
+    {
+        uInt copy_this;
+        uInt i;
+        unsigned char* to_copy;
+
+        if (ldi->avail_in_this_block==0)
+        {
+            ldi->next_datablock = allocate_new_datablock();
+            if (ldi->next_datablock == NULL)
+                return ZIP_INTERNALERROR;
+            ldi = ldi->next_datablock ;
+            ll->last_block = ldi;
+        }
+
+        if (ldi->avail_in_this_block < len)
+            copy_this = (uInt)ldi->avail_in_this_block;
+        else
+            copy_this = (uInt)len;
+
+        to_copy = &(ldi->data[ldi->filled_in_this_block]);
+
+        for (i=0;i<copy_this;i++)
+            *(to_copy+i)=*(from_copy+i);
+
+        ldi->filled_in_this_block += copy_this;
+        ldi->avail_in_this_block -= copy_this;
+        from_copy += copy_this ;
+        len -= copy_this;
+    }
+    return ZIP_OK;
+}
+
+
+
+/****************************************************************************/
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+/* ===========================================================================
+   Inputs a long in LSB order to the given file
+   nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T)
+*/
+
+local int zip64local_putValue OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte));
+local int zip64local_putValue (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte)
+{
+    unsigned char buf[8];
+    int n;
+    for (n = 0; n < nbByte; n++)
+    {
+        buf[n] = (unsigned char)(x & 0xff);
+        x >>= 8;
+    }
+    if (x != 0)
+      {     /* data overflow - hack for ZIP64 (X Roche) */
+      for (n = 0; n < nbByte; n++)
+        {
+          buf[n] = 0xff;
+        }
+      }
+
+    if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,nbByte)!=(uLong)nbByte)
+        return ZIP_ERRNO;
+    else
+        return ZIP_OK;
+}
+
+local void zip64local_putValue_inmemory OF((void* dest, ZPOS64_T x, int nbByte));
+local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte)
+{
+    unsigned char* buf=(unsigned char*)dest;
+    int n;
+    for (n = 0; n < nbByte; n++) {
+        buf[n] = (unsigned char)(x & 0xff);
+        x >>= 8;
+    }
+
+    if (x != 0)
+    {     /* data overflow - hack for ZIP64 */
+       for (n = 0; n < nbByte; n++)
+       {
+          buf[n] = 0xff;
+       }
+    }
+}
+
+/****************************************************************************/
+
+
+local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm)
+{
+    uLong year = (uLong)ptm->tm_year;
+    if (year>=1980)
+        year-=1980;
+    else if (year>=80)
+        year-=80;
+    return
+      (uLong) (((ptm->tm_mday) + (32 * (ptm->tm_mon+1)) + (512 * year)) << 16) |
+        ((ptm->tm_sec/2) + (32* ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));
+}
+
+
+/****************************************************************************/
+
+local int zip64local_getByte OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi));
+
+local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def,voidpf filestream,int* pi)
+{
+    unsigned char c;
+    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
+    if (err==1)
+    {
+        *pi = (int)c;
+        return ZIP_OK;
+    }
+    else
+    {
+        if (ZERROR64(*pzlib_filefunc_def,filestream))
+            return ZIP_ERRNO;
+        else
+            return ZIP_EOF;
+    }
+}
+
+
+/* ===========================================================================
+   Reads a long in LSB order from the given gz_stream. Sets
+*/
+local int zip64local_getShort OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
+
+local int zip64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
+{
+    uLong x ;
+    int i = 0;
+    int err;
+
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x = (uLong)i;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<8;
+
+    if (err==ZIP_OK)
+        *pX = x;
+    else
+        *pX = 0;
+    return err;
+}
+
+local int zip64local_getLong OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
+
+local int zip64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
+{
+    uLong x ;
+    int i = 0;
+    int err;
+
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x = (uLong)i;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<8;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<16;
+
+    if (err==ZIP_OK)
+        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+    x += ((uLong)i)<<24;
+
+    if (err==ZIP_OK)
+        *pX = x;
+    else
+        *pX = 0;
+    return err;
+}
+
+local int zip64local_getLong64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX));
+
+
+local int zip64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX)
+{
+  ZPOS64_T x;
+  int i = 0;
+  int err;
+
+  err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x = (ZPOS64_T)i;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<8;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<16;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<24;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<32;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<40;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<48;
+
+  if (err==ZIP_OK)
+    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+  x += ((ZPOS64_T)i)<<56;
+
+  if (err==ZIP_OK)
+    *pX = x;
+  else
+    *pX = 0;
+
+  return err;
+}
+
+#ifndef BUFREADCOMMENT
+#define BUFREADCOMMENT (0x400)
+#endif
+/*
+  Locate the Central directory of a zipfile (at the end, just before
+    the global comment)
+*/
+local ZPOS64_T zip64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
+
+local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
+{
+  unsigned char* buf;
+  ZPOS64_T uSizeFile;
+  ZPOS64_T uBackRead;
+  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+  ZPOS64_T uPosFound=0;
+
+  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+    return 0;
+
+
+  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+  if (uMaxBack>uSizeFile)
+    uMaxBack = uSizeFile;
+
+  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+  if (buf==NULL)
+    return 0;
+
+  uBackRead = 4;
+  while (uBackRead<uMaxBack)
+  {
+    uLong uReadSize;
+    ZPOS64_T uReadPos ;
+    int i;
+    if (uBackRead+BUFREADCOMMENT>uMaxBack)
+      uBackRead = uMaxBack;
+    else
+      uBackRead+=BUFREADCOMMENT;
+    uReadPos = uSizeFile-uBackRead ;
+
+    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+      break;
+
+    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+      break;
+
+    for (i=(int)uReadSize-3; (i--)>0;)
+      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+        ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
+      {
+        uPosFound = uReadPos+i;
+        break;
+      }
+
+    if (uPosFound!=0)
+      break;
+  }
+  TRYFREE(buf);
+  return uPosFound;
+}
+
+/*
+Locate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before
+the global comment)
+*/
+local ZPOS64_T zip64local_SearchCentralDir64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
+
+local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
+{
+  unsigned char* buf;
+  ZPOS64_T uSizeFile;
+  ZPOS64_T uBackRead;
+  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+  ZPOS64_T uPosFound=0;
+  uLong uL;
+  ZPOS64_T relativeOffset;
+
+  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+    return 0;
+
+  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+  if (uMaxBack>uSizeFile)
+    uMaxBack = uSizeFile;
+
+  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+  if (buf==NULL)
+    return 0;
+
+  uBackRead = 4;
+  while (uBackRead<uMaxBack)
+  {
+    uLong uReadSize;
+    ZPOS64_T uReadPos;
+    int i;
+    if (uBackRead+BUFREADCOMMENT>uMaxBack)
+      uBackRead = uMaxBack;
+    else
+      uBackRead+=BUFREADCOMMENT;
+    uReadPos = uSizeFile-uBackRead ;
+
+    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+      break;
+
+    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+      break;
+
+    for (i=(int)uReadSize-3; (i--)>0;)
+    {
+      // Signature "0x07064b50" Zip64 end of central directory locater
+      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
+      {
+        uPosFound = uReadPos+i;
+        break;
+      }
+    }
+
+      if (uPosFound!=0)
+        break;
+  }
+
+  TRYFREE(buf);
+  if (uPosFound == 0)
+    return 0;
+
+  /* Zip64 end of central directory locator */
+  if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
+    return 0;
+
+  /* the signature, already checked */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+
+  /* number of the disk with the start of the zip64 end of  central directory */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+  if (uL != 0)
+    return 0;
+
+  /* relative offset of the zip64 end of central directory record */
+  if (zip64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=ZIP_OK)
+    return 0;
+
+  /* total number of disks */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+  if (uL != 1)
+    return 0;
+
+  /* Goto Zip64 end of central directory record */
+  if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
+    return 0;
+
+  /* the signature */
+  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+    return 0;
+
+  if (uL != 0x06064b50) // signature of 'Zip64 end of central directory'
+    return 0;
+
+  return relativeOffset;
+}
+
+int LoadCentralDirectoryRecord(zip64_internal* pziinit)
+{
+  int err=ZIP_OK;
+  ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+
+  ZPOS64_T size_central_dir;     /* size of the central directory  */
+  ZPOS64_T offset_central_dir;   /* offset of start of central directory */
+  ZPOS64_T central_pos;
+  uLong uL;
+
+  uLong number_disk;          /* number of the current dist, used for
+                              spaning ZIP, unsupported, always 0*/
+  uLong number_disk_with_CD;  /* number the the disk with central dir, used
+                              for spaning ZIP, unsupported, always 0*/
+  ZPOS64_T number_entry;
+  ZPOS64_T number_entry_CD;      /* total number of entries in
+                                the central dir
+                                (same than number_entry on nospan) */
+  uLong VersionMadeBy;
+  uLong VersionNeeded;
+  uLong size_comment;
+
+  int hasZIP64Record = 0;
+
+  // check first if we find a ZIP64 record
+  central_pos = zip64local_SearchCentralDir64(&pziinit->z_filefunc,pziinit->filestream);
+  if(central_pos > 0)
+  {
+    hasZIP64Record = 1;
+  }
+  else if(central_pos == 0)
+  {
+    central_pos = zip64local_SearchCentralDir(&pziinit->z_filefunc,pziinit->filestream);
+  }
+
+/* disable to allow appending to empty ZIP archive
+        if (central_pos==0)
+            err=ZIP_ERRNO;
+*/
+
+  if(hasZIP64Record)
+  {
+    ZPOS64_T sizeEndOfCentralDirectory;
+    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos, ZLIB_FILEFUNC_SEEK_SET) != 0)
+      err=ZIP_ERRNO;
+
+    /* the signature, already checked */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* size of zip64 end of central directory record */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &sizeEndOfCentralDirectory)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* version made by */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionMadeBy)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* version needed to extract */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionNeeded)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of this disk */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of the disk with the start of the central directory */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* total number of entries in the central directory on this disk */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &number_entry)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* total number of entries in the central directory */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&number_entry_CD)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
+      err=ZIP_BADZIPFILE;
+
+    /* size of the central directory */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&size_central_dir)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* offset of start of central directory with respect to the
+    starting disk number */
+    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&offset_central_dir)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    // TODO..
+    // read the comment from the standard central header.
+    size_comment = 0;
+  }
+  else
+  {
+    // Read End of central Directory info
+    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+      err=ZIP_ERRNO;
+
+    /* the signature, already checked */
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of this disk */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* number of the disk with the start of the central directory */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
+      err=ZIP_ERRNO;
+
+    /* total number of entries in the central dir on this disk */
+    number_entry = 0;
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      number_entry = uL;
+
+    /* total number of entries in the central dir */
+    number_entry_CD = 0;
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      number_entry_CD = uL;
+
+    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
+      err=ZIP_BADZIPFILE;
+
+    /* size of the central directory */
+    size_central_dir = 0;
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      size_central_dir = uL;
+
+    /* offset of start of central directory with respect to the starting disk number */
+    offset_central_dir = 0;
+    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+      err=ZIP_ERRNO;
+    else
+      offset_central_dir = uL;
+
+
+    /* zipfile global comment length */
+    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &size_comment)!=ZIP_OK)
+      err=ZIP_ERRNO;
+  }
+
+  if ((central_pos<offset_central_dir+size_central_dir) &&
+    (err==ZIP_OK))
+    err=ZIP_BADZIPFILE;
+
+  if (err!=ZIP_OK)
+  {
+    ZCLOSE64(pziinit->z_filefunc, pziinit->filestream);
+    return ZIP_ERRNO;
+  }
+
+  if (size_comment>0)
+  {
+    pziinit->globalcomment = (char*)ALLOC(size_comment+1);
+    if (pziinit->globalcomment)
+    {
+      size_comment = ZREAD64(pziinit->z_filefunc, pziinit->filestream, pziinit->globalcomment,size_comment);
+      pziinit->globalcomment[size_comment]=0;
+    }
+  }
+
+  byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir);
+  pziinit->add_position_when_writing_offset = byte_before_the_zipfile;
+
+  {
+    ZPOS64_T size_central_dir_to_read = size_central_dir;
+    size_t buf_size = SIZEDATA_INDATABLOCK;
+    void* buf_read = (void*)ALLOC(buf_size);
+    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir + byte_before_the_zipfile, ZLIB_FILEFUNC_SEEK_SET) != 0)
+      err=ZIP_ERRNO;
+
+    while ((size_central_dir_to_read>0) && (err==ZIP_OK))
+    {
+      ZPOS64_T read_this = SIZEDATA_INDATABLOCK;
+      if (read_this > size_central_dir_to_read)
+        read_this = size_central_dir_to_read;
+
+      if (ZREAD64(pziinit->z_filefunc, pziinit->filestream,buf_read,(uLong)read_this) != read_this)
+        err=ZIP_ERRNO;
+
+      if (err==ZIP_OK)
+        err = add_data_in_datablock(&pziinit->central_dir,buf_read, (uLong)read_this);
+
+      size_central_dir_to_read-=read_this;
+    }
+    TRYFREE(buf_read);
+  }
+  pziinit->begin_pos = byte_before_the_zipfile;
+  pziinit->number_entry = number_entry_CD;
+
+  if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET) != 0)
+    err=ZIP_ERRNO;
+
+  return err;
+}
+
+
+#endif /* !NO_ADDFILEINEXISTINGZIP*/
+
+
+/************************************************************/
+extern zipFile ZEXPORT zipOpen3 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def)
+{
+    zip64_internal ziinit;
+    zip64_internal* zi;
+    int err=ZIP_OK;
+
+    ziinit.z_filefunc.zseek32_file = NULL;
+    ziinit.z_filefunc.ztell32_file = NULL;
+    if (pzlib_filefunc64_32_def==NULL)
+        fill_fopen64_filefunc(&ziinit.z_filefunc.zfile_func64);
+    else
+        ziinit.z_filefunc = *pzlib_filefunc64_32_def;
+
+    ziinit.filestream = ZOPEN64(ziinit.z_filefunc,
+                  pathname,
+                  (append == APPEND_STATUS_CREATE) ?
+                  (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) :
+                    (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING));
+
+    if (ziinit.filestream == NULL)
+        return NULL;
+
+    if (append == APPEND_STATUS_CREATEAFTER)
+        ZSEEK64(ziinit.z_filefunc,ziinit.filestream,0,SEEK_END);
+
+    ziinit.begin_pos = ZTELL64(ziinit.z_filefunc,ziinit.filestream);
+    ziinit.in_opened_file_inzip = 0;
+    ziinit.ci.stream_initialised = 0;
+    ziinit.number_entry = 0;
+    ziinit.add_position_when_writing_offset = 0;
+    init_linkedlist(&(ziinit.central_dir));
+
+
+
+    zi = (zip64_internal*)ALLOC(sizeof(zip64_internal));
+    if (zi==NULL)
+    {
+        ZCLOSE64(ziinit.z_filefunc,ziinit.filestream);
+        return NULL;
+    }
+
+    /* now we add file in a zipfile */
+#    ifndef NO_ADDFILEINEXISTINGZIP
+    ziinit.globalcomment = NULL;
+    if (append == APPEND_STATUS_ADDINZIP)
+    {
+      // Read and Cache Central Directory Records
+      err = LoadCentralDirectoryRecord(&ziinit);
+    }
+
+    if (globalcomment)
+    {
+      *globalcomment = ziinit.globalcomment;
+    }
+#    endif /* !NO_ADDFILEINEXISTINGZIP*/
+
+    if (err != ZIP_OK)
+    {
+#    ifndef NO_ADDFILEINEXISTINGZIP
+        TRYFREE(ziinit.globalcomment);
+#    endif /* !NO_ADDFILEINEXISTINGZIP*/
+        TRYFREE(zi);
+        return NULL;
+    }
+    else
+    {
+        *zi = ziinit;
+        return (zipFile)zi;
+    }
+}
+
+extern zipFile ZEXPORT zipOpen2 (const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def)
+{
+    if (pzlib_filefunc32_def != NULL)
+    {
+        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+        fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);
+        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
+    }
+    else
+        return zipOpen3(pathname, append, globalcomment, NULL);
+}
+
+extern zipFile ZEXPORT zipOpen2_64 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def)
+{
+    if (pzlib_filefunc_def != NULL)
+    {
+        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+        zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
+        zlib_filefunc64_32_def_fill.ztell32_file = NULL;
+        zlib_filefunc64_32_def_fill.zseek32_file = NULL;
+        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
+    }
+    else
+        return zipOpen3(pathname, append, globalcomment, NULL);
+}
+
+
+
+extern zipFile ZEXPORT zipOpen (const char* pathname, int append)
+{
+    return zipOpen3((const void*)pathname,append,NULL,NULL);
+}
+
+extern zipFile ZEXPORT zipOpen64 (const void* pathname, int append)
+{
+    return zipOpen3(pathname,append,NULL,NULL);
+}
+
+int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local)
+{
+  /* write the local header */
+  int err;
+  uInt size_filename = (uInt)strlen(filename);
+  uInt size_extrafield = size_extrafield_local;
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC, 4);
+
+  if (err==ZIP_OK)
+  {
+    if(zi->ci.zip64)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);/* version needed to extract */
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */
+  }
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2);
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2);
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4);
+
+  // CRC / Compressed size / Uncompressed size will be filled in later and rewritten later
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */
+  if (err==ZIP_OK)
+  {
+    if(zi->ci.zip64)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* compressed size, unknown */
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */
+  }
+  if (err==ZIP_OK)
+  {
+    if(zi->ci.zip64)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* uncompressed size, unknown */
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */
+  }
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2);
+
+  if(zi->ci.zip64)
+  {
+    size_extrafield += 20;
+  }
+
+  if (err==ZIP_OK)
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield,2);
+
+  if ((err==ZIP_OK) && (size_filename > 0))
+  {
+    if (ZWRITE64(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename)
+      err = ZIP_ERRNO;
+  }
+
+  if ((err==ZIP_OK) && (size_extrafield_local > 0))
+  {
+    if (ZWRITE64(zi->z_filefunc, zi->filestream, extrafield_local, size_extrafield_local) != size_extrafield_local)
+      err = ZIP_ERRNO;
+  }
+
+
+  if ((err==ZIP_OK) && (zi->ci.zip64))
+  {
+      // write the Zip64 extended info
+      short HeaderID = 1;
+      short DataSize = 16;
+      ZPOS64_T CompressedSize = 0;
+      ZPOS64_T UncompressedSize = 0;
+
+      // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file)
+      zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream);
+
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)HeaderID,2);
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)DataSize,2);
+
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8);
+      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8);
+  }
+
+  return err;
+}
+
+/*
+ NOTE.
+ When writing RAW the ZIP64 extended information in extrafield_local and extrafield_global needs to be stripped
+ before calling this function it can be done with zipRemoveExtraInfoBlock
+
+ It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize
+ unnecessary allocations.
+ */
+extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void* extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int raw,
+                                         int windowBits,int memLevel, int strategy,
+                                         const char* password, uLong crcForCrypting,
+                                         uLong versionMadeBy, uLong flagBase, int zip64)
+{
+    zip64_internal* zi;
+    uInt size_filename;
+    uInt size_comment;
+    uInt i;
+    int err = ZIP_OK;
+
+#    ifdef NOCRYPT
+    (crcForCrypting);
+    if (password != NULL)
+        return ZIP_PARAMERROR;
+#    endif
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+
+#ifdef HAVE_BZIP2
+    if ((method!=0) && (method!=Z_DEFLATED) && (method!=Z_BZIP2ED))
+      return ZIP_PARAMERROR;
+#else
+    if ((method!=0) && (method!=Z_DEFLATED))
+      return ZIP_PARAMERROR;
+#endif
+
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 1)
+    {
+        err = zipCloseFileInZip (file);
+        if (err != ZIP_OK)
+            return err;
+    }
+
+    if (filename==NULL)
+        filename="-";
+
+    if (comment==NULL)
+        size_comment = 0;
+    else
+        size_comment = (uInt)strlen(comment);
+
+    size_filename = (uInt)strlen(filename);
+
+    if (zipfi == NULL)
+        zi->ci.dosDate = 0;
+    else
+    {
+        if (zipfi->dosDate != 0)
+            zi->ci.dosDate = zipfi->dosDate;
+        else
+          zi->ci.dosDate = zip64local_TmzDateToDosDate(&zipfi->tmz_date);
+    }
+
+    zi->ci.flag = flagBase;
+    if ((level==8) || (level==9))
+      zi->ci.flag |= 2;
+    if (level==2)
+      zi->ci.flag |= 4;
+    if (level==1)
+      zi->ci.flag |= 6;
+    if (password != NULL)
+      zi->ci.flag |= 1;
+
+    zi->ci.crc32 = 0;
+    zi->ci.method = method;
+    zi->ci.encrypt = 0;
+    zi->ci.stream_initialised = 0;
+    zi->ci.pos_in_buffered_data = 0;
+    zi->ci.raw = raw;
+    zi->ci.pos_local_header = ZTELL64(zi->z_filefunc,zi->filestream);
+
+    zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + size_extrafield_global + size_comment;
+    zi->ci.size_centralExtraFree = 32; // Extra space we have reserved in case we need to add ZIP64 extra info data
+
+    zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader + zi->ci.size_centralExtraFree);
+
+    zi->ci.size_centralExtra = size_extrafield_global;
+    zip64local_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4);
+    /* version info */
+    zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)versionMadeBy,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4);
+    zip64local_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/
+    zip64local_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/
+    zip64local_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/
+    zip64local_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2);
+    zip64local_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/
+
+    if (zipfi==NULL)
+        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2);
+    else
+        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2);
+
+    if (zipfi==NULL)
+        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4);
+    else
+        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4);
+
+    if(zi->ci.pos_local_header >= 0xffffffff)
+      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4);
+    else
+      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writing_offset,4);
+
+    for (i=0;i<size_filename;i++)
+        *(zi->ci.central_header+SIZECENTRALHEADER+i) = *(filename+i);
+
+    for (i=0;i<size_extrafield_global;i++)
+        *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+i) =
+              *(((const char*)extrafield_global)+i);
+
+    for (i=0;i<size_comment;i++)
+        *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+
+              size_extrafield_global+i) = *(comment+i);
+    if (zi->ci.central_header == NULL)
+        return ZIP_INTERNALERROR;
+
+    zi->ci.zip64 = zip64;
+    zi->ci.totalCompressedData = 0;
+    zi->ci.totalUncompressedData = 0;
+    zi->ci.pos_zip64extrainfo = 0;
+
+    err = Write_LocalFileHeader(zi, filename, size_extrafield_local, extrafield_local);
+
+#ifdef HAVE_BZIP2
+    zi->ci.bstream.avail_in = (uInt)0;
+    zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+    zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+    zi->ci.bstream.total_in_hi32 = 0;
+    zi->ci.bstream.total_in_lo32 = 0;
+    zi->ci.bstream.total_out_hi32 = 0;
+    zi->ci.bstream.total_out_lo32 = 0;
+#endif
+
+    zi->ci.stream.avail_in = (uInt)0;
+    zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+    zi->ci.stream.next_out = zi->ci.buffered_data;
+    zi->ci.stream.total_in = 0;
+    zi->ci.stream.total_out = 0;
+    zi->ci.stream.data_type = Z_BINARY;
+
+#ifdef HAVE_BZIP2
+    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED || zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+#else
+    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+#endif
+    {
+        if(zi->ci.method == Z_DEFLATED)
+        {
+          zi->ci.stream.zalloc = (alloc_func)0;
+          zi->ci.stream.zfree = (free_func)0;
+          zi->ci.stream.opaque = (voidpf)0;
+
+          if (windowBits>0)
+              windowBits = -windowBits;
+
+          err = deflateInit2(&zi->ci.stream, level, Z_DEFLATED, windowBits, memLevel, strategy);
+
+          if (err==Z_OK)
+              zi->ci.stream_initialised = Z_DEFLATED;
+        }
+        else if(zi->ci.method == Z_BZIP2ED)
+        {
+#ifdef HAVE_BZIP2
+            // Init BZip stuff here
+          zi->ci.bstream.bzalloc = 0;
+          zi->ci.bstream.bzfree = 0;
+          zi->ci.bstream.opaque = (voidpf)0;
+
+          err = BZ2_bzCompressInit(&zi->ci.bstream, level, 0,35);
+          if(err == BZ_OK)
+            zi->ci.stream_initialised = Z_BZIP2ED;
+#endif
+        }
+
+    }
+
+#    ifndef NOCRYPT
+    zi->ci.crypt_header_size = 0;
+    if ((err==Z_OK) && (password != NULL))
+    {
+        unsigned char bufHead[RAND_HEAD_LEN];
+        unsigned int sizeHead;
+        zi->ci.encrypt = 1;
+        zi->ci.pcrc_32_tab = get_crc_table();
+        /*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/
+
+        sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);
+        zi->ci.crypt_header_size = sizeHead;
+
+        if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)
+                err = ZIP_ERRNO;
+    }
+#    endif
+
+    if (err==Z_OK)
+        zi->in_opened_file_inzip = 1;
+    return err;
+}
+
+extern int ZEXPORT zipOpenNewFileInZip4 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void* extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int raw,
+                                         int windowBits,int memLevel, int strategy,
+                                         const char* password, uLong crcForCrypting,
+                                         uLong versionMadeBy, uLong flagBase)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, raw,
+                                 windowBits, memLevel, strategy,
+                                 password, crcForCrypting, versionMadeBy, flagBase, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip3 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void* extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int raw,
+                                         int windowBits,int memLevel, int strategy,
+                                         const char* password, uLong crcForCrypting)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, raw,
+                                 windowBits, memLevel, strategy,
+                                 password, crcForCrypting, VERSIONMADEBY, 0, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                         const void* extrafield_local, uInt size_extrafield_local,
+                                         const void* extrafield_global, uInt size_extrafield_global,
+                                         const char* comment, int method, int level, int raw,
+                                         int windowBits,int memLevel, int strategy,
+                                         const char* password, uLong crcForCrypting, int zip64)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, raw,
+                                 windowBits, memLevel, strategy,
+                                 password, crcForCrypting, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, raw,
+                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                 NULL, 0, VERSIONMADEBY, 0, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void* extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int raw, int zip64)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, raw,
+                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                 NULL, 0, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void*extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level, int zip64)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, 0,
+                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                 NULL, 0, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+                                        const void* extrafield_local, uInt size_extrafield_local,
+                                        const void*extrafield_global, uInt size_extrafield_global,
+                                        const char* comment, int method, int level)
+{
+    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+                                 extrafield_local, size_extrafield_local,
+                                 extrafield_global, size_extrafield_global,
+                                 comment, method, level, 0,
+                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+                                 NULL, 0, VERSIONMADEBY, 0, 0);
+}
+
+local int zip64FlushWriteBuffer(zip64_internal* zi)
+{
+    int err=ZIP_OK;
+
+    if (zi->ci.encrypt != 0)
+    {
+#ifndef NOCRYPT
+        uInt i;
+        int t;
+        for (i=0;i<zi->ci.pos_in_buffered_data;i++)
+            zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);
+#endif
+    }
+
+    if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
+      err = ZIP_ERRNO;
+
+    zi->ci.totalCompressedData += zi->ci.pos_in_buffered_data;
+
+#ifdef HAVE_BZIP2
+    if(zi->ci.method == Z_BZIP2ED)
+    {
+      zi->ci.totalUncompressedData += zi->ci.bstream.total_in_lo32;
+      zi->ci.bstream.total_in_lo32 = 0;
+      zi->ci.bstream.total_in_hi32 = 0;
+    }
+    else
+#endif
+    {
+      zi->ci.totalUncompressedData += zi->ci.stream.total_in;
+      zi->ci.stream.total_in = 0;
+    }
+
+
+    zi->ci.pos_in_buffered_data = 0;
+
+    return err;
+}
+
+extern int ZEXPORT zipWriteInFileInZip (zipFile file,const void* buf,unsigned int len)
+{
+    zip64_internal* zi;
+    int err=ZIP_OK;
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 0)
+        return ZIP_PARAMERROR;
+
+    zi->ci.crc32 = crc32(zi->ci.crc32,buf,(uInt)len);
+
+#ifdef HAVE_BZIP2
+    if(zi->ci.method == Z_BZIP2ED && (!zi->ci.raw))
+    {
+      zi->ci.bstream.next_in = (void*)buf;
+      zi->ci.bstream.avail_in = len;
+      err = BZ_RUN_OK;
+
+      while ((err==BZ_RUN_OK) && (zi->ci.bstream.avail_in>0))
+      {
+        if (zi->ci.bstream.avail_out == 0)
+        {
+          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+            err = ZIP_ERRNO;
+          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+        }
+
+
+        if(err != BZ_RUN_OK)
+          break;
+
+        if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+        {
+          uLong uTotalOutBefore_lo = zi->ci.bstream.total_out_lo32;
+//          uLong uTotalOutBefore_hi = zi->ci.bstream.total_out_hi32;
+          err=BZ2_bzCompress(&zi->ci.bstream,  BZ_RUN);
+
+          zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore_lo) ;
+        }
+      }
+
+      if(err == BZ_RUN_OK)
+        err = ZIP_OK;
+    }
+    else
+#endif
+    {
+      zi->ci.stream.next_in = (Bytef*)buf;
+      zi->ci.stream.avail_in = len;
+
+      while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0))
+      {
+          if (zi->ci.stream.avail_out == 0)
+          {
+              if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+                  err = ZIP_ERRNO;
+              zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+              zi->ci.stream.next_out = zi->ci.buffered_data;
+          }
+
+
+          if(err != ZIP_OK)
+              break;
+
+          if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+          {
+              uLong uTotalOutBefore = zi->ci.stream.total_out;
+              err=deflate(&zi->ci.stream,  Z_NO_FLUSH);
+              if(uTotalOutBefore > zi->ci.stream.total_out)
+              {
+                int bBreak = 0;
+                bBreak++;
+              }
+
+              zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
+          }
+          else
+          {
+              uInt copy_this,i;
+              if (zi->ci.stream.avail_in < zi->ci.stream.avail_out)
+                  copy_this = zi->ci.stream.avail_in;
+              else
+                  copy_this = zi->ci.stream.avail_out;
+
+              for (i = 0; i < copy_this; i++)
+                  *(((char*)zi->ci.stream.next_out)+i) =
+                      *(((const char*)zi->ci.stream.next_in)+i);
+              {
+                  zi->ci.stream.avail_in -= copy_this;
+                  zi->ci.stream.avail_out-= copy_this;
+                  zi->ci.stream.next_in+= copy_this;
+                  zi->ci.stream.next_out+= copy_this;
+                  zi->ci.stream.total_in+= copy_this;
+                  zi->ci.stream.total_out+= copy_this;
+                  zi->ci.pos_in_buffered_data += copy_this;
+              }
+          }
+      }// while(...)
+    }
+
+    return err;
+}
+
+extern int ZEXPORT zipCloseFileInZipRaw (zipFile file, uLong uncompressed_size, uLong crc32)
+{
+    return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32);
+}
+
+extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_size, uLong crc32)
+{
+    zip64_internal* zi;
+    ZPOS64_T compressed_size;
+    uLong invalidValue = 0xffffffff;
+    short datasize = 0;
+    int err=ZIP_OK;
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 0)
+        return ZIP_PARAMERROR;
+    zi->ci.stream.avail_in = 0;
+
+    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+                {
+                        while (err==ZIP_OK)
+                        {
+                                uLong uTotalOutBefore;
+                                if (zi->ci.stream.avail_out == 0)
+                                {
+                                        if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+                                                err = ZIP_ERRNO;
+                                        zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+                                        zi->ci.stream.next_out = zi->ci.buffered_data;
+                                }
+                                uTotalOutBefore = zi->ci.stream.total_out;
+                                err=deflate(&zi->ci.stream,  Z_FINISH);
+                                zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
+                        }
+                }
+    else if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+    {
+#ifdef HAVE_BZIP2
+      err = BZ_FINISH_OK;
+      while (err==BZ_FINISH_OK)
+      {
+        uLong uTotalOutBefore;
+        if (zi->ci.bstream.avail_out == 0)
+        {
+          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+            err = ZIP_ERRNO;
+          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+        }
+        uTotalOutBefore = zi->ci.bstream.total_out_lo32;
+        err=BZ2_bzCompress(&zi->ci.bstream,  BZ_FINISH);
+        if(err == BZ_STREAM_END)
+          err = Z_STREAM_END;
+
+        zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore);
+      }
+
+      if(err == BZ_FINISH_OK)
+        err = ZIP_OK;
+#endif
+    }
+
+    if (err==Z_STREAM_END)
+        err=ZIP_OK; /* this is normal */
+
+    if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK))
+                {
+        if (zip64FlushWriteBuffer(zi)==ZIP_ERRNO)
+            err = ZIP_ERRNO;
+                }
+
+    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+    {
+        int tmp_err = deflateEnd(&zi->ci.stream);
+        if (err == ZIP_OK)
+            err = tmp_err;
+        zi->ci.stream_initialised = 0;
+    }
+#ifdef HAVE_BZIP2
+    else if((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+    {
+      int tmperr = BZ2_bzCompressEnd(&zi->ci.bstream);
+                        if (err==ZIP_OK)
+                                err = tmperr;
+                        zi->ci.stream_initialised = 0;
+    }
+#endif
+
+    if (!zi->ci.raw)
+    {
+        crc32 = (uLong)zi->ci.crc32;
+        uncompressed_size = zi->ci.totalUncompressedData;
+    }
+    compressed_size = zi->ci.totalCompressedData;
+
+#    ifndef NOCRYPT
+    compressed_size += zi->ci.crypt_header_size;
+#    endif
+
+    // update Current Item crc and sizes,
+    if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
+    {
+      /*version Made by*/
+      zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)45,2);
+      /*version needed*/
+      zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)45,2);
+
+    }
+
+    zip64local_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/
+
+
+    if(compressed_size >= 0xffffffff)
+      zip64local_putValue_inmemory(zi->ci.central_header+20, invalidValue,4); /*compr size*/
+    else
+      zip64local_putValue_inmemory(zi->ci.central_header+20, compressed_size,4); /*compr size*/
+
+    /// set internal file attributes field
+    if (zi->ci.stream.data_type == Z_ASCII)
+        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2);
+
+    if(uncompressed_size >= 0xffffffff)
+      zip64local_putValue_inmemory(zi->ci.central_header+24, invalidValue,4); /*uncompr size*/
+    else
+      zip64local_putValue_inmemory(zi->ci.central_header+24, uncompressed_size,4); /*uncompr size*/
+
+    // Add ZIP64 extra info field for uncompressed size
+    if(uncompressed_size >= 0xffffffff)
+      datasize += 8;
+
+    // Add ZIP64 extra info field for compressed size
+    if(compressed_size >= 0xffffffff)
+      datasize += 8;
+
+    // Add ZIP64 extra info field for relative offset to local file header of current file
+    if(zi->ci.pos_local_header >= 0xffffffff)
+      datasize += 8;
+
+    if(datasize > 0)
+    {
+      char* p = NULL;
+
+      if((uLong)(datasize + 4) > zi->ci.size_centralExtraFree)
+      {
+        // we can not write more data to the buffer that we have room for.
+        return ZIP_BADZIPFILE;
+      }
+
+      p = zi->ci.central_header + zi->ci.size_centralheader;
+
+      // Add Extra Information Header for 'ZIP64 information'
+      zip64local_putValue_inmemory(p, 0x0001, 2); // HeaderID
+      p += 2;
+      zip64local_putValue_inmemory(p, datasize, 2); // DataSize
+      p += 2;
+
+      if(uncompressed_size >= 0xffffffff)
+      {
+        zip64local_putValue_inmemory(p, uncompressed_size, 8);
+        p += 8;
+      }
+
+      if(compressed_size >= 0xffffffff)
+      {
+        zip64local_putValue_inmemory(p, compressed_size, 8);
+        p += 8;
+      }
+
+      if(zi->ci.pos_local_header >= 0xffffffff)
+      {
+        zip64local_putValue_inmemory(p, zi->ci.pos_local_header, 8);
+        p += 8;
+      }
+
+      // Update how much extra free space we got in the memory buffer
+      // and increase the centralheader size so the new ZIP64 fields are included
+      // ( 4 below is the size of HeaderID and DataSize field )
+      zi->ci.size_centralExtraFree -= datasize + 4;
+      zi->ci.size_centralheader += datasize + 4;
+
+      // Update the extra info size field
+      zi->ci.size_centralExtra += datasize + 4;
+      zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)zi->ci.size_centralExtra,2);
+    }
+
+    if (err==ZIP_OK)
+        err = add_data_in_datablock(&zi->central_dir, zi->ci.central_header, (uLong)zi->ci.size_centralheader);
+
+    free(zi->ci.central_header);
+
+    if (err==ZIP_OK)
+    {
+        // Update the LocalFileHeader with the new values.
+
+        ZPOS64_T cur_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
+
+        if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            err = ZIP_ERRNO;
+
+        if (err==ZIP_OK)
+            err = zip64local_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */
+
+        if(uncompressed_size >= 0xffffffff || compressed_size >= 0xffffffff )
+        {
+          if(zi->ci.pos_zip64extrainfo > 0)
+          {
+            // Update the size in the ZIP64 extended field.
+            if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_zip64extrainfo + 4,ZLIB_FILEFUNC_SEEK_SET)!=0)
+              err = ZIP_ERRNO;
+
+            if (err==ZIP_OK) /* compressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, uncompressed_size, 8);
+
+            if (err==ZIP_OK) /* uncompressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, compressed_size, 8);
+          }
+          else
+              err = ZIP_BADZIPFILE; // Caller passed zip64 = 0, so no room for zip64 info -> fatal
+        }
+        else
+        {
+          if (err==ZIP_OK) /* compressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4);
+
+          if (err==ZIP_OK) /* uncompressed size, unknown */
+              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4);
+        }
+
+        if (ZSEEK64(zi->z_filefunc,zi->filestream, cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0)
+            err = ZIP_ERRNO;
+    }
+
+    zi->number_entry ++;
+    zi->in_opened_file_inzip = 0;
+
+    return err;
+}
+
+extern int ZEXPORT zipCloseFileInZip (zipFile file)
+{
+    return zipCloseFileInZipRaw (file,0,0);
+}
+
+int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip)
+{
+  int err = ZIP_OK;
+  ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset;
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4);
+
+  /*num disks*/
+    if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+  /*relative offset*/
+    if (err==ZIP_OK) /* Relative offset to the Zip64EndOfCentralDirectory */
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, pos,8);
+
+  /*total disks*/ /* Do not support spawning of disk so always say 1 here*/
+    if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)1,4);
+
+    return err;
+}
+
+int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
+{
+  int err = ZIP_OK;
+
+  uLong Zip64DataSize = 44;
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDHEADERMAGIC,4);
+
+  if (err==ZIP_OK) /* size of this 'zip64 end of central directory' */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)Zip64DataSize,8); // why ZPOS64_T of this ?
+
+  if (err==ZIP_OK) /* version made by */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
+
+  if (err==ZIP_OK) /* version needed */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
+
+  if (err==ZIP_OK) /* number of this disk */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+  if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
+    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
+
+  if (err==ZIP_OK) /* total number of entries in the central dir */
+    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
+
+  if (err==ZIP_OK) /* size of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)size_centraldir,8);
+
+  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
+  {
+    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8);
+  }
+  return err;
+}
+int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
+{
+  int err = ZIP_OK;
+
+  /*signature*/
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4);
+
+  if (err==ZIP_OK) /* number of this disk */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
+
+  if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
+
+  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
+  {
+    {
+      if(zi->number_entry >= 0xFFFF)
+        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
+      else
+        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
+    }
+  }
+
+  if (err==ZIP_OK) /* total number of entries in the central dir */
+  {
+    if(zi->number_entry >= 0xFFFF)
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
+  }
+
+  if (err==ZIP_OK) /* size of the central directory */
+    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4);
+
+  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
+  {
+    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+    if(pos >= 0xffffffff)
+    {
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4);
+    }
+    else
+      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writing_offset),4);
+  }
+
+   return err;
+}
+
+int Write_GlobalComment(zip64_internal* zi, const char* global_comment)
+{
+  int err = ZIP_OK;
+  uInt size_global_comment = 0;
+
+  if(global_comment != NULL)
+    size_global_comment = (uInt)strlen(global_comment);
+
+  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2);
+
+  if (err == ZIP_OK && size_global_comment > 0)
+  {
+    if (ZWRITE64(zi->z_filefunc,zi->filestream, global_comment, size_global_comment) != size_global_comment)
+      err = ZIP_ERRNO;
+  }
+  return err;
+}
+
+extern int ZEXPORT zipClose (zipFile file, const char* global_comment)
+{
+    zip64_internal* zi;
+    int err = 0;
+    uLong size_centraldir = 0;
+    ZPOS64_T centraldir_pos_inzip;
+    ZPOS64_T pos;
+
+    if (file == NULL)
+        return ZIP_PARAMERROR;
+
+    zi = (zip64_internal*)file;
+
+    if (zi->in_opened_file_inzip == 1)
+    {
+        err = zipCloseFileInZip (file);
+    }
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+    if (global_comment==NULL)
+        global_comment = zi->globalcomment;
+#endif
+
+    centraldir_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
+
+    if (err==ZIP_OK)
+    {
+        linkedlist_datablock_internal* ldi = zi->central_dir.first_block;
+        while (ldi!=NULL)
+        {
+            if ((err==ZIP_OK) && (ldi->filled_in_this_block>0))
+            {
+                if (ZWRITE64(zi->z_filefunc,zi->filestream, ldi->data, ldi->filled_in_this_block) != ldi->filled_in_this_block)
+                    err = ZIP_ERRNO;
+            }
+
+            size_centraldir += ldi->filled_in_this_block;
+            ldi = ldi->next_datablock;
+        }
+    }
+    free_linkedlist(&(zi->central_dir));
+
+    pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+    if(pos >= 0xffffffff || zi->number_entry > 0xFFFF)
+    {
+      ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream);
+      Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
+
+      Write_Zip64EndOfCentralDirectoryLocator(zi, Zip64EOCDpos);
+    }
+
+    if (err==ZIP_OK)
+      err = Write_EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
+
+    if(err == ZIP_OK)
+      err = Write_GlobalComment(zi, global_comment);
+
+    if (ZCLOSE64(zi->z_filefunc,zi->filestream) != 0)
+        if (err == ZIP_OK)
+            err = ZIP_ERRNO;
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+    TRYFREE(zi->globalcomment);
+#endif
+    TRYFREE(zi);
+
+    return err;
+}
+
+extern int ZEXPORT zipRemoveExtraInfoBlock (char* pData, int* dataLen, short sHeader)
+{
+  char* p = pData;
+  int size = 0;
+  char* pNewHeader;
+  char* pTmp;
+  short header;
+  short dataSize;
+
+  int retVal = ZIP_OK;
+
+  if(pData == NULL || *dataLen < 4)
+    return ZIP_PARAMERROR;
+
+  pNewHeader = (char*)ALLOC(*dataLen);
+  pTmp = pNewHeader;
+
+  while(p < (pData + *dataLen))
+  {
+    header = *(short*)p;
+    dataSize = *(((short*)p)+1);
+
+    if( header == sHeader ) // Header found.
+    {
+      p += dataSize + 4; // skip it. do not copy to temp buffer
+    }
+    else
+    {
+      // Extra Info block should not be removed, So copy it to the temp buffer.
+      memcpy(pTmp, p, dataSize + 4);
+      p += dataSize + 4;
+      size += dataSize + 4;
+    }
+
+  }
+
+  if(size < *dataLen)
+  {
+    // clean old extra info block.
+    memset(pData,0, *dataLen);
+
+    // copy the new extra info block over the old
+    if(size > 0)
+      memcpy(pData, pNewHeader, size);
+
+    // set the new extra info size
+    *dataLen = size;
+
+    retVal = ZIP_OK;
+  }
+  else
+    retVal = ZIP_ERRNO;
+
+  TRYFREE(pNewHeader);
+
+  return retVal;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/minizip/zip.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,362 @@
+/* zip.h -- IO on .zip files using zlib
+   Version 1.1, February 14h, 2010
+   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+         Modifications for Zip64 support
+         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+         For more info read MiniZip_info.txt
+
+         ---------------------------------------------------------------------------
+
+   Condition of use and distribution are the same than zlib :
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+
+        ---------------------------------------------------------------------------
+
+        Changes
+
+        See header of zip.h
+
+*/
+
+#ifndef _zip12_H
+#define _zip12_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//#define HAVE_BZIP2
+
+#ifndef _ZLIB_H
+#include "zlib.h"
+#endif
+
+#ifndef _ZLIBIOAPI_H
+#include "ioapi.h"
+#endif
+
+#ifdef HAVE_BZIP2
+#include "bzlib.h"
+#endif
+
+#define Z_BZIP2ED 12
+
+#if defined(STRICTZIP) || defined(STRICTZIPUNZIP)
+/* like the STRICT of WIN32, we define a pointer that cannot be converted
+    from (void*) without cast */
+typedef struct TagzipFile__ { int unused; } zipFile__;
+typedef zipFile__ *zipFile;
+#else
+typedef voidp zipFile;
+#endif
+
+#define ZIP_OK                          (0)
+#define ZIP_EOF                         (0)
+#define ZIP_ERRNO                       (Z_ERRNO)
+#define ZIP_PARAMERROR                  (-102)
+#define ZIP_BADZIPFILE                  (-103)
+#define ZIP_INTERNALERROR               (-104)
+
+#ifndef DEF_MEM_LEVEL
+#  if MAX_MEM_LEVEL >= 8
+#    define DEF_MEM_LEVEL 8
+#  else
+#    define DEF_MEM_LEVEL  MAX_MEM_LEVEL
+#  endif
+#endif
+/* default memLevel */
+
+/* tm_zip contain date/time info */
+typedef struct tm_zip_s
+{
+    uInt tm_sec;            /* seconds after the minute - [0,59] */
+    uInt tm_min;            /* minutes after the hour - [0,59] */
+    uInt tm_hour;           /* hours since midnight - [0,23] */
+    uInt tm_mday;           /* day of the month - [1,31] */
+    uInt tm_mon;            /* months since January - [0,11] */
+    uInt tm_year;           /* years - [1980..2044] */
+} tm_zip;
+
+typedef struct
+{
+    tm_zip      tmz_date;       /* date in understandable format           */
+    uLong       dosDate;       /* if dos_date == 0, tmu_date is used      */
+/*    uLong       flag;        */   /* general purpose bit flag        2 bytes */
+
+    uLong       internal_fa;    /* internal file attributes        2 bytes */
+    uLong       external_fa;    /* external file attributes        4 bytes */
+} zip_fileinfo;
+
+typedef const char* zipcharpc;
+
+
+#define APPEND_STATUS_CREATE        (0)
+#define APPEND_STATUS_CREATEAFTER   (1)
+#define APPEND_STATUS_ADDINZIP      (2)
+
+extern zipFile ZEXPORT zipOpen OF((const char *pathname, int append));
+extern zipFile ZEXPORT zipOpen64 OF((const void *pathname, int append));
+/*
+  Create a zipfile.
+     pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on
+       an Unix computer "zlib/zlib113.zip".
+     if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip
+       will be created at the end of the file.
+         (useful if the file contain a self extractor code)
+     if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will
+       add files in existing zip (be sure you don't add file that doesn't exist)
+     If the zipfile cannot be opened, the return value is NULL.
+     Else, the return value is a zipFile Handle, usable with other function
+       of this zip package.
+*/
+
+/* Note : there is no delete function into a zipfile.
+   If you want delete file into a zipfile, you must open a zipfile, and create another
+   Of couse, you can use RAW reading and writing to copy the file you did not want delte
+*/
+
+extern zipFile ZEXPORT zipOpen2 OF((const char *pathname,
+                                   int append,
+                                   zipcharpc* globalcomment,
+                                   zlib_filefunc_def* pzlib_filefunc_def));
+
+extern zipFile ZEXPORT zipOpen2_64 OF((const void *pathname,
+                                   int append,
+                                   zipcharpc* globalcomment,
+                                   zlib_filefunc64_def* pzlib_filefunc_def));
+
+extern int ZEXPORT zipOpenNewFileInZip OF((zipFile file,
+                       const char* filename,
+                       const zip_fileinfo* zipfi,
+                       const void* extrafield_local,
+                       uInt size_extrafield_local,
+                       const void* extrafield_global,
+                       uInt size_extrafield_global,
+                       const char* comment,
+                       int method,
+                       int level));
+
+extern int ZEXPORT zipOpenNewFileInZip64 OF((zipFile file,
+                       const char* filename,
+                       const zip_fileinfo* zipfi,
+                       const void* extrafield_local,
+                       uInt size_extrafield_local,
+                       const void* extrafield_global,
+                       uInt size_extrafield_global,
+                       const char* comment,
+                       int method,
+                       int level,
+                       int zip64));
+
+/*
+  Open a file in the ZIP for writing.
+  filename : the filename in zip (if NULL, '-' without quote will be used
+  *zipfi contain supplemental information
+  if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local
+    contains the extrafield data the the local header
+  if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global
+    contains the extrafield data the the local header
+  if comment != NULL, comment contain the comment string
+  method contain the compression method (0 for store, Z_DEFLATED for deflate)
+  level contain the level of compression (can be Z_DEFAULT_COMPRESSION)
+  zip64 is set to 1 if a zip64 extended information block should be added to the local file header.
+                    this MUST be '1' if the uncompressed size is >= 0xffffffff.
+
+*/
+
+
+extern int ZEXPORT zipOpenNewFileInZip2 OF((zipFile file,
+                                            const char* filename,
+                                            const zip_fileinfo* zipfi,
+                                            const void* extrafield_local,
+                                            uInt size_extrafield_local,
+                                            const void* extrafield_global,
+                                            uInt size_extrafield_global,
+                                            const char* comment,
+                                            int method,
+                                            int level,
+                                            int raw));
+
+
+extern int ZEXPORT zipOpenNewFileInZip2_64 OF((zipFile file,
+                                            const char* filename,
+                                            const zip_fileinfo* zipfi,
+                                            const void* extrafield_local,
+                                            uInt size_extrafield_local,
+                                            const void* extrafield_global,
+                                            uInt size_extrafield_global,
+                                            const char* comment,
+                                            int method,
+                                            int level,
+                                            int raw,
+                                            int zip64));
+/*
+  Same than zipOpenNewFileInZip, except if raw=1, we write raw file
+ */
+
+extern int ZEXPORT zipOpenNewFileInZip3 OF((zipFile file,
+                                            const char* filename,
+                                            const zip_fileinfo* zipfi,
+                                            const void* extrafield_local,
+                                            uInt size_extrafield_local,
+                                            const void* extrafield_global,
+                                            uInt size_extrafield_global,
+                                            const char* comment,
+                                            int method,
+                                            int level,
+                                            int raw,
+                                            int windowBits,
+                                            int memLevel,
+                                            int strategy,
+                                            const char* password,
+                                            uLong crcForCrypting));
+
+extern int ZEXPORT zipOpenNewFileInZip3_64 OF((zipFile file,
+                                            const char* filename,
+                                            const zip_fileinfo* zipfi,
+                                            const void* extrafield_local,
+                                            uInt size_extrafield_local,
+                                            const void* extrafield_global,
+                                            uInt size_extrafield_global,
+                                            const char* comment,
+                                            int method,
+                                            int level,
+                                            int raw,
+                                            int windowBits,
+                                            int memLevel,
+                                            int strategy,
+                                            const char* password,
+                                            uLong crcForCrypting,
+                                            int zip64
+                                            ));
+
+/*
+  Same than zipOpenNewFileInZip2, except
+    windowBits,memLevel,,strategy : see parameter strategy in deflateInit2
+    password : crypting password (NULL for no crypting)
+    crcForCrypting : crc of file to compress (needed for crypting)
+ */
+
+extern int ZEXPORT zipOpenNewFileInZip4 OF((zipFile file,
+                                            const char* filename,
+                                            const zip_fileinfo* zipfi,
+                                            const void* extrafield_local,
+                                            uInt size_extrafield_local,
+                                            const void* extrafield_global,
+                                            uInt size_extrafield_global,
+                                            const char* comment,
+                                            int method,
+                                            int level,
+                                            int raw,
+                                            int windowBits,
+                                            int memLevel,
+                                            int strategy,
+                                            const char* password,
+                                            uLong crcForCrypting,
+                                            uLong versionMadeBy,
+                                            uLong flagBase
+                                            ));
+
+
+extern int ZEXPORT zipOpenNewFileInZip4_64 OF((zipFile file,
+                                            const char* filename,
+                                            const zip_fileinfo* zipfi,
+                                            const void* extrafield_local,
+                                            uInt size_extrafield_local,
+                                            const void* extrafield_global,
+                                            uInt size_extrafield_global,
+                                            const char* comment,
+                                            int method,
+                                            int level,
+                                            int raw,
+                                            int windowBits,
+                                            int memLevel,
+                                            int strategy,
+                                            const char* password,
+                                            uLong crcForCrypting,
+                                            uLong versionMadeBy,
+                                            uLong flagBase,
+                                            int zip64
+                                            ));
+/*
+  Same than zipOpenNewFileInZip4, except
+    versionMadeBy : value for Version made by field
+    flag : value for flag field (compression level info will be added)
+ */
+
+
+extern int ZEXPORT zipWriteInFileInZip OF((zipFile file,
+                       const void* buf,
+                       unsigned len));
+/*
+  Write data in the zipfile
+*/
+
+extern int ZEXPORT zipCloseFileInZip OF((zipFile file));
+/*
+  Close the current file in the zipfile
+*/
+
+extern int ZEXPORT zipCloseFileInZipRaw OF((zipFile file,
+                                            uLong uncompressed_size,
+                                            uLong crc32));
+
+extern int ZEXPORT zipCloseFileInZipRaw64 OF((zipFile file,
+                                            ZPOS64_T uncompressed_size,
+                                            uLong crc32));
+
+/*
+  Close the current file in the zipfile, for file opened with
+    parameter raw=1 in zipOpenNewFileInZip2
+  uncompressed_size and crc32 are value for the uncompressed size
+*/
+
+extern int ZEXPORT zipClose OF((zipFile file,
+                const char* global_comment));
+/*
+  Close the zipfile
+*/
+
+
+extern int ZEXPORT zipRemoveExtraInfoBlock OF((char* pData, int* dataLen, short sHeader));
+/*
+  zipRemoveExtraInfoBlock -  Added by Mathias Svensson
+
+  Remove extra information block from a extra information data for the local file header or central directory header
+
+  It is needed to remove ZIP64 extra information blocks when before data is written if using RAW mode.
+
+  0x0001 is the signature header for the ZIP64 extra information blocks
+
+  usage.
+                        Remove ZIP64 Extra information from a central director extra field data
+              zipRemoveExtraInfoBlock(pCenDirExtraFieldData, &nCenDirExtraFieldDataLen, 0x0001);
+
+                        Remove ZIP64 Extra information from a Local File Header extra field data
+        zipRemoveExtraInfoBlock(pLocalHeaderExtraFieldData, &nLocalHeaderExtraFieldDataLen, 0x0001);
+*/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _zip64_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/patch/NOTES.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,70 @@
+===========
+INFORMATION
+===========
+
+This is a precompiled version of the "patch" standard tool for
+Windows. It was compiled using the MSYS framework.
+
+The binaries originate from the "Git for Windows 1.9.5" package
+(https://msysgit.github.io/). The build instructions have been
+provided on the discussion group of Git for Windows [1]. They are
+copied/pasted below for reference.
+
+
+
+================
+UPSTREAM PROJECT
+================
+
+URL to the upstream project:
+http://savannah.gnu.org/projects/patch/
+
+License of patch: GPLv2 (GNU General Public License v2)
+
+Copyright (C) 1988 Larry Wall "with lots o' patches by Paul Eggert"
+Copyright (C) 1997 Free Software Foundation, Inc.
+
+
+
+======================
+BUILD INSTRUCTIONS [1]
+======================
+
+The easiest way to find out about this is to install the Git SDK, then 
+run 
+
+     pacman -Qu $(which patch.exe) 
+
+to find out which package contains the `patch.exe` binary. It so happens 
+to be patch.2.7.5-1 at the moment. Since this is an MSys2 package (not a 
+MinGW one, otherwise the patch utility would be in /mingw64/bin/, not 
+/usr/bin/), this package is built from the recipes in 
+
+     https://github.com/msys2/MSYS2-packages 
+
+The `patch` package is obviously built from the subdirectory 
+
+     https://github.com/Alexpux/MSYS2-packages/tree/master/patch 
+
+and the PKGBUILD file specifies that the source is fetched from 
+ftp://ftp.gnu.org/gnu/patch/patch-2.7.5.tar.xz: 
+
+https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/PKGBUILD#L14 
+
+and then these two patches are applied before building: 
+     
+https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/msys2-patch-2.7.1.patch 
+
+and 
+      
+https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/msys2-patch-manifest.patch 
+
+As you can see, some light changes are applied, i.e. `patch.exe` will 
+always write in binary mode with MSys2, and the executable will have a 
+manifest embedded that allows it to run as non-administrator. 
+
+Ciao, 
+Johannes Schindelin
+
+
+[1] https://groups.google.com/d/msg/git-for-windows/xWyVr4z6Ri0/6RKeV028EAAJ
Binary file OrthancFramework/Resources/ThirdParty/patch/msys-1.0.dll has changed
Binary file OrthancFramework/Resources/ThirdParty/patch/patch.exe has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/ThirdParty/patch/patch.exe.manifest	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+  <assemblyIdentity version="7.95.0.0"
+     processorArchitecture="X86"
+     name="patch.exe"
+     type="win32"/>
+
+  <!-- Identify the application security requirements. -->
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
+    <security>
+      <requestedPrivileges>
+        <requestedExecutionLevel
+          level="asInvoker"
+          uiAccess="false"/>
+        </requestedPrivileges>
+       </security>
+  </trustInfo>
+</assembly>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/WindowsResources.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,90 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 sys
+import datetime
+
+if len(sys.argv) != 5:
+    sys.stderr.write('Usage: %s <Version> <ProductName> <Filename> <Description>\n\n' % sys.argv[0])
+    sys.stderr.write('Example: %s 0.9.1 Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging"\n' % sys.argv[0])
+    sys.exit(-1)
+
+SOURCE = os.path.join(os.path.dirname(__file__), 'WindowsResources.rc')
+
+VERSION = sys.argv[1]
+PRODUCT = sys.argv[2]
+FILENAME = sys.argv[3]
+DESCRIPTION = sys.argv[4]
+
+if VERSION == 'mainline':
+    VERSION = '999.999.999'
+    RELEASE = 'This is a mainline build, not an official release'
+else:
+    RELEASE = 'Release %s' % VERSION
+
+v = VERSION.split('.')
+if len(v) != 2 and len(v) != 3:
+    sys.stderr.write('Bad version number: %s\n' % VERSION)
+    sys.exit(-1)
+
+if len(v) == 2:
+    v.append('0')
+
+extension = os.path.splitext(FILENAME)[1]
+if extension.lower() == '.dll':
+    BLOCK = '040904E4'
+    TYPE = 'VFT_DLL'
+elif extension.lower() == '.exe':
+    #BLOCK = '040904B0'   # LANG_ENGLISH/SUBLANG_ENGLISH_US,
+    BLOCK = '040904E4'   # Lang=US English, CharSet=Windows Multilingual
+    TYPE = 'VFT_APP'
+else:
+    sys.stderr.write('Unsupported extension (.EXE or .DLL only): %s\n' % extension)
+    sys.exit(-1)
+
+
+with open(SOURCE, 'r') as source:
+    content = source.read()
+    content = content.replace('${VERSION_MAJOR}', v[0])
+    content = content.replace('${VERSION_MINOR}', v[1])
+    content = content.replace('${VERSION_PATCH}', v[2])
+    content = content.replace('${RELEASE}', RELEASE)
+    content = content.replace('${DESCRIPTION}', DESCRIPTION)
+    content = content.replace('${PRODUCT}', PRODUCT)   
+    content = content.replace('${FILENAME}', FILENAME)   
+    content = content.replace('${YEAR}', str(datetime.datetime.now().year))
+    content = content.replace('${BLOCK}', BLOCK)
+    content = content.replace('${TYPE}', TYPE)
+
+    sys.stdout.write(content)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Resources/WindowsResources.rc	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,30 @@
+#include <winver.h>
+
+VS_VERSION_INFO VERSIONINFO
+   FILEVERSION ${VERSION_MAJOR},${VERSION_MINOR},0,${VERSION_PATCH}
+   PRODUCTVERSION ${VERSION_MAJOR},${VERSION_MINOR},0,0
+   FILEOS VOS_NT_WINDOWS32
+   FILETYPE ${TYPE}
+   BEGIN
+      BLOCK "StringFileInfo"
+      BEGIN
+         BLOCK "${BLOCK}"
+         BEGIN
+            VALUE "Comments", "${RELEASE}"
+            VALUE "CompanyName", "Osimis SA, Belgium"
+            VALUE "FileDescription", "${DESCRIPTION}"
+            VALUE "FileVersion", "${VERSION_MAJOR}.${VERSION_MINOR}.0.${VERSION_PATCH}"
+            VALUE "InternalName", "${PRODUCT}"
+            VALUE "LegalCopyright", "(c) 2012-${YEAR}, Sebastien Jodogne, University Hospital of Liege, and Osimis SA, Belgium"
+            VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/"
+            VALUE "OriginalFilename", "${FILENAME}"
+            VALUE "ProductName", "${PRODUCT}"
+            VALUE "ProductVersion", "${VERSION_MAJOR}.${VERSION_MINOR}"
+         END
+      END
+
+      BLOCK "VarFileInfo"
+      BEGIN
+        VALUE "Translation", 0x409, 1252  // U.S. English
+      END
+   END
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/ICachePageProvider.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../IDynamicObject.h"
+
+namespace Orthanc
+{
+  namespace Deprecated
+  {
+    class ICachePageProvider
+    {
+    public:
+      virtual ~ICachePageProvider()
+      {
+      }
+
+      virtual IDynamicObject* Provide(const std::string& id) = 0;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/ICacheable.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 ICacheable : public boost::noncopyable
+  {
+  public:
+    virtual ~ICacheable()
+    {
+    }
+
+    virtual size_t GetMemoryUsage() const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <map>
+#include <vector>
+#include <boost/noncopyable.hpp>
+#include <cassert>
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  /**
+   * This class implements the index of a cache with least recently
+   * used (LRU) recycling policy. All the items of the cache index
+   * can be associated with a payload.
+   * Reference: http://stackoverflow.com/a/2504317
+   **/
+  template <typename T, typename Payload = NullType>
+  class LeastRecentlyUsedIndex : public boost::noncopyable
+  {
+  private:
+    typedef std::list< std::pair<T, Payload> >  Queue;
+    typedef std::map<T, typename Queue::iterator>  Index;
+
+    Index  index_;
+    Queue  queue_;
+
+    /**
+     * Internal method for debug builds to check whether the internal
+     * data structures are not corrupted.
+     **/
+    void CheckInvariants() const;
+
+  public:
+    /**
+     * Add a new element to the cache index, and make it the most
+     * recent element.
+     * \param id The ID of the element.
+     * \param payload The payload of the element.
+     **/
+    void Add(T id, Payload payload = Payload());
+
+    void AddOrMakeMostRecent(T id, Payload payload = Payload());
+
+    /**
+     * When accessing an element of the cache, this method tags the
+     * element as the most recently used.
+     * \param id The most recently accessed item.
+     **/
+    void MakeMostRecent(T id);
+
+    void MakeMostRecent(T id, Payload updatedPayload);
+
+    /**
+     * Remove an element from the cache index.
+     * \param id The item to remove.
+     **/
+    Payload Invalidate(T id);
+
+    /**
+     * Get the oldest element in the cache and remove it.
+     * \return The oldest item.
+     **/
+    T RemoveOldest();
+
+    /**
+     * Get the oldest element in the cache, remove it and return the
+     * associated payload.
+     * \param payload Where to store the associated payload.
+     * \return The oldest item.
+     **/
+    T RemoveOldest(Payload& payload);
+
+    /**
+     * Check whether an element is contained in the cache.
+     * \param id The item.
+     * \return \c true iff the item is indexed by the cache.
+     **/
+    bool Contains(T id) const
+    {
+      return index_.find(id) != index_.end();
+    }
+
+    bool Contains(T id, Payload& payload) const
+    {
+      typename Index::const_iterator it = index_.find(id);
+      if (it == index_.end())
+      {
+        return false;
+      }
+      else
+      {
+        payload = it->second->second;
+        return true;
+      }
+    }
+
+    /**
+     * Return the number of elements in the cache.
+     * \return The number of elements.
+     **/
+    size_t GetSize() const
+    {
+      assert(index_.size() == queue_.size());
+      return queue_.size();
+    }
+
+    /**
+     * Check whether the cache index is empty.
+     * \return \c true iff the cache is empty.
+     **/
+    bool IsEmpty() const
+    {
+      return index_.empty();
+    }
+
+    const T& GetOldest() const;
+    
+    const Payload& GetOldestPayload() const;
+
+    void GetAllKeys(std::vector<T>& keys) const
+    {
+      keys.clear();
+      keys.reserve(GetSize());
+      for (typename Index::const_iterator it = index_.begin(); it != index_.end(); it++)
+      {
+        keys.push_back(it->first);
+      }
+    }
+
+  };
+
+
+
+
+  /******************************************************************
+   ** Implementation of the template
+   ******************************************************************/
+
+  template <typename T, typename Payload>
+  void LeastRecentlyUsedIndex<T, Payload>::CheckInvariants() const
+  {
+#ifndef NDEBUG
+    assert(index_.size() == queue_.size());
+
+    for (typename Index::const_iterator 
+           it = index_.begin(); it != index_.end(); it++)
+    {
+      assert(it->second != queue_.end());
+      assert(it->second->first == it->first);
+    }
+#endif
+  }
+
+
+  template <typename T, typename Payload>
+  void LeastRecentlyUsedIndex<T, Payload>::Add(T id, Payload payload)
+  {
+    if (Contains(id))
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    queue_.push_front(std::make_pair(id, payload));
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template <typename T, typename Payload>
+  void LeastRecentlyUsedIndex<T, Payload>::MakeMostRecent(T id)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    std::pair<T, Payload> item = *(it->second);
+    
+    queue_.erase(it->second);
+    queue_.push_front(item);
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template <typename T, typename Payload>
+  void LeastRecentlyUsedIndex<T, Payload>::AddOrMakeMostRecent(T id, Payload payload)
+  {
+    typename Index::iterator it = index_.find(id);
+
+    if (it != index_.end())
+    {
+      // Already existing. Make it most recent.
+      std::pair<T, Payload> item = *(it->second);
+      item.second = payload;
+      queue_.erase(it->second);
+      queue_.push_front(item);
+    }
+    else
+    {
+      // New item
+      queue_.push_front(std::make_pair(id, payload));
+    }
+
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template <typename T, typename Payload>
+  void LeastRecentlyUsedIndex<T, Payload>::MakeMostRecent(T id, Payload updatedPayload)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    std::pair<T, Payload> item = *(it->second);
+    item.second = updatedPayload;
+    
+    queue_.erase(it->second);
+    queue_.push_front(item);
+    index_[id] = queue_.begin();
+
+    CheckInvariants();
+  }
+
+
+  template <typename T, typename Payload>
+  Payload LeastRecentlyUsedIndex<T, Payload>::Invalidate(T id)
+  {
+    if (!Contains(id))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    typename Index::iterator it = index_.find(id);
+    assert(it != index_.end());
+
+    Payload payload = it->second->second;
+    queue_.erase(it->second);
+    index_.erase(it);
+
+    CheckInvariants();
+    return payload;
+  }
+
+
+  template <typename T, typename Payload>
+  T LeastRecentlyUsedIndex<T, Payload>::RemoveOldest(Payload& payload)
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::pair<T, Payload> item = queue_.back();
+    T oldest = item.first;
+    payload = item.second;
+
+    queue_.pop_back();
+    assert(index_.find(oldest) != index_.end());
+    index_.erase(oldest);
+
+    CheckInvariants();
+
+    return oldest;
+  }
+
+
+  template <typename T, typename Payload>
+  T LeastRecentlyUsedIndex<T, Payload>::RemoveOldest()
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::pair<T, Payload> item = queue_.back();
+    T oldest = item.first;
+
+    queue_.pop_back();
+    assert(index_.find(oldest) != index_.end());
+    index_.erase(oldest);
+
+    CheckInvariants();
+
+    return oldest;
+  }
+
+
+  template <typename T, typename Payload>
+  const T& LeastRecentlyUsedIndex<T, Payload>::GetOldest() const
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return queue_.back().first;
+  }
+
+
+  template <typename T, typename Payload>
+  const Payload& LeastRecentlyUsedIndex<T, Payload>::GetOldestPayload() const
+  {
+    if (IsEmpty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    return queue_.back().second;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/MemoryCache.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MemoryCache.h"
+
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  namespace Deprecated
+  {
+    MemoryCache::Page& MemoryCache::Load(const std::string& id)
+    {
+      // Reuse the cache entry if it already exists
+      Page* p = NULL;
+      if (index_.Contains(id, p))
+      {
+        VLOG(1) << "Reusing a cache page";
+        assert(p != NULL);
+        index_.MakeMostRecent(id);
+        return *p;
+      }
+
+      // The id is not in the cache yet. Make some room if the cache
+      // is full.
+      if (index_.GetSize() == cacheSize_)
+      {
+        VLOG(1) << "Dropping the oldest cache page";
+        index_.RemoveOldest(p);
+        delete p;
+      }
+
+      // Create a new cache page
+      std::unique_ptr<Page> result(new Page);
+      result->id_ = id;
+      result->content_.reset(provider_.Provide(id));
+
+      // Add the newly create page to the cache
+      VLOG(1) << "Registering new data in a cache page";
+      p = result.release();
+      index_.Add(id, p);
+      return *p;
+    }
+
+    MemoryCache::MemoryCache(ICachePageProvider& provider,
+                             size_t cacheSize) : 
+      provider_(provider),
+      cacheSize_(cacheSize)
+    {
+    }
+
+    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())
+      {
+        Page* element = NULL;
+        index_.RemoveOldest(element);
+        assert(element != NULL);
+        delete element;
+      }
+    }
+
+    IDynamicObject& MemoryCache::Access(const std::string& id)
+    {
+      return *Load(id).content_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/MemoryCache.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.h"
+#include "ICachePageProvider.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  namespace Deprecated
+  {
+    /**
+     * WARNING: This class is NOT thread-safe.
+     **/
+    class MemoryCache
+    {
+    private:
+      struct Page
+      {
+        std::string id_;
+        std::unique_ptr<IDynamicObject> content_;
+      };
+
+      ICachePageProvider& provider_;
+      size_t cacheSize_;
+      LeastRecentlyUsedIndex<std::string, Page*>  index_;
+
+      Page& Load(const std::string& id);
+
+    public:
+      MemoryCache(ICachePageProvider& provider,
+                  size_t cacheSize);
+
+      ~MemoryCache();
+
+      IDynamicObject& Access(const std::string& id);
+
+      void Invalidate(const std::string& id);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,282 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MemoryObjectCache.h"
+
+#include "../Compatibility.h"
+
+namespace Orthanc
+{
+  class MemoryObjectCache::Item : public boost::noncopyable
+  {
+  private:
+    ICacheable*               value_;
+    boost::posix_time::ptime  time_;
+
+  public:
+    explicit Item(ICacheable* value) :   // Takes ownership
+    value_(value),
+    time_(boost::posix_time::second_clock::local_time())
+    {
+      if (value == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    ~Item()
+    {
+      assert(value_ != NULL);
+      delete value_;
+    }
+
+    ICacheable& GetValue() const
+    {
+      assert(value_ != NULL);
+      return *value_;
+    }
+
+    const boost::posix_time::ptime& GetTime() const
+    {
+      return time_;
+    }
+  };
+
+
+  void MemoryObjectCache::Recycle(size_t targetSize)
+  {
+    // WARNING: "cacheMutex_" must be locked
+    while (currentSize_ > targetSize)
+    {
+      assert(!content_.IsEmpty());
+        
+      Item* item = NULL;
+      content_.RemoveOldest(item);
+
+      assert(item != NULL);
+      const size_t size = item->GetValue().GetMemoryUsage();
+      delete item;
+
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+
+    // Post-condition: "currentSize_ <= targetSize"
+  }
+    
+
+  MemoryObjectCache::MemoryObjectCache() :
+    currentSize_(0),
+    maxSize_(100 * 1024 * 1024)  // 100 MB
+  {
+  }
+
+
+  MemoryObjectCache::~MemoryObjectCache()
+  {
+    Recycle(0);
+    assert(content_.IsEmpty());
+  }
+
+
+  size_t MemoryObjectCache::GetMaximumSize()
+  {
+#if !defined(__EMSCRIPTEN__)
+    boost::mutex::scoped_lock lock(cacheMutex_);
+#endif
+
+    return maxSize_;
+  }
+
+
+  void MemoryObjectCache::SetMaximumSize(size_t size)
+  {
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+      
+#if !defined(__EMSCRIPTEN__)
+    // Make sure no accessor is currently open (as its data may be
+    // removed if recycling is needed)
+    WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+    Recycle(size);
+    maxSize_ = size;
+  }
+
+
+  void MemoryObjectCache::Acquire(const std::string& key,
+                                  ICacheable* value)
+  {
+    std::unique_ptr<Item> item(new Item(value));
+
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+#if !defined(__EMSCRIPTEN__)
+      // Make sure no accessor is currently open (as its data may be
+      // removed if recycling is needed)
+      WriterLock contentLock(contentMutex_);
+
+      // Lock the global structure of the cache
+      boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+      const size_t size = item->GetValue().GetMemoryUsage();
+
+      if (size > maxSize_)
+      {
+        // This object is too large to be stored in the cache, discard it
+      }
+      else if (content_.Contains(key))
+      {
+        // Value already stored, don't overwrite the old value
+        content_.MakeMostRecent(key);
+      }
+      else
+      {
+        Recycle(maxSize_ - size);   // Post-condition: currentSize_ <= maxSize_ - size
+        assert(currentSize_ + size <= maxSize_);
+
+        content_.Add(key, item.release());
+        currentSize_ += size;
+      }
+    }
+  }
+
+
+  void MemoryObjectCache::Invalidate(const std::string& key)
+  {
+#if !defined(__EMSCRIPTEN__)
+    // Make sure no accessor is currently open (as it may correspond
+    // to the key to remove)
+    WriterLock contentLock(contentMutex_);
+
+    // Lock the global structure of the cache
+    boost::mutex::scoped_lock cacheLock(cacheMutex_);
+#endif
+
+    Item* item = NULL;
+    if (content_.Contains(key, item))
+    {
+      assert(item != NULL);
+      const size_t size = item->GetValue().GetMemoryUsage();
+      delete item;
+
+      content_.Invalidate(key);
+          
+      assert(currentSize_ >= size);
+      currentSize_ -= size;
+    }
+  }
+
+
+  MemoryObjectCache::Accessor::Accessor(MemoryObjectCache& cache,
+                                        const std::string& key,
+                                        bool unique) :
+    item_(NULL)
+  {
+#if !defined(__EMSCRIPTEN__)
+    if (unique)
+    {
+      writerLock_ = WriterLock(cache.contentMutex_);
+    }
+    else
+    {
+      readerLock_ = ReaderLock(cache.contentMutex_);
+    }
+
+    // Lock the global structure of the cache, must be *after* the
+    // reader/writer lock
+    cacheLock_ = boost::mutex::scoped_lock(cache.cacheMutex_);
+#endif
+
+    if (cache.content_.Contains(key, item_))
+    {
+      cache.content_.MakeMostRecent(key);
+    }
+    
+#if !defined(__EMSCRIPTEN__)
+    cacheLock_.unlock();
+
+    if (item_ == NULL)
+    {
+      // This item does not exist in the cache, we can release the
+      // reader/writer lock
+      if (unique)
+      {
+        writerLock_.unlock();
+      }
+      else
+      {
+        readerLock_.unlock();
+      }
+    }
+#endif
+  }
+
+
+  ICacheable& MemoryObjectCache::Accessor::GetValue() const
+  {
+    if (IsValid())
+    {
+      return item_->GetValue();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const boost::posix_time::ptime& MemoryObjectCache::Accessor::GetTime() const
+  {
+    if (IsValid())
+    {
+      return item_->GetTime();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }        
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/MemoryObjectCache.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ICacheable.h"
+#include "LeastRecentlyUsedIndex.h"
+
+#if !defined(__EMSCRIPTEN__)
+// Multithreading is not supported in WebAssembly
+#  include <boost/thread/mutex.hpp>
+#  include <boost/thread/shared_mutex.hpp>
+#endif
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+
+namespace Orthanc
+{
+  class MemoryObjectCache : public boost::noncopyable
+  {
+  private:
+    class Item;
+
+#if !defined(__EMSCRIPTEN__)
+    typedef boost::unique_lock<boost::shared_mutex> WriterLock;
+    typedef boost::shared_lock<boost::shared_mutex> ReaderLock;
+
+    // This mutex protects modifications to the structure of the cache (monitor)
+    boost::mutex   cacheMutex_;
+
+    // This mutex protects modifications to the items that are stored in the cache
+    boost::shared_mutex contentMutex_;
+#endif
+
+    size_t currentSize_;
+    size_t maxSize_;
+    LeastRecentlyUsedIndex<std::string, Item*>  content_;
+
+    void Recycle(size_t targetSize);
+    
+  public:
+    MemoryObjectCache();
+
+    ~MemoryObjectCache();
+
+    size_t GetMaximumSize();
+
+    void SetMaximumSize(size_t size);
+
+    void Acquire(const std::string& key,
+                 ICacheable* value);
+
+    void Invalidate(const std::string& key);
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+#if !defined(__EMSCRIPTEN__)
+      ReaderLock                 readerLock_;
+      WriterLock                 writerLock_;
+      boost::mutex::scoped_lock  cacheLock_;
+#endif
+      
+      Item*  item_;
+
+    public:
+      Accessor(MemoryObjectCache& cache,
+               const std::string& key,
+               bool unique);
+
+      bool IsValid() const
+      {
+        return item_ != NULL;
+      }
+
+      ICacheable& GetValue() const;
+
+      const boost::posix_time::ptime& GetTime() const;
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MemoryStringCache.h"
+
+namespace Orthanc
+{
+  class MemoryStringCache::StringValue : public ICacheable
+  {
+  private:
+    std::string  content_;
+
+  public:
+    StringValue(const std::string& content) :
+      content_(content)
+    {
+    }
+      
+    const std::string& GetContent() const
+    {
+      return content_;
+    }
+
+    virtual size_t GetMemoryUsage() const
+    {
+      return content_.size();
+    }      
+  };
+
+
+  void MemoryStringCache::Add(const std::string& key,
+                              const std::string& value)
+  {
+    cache_.Acquire(key, new StringValue(value));
+  }
+
+  
+  bool MemoryStringCache::Fetch(std::string& value,
+                                const std::string& key)
+  {
+    MemoryObjectCache::Accessor reader(cache_, key, false /* multiple readers are allowed */);
+
+    if (reader.IsValid())
+    {
+      value = dynamic_cast<StringValue&>(reader.GetValue()).GetContent();
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/MemoryStringCache.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MemoryObjectCache.h"
+
+namespace Orthanc
+{
+  /**
+   * Facade object around "MemoryObjectCache" that caches a dictionary
+   * of strings, using the "fetch/add" paradigm of memcached.
+   **/
+  class MemoryStringCache : public boost::noncopyable
+  {
+  private:
+    class StringValue;
+
+    MemoryObjectCache  cache_;
+
+  public:
+    size_t GetMaximumSize()
+    {
+      return cache_.GetMaximumSize();
+    }
+    
+    void SetMaximumSize(size_t size)
+    {
+      cache_.SetMaximumSize(size);
+    }
+
+    void Add(const std::string& key,
+             const std::string& value);
+    
+    void Invalidate(const std::string& key)
+    {
+      cache_.Invalidate(key);
+    }
+
+    bool Fetch(std::string& value,
+               const std::string& key);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/SharedArchive.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "SharedArchive.h"
+
+#include "../Toolbox.h"
+
+
+namespace Orthanc
+{
+  void SharedArchive::RemoveInternal(const std::string& id)
+  {
+    Archive::iterator it = archive_.find(id);
+
+    if (it != archive_.end())
+    {
+      delete it->second;
+      archive_.erase(it);
+
+      lru_.Invalidate(id);
+    }
+  }
+
+
+  SharedArchive::Accessor::Accessor(SharedArchive& that,
+                                    const std::string& id) :
+    lock_(that.mutex_)
+  {
+    Archive::iterator it = that.archive_.find(id);
+
+    if (it == that.archive_.end())
+    {
+      item_ = NULL;
+    }
+    else
+    {
+      that.lru_.MakeMostRecent(id);
+      item_ = it->second;
+    }
+  }
+
+
+  IDynamicObject& SharedArchive::Accessor::GetItem() const
+  {
+    if (item_ == NULL)
+    {
+      // "IsValid()" should have been called
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *item_;
+    }
+  }  
+
+
+  SharedArchive::SharedArchive(size_t maxSize) : 
+    maxSize_(maxSize)
+  {
+    if (maxSize == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  SharedArchive::~SharedArchive()
+  {
+    for (Archive::iterator it = archive_.begin();
+         it != archive_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  std::string SharedArchive::Add(IDynamicObject* obj)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (archive_.size() == maxSize_)
+    {
+      // The quota has been reached, remove the oldest element
+      RemoveInternal(lru_.GetOldest());
+    }
+
+    std::string id = Toolbox::GenerateUuid();
+    RemoveInternal(id);  // Should never be useful because of UUID
+
+    archive_[id] = obj;
+    lru_.Add(id);
+
+    return id;
+  }
+
+
+  void SharedArchive::Remove(const std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    RemoveInternal(id);      
+  }
+
+
+  void SharedArchive::List(std::list<std::string>& items)
+  {
+    items.clear();
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      for (Archive::const_iterator it = archive_.begin();
+           it != archive_.end(); ++it)
+      {
+        items.push_back(it->first);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Cache/SharedArchive.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,96 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 SharedArchive cannot be used in sandboxed environments
+#endif
+
+#include "LeastRecentlyUsedIndex.h"
+#include "../IDynamicObject.h"
+
+#include <map>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class SharedArchive : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, IDynamicObject*>  Archive;
+
+    size_t         maxSize_;
+    boost::mutex   mutex_;
+    Archive        archive_;
+    LeastRecentlyUsedIndex<std::string> lru_;
+
+    void RemoveInternal(const std::string& id);
+
+  public:
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      IDynamicObject*            item_;
+
+    public:
+      Accessor(SharedArchive& that,
+               const std::string& id);
+
+      bool IsValid() const
+      {
+        return item_ != NULL;
+      }
+      
+      IDynamicObject& GetItem() const;
+    };
+
+
+    SharedArchive(size_t maxSize);
+
+    ~SharedArchive();
+
+    std::string Add(IDynamicObject* obj);  // Takes the ownership
+
+    void Remove(const std::string& id);
+
+    void List(std::list<std::string>& items);
+  };
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/ChunkedBuffer.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,116 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ChunkedBuffer.h"
+
+#include <cassert>
+#include <string.h>
+
+
+namespace Orthanc
+{
+  void ChunkedBuffer::Clear()
+  {
+    numBytes_ = 0;
+
+    for (Chunks::iterator it = chunks_.begin(); 
+         it != chunks_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  void ChunkedBuffer::AddChunk(const void* chunkData,
+                               size_t chunkSize)
+  {
+    if (chunkSize == 0)
+    {
+      return;
+    }
+    else
+    {
+      assert(chunkData != NULL);
+      chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+      numBytes_ += chunkSize;
+    }
+  }
+
+
+  void ChunkedBuffer::AddChunk(const std::string& chunk)
+  {
+    if (chunk.size() > 0)
+    {
+      AddChunk(&chunk[0], chunk.size());
+    }
+  }
+
+
+  void ChunkedBuffer::AddChunkDestructive(std::string& chunk)
+  {
+    size_t chunkSize = chunk.size();
+    
+    if (chunkSize > 0)
+    {
+      chunks_.push_back(new std::string);
+      chunks_.back()->swap(chunk);
+      numBytes_ += chunkSize;
+    }
+  }
+
+
+  void ChunkedBuffer::Flatten(std::string& result)
+  {
+    result.resize(numBytes_);
+
+    size_t pos = 0;
+    for (Chunks::iterator it = chunks_.begin(); 
+         it != chunks_.end(); ++it)
+    {
+      assert(*it != NULL);
+
+      size_t s = (*it)->size();
+      if (s != 0)
+      {
+        memcpy(&result[pos], (*it)->c_str(), s);
+        pos += s;
+      }
+
+      delete *it;
+    }
+
+    chunks_.clear();
+    numBytes_ = 0;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/ChunkedBuffer.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+#include <string>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ChunkedBuffer : public boost::noncopyable
+  {
+  private:
+    typedef std::list<std::string*>  Chunks;
+    size_t numBytes_;
+    Chunks chunks_;
+  
+    void Clear();
+
+  public:
+    ChunkedBuffer() : numBytes_(0)
+    {
+    }
+
+    ~ChunkedBuffer()
+    {
+      Clear();
+    }
+
+    size_t GetNumBytes() const
+    {
+      return numBytes_;
+    }
+
+    void AddChunk(const void* chunkData,
+                  size_t chunkSize);
+
+    void AddChunk(const std::string& chunk);
+
+    // The source content will be emptied
+    void AddChunkDestructive(std::string& chunk);
+
+    void Flatten(std::string& result);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compatibility.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+//#define Orthanc_Compatibility_h_STR2(x) #x
+//#define Orthanc_Compatibility_h_STR1(x) Orthanc_Compatibility_h_STR2(x)
+
+//#pragma message("__cplusplus = " Orthanc_Compatibility_h_STR1(__cplusplus))
+
+#if (defined _MSC_VER)
+//#  pragma message("_MSC_VER = " Orthanc_Compatibility_h_STR1(_MSC_VER))
+//#  pragma message("_MSVC_LANG = " Orthanc_Compatibility_h_STR1(_MSVC_LANG))
+// The __cplusplus macro cannot be used in Visual C++ < 1914 (VC++ 15.7)
+// However, even in recent versions, __cplusplus will only be correct (that is,
+// correctly defines the supported C++ version) if a special flag is passed to
+// the compiler ("/Zc:__cplusplus")
+// To make this header more robust, we use the _MSVC_LANG equivalent macro.
+
+// please note that not all C++11 features are supported when _MSC_VER == 1600
+// (or higher). This header file can be made for fine-grained, if required, 
+// based on specific _MSC_VER values
+
+#  if _MSC_VER >= 1600
+#    define ORTHANC_Cxx03_DETECTED 0
+#  else
+#    define ORTHANC_Cxx03_DETECTED 1
+#  endif
+
+#else
+// of _MSC_VER is not defined, we assume __cplusplus is correctly defined
+// if __cplusplus is not defined (very old compilers??), then the following
+// test will compare 0 < 201103L and will be true --> safe.
+#  if __cplusplus < 201103L
+#    define ORTHANC_Cxx03_DETECTED 1
+#  else
+#    define ORTHANC_Cxx03_DETECTED 0
+#  endif
+#endif
+
+#if ORTHANC_Cxx03_DETECTED == 1
+//#pragma message("C++ 11 support is not present.")
+
+/**
+ * "std::unique_ptr" was introduced in C++11, and "std::auto_ptr" was
+ * removed in C++17. We emulate "std::auto_ptr" using boost: "The
+ * smart pointer unique_ptr [is] a drop-in replacement for
+ * std::unique_ptr, usable also from C++03 compilers." This is only
+ * available if Boost >= 1.57.0 (from November 2014).
+ * https://www.boost.org/doc/libs/1_57_0/doc/html/move/reference.html#header.boost.move.unique_ptr_hpp
+ **/
+
+#include <boost/move/unique_ptr.hpp>
+
+namespace std
+{
+  template <typename T>
+  class unique_ptr : public boost::movelib::unique_ptr<T>
+  {
+  public:
+    explicit unique_ptr() :
+      boost::movelib::unique_ptr<T>()
+    {
+    }      
+
+    explicit unique_ptr(T* p) :
+      boost::movelib::unique_ptr<T>(p)
+    {
+    }      
+  };
+}
+#else
+//# pragma message("C++ 11 support is present.")
+# include <memory>
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DeflateBaseCompressor.h"
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+#include <string.h>
+
+namespace Orthanc
+{
+  void DeflateBaseCompressor::SetCompressionLevel(uint8_t level)
+  {
+    if (level >= 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Zlib compression level must be between 0 (no compression) and 9 (highest compression)");
+    }
+
+    compressionLevel_ = level;
+  }
+
+
+  uint64_t DeflateBaseCompressor::ReadUncompressedSizePrefix(const void* compressed,
+                                                             size_t compressedSize)
+  {
+    if (compressedSize == 0)
+    {
+      return 0;
+    }
+
+    if (compressedSize < sizeof(uint64_t))
+    {
+      throw OrthancException(ErrorCode_CorruptedFile, "The compressed buffer is ill-formed");
+    }
+
+    uint64_t size;
+    memcpy(&size, compressed, sizeof(uint64_t));
+
+    return size;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/DeflateBaseCompressor.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "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
+{
+  class ORTHANC_PUBLIC DeflateBaseCompressor : public IBufferCompressor
+  {
+  private:
+    uint8_t compressionLevel_;
+    bool    prefixWithUncompressedSize_;
+
+  protected:
+    uint64_t ReadUncompressedSizePrefix(const void* compressed,
+                                        size_t compressedSize);
+
+  public:
+    DeflateBaseCompressor() : 
+      compressionLevel_(6),
+      prefixWithUncompressedSize_(false)
+    {
+    }
+
+    void SetCompressionLevel(uint8_t level);
+    
+    void SetPrefixWithUncompressedSize(bool prefix)
+    {
+      prefixWithUncompressedSize_ = prefix;
+    }
+
+    bool HasPrefixWithUncompressedSize() const
+    {
+      return prefixWithUncompressedSize_;
+    }
+
+    uint8_t GetCompressionLevel() const
+    {
+      return compressionLevel_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,280 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "GzipCompressor.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <zlib.h>
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  uint64_t GzipCompressor::GuessUncompressedSize(const void* compressed,
+                                                 size_t compressedSize)
+  {
+    /**
+     * "Is there a way to find out the size of the original file which
+     * is inside a GZIP file? [...] There is no truly reliable way,
+     * other than gunzipping the stream. You do not need to save the
+     * result of the decompression, so you can determine the size by
+     * simply reading and decoding the entire file without taking up
+     * space with the decompressed result.
+     *
+     * There is an unreliable way to determine the uncompressed size,
+     * which is to look at the last four bytes of the gzip file, which
+     * is the uncompressed length of that entry modulo 232 in little
+     * endian order.
+     * 
+     * It is unreliable because a) the uncompressed data may be longer
+     * than 2^32 bytes, and b) the gzip file may consist of multiple
+     * gzip streams, in which case you would find the length of only
+     * the last of those streams.
+     * 
+     * If you are in control of the source of the gzip files, you know
+     * that they consist of single gzip streams, and you know that
+     * they are less than 2^32 bytes uncompressed, then and only then
+     * can you use those last four bytes with confidence."
+     *
+     * http://stackoverflow.com/a/9727599/881731
+     **/
+
+    if (compressedSize < 4)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    const uint8_t* p = reinterpret_cast<const uint8_t*>(compressed) + compressedSize - 4;
+
+    return ((static_cast<uint32_t>(p[0]) << 0) +
+            (static_cast<uint32_t>(p[1]) << 8) +
+            (static_cast<uint32_t>(p[2]) << 16) +
+            (static_cast<uint32_t>(p[3]) << 24));            
+  }
+
+
+
+  void GzipCompressor::Compress(std::string& compressed,
+                                const void* uncompressed,
+                                size_t uncompressedSize)
+  {
+    uLongf compressedSize = compressBound(static_cast<uLong>(uncompressedSize))
+      + 1024 /* security margin */;
+    
+    if (compressedSize == 0)
+    {
+      compressedSize = 1;
+    }
+
+    uint8_t* target;
+    if (HasPrefixWithUncompressedSize())
+    {
+      compressed.resize(compressedSize + sizeof(uint64_t));
+      target = reinterpret_cast<uint8_t*>(&compressed[0]) + sizeof(uint64_t);
+    }
+    else
+    {
+      compressed.resize(compressedSize);
+      target = reinterpret_cast<uint8_t*>(&compressed[0]);
+    }
+
+    z_stream stream;
+    memset(&stream, 0, sizeof(stream));
+
+    stream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(uncompressed));
+    stream.next_out = reinterpret_cast<Bytef*>(target);
+
+    stream.avail_in = static_cast<uInt>(uncompressedSize);
+    stream.avail_out = static_cast<uInt>(compressedSize);
+
+    // Ensure no overflow (if the buffer is too large for the current archicture)
+    if (static_cast<size_t>(stream.avail_in) != uncompressedSize ||
+        static_cast<size_t>(stream.avail_out) != compressedSize)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+    
+    // Initialize the compression engine
+    int error = deflateInit2(&stream, 
+                             GetCompressionLevel(), 
+                             Z_DEFLATED,
+                             MAX_WBITS + 16,      // ask for gzip output
+                             8,                   // default memory level
+                             Z_DEFAULT_STRATEGY);
+
+    if (error != Z_OK)
+    {
+      // Cannot initialize zlib
+      compressed.clear();
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Compress the input buffer
+    error = deflate(&stream, Z_FINISH);
+
+    if (error != Z_STREAM_END)
+    {
+      deflateEnd(&stream);
+      compressed.clear();
+
+      switch (error)
+      {
+      case Z_MEM_ERROR:
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+
+    size_t size = stream.total_out;
+
+    if (deflateEnd(&stream) != Z_OK)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // The compression was successful
+    if (HasPrefixWithUncompressedSize())
+    {
+      uint64_t s = static_cast<uint64_t>(uncompressedSize);
+      memcpy(&compressed[0], &s, sizeof(uint64_t));
+      compressed.resize(size + sizeof(uint64_t));
+    }
+    else
+    {
+      compressed.resize(size);
+    }
+  }
+
+
+  void GzipCompressor::Uncompress(std::string& uncompressed,
+                                  const void* compressed,
+                                  size_t compressedSize)
+  {
+    uint64_t uncompressedSize;
+    const uint8_t* source = reinterpret_cast<const uint8_t*>(compressed);
+
+    if (HasPrefixWithUncompressedSize())
+    {
+      uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
+      source += sizeof(uint64_t);
+      compressedSize -= sizeof(uint64_t);
+    }
+    else
+    {
+      uncompressedSize = GuessUncompressedSize(compressed, compressedSize);
+    }
+
+    try
+    {
+      uncompressed.resize(static_cast<size_t>(uncompressedSize));
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    z_stream stream;
+    memset(&stream, 0, sizeof(stream));
+
+    char dummy = '\0';  // zlib does not like NULL output buffers (even if the uncompressed data is empty)
+    stream.next_in = const_cast<Bytef*>(source);
+    stream.next_out = reinterpret_cast<Bytef*>(uncompressedSize == 0 ? &dummy : &uncompressed[0]);
+
+    stream.avail_in = static_cast<uInt>(compressedSize);
+    stream.avail_out = static_cast<uInt>(uncompressedSize);
+
+    // Ensure no overflow (if the buffer is too large for the current archicture)
+    if (static_cast<size_t>(stream.avail_in) != compressedSize ||
+        static_cast<size_t>(stream.avail_out) != uncompressedSize)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    // Initialize the compression engine
+    int error = inflateInit2(&stream, 
+                             MAX_WBITS + 16);  // this is a gzip input
+
+    if (error != Z_OK)
+    {
+      // Cannot initialize zlib
+      uncompressed.clear();
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Uncompress the input buffer
+    error = inflate(&stream, Z_FINISH);
+
+    if (error != Z_STREAM_END)
+    {
+      inflateEnd(&stream);
+      uncompressed.clear();
+
+      switch (error)
+      {
+        case Z_MEM_ERROR:
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+          
+        case Z_BUF_ERROR:
+        case Z_NEED_DICT:
+          throw OrthancException(ErrorCode_BadFileFormat);
+          
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    size_t size = stream.total_out;
+
+    if (inflateEnd(&stream) != Z_OK)
+    {
+      uncompressed.clear();
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (size != uncompressedSize)
+    {
+      uncompressed.clear();
+
+      // The uncompressed size was not that properly guess, presumably
+      // because of a file size over 4GB. Should fallback to
+      // stream-based decompression.
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The uncompressed size of a gzip-encoded buffer was not properly guessed");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/GzipCompressor.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DeflateBaseCompressor.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC GzipCompressor : public DeflateBaseCompressor
+  {
+  private:
+    uint64_t GuessUncompressedSize(const void* compressed,
+                                   size_t compressedSize);
+
+  public:
+    GzipCompressor()
+    {
+      SetPrefixWithUncompressedSize(false);
+    }
+
+    virtual void Compress(std::string& compressed,
+                          const void* uncompressed,
+                          size_t uncompressedSize);
+
+    virtual void Uncompress(std::string& uncompressed,
+                            const void* compressed,
+                            size_t compressedSize);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,182 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HierarchicalZipWriter.h"
+
+#include "../Toolbox.h"
+#include "../OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  std::string HierarchicalZipWriter::Index::KeepAlphanumeric(const std::string& source)
+  {
+    std::string result;
+
+    bool lastSpace = false;
+
+    result.reserve(source.size());
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      char c = source[i];
+      if (c == '^')
+        c = ' ';
+
+      if (c <= 127 && 
+          c >= 0)
+      {
+        if (isspace(c)) 
+        {
+          if (!lastSpace)
+          {
+            lastSpace = true;
+            result.push_back(' ');
+          }
+        }
+        else if (isalnum(c) || 
+                 c == '.' || 
+                 c == '_')
+        {
+          result.push_back(c);
+          lastSpace = false;
+        }
+      }
+    }
+
+    return Toolbox::StripSpaces(result);
+  }
+
+  std::string HierarchicalZipWriter::Index::GetCurrentDirectoryPath() const
+  {
+    std::string result;
+
+    Stack::const_iterator it = stack_.begin();
+    ++it;  // Skip the root node (to avoid absolute paths)
+
+    while (it != stack_.end())
+    {
+      result += (*it)->name_ + "/";
+      ++it;
+    }
+
+    return result;
+  }
+
+  std::string HierarchicalZipWriter::Index::EnsureUniqueFilename(const char* filename)
+  {
+    std::string standardized = KeepAlphanumeric(filename);
+
+    Directory& d = *stack_.back();
+    Directory::Content::iterator it = d.content_.find(standardized);
+
+    if (it == d.content_.end())
+    {
+      d.content_[standardized] = 1;
+      return standardized;
+    }
+    else
+    {
+      it->second++;
+      return standardized + "-" + boost::lexical_cast<std::string>(it->second);
+    }    
+  }
+
+  HierarchicalZipWriter::Index::Index()
+  {
+    stack_.push_back(new Directory);
+  }
+
+  HierarchicalZipWriter::Index::~Index()
+  {
+    for (Stack::iterator it = stack_.begin(); it != stack_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+  std::string HierarchicalZipWriter::Index::OpenFile(const char* name)
+  {
+    return GetCurrentDirectoryPath() + EnsureUniqueFilename(name);
+  }
+
+  void HierarchicalZipWriter::Index::OpenDirectory(const char* name)
+  {
+    std::string d = EnsureUniqueFilename(name);
+
+    // Push the new directory onto the stack
+    stack_.push_back(new Directory);
+    stack_.back()->name_ = d;
+  }
+
+  void HierarchicalZipWriter::Index::CloseDirectory()
+  {
+    if (IsRoot())
+    {
+      // Cannot close the root node
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    delete stack_.back();
+    stack_.pop_back();
+  }
+
+
+  HierarchicalZipWriter::HierarchicalZipWriter(const char* path)
+  {
+    writer_.SetOutputPath(path);
+    writer_.Open();
+  }
+
+  HierarchicalZipWriter::~HierarchicalZipWriter()
+  {
+    writer_.Close();
+  }
+
+  void HierarchicalZipWriter::OpenFile(const char* name)
+  {
+    std::string p = indexer_.OpenFile(name);
+    writer_.OpenFile(p.c_str());
+  }
+
+  void HierarchicalZipWriter::OpenDirectory(const char* name)
+  {
+    indexer_.OpenDirectory(name);
+  }
+
+  void HierarchicalZipWriter::CloseDirectory()
+  {
+    indexer_.CloseDirectory();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/HierarchicalZipWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ZipWriter.h"
+
+#include <map>
+#include <list>
+#include <boost/lexical_cast.hpp>
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HierarchicalZipWriter : public boost::noncopyable
+  {
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+    FRIEND_TEST(HierarchicalZipWriter, Index);
+    FRIEND_TEST(HierarchicalZipWriter, Filenames);
+#endif
+
+  private:
+    class ORTHANC_PUBLIC Index
+    {
+    private:
+      struct Directory
+      {
+        typedef std::map<std::string, unsigned int>  Content;
+
+        std::string name_;
+        Content  content_;
+      };
+
+      typedef std::list<Directory*> Stack;
+  
+      Stack stack_;
+
+      std::string EnsureUniqueFilename(const char* filename);
+
+    public:
+      Index();
+
+      ~Index();
+
+      bool IsRoot() const
+      {
+        return stack_.size() == 1;
+      }
+
+      std::string OpenFile(const char* name);
+
+      void OpenDirectory(const char* name);
+
+      void CloseDirectory();
+
+      std::string GetCurrentDirectoryPath() const;
+
+      static std::string KeepAlphanumeric(const std::string& source);
+    };
+
+    Index indexer_;
+    ZipWriter writer_;
+
+  public:
+    HierarchicalZipWriter(const char* path);
+
+    ~HierarchicalZipWriter();
+
+    void SetZip64(bool isZip64)
+    {
+      writer_.SetZip64(isZip64);
+    }
+
+    bool IsZip64() const
+    {
+      return writer_.IsZip64();
+    }
+
+    void SetCompressionLevel(uint8_t level)
+    {
+      writer_.SetCompressionLevel(level);
+    }
+
+    uint8_t GetCompressionLevel() const
+    {
+      return writer_.GetCompressionLevel();
+    }
+
+    void SetAppendToExisting(bool append)
+    {
+      writer_.SetAppendToExisting(append);
+    }
+    
+    bool IsAppendToExisting() const
+    {
+      return writer_.IsAppendToExisting();
+    }
+    
+    void OpenFile(const char* name);
+
+    void OpenDirectory(const char* name);
+
+    void CloseDirectory();
+
+    std::string GetCurrentDirectoryPath() const
+    {
+      return indexer_.GetCurrentDirectoryPath();
+    }
+
+    void Write(const void* data, size_t length)
+    {
+      writer_.Write(data, length);
+    }
+
+    void Write(const std::string& data)
+    {
+      writer_.Write(data);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/IBufferCompressor.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC IBufferCompressor : public boost::noncopyable
+  {
+  public:
+    virtual ~IBufferCompressor()
+    {
+    }
+
+    virtual void Compress(std::string& compressed,
+                          const void* uncompressed,
+                          size_t uncompressedSize) = 0;
+
+    virtual void Uncompress(std::string& uncompressed,
+                            const void* compressed,
+                            size_t compressedSize) = 0;
+
+    static void Compress(std::string& compressed,
+                         IBufferCompressor& compressor,
+                         const std::string& uncompressed)
+    {
+      compressor.Compress(compressed, 
+                          uncompressed.size() == 0 ? NULL : uncompressed.c_str(), 
+                          uncompressed.size());
+    }
+
+    static void Uncompress(std::string& uncompressed,
+                           IBufferCompressor& compressor,
+                           const std::string& compressed)
+    {
+      compressor.Uncompress(uncompressed, 
+                            compressed.size() == 0 ? NULL : compressed.c_str(), 
+                            compressed.size());
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,262 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+#include "ZipWriter.h"
+
+#include <limits>
+#include <boost/filesystem.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+#include "../../Resources/ThirdParty/minizip/zip.h"
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+
+static void PrepareFileInfo(zip_fileinfo& zfi)
+{
+  memset(&zfi, 0, sizeof(zfi));
+
+  using namespace boost::posix_time;
+  ptime now = second_clock::local_time();
+
+  boost::gregorian::date today = now.date();
+  ptime midnight(today);
+
+  time_duration sinceMidnight = now - midnight;
+  zfi.tmz_date.tm_sec = static_cast<unsigned int>(sinceMidnight.seconds());  // seconds after the minute - [0,59]
+  zfi.tmz_date.tm_min = static_cast<unsigned int>(sinceMidnight.minutes());  // minutes after the hour - [0,59]
+  zfi.tmz_date.tm_hour = static_cast<unsigned int>(sinceMidnight.hours());  // hours since midnight - [0,23]
+
+  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_day.html
+  zfi.tmz_date.tm_mday = today.day();  // day of the month - [1,31]
+
+  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_month.html
+  zfi.tmz_date.tm_mon = today.month() - 1;  // months since January - [0,11]
+
+  // http://www.boost.org/doc/libs/1_35_0/doc/html/boost/gregorian/greg_year.html
+  zfi.tmz_date.tm_year = today.year();  // years - [1980..2044]
+}
+
+
+
+namespace Orthanc
+{
+  struct ZipWriter::PImpl
+  {
+    zipFile file_;
+
+    PImpl() : file_(NULL)
+    {
+    }
+  };
+
+  ZipWriter::ZipWriter() :
+    pimpl_(new PImpl),
+    isZip64_(false),
+    hasFileInZip_(false),
+    append_(false),
+    compressionLevel_(6)
+  {
+  }
+
+  ZipWriter::~ZipWriter()
+  {
+    Close();
+  }
+
+  void ZipWriter::Close()
+  {
+    if (IsOpen())
+    {
+      zipClose(pimpl_->file_, "Created by Orthanc");
+      pimpl_->file_ = NULL;
+      hasFileInZip_ = false;
+    }
+  }
+
+  bool ZipWriter::IsOpen() const
+  {
+    return pimpl_->file_ != NULL;
+  }
+
+  void ZipWriter::Open()
+  {
+    if (IsOpen())
+    {
+      return;
+    }
+
+    if (path_.size() == 0)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Please call SetOutputPath() before creating the file");
+    }
+
+    hasFileInZip_ = false;
+
+    int mode = APPEND_STATUS_CREATE;
+    if (append_ && 
+        boost::filesystem::exists(path_))
+    {
+      mode = APPEND_STATUS_ADDINZIP;
+    }
+
+    if (isZip64_)
+    {
+      pimpl_->file_ = zipOpen64(path_.c_str(), mode);
+    }
+    else
+    {
+      pimpl_->file_ = zipOpen(path_.c_str(), mode);
+    }
+
+    if (!pimpl_->file_)
+    {
+      throw OrthancException(ErrorCode_CannotWriteFile,
+                             "Cannot create new ZIP archive: " + path_);
+    }
+  }
+
+  void ZipWriter::SetOutputPath(const char* path)
+  {
+    Close();
+    path_ = path;
+  }
+
+  void ZipWriter::SetZip64(bool isZip64)
+  {
+    Close();
+    isZip64_ = isZip64;
+  }
+
+  void ZipWriter::SetCompressionLevel(uint8_t level)
+  {
+    if (level >= 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "ZIP compression level must be between 0 (no compression) "
+                             "and 9 (highest compression)");
+    }
+
+    Close();
+    compressionLevel_ = level;
+  }
+
+  void ZipWriter::OpenFile(const char* path)
+  {
+    Open();
+
+    zip_fileinfo zfi;
+    PrepareFileInfo(zfi);
+
+    int result;
+
+    if (isZip64_)
+    {
+      result = zipOpenNewFileInZip64(pimpl_->file_, path,
+                                     &zfi,
+                                     NULL,   0,
+                                     NULL,   0,
+                                     "",  // Comment
+                                     Z_DEFLATED,
+                                     compressionLevel_, 1);
+    }
+    else
+    {
+      result = zipOpenNewFileInZip(pimpl_->file_, path,
+                                   &zfi,
+                                   NULL,   0,
+                                   NULL,   0,
+                                   "",  // Comment
+                                   Z_DEFLATED,
+                                   compressionLevel_);
+    }
+
+    if (result != 0)
+    {
+      throw OrthancException(ErrorCode_CannotWriteFile,
+                             "Cannot add new file inside ZIP archive: " + std::string(path));
+    }
+
+    hasFileInZip_ = true;
+  }
+
+
+  void ZipWriter::Write(const std::string& data)
+  {
+    if (data.size())
+    {
+      Write(&data[0], data.size());
+    }
+  }
+
+
+  void ZipWriter::Write(const void* data, size_t length)
+  {
+    if (!hasFileInZip_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Call first OpenFile()");
+    }
+
+    const size_t maxBytesInAStep = std::numeric_limits<int32_t>::max();
+
+    const char* p = reinterpret_cast<const char*>(data);
+    
+    while (length > 0)
+    {
+      int bytes = static_cast<int32_t>(length <= maxBytesInAStep ? length : maxBytesInAStep);
+
+      if (zipWriteInFileInZip(pimpl_->file_, p, bytes))
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile,
+                               "Cannot write data to ZIP archive: " + path_);
+      }
+      
+      p += bytes;
+      length -= bytes;
+    }
+  }
+
+
+  void ZipWriter::SetAppendToExisting(bool append)
+  {
+    Close();
+    append_ = append;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/ZipWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.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>
+#include <string>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ZipWriter : public boost::noncopyable
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    bool isZip64_;
+    bool hasFileInZip_;
+    bool append_;
+    uint8_t compressionLevel_;
+    std::string path_;
+
+  public:
+    ZipWriter();
+
+    ~ZipWriter();
+
+    void SetZip64(bool isZip64);
+
+    bool IsZip64() const
+    {
+      return isZip64_;
+    }
+
+    void SetCompressionLevel(uint8_t level);
+
+    uint8_t GetCompressionLevel() const
+    {
+      return compressionLevel_;
+    }
+
+    void SetAppendToExisting(bool append);
+    
+    bool IsAppendToExisting() const
+    {
+      return append_;
+    }
+    
+    void Open();
+
+    void Close();
+
+    bool IsOpen() const;
+
+    void SetOutputPath(const char* path);
+
+    const std::string& GetOutputPath() const
+    {
+      return path_;
+    }
+
+    void OpenFile(const char* path);
+
+    void Write(const void* data, size_t length);
+
+    void Write(const std::string& data);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,160 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ZlibCompressor.h"
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <zlib.h>
+
+namespace Orthanc
+{
+  void ZlibCompressor::Compress(std::string& compressed,
+                                const void* uncompressed,
+                                size_t uncompressedSize)
+  {
+    if (uncompressedSize == 0)
+    {
+      compressed.clear();
+      return;
+    }
+
+    uLongf compressedSize = compressBound(static_cast<uLong>(uncompressedSize))
+      + 1024 /* security margin */;
+    if (compressedSize == 0)
+    {
+      compressedSize = 1;
+    }
+
+    uint8_t* target;
+    if (HasPrefixWithUncompressedSize())
+    {
+      compressed.resize(compressedSize + sizeof(uint64_t));
+      target = reinterpret_cast<uint8_t*>(&compressed[0]) + sizeof(uint64_t);
+    }
+    else
+    {
+      compressed.resize(compressedSize);
+      target = reinterpret_cast<uint8_t*>(&compressed[0]);
+    }
+
+    int error = compress2(target,
+                          &compressedSize,
+                          const_cast<Bytef *>(static_cast<const Bytef *>(uncompressed)), 
+                          static_cast<uLong>(uncompressedSize),
+                          GetCompressionLevel());
+
+    if (error != Z_OK)
+    {
+      compressed.clear();
+
+      switch (error)
+      {
+      case Z_MEM_ERROR:
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+
+    // The compression was successful
+    if (HasPrefixWithUncompressedSize())
+    {
+      uint64_t s = static_cast<uint64_t>(uncompressedSize);
+      memcpy(&compressed[0], &s, sizeof(uint64_t));
+      compressed.resize(compressedSize + sizeof(uint64_t));
+    }
+    else
+    {
+      compressed.resize(compressedSize);
+    }
+  }
+
+
+  void ZlibCompressor::Uncompress(std::string& uncompressed,
+                                  const void* compressed,
+                                  size_t compressedSize)
+  {
+    if (compressedSize == 0)
+    {
+      uncompressed.clear();
+      return;
+    }
+
+    if (!HasPrefixWithUncompressedSize())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot guess the uncompressed size of a zlib-encoded buffer");
+    }
+
+    uint64_t uncompressedSize = ReadUncompressedSizePrefix(compressed, compressedSize);
+    
+    try
+    {
+      uncompressed.resize(static_cast<size_t>(uncompressedSize));
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    uLongf tmp = static_cast<uLongf>(uncompressedSize);
+    int error = uncompress
+      (reinterpret_cast<uint8_t*>(&uncompressed[0]), 
+       &tmp,
+       reinterpret_cast<const uint8_t*>(compressed) + sizeof(uint64_t),
+        static_cast<uLong>(compressedSize - sizeof(uint64_t)));
+
+    if (error != Z_OK)
+    {
+      uncompressed.clear();
+
+      switch (error)
+      {
+      case Z_DATA_ERROR:
+        throw OrthancException(ErrorCode_CorruptedFile);
+
+      case Z_MEM_ERROR:
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Compression/ZlibCompressor.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DeflateBaseCompressor.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ZlibCompressor : public DeflateBaseCompressor
+  {
+  public:
+    ZlibCompressor()
+    {
+      SetPrefixWithUncompressedSize(true);
+    }
+
+    virtual void Compress(std::string& compressed,
+                          const void* uncompressed,
+                          size_t uncompressedSize);
+
+    virtual void Uncompress(std::string& uncompressed,
+                            const void* compressed,
+                            size_t compressedSize);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomArray.h"
+
+#include <stdio.h>
+
+namespace Orthanc
+{
+  DicomArray::DicomArray(const DicomMap& map)
+  {
+    elements_.reserve(map.content_.size());
+    
+    for (DicomMap::Content::const_iterator it = 
+           map.content_.begin(); it != map.content_.end(); ++it)
+    {
+      elements_.push_back(new DicomElement(it->first, *it->second));
+    }
+  }
+
+
+  DicomArray::~DicomArray()
+  {
+    for (size_t i = 0; i < elements_.size(); i++)
+    {
+      delete elements_[i];
+    }
+  }
+
+
+  void DicomArray::Print(FILE* fp) const
+  {
+    for (size_t  i = 0; i < elements_.size(); i++)
+    {
+      DicomTag t = elements_[i]->GetTag();
+      const DicomValue& v = elements_[i]->GetValue();
+      std::string s = v.IsNull() ? "(null)" : v.GetContent();
+      printf("0x%04x 0x%04x [%s]\n", t.GetGroup(), t.GetElement(), s.c_str());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomArray.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomElement.h"
+#include "DicomMap.h"
+
+#include <vector>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomArray : public boost::noncopyable
+  {
+  private:
+    typedef std::vector<DicomElement*>  Elements;
+
+    Elements  elements_;
+
+  public:
+    explicit DicomArray(const DicomMap& map);
+
+    ~DicomArray();
+
+    size_t GetSize() const
+    {
+      return elements_.size();
+    }
+
+    const DicomElement& GetElement(size_t i) const
+    {
+      return *elements_[i];
+    }
+
+    void Print(FILE* fp) const;  // For debugging only
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomElement.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomValue.h"
+#include "DicomTag.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomElement : public boost::noncopyable
+  {
+  private:
+    DicomTag tag_;
+    DicomValue* value_;
+
+  public:
+    DicomElement(uint16_t group,
+                 uint16_t element,
+                 const DicomValue& value) :
+      tag_(group, element),
+      value_(value.Clone())
+    {
+    }
+
+    DicomElement(const DicomTag& tag,
+                 const DicomValue& value) :
+      tag_(tag),
+      value_(value.Clone())
+    {
+    }
+
+    ~DicomElement()
+    {
+      delete value_;
+    }
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    const DicomValue& GetValue() const
+    {
+      return *value_;
+    }
+
+    uint16_t GetTagGroup() const
+    {
+      return tag_.GetGroup();
+    }
+
+    uint16_t GetTagElement() const
+    {
+      return tag_.GetElement();
+    }
+
+    bool operator< (const DicomElement& other) const
+    {
+      return GetTag() < other.GetTag();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,310 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+#include "DicomImageInformation.h"
+
+#include "../Compatibility.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include <boost/lexical_cast.hpp>
+#include <limits>
+#include <cassert>
+#include <stdio.h>
+#include <memory>
+
+namespace Orthanc
+{
+  DicomImageInformation::DicomImageInformation(const DicomMap& values)
+  {
+    unsigned int pixelRepresentation;
+    unsigned int planarConfiguration = 0;
+
+    try
+    {
+      std::string p = values.GetValue(DICOM_TAG_PHOTOMETRIC_INTERPRETATION).GetContent();
+      Toolbox::ToUpperCase(p);
+
+      if (p == "RGB")
+      {
+        photometric_ = PhotometricInterpretation_RGB;
+      }
+      else if (p == "MONOCHROME1")
+      {
+        photometric_ = PhotometricInterpretation_Monochrome1;
+      }
+      else if (p == "MONOCHROME2")
+      {
+        photometric_ = PhotometricInterpretation_Monochrome2;
+      }
+      else if (p == "PALETTE COLOR")
+      {
+        photometric_ = PhotometricInterpretation_Palette;
+      }
+      else if (p == "HSV")
+      {
+        photometric_ = PhotometricInterpretation_HSV;
+      }
+      else if (p == "ARGB")
+      {
+        photometric_ = PhotometricInterpretation_ARGB;
+      }
+      else if (p == "CMYK")
+      {
+        photometric_ = PhotometricInterpretation_CMYK;
+      }
+      else if (p == "YBR_FULL")
+      {
+        photometric_ = PhotometricInterpretation_YBRFull;
+      }
+      else if (p == "YBR_FULL_422")
+      {
+        photometric_ = PhotometricInterpretation_YBRFull422;
+      }
+      else if (p == "YBR_PARTIAL_420")
+      {
+        photometric_ = PhotometricInterpretation_YBRPartial420;
+      }
+      else if (p == "YBR_PARTIAL_422")
+      {
+        photometric_ = PhotometricInterpretation_YBRPartial422;
+      }
+      else if (p == "YBR_ICT")
+      {
+        photometric_ = PhotometricInterpretation_YBR_ICT;
+      }
+      else if (p == "YBR_RCT")
+      {
+        photometric_ = PhotometricInterpretation_YBR_RCT;
+      }
+      else
+      {
+        photometric_ = PhotometricInterpretation_Unknown;
+      }
+
+      values.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(width_); // in some US images, we've seen tag values of "800\0"; that's why we parse the 'first' value
+      values.GetValue(DICOM_TAG_ROWS).ParseFirstUnsignedInteger(height_);
+
+      bitsAllocated_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_ALLOCATED).GetContent());
+
+      try
+      {
+        samplesPerPixel_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_SAMPLES_PER_PIXEL).GetContent());
+      }
+      catch (OrthancException&)
+      {
+        samplesPerPixel_ = 1;  // Assume 1 color channel
+      }
+
+      try
+      {
+        bitsStored_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_STORED).GetContent());
+      }
+      catch (OrthancException&)
+      {
+        bitsStored_ = bitsAllocated_;
+      }
+
+      try
+      {
+        highBit_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_HIGH_BIT).GetContent());
+      }
+      catch (OrthancException&)
+      {
+        highBit_ = bitsStored_ - 1;
+      }
+
+      try
+      {
+        pixelRepresentation = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_PIXEL_REPRESENTATION).GetContent());
+      }
+      catch (OrthancException&)
+      {
+        pixelRepresentation = 0;  // Assume unsigned pixels
+      }
+
+      if (samplesPerPixel_ > 1)
+      {
+        // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1
+        // 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());
+        }
+        catch (OrthancException&)
+        {
+          planarConfiguration = 0;  // Assume interleaved color channels
+        }
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+    catch (OrthancException&)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    if (values.HasTag(DICOM_TAG_NUMBER_OF_FRAMES))
+    {
+      try
+      {
+        numberOfFrames_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+    else
+    {
+      numberOfFrames_ = 1;
+    }
+
+    if (bitsAllocated_ != 8 && bitsAllocated_ != 16 &&
+        bitsAllocated_ != 24 && bitsAllocated_ != 32)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: " + boost::lexical_cast<std::string>(bitsAllocated_) + " bits allocated");
+    }
+    else if (numberOfFrames_ == 0)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported (no frames)");
+    }
+    else if (planarConfiguration != 0 && planarConfiguration != 1)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: planar configuration is " + boost::lexical_cast<std::string>(planarConfiguration));
+    }
+
+    if (samplesPerPixel_ == 0)
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat, "Image not supported: samples per pixel is 0");
+    }
+
+    bytesPerValue_ = bitsAllocated_ / 8;
+
+    isPlanar_ = (planarConfiguration != 0 ? true : false);
+    isSigned_ = (pixelRepresentation != 0 ? true : false);
+  }
+
+  DicomImageInformation* DicomImageInformation::Clone() const
+  {
+    std::unique_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_;
+
+    return target.release();
+  }
+
+  bool DicomImageInformation::ExtractPixelFormat(PixelFormat& format,
+                                                 bool ignorePhotometricInterpretation) const
+  {
+    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())
+      {
+        format = PixelFormat_Grayscale8;
+        return true;
+      }
+      
+      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale16;
+        return true;
+      }
+
+      if (GetBitsAllocated() == 16 && GetChannelCount() == 1 && IsSigned())
+      {
+        format = PixelFormat_SignedGrayscale16;
+        return true;
+      }
+      
+      if (GetBitsAllocated() == 32 && GetChannelCount() == 1 && !IsSigned())
+      {
+        format = PixelFormat_Grayscale32;
+        return true;
+      }
+    }
+
+    if (GetBitsStored() == 8 &&
+        GetChannelCount() == 3 &&
+        !IsSigned() &&
+        (ignorePhotometricInterpretation || photometric_ == PhotometricInterpretation_RGB))
+    {
+      format = PixelFormat_RGB24;
+      return true;
+    }
+
+    return false;
+  }
+
+
+  size_t DicomImageInformation::GetFrameSize() const
+  {
+    return (GetHeight() *
+            GetWidth() *
+            GetBytesPerValue() *
+            GetChannelCount());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomImageInformation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomMap.h"
+
+#include <stdint.h>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomImageInformation
+  {
+  private:
+    unsigned int width_;
+    unsigned int height_;
+    unsigned int samplesPerPixel_;
+    unsigned int numberOfFrames_;
+
+    bool isPlanar_;
+    bool isSigned_;
+    size_t bytesPerValue_;
+
+    unsigned int bitsAllocated_;
+    unsigned int bitsStored_;
+    unsigned int highBit_;
+
+    PhotometricInterpretation  photometric_;
+
+  protected:
+    explicit DicomImageInformation()
+    {
+    }
+
+  public:
+    explicit DicomImageInformation(const DicomMap& values);
+
+    DicomImageInformation* Clone() const;
+
+    unsigned int GetWidth() const
+    {
+      return width_;
+    }
+
+    unsigned int GetHeight() const
+    {
+      return height_;
+    }
+
+    unsigned int GetNumberOfFrames() const
+    {
+      return numberOfFrames_;
+    }
+
+    unsigned int GetChannelCount() const
+    {
+      return samplesPerPixel_;
+    }
+
+    unsigned int GetBitsStored() const
+    {
+      return bitsStored_;
+    }
+
+    size_t GetBytesPerValue() const
+    {
+      return bytesPerValue_;
+    }
+
+    bool IsSigned() const
+    {
+      return isSigned_;
+    }
+
+    unsigned int GetBitsAllocated() const
+    {
+      return bitsAllocated_;
+    }
+
+    unsigned int GetHighBit() const
+    {
+      return highBit_;
+    }
+
+    bool IsPlanar() const
+    {
+      return isPlanar_;
+    }
+
+    unsigned int GetShift() const
+    {
+      return highBit_ + 1 - bitsStored_;
+    }
+
+    PhotometricInterpretation GetPhotometricInterpretation() const
+    {
+      return photometric_;
+    }
+
+    bool ExtractPixelFormat(PixelFormat& format,
+                            bool ignorePhotometricInterpretation) const;
+
+    size_t GetFrameSize() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomInstanceHasher.h"
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  void DicomInstanceHasher::Setup(const std::string& patientId,
+                                  const std::string& studyUid,
+                                  const std::string& seriesUid,
+                                  const std::string& instanceUid)
+  {
+    patientId_ = patientId;
+    studyUid_ = studyUid;
+    seriesUid_ = seriesUid;
+    instanceUid_ = instanceUid;
+
+    if (studyUid_.size() == 0 ||
+        seriesUid_.size() == 0 ||
+        instanceUid_.size() == 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
+    }
+  }
+
+  DicomInstanceHasher::DicomInstanceHasher(const DicomMap& instance)
+  {
+    const DicomValue* patientId = instance.TestAndGetValue(DICOM_TAG_PATIENT_ID);
+
+    Setup(patientId == NULL ? "" : patientId->GetContent(),
+          instance.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent(),
+          instance.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent(),
+          instance.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent());
+  }
+
+  const std::string& DicomInstanceHasher::HashPatient()
+  {
+    if (patientHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(patientHash_, patientId_);
+    }
+
+    return patientHash_;
+  }
+
+  const std::string& DicomInstanceHasher::HashStudy()
+  {
+    if (studyHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(studyHash_, patientId_ + "|" + studyUid_);
+    }
+
+    return studyHash_;
+  }
+
+  const std::string& DicomInstanceHasher::HashSeries()
+  {
+    if (seriesHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(seriesHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_);
+    }
+
+    return seriesHash_;
+  }
+
+  const std::string& DicomInstanceHasher::HashInstance()
+  {
+    if (instanceHash_.size() == 0)
+    {
+      Toolbox::ComputeSHA1(instanceHash_, patientId_ + "|" + studyUid_ + "|" + seriesUid_ + "|" + instanceUid_);
+    }
+
+    return instanceHash_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomInstanceHasher.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomMap.h"
+
+namespace Orthanc
+{
+  /**
+   * This class implements the hashing mechanism that is used to
+   * convert DICOM unique identifiers to Orthanc identifiers. Any
+   * Orthanc identifier for a DICOM resource corresponds to the SHA-1
+   * hash of the DICOM identifiers. 
+
+   * \note SHA-1 hash is used because it is less sensitive to
+   * collision attacks than MD5. <a
+   * href="http://en.wikipedia.org/wiki/SHA-256#Comparison_of_SHA_functions">[Reference]</a>
+   **/
+  class DicomInstanceHasher
+  {
+  private:
+    std::string patientId_;
+    std::string studyUid_;
+    std::string seriesUid_;
+    std::string instanceUid_;
+
+    std::string patientHash_;
+    std::string studyHash_;
+    std::string seriesHash_;
+    std::string instanceHash_;
+
+    void Setup(const std::string& patientId,
+               const std::string& studyUid,
+               const std::string& seriesUid,
+               const std::string& instanceUid);
+
+  public:
+    DicomInstanceHasher(const DicomMap& instance);
+
+    DicomInstanceHasher(const std::string& patientId,
+                        const std::string& studyUid,
+                        const std::string& seriesUid,
+                        const std::string& instanceUid)
+    {
+      Setup(patientId, studyUid, seriesUid, instanceUid);
+    }
+
+    const std::string& GetPatientId() const
+    {
+      return patientId_;
+    }
+
+    const std::string& GetStudyUid() const
+    {
+      return studyUid_;
+    }
+
+    const std::string& GetSeriesUid() const
+    {
+      return seriesUid_;
+    }
+
+    const std::string& GetInstanceUid() const
+    {
+      return instanceUid_;
+    }
+
+    const std::string& HashPatient();
+
+    const std::string& HashStudy();
+
+    const std::string& HashSeries();
+
+    const std::string& HashInstance();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,204 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+#include "DicomIntegerPixelAccessor.h"
+
+#include "../OrthancException.h"
+#include <boost/lexical_cast.hpp>
+#include <limits>
+#include <cassert>
+#include <stdio.h>
+
+namespace Orthanc
+{
+  DicomIntegerPixelAccessor::DicomIntegerPixelAccessor(const DicomMap& values,
+                                                       const void* pixelData,
+                                                       size_t size) :
+    information_(values),
+    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_.GetFrameSize();
+
+    if (information_.GetNumberOfFrames() * frameOffset_ > size)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    if (information_.IsSigned())
+    {
+      // Pixels are signed
+      mask_ = (1 << (information_.GetBitsStored() - 1)) - 1;
+      signMask_ = (1 << (information_.GetBitsStored() - 1));
+    }
+    else
+    {
+      // Pixels are unsigned
+      mask_ = (1 << information_.GetBitsStored()) - 1;
+      signMask_ = 0;
+    }
+
+    if (information_.IsPlanar())
+    {
+      /**
+       * Each color plane shall be sent contiguously. For RGB images,
+       * this means the order of the pixel values sent is R1, R2, R3,
+       * ..., G1, G2, G3, ..., B1, B2, B3, etc.
+       **/
+      rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue();
+    }
+    else
+    {
+      /**
+       * The sample values for the first pixel are followed by the
+       * sample values for the second pixel, etc. For RGB images, this
+       * means the order of the pixel values sent shall be R1, G1, B1,
+       * R2, G2, B2, ..., etc.
+       **/
+      rowOffset_ = information_.GetWidth() * information_.GetBytesPerValue() * information_.GetChannelCount();
+    }
+  }
+
+
+  void DicomIntegerPixelAccessor::GetExtremeValues(int32_t& min, 
+                                                   int32_t& max) const
+  {
+    if (information_.GetHeight() == 0 || information_.GetWidth() == 0)
+    {
+      min = max = 0;
+      return;
+    }
+
+    min = std::numeric_limits<int32_t>::max();
+    max = std::numeric_limits<int32_t>::min();
+    
+    for (unsigned int y = 0; y < information_.GetHeight(); y++)
+    {
+      for (unsigned int x = 0; x < information_.GetWidth(); x++)
+      {
+        for (unsigned int c = 0; c < information_.GetChannelCount(); c++)
+        {
+          int32_t v = GetValue(x, y);
+          if (v < min)
+            min = v;
+          if (v > max)
+            max = v;
+        }
+      }
+    }
+  }
+
+
+  int32_t DicomIntegerPixelAccessor::GetValue(unsigned int x, 
+                                              unsigned int y,
+                                              unsigned int channel) const
+  {
+    assert(x < information_.GetWidth() && 
+           y < information_.GetHeight() && 
+           channel < information_.GetChannelCount());
+    
+    const uint8_t* pixel = reinterpret_cast<const uint8_t*>(pixelData_) + 
+      y * rowOffset_ + frame_ * frameOffset_;
+
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.3
+    if (information_.IsPlanar())
+    {
+      /**
+       * Each color plane shall be sent contiguously. For RGB images,
+       * this means the order of the pixel values sent is R1, R2, R3,
+       * ..., G1, G2, G3, ..., B1, B2, B3, etc.
+       **/
+      assert(frameOffset_ % information_.GetChannelCount() == 0);
+      pixel += channel * frameOffset_ / information_.GetChannelCount() + x * information_.GetBytesPerValue();
+    }
+    else
+    {
+      /**
+       * The sample values for the first pixel are followed by the
+       * sample values for the second pixel, etc. For RGB images, this
+       * means the order of the pixel values sent shall be R1, G1, B1,
+       * R2, G2, B2, ..., etc.
+       **/
+      pixel += channel * information_.GetBytesPerValue() + x * information_.GetChannelCount() * information_.GetBytesPerValue();
+    }
+
+    uint32_t v;
+    v = pixel[0];
+    if (information_.GetBytesPerValue() >= 2)
+      v = v + (static_cast<uint32_t>(pixel[1]) << 8);
+    if (information_.GetBytesPerValue() >= 3)
+      v = v + (static_cast<uint32_t>(pixel[2]) << 16);
+    if (information_.GetBytesPerValue() >= 4)
+      v = v + (static_cast<uint32_t>(pixel[3]) << 24);
+
+    v = v >> information_.GetShift();
+
+    if (v & signMask_)
+    {
+      // Signed value
+      // http://en.wikipedia.org/wiki/Two%27s_complement#Subtraction_from_2N
+      return -static_cast<int32_t>(mask_) + static_cast<int32_t>(v & mask_) - 1;
+    }
+    else
+    {
+      // Unsigned value
+      return static_cast<int32_t>(v & mask_);
+    }
+  }
+
+
+  void DicomIntegerPixelAccessor::SetCurrentFrame(unsigned int frame)
+  {
+    if (frame >= information_.GetNumberOfFrames())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    frame_ = frame;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomIntegerPixelAccessor.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,90 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomMap.h"
+
+#include "DicomImageInformation.h"
+
+#include <stdint.h>
+
+namespace Orthanc
+{
+  class DicomIntegerPixelAccessor
+  {
+  private:
+    DicomImageInformation information_;
+
+    uint32_t signMask_;
+    uint32_t mask_;
+
+    const void* pixelData_;
+    size_t size_;
+    unsigned int frame_;
+    size_t frameOffset_;
+    size_t rowOffset_;
+
+  public:
+    DicomIntegerPixelAccessor(const DicomMap& values,
+                              const void* pixelData,
+                              size_t size);
+
+    const DicomImageInformation GetInformation() const
+    {
+      return information_;
+    }
+
+    unsigned int GetCurrentFrame() const
+    {
+      return frame_;
+    }
+
+    void SetCurrentFrame(unsigned int frame);
+
+    void GetExtremeValues(int32_t& min, 
+                          int32_t& max) const;
+
+    int32_t GetValue(unsigned int x, unsigned int y, unsigned int channel = 0) const;
+
+    const void* GetPixelData() const
+    {
+      return pixelData_;
+    }
+
+    size_t GetSize() const
+    {
+      return size_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1424 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomMap.h"
+
+#include <stdio.h>
+#include <memory>
+
+#include "../Compatibility.h"
+#include "../Endianness.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "DicomArray.h"
+
+
+namespace Orthanc
+{
+  namespace
+  {
+    struct MainDicomTag
+    {
+      const DicomTag tag_;
+      const char*    name_;
+    };
+  }
+
+  static const MainDicomTag PATIENT_MAIN_DICOM_TAGS[] =
+  {
+    // { DicomTag(0x0010, 0x1010), "PatientAge" },
+    // { DicomTag(0x0010, 0x1040), "PatientAddress" },
+    { DicomTag(0x0010, 0x0010), "PatientName" },
+    { DicomTag(0x0010, 0x0030), "PatientBirthDate" },
+    { DicomTag(0x0010, 0x0040), "PatientSex" },
+    { DicomTag(0x0010, 0x1000), "OtherPatientIDs" },
+    { DICOM_TAG_PATIENT_ID, "PatientID" }
+  };
+    
+  static const MainDicomTag STUDY_MAIN_DICOM_TAGS[] =
+  {
+    // { DicomTag(0x0010, 0x1020), "PatientSize" },
+    // { DicomTag(0x0010, 0x1030), "PatientWeight" },
+    { DICOM_TAG_STUDY_DATE, "StudyDate" },
+    { DicomTag(0x0008, 0x0030), "StudyTime" },
+    { DicomTag(0x0020, 0x0010), "StudyID" },
+    { DICOM_TAG_STUDY_DESCRIPTION, "StudyDescription" },
+    { DICOM_TAG_ACCESSION_NUMBER, "AccessionNumber" },
+    { DICOM_TAG_STUDY_INSTANCE_UID, "StudyInstanceUID" },
+
+    // New in db v6
+    { DICOM_TAG_REQUESTED_PROCEDURE_DESCRIPTION, "RequestedProcedureDescription" },
+    { DICOM_TAG_INSTITUTION_NAME, "InstitutionName" },
+    { DICOM_TAG_REQUESTING_PHYSICIAN, "RequestingPhysician" },
+    { DICOM_TAG_REFERRING_PHYSICIAN_NAME, "ReferringPhysicianName" }
+  };
+    
+  static const MainDicomTag SERIES_MAIN_DICOM_TAGS[] =
+  {
+    // { DicomTag(0x0010, 0x1080), "MilitaryRank" },
+    { DicomTag(0x0008, 0x0021), "SeriesDate" },
+    { DicomTag(0x0008, 0x0031), "SeriesTime" },
+    { DICOM_TAG_MODALITY, "Modality" },
+    { DicomTag(0x0008, 0x0070), "Manufacturer" },
+    { DicomTag(0x0008, 0x1010), "StationName" },
+    { DICOM_TAG_SERIES_DESCRIPTION, "SeriesDescription" },
+    { DicomTag(0x0018, 0x0015), "BodyPartExamined" },
+    { DicomTag(0x0018, 0x0024), "SequenceName" },
+    { DicomTag(0x0018, 0x1030), "ProtocolName" },
+    { DicomTag(0x0020, 0x0011), "SeriesNumber" },
+    { DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES, "CardiacNumberOfImages" },
+    { DICOM_TAG_IMAGES_IN_ACQUISITION, "ImagesInAcquisition" },
+    { DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS, "NumberOfTemporalPositions" },
+    { DICOM_TAG_NUMBER_OF_SLICES, "NumberOfSlices" },
+    { DICOM_TAG_NUMBER_OF_TIME_SLICES, "NumberOfTimeSlices" },
+    { DICOM_TAG_SERIES_INSTANCE_UID, "SeriesInstanceUID" },
+
+    // New in db v6
+    { DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "ImageOrientationPatient" },
+    { DICOM_TAG_SERIES_TYPE, "SeriesType" },
+    { DICOM_TAG_OPERATOR_NAME, "OperatorsName" },
+    { DICOM_TAG_PERFORMED_PROCEDURE_STEP_DESCRIPTION, "PerformedProcedureStepDescription" },
+    { DICOM_TAG_ACQUISITION_DEVICE_PROCESSING_DESCRIPTION, "AcquisitionDeviceProcessingDescription" },
+    { DICOM_TAG_CONTRAST_BOLUS_AGENT, "ContrastBolusAgent" }
+  };
+    
+  static const MainDicomTag INSTANCE_MAIN_DICOM_TAGS[] =
+  {
+    { DicomTag(0x0008, 0x0012), "InstanceCreationDate" },
+    { DicomTag(0x0008, 0x0013), "InstanceCreationTime" },
+    { DicomTag(0x0020, 0x0012), "AcquisitionNumber" },
+    { DICOM_TAG_IMAGE_INDEX, "ImageIndex" },
+    { DICOM_TAG_INSTANCE_NUMBER, "InstanceNumber" },
+    { DICOM_TAG_NUMBER_OF_FRAMES, "NumberOfFrames" },
+    { DICOM_TAG_TEMPORAL_POSITION_IDENTIFIER, "TemporalPositionIdentifier" },
+    { DICOM_TAG_SOP_INSTANCE_UID, "SOPInstanceUID" },
+
+    // New in db v6
+    { DICOM_TAG_IMAGE_POSITION_PATIENT, "ImagePositionPatient" },
+    { DICOM_TAG_IMAGE_COMMENTS, "ImageComments" },
+
+    /**
+     * 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, "ImageOrientationPatient" }  // New in Orthanc 1.4.2
+  };
+
+
+  static void LoadMainDicomTags(const MainDicomTag*& tags,
+                                size_t& size,
+                                ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        tags = PATIENT_MAIN_DICOM_TAGS;
+        size = sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+        break;
+
+      case ResourceType_Study:
+        tags = STUDY_MAIN_DICOM_TAGS;
+        size = sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+        break;
+
+      case ResourceType_Series:
+        tags = SERIES_MAIN_DICOM_TAGS;
+        size = sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+        break;
+
+      case ResourceType_Instance:
+        tags = INSTANCE_MAIN_DICOM_TAGS;
+        size = sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void LoadMainDicomTags(std::map<DicomTag, std::string>& target,
+                                ResourceType level)
+  {
+    const MainDicomTag* tags = NULL;
+    size_t size;
+    LoadMainDicomTags(tags, size, level);
+
+    assert(tags != NULL &&
+           size != 0);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      assert(target.find(tags[i].tag_) == target.end());
+      
+      target[tags[i].tag_] = tags[i].name_;
+    }
+  }
+
+
+  namespace
+  {
+    class DicomTag2 : public DicomTag
+    {
+    public:
+      DicomTag2() :
+        DicomTag(0, 0)   // To make std::map<> happy
+      {
+      }
+
+      DicomTag2(const DicomTag& tag) :
+        DicomTag(tag)
+      {
+      }
+    };
+  }
+
+
+  static void LoadMainDicomTags(std::map<std::string, DicomTag2>& target,
+                                ResourceType level)
+  {
+    const MainDicomTag* tags = NULL;
+    size_t size;
+    LoadMainDicomTags(tags, size, level);
+
+    assert(tags != NULL &&
+           size != 0);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      assert(target.find(tags[i].name_) == target.end());
+      
+      target[tags[i].name_] = tags[i].tag_;
+    }
+  }
+
+
+  void DicomMap::SetValueInternal(uint16_t group, 
+                                  uint16_t element, 
+                                  DicomValue* value)
+  {
+    DicomTag tag(group, element);
+    Content::iterator it = content_.find(tag);
+
+    if (it != content_.end())
+    {
+      delete it->second;
+      it->second = value;
+    }
+    else
+    {
+      content_.insert(std::make_pair(tag, value));
+    }
+  }
+
+
+  void DicomMap::Clear()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+
+    content_.clear();
+  }
+
+
+  static void ExtractTags(DicomMap& result,
+                          const DicomMap::Content& source,
+                          const MainDicomTag* tags,
+                          size_t count)
+  {
+    result.Clear();
+
+    for (unsigned int i = 0; i < count; i++)
+    {
+      DicomMap::Content::const_iterator it = source.find(tags[i].tag_);
+      if (it != source.end())
+      {
+        result.SetValue(it->first, *it->second /* value will be cloned */);
+      }
+    }
+  }
+
+
+  void DicomMap::ExtractPatientInformation(DicomMap& result) const
+  {
+    ExtractTags(result, content_, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+  }
+
+  void DicomMap::ExtractStudyInformation(DicomMap& result) const
+  {
+    ExtractTags(result, content_, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+  }
+
+  void DicomMap::ExtractSeriesInformation(DicomMap& result) const
+  {
+    ExtractTags(result, content_, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+  }
+
+  void DicomMap::ExtractInstanceInformation(DicomMap& result) const
+  {
+    ExtractTags(result, content_, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+  }
+
+
+
+  DicomMap* DicomMap::Clone() const
+  {
+    std::unique_ptr<DicomMap> result(new DicomMap);
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      result->content_.insert(std::make_pair(it->first, it->second->Clone()));
+    }
+
+    return result.release();
+  }
+
+
+  void DicomMap::Assign(const DicomMap& other)
+  {
+    Clear();
+
+    for (Content::const_iterator it = other.content_.begin(); it != other.content_.end(); ++it)
+    {
+      content_.insert(std::make_pair(it->first, it->second->Clone()));
+    }
+  }
+
+
+  const DicomValue& DicomMap::GetValue(const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value)
+    {
+      return *value;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentTag);
+    }
+  }
+
+
+  const DicomValue* DicomMap::TestAndGetValue(const DicomTag& tag) const
+  {
+    Content::const_iterator it = content_.find(tag);
+
+    if (it == content_.end())
+    {
+      return NULL;
+    }
+    else
+    {
+      return it->second;
+    }
+  }
+
+
+  void DicomMap::Remove(const DicomTag& tag) 
+  {
+    Content::iterator it = content_.find(tag);
+    if (it != content_.end())
+    {
+      delete it->second;
+      content_.erase(it);
+    }
+  }
+
+
+  static void SetupFindTemplate(DicomMap& result,
+                                const MainDicomTag* tags,
+                                size_t count) 
+  {
+    result.Clear();
+
+    for (size_t i = 0; i < count; i++)
+    {
+      result.SetValue(tags[i].tag_, "", false);
+    }
+  }
+
+  void DicomMap::SetupFindPatientTemplate(DicomMap& result)
+  {
+    SetupFindTemplate(result, PATIENT_MAIN_DICOM_TAGS, sizeof(PATIENT_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+  }
+
+  void DicomMap::SetupFindStudyTemplate(DicomMap& result)
+  {
+    SetupFindTemplate(result, STUDY_MAIN_DICOM_TAGS, sizeof(STUDY_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    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, SERIES_MAIN_DICOM_TAGS, sizeof(SERIES_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    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
+    result.Remove(DicomTag(0x0008, 0x1010));  // Station name
+    result.Remove(DicomTag(0x0018, 0x0024));  // Sequence name
+    result.Remove(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES);
+    result.Remove(DICOM_TAG_IMAGES_IN_ACQUISITION);
+    result.Remove(DICOM_TAG_NUMBER_OF_SLICES);
+    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, INSTANCE_MAIN_DICOM_TAGS, sizeof(INSTANCE_MAIN_DICOM_TAGS) / sizeof(MainDicomTag));
+    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);
+  }
+
+
+  void DicomMap::CopyTagIfExists(const DicomMap& source,
+                                 const DicomTag& tag)
+  {
+    if (source.HasTag(tag))
+    {
+      SetValue(tag, source.GetValue(tag));
+    }
+  }
+
+
+  bool DicomMap::IsMainDicomTag(const DicomTag& tag, ResourceType level)
+  {
+    const MainDicomTag *tags = NULL;
+    size_t size;
+    LoadMainDicomTags(tags, size, level);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      if (tags[i].tag_ == tag)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool DicomMap::IsMainDicomTag(const DicomTag& tag)
+  {
+    return (IsMainDicomTag(tag, ResourceType_Patient) ||
+            IsMainDicomTag(tag, ResourceType_Study) ||
+            IsMainDicomTag(tag, ResourceType_Series) ||
+            IsMainDicomTag(tag, ResourceType_Instance));
+  }
+
+
+  void DicomMap::GetMainDicomTagsInternal(std::set<DicomTag>& result, ResourceType level)
+  {
+    const MainDicomTag *tags = NULL;
+    size_t size;
+    LoadMainDicomTags(tags, size, level);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      result.insert(tags[i].tag_);
+    }
+  }
+
+
+  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result, ResourceType level)
+  {
+    result.clear();
+    GetMainDicomTagsInternal(result, level);
+  }
+
+
+  void DicomMap::GetMainDicomTags(std::set<DicomTag>& result)
+  {
+    result.clear();
+    GetMainDicomTagsInternal(result, ResourceType_Patient);
+    GetMainDicomTagsInternal(result, ResourceType_Study);
+    GetMainDicomTagsInternal(result, ResourceType_Series);
+    GetMainDicomTagsInternal(result, ResourceType_Instance);
+  }
+
+
+  void DicomMap::GetTags(std::set<DicomTag>& tags) const
+  {
+    tags.clear();
+
+    for (Content::const_iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      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::IsDicomFile(const void* 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.
+     **/
+
+    const uint8_t* p = reinterpret_cast<const uint8_t*>(dicom);
+
+    return (size >= 132 &&
+            p[128] == 'D' &&
+            p[129] == 'I' &&
+            p[130] == 'C' &&
+            p[131] == 'M');
+  }
+    
+
+  bool DicomMap::ParseDicomMetaInformation(DicomMap& result,
+                                           const void* dicom,
+                                           size_t size)
+  {
+    if (!IsDicomFile(dicom, size))
+    {
+      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, reinterpret_cast<const char*>(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, reinterpret_cast<const char*>(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::LookupStringValue(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::ParseFirstFloat(float& result,
+                                 const DicomTag& tag) const
+  {
+    const DicomValue* value = TestAndGetValue(tag);
+
+    if (value == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      return value->ParseFirstFloat(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::FromDicomAsJson(const Json::Value& dicomAsJson)
+  {
+    if (dicomAsJson.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    
+    Clear();
+    
+    Json::Value::Members tags = dicomAsJson.getMemberNames();
+    for (Json::Value::Members::const_iterator
+           it = tags.begin(); it != tags.end(); ++it)
+    {
+      DicomTag tag(0, 0);
+      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      const Json::Value& value = dicomAsJson[*it];
+
+      if (value.type() != Json::objectValue ||
+          !value.isMember("Type") ||
+          !value.isMember("Value") ||
+          value["Type"].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      if (value["Type"] == "String")
+      {
+        if (value["Value"].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_CorruptedFile);
+        }
+        else
+        {
+          SetValue(tag, value["Value"].asString(), false /* not binary */);
+        }
+      }
+    }
+  }
+
+
+  void DicomMap::Merge(const DicomMap& other)
+  {
+    for (Content::const_iterator it = other.content_.begin();
+         it != other.content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (content_.find(it->first) == content_.end())
+      {
+        content_[it->first] = it->second->Clone();
+      }
+    }
+  }
+
+
+  void DicomMap::MergeMainDicomTags(const DicomMap& other,
+                                    ResourceType level)
+  {
+    const MainDicomTag* tags = NULL;
+    size_t size = 0;
+
+    LoadMainDicomTags(tags, size, level);
+    assert(tags != NULL && size > 0);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      Content::const_iterator found = other.content_.find(tags[i].tag_);
+
+      if (found != other.content_.end() &&
+          content_.find(tags[i].tag_) == content_.end())
+      {
+        assert(found->second != NULL);
+        content_[tags[i].tag_] = found->second->Clone();
+      }
+    }
+  }
+    
+
+  void DicomMap::ExtractMainDicomTags(const DicomMap& other)
+  {
+    Clear();
+    MergeMainDicomTags(other, ResourceType_Patient);
+    MergeMainDicomTags(other, ResourceType_Study);
+    MergeMainDicomTags(other, ResourceType_Series);
+    MergeMainDicomTags(other, ResourceType_Instance);
+  }    
+
+
+  bool DicomMap::HasOnlyMainDicomTags() const
+  {
+    // TODO - Speed up possible by making this std::set a global variable
+
+    std::set<DicomTag> mainDicomTags;
+    GetMainDicomTags(mainDicomTags);
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      if (mainDicomTags.find(it->first) == mainDicomTags.end())
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+    
+
+  void DicomMap::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    for (Content::const_iterator it = content_.begin(); it != content_.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()) ||
+          content_.find(tag) != content_.end())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      std::unique_ptr<DicomValue> value(new DicomValue);
+      value->Unserialize(source[tags[i]]);
+
+      content_[tag] = value.release();
+    }
+  }
+
+
+  void DicomMap::FromDicomWeb(const Json::Value& source)
+  {
+    static const char* const ALPHABETIC = "Alphabetic";
+    static const char* const IDEOGRAPHIC = "Ideographic";
+    static const char* const INLINE_BINARY = "InlineBinary";
+    static const char* const PHONETIC = "Phonetic";
+    static const char* const VALUE = "Value";
+    static const char* const VR = "vr";
+  
+    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++)
+    {
+      const Json::Value& item = source[tags[i]];
+      DicomTag tag(0, 0);
+
+      if (item.type() != Json::objectValue ||
+          !item.isMember(VR) ||
+          item[VR].type() != Json::stringValue ||
+          !DicomTag::ParseHexadecimal(tag, tags[i].c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      ValueRepresentation vr = StringToValueRepresentation(item[VR].asString(), false);
+
+      if (item.isMember(INLINE_BINARY))
+      {
+        const Json::Value& value = item[INLINE_BINARY];
+
+        if (value.type() == Json::stringValue)
+        {
+          std::string decoded;
+          Toolbox::DecodeBase64(decoded, value.asString());
+          SetValue(tag, decoded, true /* binary data */);
+        }
+      }
+      else if (!item.isMember(VALUE))
+      {
+        // Tag is present, but it has a null value
+        SetValue(tag, "", false /* not binary */);
+      }
+      else
+      {
+        const Json::Value& value = item[VALUE];
+
+        if (value.type() == Json::arrayValue)
+        {
+          bool supported = true;
+          
+          std::string s;
+          for (Json::Value::ArrayIndex i = 0; i < value.size() && supported; i++)
+          {
+            if (!s.empty())
+            {
+              s += '\\';
+            }
+
+            switch (value[i].type())
+            {
+              case Json::objectValue:
+                if (vr == ValueRepresentation_PersonName &&
+                    value[i].type() == Json::objectValue)
+                {
+                  if (value[i].isMember(ALPHABETIC) &&
+                      value[i][ALPHABETIC].type() == Json::stringValue)
+                  {
+                    s += value[i][ALPHABETIC].asString();
+                  }
+
+                  bool hasIdeographic = false;
+                  
+                  if (value[i].isMember(IDEOGRAPHIC) &&
+                      value[i][IDEOGRAPHIC].type() == Json::stringValue)
+                  {
+                    s += '=' + value[i][IDEOGRAPHIC].asString();
+                    hasIdeographic = true;
+                  }
+                  
+                  if (value[i].isMember(PHONETIC) &&
+                      value[i][PHONETIC].type() == Json::stringValue)
+                  {
+                    if (!hasIdeographic)
+                    {
+                      s += '=';
+                    }
+                      
+                    s += '=' + value[i][PHONETIC].asString();
+                  }
+                }
+                else
+                {
+                  // This is the case of sequences
+                  supported = false;
+                }
+
+                break;
+            
+              case Json::stringValue:
+                s += value[i].asString();
+                break;
+              
+              case Json::intValue:
+                s += boost::lexical_cast<std::string>(value[i].asInt());
+                break;
+              
+              case Json::uintValue:
+                s += boost::lexical_cast<std::string>(value[i].asUInt());
+                break;
+              
+              case Json::realValue:
+                s += boost::lexical_cast<std::string>(value[i].asDouble());
+                break;
+              
+              default:
+                break;
+            }
+          }
+
+          if (supported)
+          {
+            SetValue(tag, s, false /* not binary */);
+          }
+        }
+      }
+    }
+  }
+
+
+  std::string DicomMap::GetStringValue(const DicomTag& tag,
+                                       const std::string& defaultValue,
+                                       bool allowBinary) const
+  {
+    std::string s;
+    if (LookupStringValue(s, tag, allowBinary))
+    {
+      return s;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  void DicomMap::RemoveBinaryTags()
+  {
+    Content kept;
+
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!it->second->IsBinary() &&
+          !it->second->IsNull())
+      {
+        kept[it->first] = it->second;
+      }
+      else
+      {
+        delete it->second;
+      }
+    }
+
+    content_ = kept;
+  }
+
+
+  void DicomMap::DumpMainDicomTags(Json::Value& target,
+                                   ResourceType level) const
+  {
+    std::map<DicomTag, std::string> mainTags;   // TODO - Create a singleton to hold this map
+    LoadMainDicomTags(mainTags, level);
+    
+    target = Json::objectValue;
+
+    for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      
+      if (!it->second->IsBinary() &&
+          !it->second->IsNull())
+      {
+        std::map<DicomTag, std::string>::const_iterator found = mainTags.find(it->first);
+
+        if (found != mainTags.end())
+        {
+          target[found->second] = it->second->GetContent();
+        }
+      }
+    }    
+  }
+  
+
+  void DicomMap::ParseMainDicomTags(const Json::Value& source,
+                                    ResourceType level)
+  {
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    
+    std::map<std::string, DicomTag2> mainTags;   // TODO - Create a singleton to hold this map
+    LoadMainDicomTags(mainTags, level);
+    
+    Json::Value::Members members = source.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      std::map<std::string, DicomTag2>::const_iterator found = mainTags.find(members[i]);
+
+      if (found != mainTags.end())
+      {
+        const Json::Value& value = source[members[i]];
+        if (value.type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          SetValue(found->second, value.asString(), false);
+        }
+      }
+    }
+  }
+
+
+  void DicomMap::Print(FILE* fp) const
+  {
+    DicomArray a(*this);
+    a.Print(fp);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomMap.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,248 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomValue.h"
+#include "../Enumerations.h"
+
+#include <set>
+#include <map>
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomMap : public boost::noncopyable
+  {
+  public:
+    typedef std::map<DicomTag, DicomValue*>  Content;
+    
+  private:
+    friend class DicomArray;
+    friend class FromDcmtkBridge;
+    friend class ParsedDicomFile;
+
+    Content content_;
+
+    // Warning: This takes the ownership of "value"
+    void SetValueInternal(uint16_t group, 
+                          uint16_t element, 
+                          DicomValue* value);
+
+    static void GetMainDicomTagsInternal(std::set<DicomTag>& result,
+                                         ResourceType level);
+
+  public:
+    DicomMap()
+    {
+    }
+
+    ~DicomMap()
+    {
+      Clear();
+    }
+
+    size_t GetSize() const
+    {
+      return content_.size();
+    }
+    
+    DicomMap* Clone() const;
+
+    void Assign(const DicomMap& other);
+
+    void Clear();
+
+    void SetNullValue(uint16_t group, 
+                      uint16_t element)
+    {
+      SetValueInternal(group, element, new DicomValue);
+    }
+    
+    void SetNullValue(const DicomTag& tag)
+    {
+      SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue);
+    }
+    
+    void SetValue(uint16_t group, 
+                  uint16_t element, 
+                  const DicomValue& value)
+    {
+      SetValueInternal(group, element, value.Clone());
+    }
+
+    void SetValue(const DicomTag& tag,
+                  const DicomValue& value)
+    {
+      SetValueInternal(tag.GetGroup(), tag.GetElement(), value.Clone());
+    }
+
+    void SetValue(const DicomTag& tag,
+                  const std::string& str,
+                  bool isBinary)
+    {
+      SetValueInternal(tag.GetGroup(), tag.GetElement(), new DicomValue(str, isBinary));
+    }
+
+    void SetValue(uint16_t group, 
+                  uint16_t element, 
+                  const std::string& str,
+                  bool isBinary)
+    {
+      SetValueInternal(group, element, new DicomValue(str, isBinary));
+    }
+
+    bool HasTag(uint16_t group, uint16_t element) const
+    {
+      return HasTag(DicomTag(group, element));
+    }
+
+    bool HasTag(const DicomTag& tag) const
+    {
+      return content_.find(tag) != content_.end();
+    }
+
+    const DicomValue& GetValue(uint16_t group, uint16_t element) const
+    {
+      return GetValue(DicomTag(group, element));
+    }
+
+    const DicomValue& GetValue(const DicomTag& tag) const;
+
+    // DO NOT delete the returned value!
+    const DicomValue* TestAndGetValue(uint16_t group, uint16_t element) const
+    {
+      return TestAndGetValue(DicomTag(group, element));
+    }       
+
+    // DO NOT delete the returned value!
+    const DicomValue* TestAndGetValue(const DicomTag& tag) const;
+
+    void Remove(const DicomTag& tag);
+
+    void ExtractPatientInformation(DicomMap& result) const;
+
+    void ExtractStudyInformation(DicomMap& result) const;
+
+    void ExtractSeriesInformation(DicomMap& result) const;
+
+    void ExtractInstanceInformation(DicomMap& result) const;
+
+    static void SetupFindPatientTemplate(DicomMap& result);
+
+    static void SetupFindStudyTemplate(DicomMap& result);
+
+    static void SetupFindSeriesTemplate(DicomMap& result);
+
+    static void SetupFindInstanceTemplate(DicomMap& result);
+
+    void CopyTagIfExists(const DicomMap& source,
+                         const DicomTag& tag);
+
+    static bool IsMainDicomTag(const DicomTag& tag, ResourceType level);
+
+    static bool IsMainDicomTag(const DicomTag& tag);
+
+    static void GetMainDicomTags(std::set<DicomTag>& result, ResourceType level);
+
+    static void GetMainDicomTags(std::set<DicomTag>& result);
+
+    void GetTags(std::set<DicomTag>& tags) const;
+
+    static bool IsDicomFile(const void* dicom,
+                            size_t size);
+    
+    static bool ParseDicomMetaInformation(DicomMap& result,
+                                          const void* dicom,
+                                          size_t size);
+
+    void LogMissingTagsForStore() const;
+
+    bool LookupStringValue(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 ParseFirstFloat(float& result,
+                         const DicomTag& tag) const;
+
+    bool ParseDouble(double& result,
+                     const DicomTag& tag) const;
+
+    void FromDicomAsJson(const Json::Value& dicomAsJson);
+
+    void Merge(const DicomMap& other);
+
+    void MergeMainDicomTags(const DicomMap& other,
+                            ResourceType level);
+
+    void ExtractMainDicomTags(const DicomMap& other);
+
+    bool HasOnlyMainDicomTags() const;
+    
+    void Serialize(Json::Value& target) const;
+
+    void Unserialize(const Json::Value& source);
+
+    void FromDicomWeb(const Json::Value& source);
+
+    std::string GetStringValue(const DicomTag& tag,
+                               const std::string& defaultValue,
+                               bool allowBinary) const;
+
+    void RemoveBinaryTags();
+
+    void DumpMainDicomTags(Json::Value& target,
+                           ResourceType level) const;
+
+    void ParseMainDicomTags(const Json::Value& source,
+                            ResourceType level);
+
+    void Print(FILE* fp) const;  // For debugging only
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,323 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomTag.h"
+
+#include "../OrthancException.h"
+
+#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_)
+      return true;
+
+    if (group_ > other.group_)
+      return false;
+
+    return element_ < other.element_;
+  }
+
+
+  std::ostream& operator<< (std::ostream& o, const DicomTag& tag)
+  {
+    using namespace std;
+    ios_base::fmtflags state = o.flags();
+    o.flags(ios::right | ios::hex);
+    o << "(" << setfill('0') << setw(4) << tag.GetGroup()
+      << "," << setw(4) << tag.GetElement() << ")";
+    o.flags(state);
+    return o;
+  }
+
+
+  std::string DicomTag::Format() const
+  {
+    char b[16];
+    sprintf(b, "%04x,%04x", group_, element_);
+    return std::string(b);
+  }
+
+
+  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)
+      return "AccessionNumber";
+
+    if (*this == DICOM_TAG_SOP_INSTANCE_UID)
+      return "SOPInstanceUID";
+
+    if (*this == DICOM_TAG_PATIENT_ID)
+      return "PatientID";
+
+    if (*this == DICOM_TAG_SERIES_INSTANCE_UID)
+      return "SeriesInstanceUID";
+
+    if (*this == DICOM_TAG_STUDY_INSTANCE_UID)
+      return "StudyInstanceUID"; 
+
+    if (*this == DICOM_TAG_PIXEL_DATA)
+      return "PixelData";
+
+    if (*this == DICOM_TAG_IMAGE_INDEX)
+      return "ImageIndex";
+
+    if (*this == DICOM_TAG_INSTANCE_NUMBER)
+      return "InstanceNumber";
+
+    if (*this == DICOM_TAG_NUMBER_OF_SLICES)
+      return "NumberOfSlices";
+
+    if (*this == DICOM_TAG_NUMBER_OF_FRAMES)
+      return "NumberOfFrames";
+
+    if (*this == DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)
+      return "CardiacNumberOfImages";
+
+    if (*this == DICOM_TAG_IMAGES_IN_ACQUISITION)
+      return "ImagesInAcquisition";
+
+    if (*this == DICOM_TAG_PATIENT_NAME)
+      return "PatientName";
+
+    if (*this == DICOM_TAG_IMAGE_POSITION_PATIENT)
+      return "ImagePositionPatient";
+
+    if (*this == DICOM_TAG_IMAGE_ORIENTATION_PATIENT)
+      return "ImageOrientationPatient";
+
+    // New in Orthanc 1.6.0, as tagged as "RETIRED_" since DCMTK 3.6.4
+    if (*this == DICOM_TAG_OTHER_PATIENT_IDS)
+      return "OtherPatientIDs";
+
+    return "";
+  }
+
+
+  void DicomTag::AddTagsForModule(std::set<DicomTag>& target,
+                                  DicomModule module)
+  {
+    // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions
+
+    switch (module)
+    {
+      case DicomModule_Patient:
+        // This is Table C.7-1 "Patient Module Attributes" (p. 373)
+        target.insert(DicomTag(0x0010, 0x0010));   // Patient's name
+        target.insert(DicomTag(0x0010, 0x0020));   // Patient ID
+        target.insert(DicomTag(0x0010, 0x0030));   // Patient's birth date
+        target.insert(DicomTag(0x0010, 0x0040));   // Patient's sex
+        target.insert(DicomTag(0x0008, 0x1120));   // Referenced patient sequence
+        target.insert(DicomTag(0x0010, 0x0032));   // Patient's birth time
+        target.insert(DicomTag(0x0010, 0x1000));   // Other patient IDs
+        target.insert(DicomTag(0x0010, 0x1002));   // Other patient IDs sequence
+        target.insert(DicomTag(0x0010, 0x1001));   // Other patient names
+        target.insert(DicomTag(0x0010, 0x2160));   // Ethnic group
+        target.insert(DicomTag(0x0010, 0x4000));   // Patient comments
+        target.insert(DicomTag(0x0010, 0x2201));   // Patient species description
+        target.insert(DicomTag(0x0010, 0x2202));   // Patient species code sequence
+        target.insert(DicomTag(0x0010, 0x2292));   // Patient breed description
+        target.insert(DicomTag(0x0010, 0x2293));   // Patient breed code sequence
+        target.insert(DicomTag(0x0010, 0x2294));   // Breed registration sequence
+        target.insert(DicomTag(0x0010, 0x2297));   // Responsible person
+        target.insert(DicomTag(0x0010, 0x2298));   // Responsible person role
+        target.insert(DicomTag(0x0010, 0x2299));   // Responsible organization
+        target.insert(DicomTag(0x0012, 0x0062));   // Patient identity removed
+        target.insert(DicomTag(0x0012, 0x0063));   // De-identification method
+        target.insert(DicomTag(0x0012, 0x0064));   // De-identification method code sequence
+
+        // Table 10-18 ISSUER OF PATIENT ID MACRO (p. 112)
+        target.insert(DicomTag(0x0010, 0x0021));   // Issuer of Patient ID
+        target.insert(DicomTag(0x0010, 0x0024));   // Issuer of Patient ID qualifiers sequence
+        break;
+
+      case DicomModule_Study:
+        // This is Table C.7-3 "General Study Module Attributes" (p. 378)
+        target.insert(DicomTag(0x0020, 0x000d));   // Study instance UID
+        target.insert(DicomTag(0x0008, 0x0020));   // Study date
+        target.insert(DicomTag(0x0008, 0x0030));   // Study time
+        target.insert(DicomTag(0x0008, 0x0090));   // Referring physician's name
+        target.insert(DicomTag(0x0008, 0x0096));   // Referring physician identification sequence
+        target.insert(DicomTag(0x0020, 0x0010));   // Study ID
+        target.insert(DicomTag(0x0008, 0x0050));   // Accession number
+        target.insert(DicomTag(0x0008, 0x0051));   // Issuer of accession number sequence
+        target.insert(DicomTag(0x0008, 0x1030));   // Study description
+        target.insert(DicomTag(0x0008, 0x1048));   // Physician(s) of record
+        target.insert(DicomTag(0x0008, 0x1049));   // Physician(s) of record identification sequence
+        target.insert(DicomTag(0x0008, 0x1060));   // Name of physician(s) reading study
+        target.insert(DicomTag(0x0008, 0x1062));   // Physician(s) reading study identification sequence
+        target.insert(DicomTag(0x0032, 0x1034));   // Requesting service code sequence
+        target.insert(DicomTag(0x0008, 0x1110));   // Referenced study sequence
+        target.insert(DicomTag(0x0008, 0x1032));   // Procedure code sequence
+        target.insert(DicomTag(0x0040, 0x1012));   // Reason for performed procedure code sequence
+        break;
+
+      case DicomModule_Series:
+        // This is Table C.7-5 "General Series Module Attributes" (p. 385)
+        target.insert(DicomTag(0x0008, 0x0060));   // Modality 
+        target.insert(DicomTag(0x0020, 0x000e));   // Series Instance UID 
+        target.insert(DicomTag(0x0020, 0x0011));   // Series Number 
+        target.insert(DicomTag(0x0020, 0x0060));   // Laterality 
+        target.insert(DicomTag(0x0008, 0x0021));   // Series Date 
+        target.insert(DicomTag(0x0008, 0x0031));   // Series Time 
+        target.insert(DicomTag(0x0008, 0x1050));   // Performing Physicians’ Name 
+        target.insert(DicomTag(0x0008, 0x1052));   // Performing Physician Identification Sequence 
+        target.insert(DicomTag(0x0018, 0x1030));   // Protocol Name
+        target.insert(DicomTag(0x0008, 0x103e));   // Series Description 
+        target.insert(DicomTag(0x0008, 0x103f));   // Series Description Code Sequence 
+        target.insert(DicomTag(0x0008, 0x1070));   // Operators' Name 
+        target.insert(DicomTag(0x0008, 0x1072));   // Operator Identification Sequence 
+        target.insert(DicomTag(0x0008, 0x1111));   // Referenced Performed Procedure Step Sequence
+        target.insert(DicomTag(0x0008, 0x1250));   // Related Series Sequence
+        target.insert(DicomTag(0x0018, 0x0015));   // Body Part Examined
+        target.insert(DicomTag(0x0018, 0x5100));   // Patient Position
+        target.insert(DicomTag(0x0028, 0x0108));   // Smallest Pixel Value in Series 
+        target.insert(DicomTag(0x0029, 0x0109));   // Largest Pixel Value in Series 
+        target.insert(DicomTag(0x0040, 0x0275));   // Request Attributes Sequence 
+        target.insert(DicomTag(0x0010, 0x2210));   // Anatomical Orientation Type
+
+        // Table 10-16 PERFORMED PROCEDURE STEP SUMMARY MACRO ATTRIBUTES
+        target.insert(DicomTag(0x0040, 0x0253));   // Performed Procedure Step ID 
+        target.insert(DicomTag(0x0040, 0x0244));   // Performed Procedure Step Start Date 
+        target.insert(DicomTag(0x0040, 0x0245));   // Performed Procedure Step Start Time 
+        target.insert(DicomTag(0x0040, 0x0254));   // Performed Procedure Step Description 
+        target.insert(DicomTag(0x0040, 0x0260));   // Performed Protocol Code Sequence 
+        target.insert(DicomTag(0x0040, 0x0280));   // Comments on the Performed Procedure Step
+        break;
+
+      case DicomModule_Instance:
+        // This is Table C.12-1 "SOP Common Module Attributes" (p. 1207)
+        target.insert(DicomTag(0x0008, 0x0016));   // SOP Class UID
+        target.insert(DicomTag(0x0008, 0x0018));   // SOP Instance UID 
+        target.insert(DicomTag(0x0008, 0x0005));   // Specific Character Set 
+        target.insert(DicomTag(0x0008, 0x0012));   // Instance Creation Date 
+        target.insert(DicomTag(0x0008, 0x0013));   // Instance Creation Time 
+        target.insert(DicomTag(0x0008, 0x0014));   // Instance Creator UID 
+        target.insert(DicomTag(0x0008, 0x001a));   // Related General SOP Class UID 
+        target.insert(DicomTag(0x0008, 0x001b));   // Original Specialized SOP Class UID 
+        target.insert(DicomTag(0x0008, 0x0110));   // Coding Scheme Identification Sequence 
+        target.insert(DicomTag(0x0008, 0x0201));   // Timezone Offset From UTC 
+        target.insert(DicomTag(0x0018, 0xa001));   // Contributing Equipment Sequence
+        target.insert(DicomTag(0x0020, 0x0013));   // Instance Number 
+        target.insert(DicomTag(0x0100, 0x0410));   // SOP Instance Status 
+        target.insert(DicomTag(0x0100, 0x0420));   // SOP Authorization DateTime 
+        target.insert(DicomTag(0x0100, 0x0424));   // SOP Authorization Comment 
+        target.insert(DicomTag(0x0100, 0x0426));   // Authorization Equipment Certification Number
+        target.insert(DicomTag(0x0400, 0x0500));   // Encrypted Attributes Sequence
+        target.insert(DicomTag(0x0400, 0x0561));   // Original Attributes Sequence 
+        target.insert(DicomTag(0x0040, 0xa390));   // HL7 Structured Document Reference Sequence
+        target.insert(DicomTag(0x0028, 0x0303));   // Longitudinal Temporal Information Modified 
+
+        // Table C.12-6 "DIGITAL SIGNATURES MACRO ATTRIBUTES" (p. 1216)
+        target.insert(DicomTag(0x4ffe, 0x0001));   // MAC Parameters sequence
+        target.insert(DicomTag(0xfffa, 0xfffa));   // Digital signatures sequence
+        break;
+
+        // TODO IMAGE MODULE?
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomTag.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,243 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <set>
+#include <stdint.h>
+
+#include "../Enumerations.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomTag
+  {
+    // This must stay a POD (plain old data structure) 
+
+  private:
+    uint16_t group_;
+    uint16_t element_;
+
+  public:
+    DicomTag(uint16_t group,
+             uint16_t element) :
+      group_(group),
+      element_(element)
+    {
+    }
+
+    uint16_t GetGroup() const
+    {
+      return group_;
+    }
+
+    uint16_t GetElement() const
+    {
+      return element_;
+    }
+
+    bool IsPrivate() const
+    {
+      return group_ % 2 == 1;
+    }
+
+    const char* GetMainTagsName() const;
+
+    bool operator< (const DicomTag& other) const;
+
+    bool operator== (const DicomTag& other) const
+    {
+      return group_ == other.group_ && element_ == other.element_;
+    }
+
+    bool operator!= (const DicomTag& other) const
+    {
+      return !(*this == other);
+    }
+
+    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,
+                                 DicomModule module);
+  };
+
+  // Aliases for the most useful tags
+  static const DicomTag DICOM_TAG_ACCESSION_NUMBER(0x0008, 0x0050);
+  static const DicomTag DICOM_TAG_SOP_INSTANCE_UID(0x0008, 0x0018);
+  static const DicomTag DICOM_TAG_PATIENT_ID(0x0010, 0x0020);
+  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);
+
+  static const DicomTag DICOM_TAG_NUMBER_OF_SLICES(0x0054, 0x0081);
+  static const DicomTag DICOM_TAG_NUMBER_OF_TIME_SLICES(0x0054, 0x0101);
+  static const DicomTag DICOM_TAG_NUMBER_OF_FRAMES(0x0028, 0x0008);
+  static const DicomTag DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES(0x0018, 0x1090);
+  static const DicomTag DICOM_TAG_IMAGES_IN_ACQUISITION(0x0020, 0x1002);
+  static const DicomTag DICOM_TAG_PATIENT_NAME(0x0010, 0x0010);
+  static const DicomTag DICOM_TAG_ENCAPSULATED_DOCUMENT(0x0042, 0x0011);
+
+  static const DicomTag DICOM_TAG_STUDY_DESCRIPTION(0x0008, 0x1030);
+  static const DicomTag DICOM_TAG_SERIES_DESCRIPTION(0x0008, 0x103e);
+  static const DicomTag DICOM_TAG_MODALITY(0x0008, 0x0060);
+
+  // The following is used for "modify/anonymize" operations
+  static const DicomTag DICOM_TAG_SOP_CLASS_UID(0x0008, 0x0016);
+  static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID(0x0002, 0x0002);
+  static const DicomTag DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID(0x0002, 0x0003);
+  static const DicomTag DICOM_TAG_DEIDENTIFICATION_METHOD(0x0012, 0x0063);
+
+  // DICOM tags used for fMRI (thanks to Will Ryder)
+  static const DicomTag DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS(0x0020, 0x0105);
+  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);
+
+  // Tags for images
+  static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011);
+  static const DicomTag DICOM_TAG_ROWS(0x0028, 0x0010);
+  static const DicomTag DICOM_TAG_SAMPLES_PER_PIXEL(0x0028, 0x0002);
+  static const DicomTag DICOM_TAG_BITS_ALLOCATED(0x0028, 0x0100);
+  static const DicomTag DICOM_TAG_BITS_STORED(0x0028, 0x0101);
+  static const DicomTag DICOM_TAG_HIGH_BIT(0x0028, 0x0102);
+  static const DicomTag DICOM_TAG_PIXEL_REPRESENTATION(0x0028, 0x0103);
+  static const DicomTag DICOM_TAG_PLANAR_CONFIGURATION(0x0028, 0x0006);
+  static const DicomTag DICOM_TAG_PHOTOMETRIC_INTERPRETATION(0x0028, 0x0004);
+  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_LARGEST_IMAGE_PIXEL_VALUE(0x0028, 0x0107);
+  static const DicomTag DICOM_TAG_SMALLEST_IMAGE_PIXEL_VALUE(0x0028, 0x0106);
+
+  // Tags related to date and time
+  static const DicomTag DICOM_TAG_ACQUISITION_DATE(0x0008, 0x0022);
+  static const DicomTag DICOM_TAG_ACQUISITION_TIME(0x0008, 0x0032);
+  static const DicomTag DICOM_TAG_CONTENT_DATE(0x0008, 0x0023);
+  static const DicomTag DICOM_TAG_CONTENT_TIME(0x0008, 0x0033);
+  static const DicomTag DICOM_TAG_INSTANCE_CREATION_DATE(0x0008, 0x0012);
+  static const DicomTag DICOM_TAG_INSTANCE_CREATION_TIME(0x0008, 0x0013);
+  static const DicomTag DICOM_TAG_PATIENT_BIRTH_DATE(0x0010, 0x0030);
+  static const DicomTag DICOM_TAG_PATIENT_BIRTH_TIME(0x0010, 0x0032);
+  static const DicomTag DICOM_TAG_SERIES_DATE(0x0008, 0x0021);
+  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_ACQUISITION_DEVICE_PROCESSING_CODE(0x0018, 0x1401);
+  static const DicomTag DICOM_TAG_CASSETTE_ORIENTATION(0x0018, 0x1402);
+  static const DicomTag DICOM_TAG_CASSETTE_SIZE(0x0018, 0x1403);
+  static const DicomTag DICOM_TAG_CONTRAST_BOLUS_AGENT(0x0018, 0x0010);
+  static const DicomTag DICOM_TAG_STUDY_ID(0x0020, 0x0010);
+  static const DicomTag DICOM_TAG_SERIES_NUMBER(0x0020, 0x0011);
+  static const DicomTag DICOM_TAG_PATIENT_SEX(0x0010, 0x0040);
+  static const DicomTag DICOM_TAG_LATERALITY(0x0020, 0x0060);
+  static const DicomTag DICOM_TAG_BODY_PART_EXAMINED(0x0018, 0x0015);
+  static const DicomTag DICOM_TAG_VIEW_POSITION(0x0018, 0x5101);
+  static const DicomTag DICOM_TAG_MANUFACTURER(0x0008, 0x0070);
+  static const DicomTag DICOM_TAG_PATIENT_ORIENTATION(0x0020, 0x0020);
+  static const DicomTag DICOM_TAG_PATIENT_COMMENTS(0x0010, 0x4000);
+  static const DicomTag DICOM_TAG_PATIENT_SPECIES_DESCRIPTION(0x0010, 0x2201);
+  static const DicomTag DICOM_TAG_STUDY_COMMENTS(0x0032, 0x4000);
+  static const DicomTag DICOM_TAG_OTHER_PATIENT_IDS(0x0010, 0x1000);
+
+  // 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);
+  static const DicomTag DICOM_TAG_RED_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1201);
+  static const DicomTag DICOM_TAG_GREEN_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1202);
+  static const DicomTag DICOM_TAG_BLUE_PALETTE_COLOR_LOOKUP_TABLE_DATA(0x0028, 0x1203);
+  static const DicomTag DICOM_TAG_RED_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1101);
+  static const DicomTag DICOM_TAG_GREEN_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1102);
+  static const DicomTag DICOM_TAG_BLUE_PALETTE_COLOR_LOOKUP_TABLE_DESCRIPTOR(0x0028, 0x1103);
+  static const DicomTag DICOM_TAG_CONTOUR_DATA(0x3006, 0x0050);
+                             
+  // 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);
+  static const DicomTag DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE(0x3006, 0x0010);
+  static const DicomTag DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE(0x3006, 0x0012);
+  static const DicomTag DICOM_TAG_RT_REFERENCED_SERIES_SEQUENCE(0x3006, 0x0014);
+
+  // Tags for DICOMDIR
+  static const DicomTag DICOM_TAG_DIRECTORY_RECORD_TYPE(0x0004, 0x1430);
+  static const DicomTag DICOM_TAG_OFFSET_OF_THE_NEXT_DIRECTORY_RECORD(0x0004, 0x1400);
+  static const DicomTag DICOM_TAG_OFFSET_OF_REFERENCED_LOWER_LEVEL_DIRECTORY_ENTITY(0x0004, 0x1420);
+  static const DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID_IN_FILE(0x0004, 0x1511);
+  static const DicomTag DICOM_TAG_REFERENCED_FILE_ID(0x0004, 0x1500);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,322 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomValue.h"
+
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "../Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  DicomValue::DicomValue(const DicomValue& other) : 
+    type_(other.type_),
+    content_(other.content_)
+  {
+  }
+
+
+  DicomValue::DicomValue(const std::string& content,
+                         bool isBinary) :
+    type_(isBinary ? Type_Binary : Type_String),
+    content_(content)
+  {
+  }
+  
+  
+  DicomValue::DicomValue(const char* data,
+                         size_t size,
+                         bool isBinary) :
+    type_(isBinary ? Type_Binary : Type_String)
+  {
+    content_.assign(data, size);
+  }
+    
+  
+  const std::string& DicomValue::GetContent() const
+  {
+    if (type_ == Type_Null)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      return content_;
+    }
+  }
+
+
+  DicomValue* DicomValue::Clone() const
+  {
+    return new DicomValue(*this);
+  }
+
+  
+#if ORTHANC_ENABLE_BASE64 == 1
+  void DicomValue::FormatDataUriScheme(std::string& target,
+                                       const std::string& mime) const
+  {
+    Toolbox::EncodeBase64(target, GetContent());
+    target.insert(0, "data:" + mime + ";base64,");
+  }
+#endif
+
+  // same as ParseValue but in case the value actually contains a sequence,
+  // it will return the first value
+  // this has been introduced to support invalid "width/height" DICOM tags in some US
+  // images where the width is stored as "800\0" !
+  template <typename T,
+            bool allowSigned>
+  static bool ParseFirstValue(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;
+      }
+
+      if (value.find("\\") == std::string::npos)
+      {
+        result = boost::lexical_cast<T>(value);
+        return true;
+      }
+      else
+      {
+        std::vector<std::string> tokens;
+        Toolbox::TokenizeString(tokens, value, '\\');
+
+        if (tokens.size() >= 1)
+        {
+          result = boost::lexical_cast<T>(tokens[0]);
+          return true;
+        }
+
+        return false;
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return false;
+    }
+  }
+
+
+  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::ParseFirstFloat(float& result) const
+  {
+    return ParseFirstValue<float, true>(result, *this);
+  }
+
+  bool DicomValue::ParseFirstUnsignedInteger(unsigned int& result) const
+  {
+    return ParseFirstValue<unsigned int, 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);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomFormat/DicomValue.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <boost/noncopyable.hpp>
+#include <json/value.h>
+
+#if !defined(ORTHANC_ENABLE_BASE64)
+#  error The macro ORTHANC_ENABLE_BASE64 must be defined
+#endif
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DicomValue : public boost::noncopyable
+  {
+  private:
+    enum Type
+    {
+      Type_Null,
+      Type_String,
+      Type_Binary
+    };
+
+    Type         type_;
+    std::string  content_;
+
+    DicomValue(const DicomValue& other);
+
+  public:
+    DicomValue() : type_(Type_Null)
+    {
+    }
+    
+    DicomValue(const std::string& content,
+               bool isBinary);
+    
+    DicomValue(const char* data,
+               size_t size,
+               bool isBinary);
+    
+    const std::string& GetContent() const;
+
+    bool IsNull() const
+    {
+      return type_ == Type_Null;
+    }
+
+    bool IsBinary() const
+    {
+      return type_ == Type_Binary;
+    }
+    
+    DicomValue* Clone() const;
+
+#if ORTHANC_ENABLE_BASE64 == 1
+    void FormatDataUriScheme(std::string& target,
+                             const std::string& mime) const;
+
+    void FormatDataUriScheme(std::string& target) const
+    {
+      FormatDataUriScheme(target, MIME_BINARY);
+    }
+#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;
+
+    bool ParseFirstFloat(float& result) const;
+
+    bool ParseFirstUnsignedInteger(unsigned int& 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/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,860 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomAssociation.h"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "NetworkingCompatibility.h"
+
+#include <dcmtk/dcmnet/diutil.h>  // For dcmConnectionTimeout()
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+namespace Orthanc
+{
+  static void FillSopSequence(DcmDataset& dataset,
+                              const DcmTagKey& tag,
+                              const std::vector<std::string>& sopClassUids,
+                              const std::vector<std::string>& sopInstanceUids,
+                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
+                              bool hasFailureReasons)
+  {
+    assert(sopClassUids.size() == sopInstanceUids.size() &&
+           (hasFailureReasons ?
+            failureReasons.size() == sopClassUids.size() :
+            failureReasons.empty()));
+
+    if (sopInstanceUids.empty())
+    {
+      // Add an empty sequence
+      if (!dataset.insertEmptyElement(tag).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      for (size_t i = 0; i < sopClassUids.size(); i++)
+      {
+        std::unique_ptr<DcmItem> item(new DcmItem);
+        if (!item->putAndInsertString(DCM_ReferencedSOPClassUID, sopClassUids[i].c_str()).good() ||
+            !item->putAndInsertString(DCM_ReferencedSOPInstanceUID, sopInstanceUids[i].c_str()).good() ||
+            (hasFailureReasons &&
+             !item->putAndInsertUint16(DCM_FailureReason, failureReasons[i]).good()) ||
+            !dataset.insertSequenceItem(tag, item.release()).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+  }                              
+
+
+  void DicomAssociation::Initialize()
+  {
+    role_ = DicomAssociationRole_Default;
+    isOpen_ = false;
+    net_ = NULL; 
+    params_ = NULL;
+    assoc_ = NULL;      
+
+    // Must be after "isOpen_ = false"
+    ClearPresentationContexts();
+  }
+
+    
+  void DicomAssociation::CheckConnecting(const DicomAssociationParameters& parameters,
+                                         const OFCondition& cond)
+  {
+    try
+    {
+      CheckCondition(cond, parameters, "connecting");
+    }
+    catch (OrthancException&)
+    {
+      CloseInternal();
+      throw;
+    }
+  }
+
+    
+  void DicomAssociation::CloseInternal()
+  {
+    if (assoc_ != NULL)
+    {
+      ASC_releaseAssociation(assoc_);
+      ASC_destroyAssociation(&assoc_);
+      assoc_ = NULL;
+      params_ = NULL;
+    }
+    else
+    {
+      if (params_ != NULL)
+      {
+        ASC_destroyAssociationParameters(&params_);
+        params_ = NULL;
+      }
+    }
+
+    if (net_ != NULL)
+    {
+      ASC_dropNetwork(&net_);
+      net_ = NULL;
+    }
+
+    accepted_.clear();
+    isOpen_ = false;
+  }
+
+    
+  void DicomAssociation::AddAccepted(const std::string& abstractSyntax,
+                                     DicomTransferSyntax syntax,
+                                     uint8_t presentationContextId)
+  {
+    AcceptedPresentationContexts::iterator found = accepted_.find(abstractSyntax);
+
+    if (found == accepted_.end())
+    {
+      std::map<DicomTransferSyntax, uint8_t> syntaxes;
+      syntaxes[syntax] = presentationContextId;
+      accepted_[abstractSyntax] = syntaxes;
+    }      
+    else
+    {
+      if (found->second.find(syntax) != found->second.end())
+      {
+        LOG(WARNING) << "The same transfer syntax ("
+                     << GetTransferSyntaxUid(syntax)
+                     << ") was accepted twice for the same abstract syntax UID ("
+                     << abstractSyntax << ")";
+      }
+      else
+      {
+        found->second[syntax] = presentationContextId;
+      }
+    }
+  }
+
+
+  DicomAssociation::~DicomAssociation()
+  {
+    try
+    {
+      Close();
+    }
+    catch (OrthancException& e)
+    {
+      // Don't throw exception in destructors
+      LOG(ERROR) << "Error while destroying a DICOM association: " << e.What();
+    }
+  }
+
+
+  void DicomAssociation::SetRole(DicomAssociationRole role)
+  {
+    if (role_ != role)
+    {
+      Close();
+      role_ = role;
+    }
+  }
+
+  
+  void DicomAssociation::ClearPresentationContexts()
+  {
+    Close();
+    proposed_.clear();
+    proposed_.reserve(MAX_PROPOSED_PRESENTATIONS);
+  }
+
+  
+  void DicomAssociation::Open(const DicomAssociationParameters& parameters)
+  {
+    if (isOpen_)
+    {
+      return;  // Already open
+    }
+
+    // Timeout used during association negociation and ASC_releaseAssociation()
+    uint32_t acseTimeout = parameters.GetTimeout();
+    if (acseTimeout == 0)
+    {
+      /**
+       * Timeout is disabled. Global timeout (seconds) for
+       * connecting to remote hosts.  Default value is -1 which
+       * selects infinite timeout, i.e. blocking connect().
+       **/
+      dcmConnectionTimeout.set(-1);
+      acseTimeout = 10;
+    }
+    else
+    {
+      dcmConnectionTimeout.set(acseTimeout);
+    }
+      
+    T_ASC_SC_ROLE dcmtkRole;
+    switch (role_)
+    {
+      case DicomAssociationRole_Default:
+        dcmtkRole = ASC_SC_ROLE_DEFAULT;
+        break;
+
+      case DicomAssociationRole_Scu:
+        dcmtkRole = ASC_SC_ROLE_SCU;
+        break;
+
+      case DicomAssociationRole_Scp:
+        dcmtkRole = ASC_SC_ROLE_SCP;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(net_ == NULL &&
+           params_ == NULL &&
+           assoc_ == NULL);
+
+    if (proposed_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No presentation context was proposed");
+    }
+
+    LOG(INFO) << "Opening a DICOM SCU connection from AET \""
+              << parameters.GetLocalApplicationEntityTitle() 
+              << "\" to AET \"" << parameters.GetRemoteModality().GetApplicationEntityTitle()
+              << "\" on host " << parameters.GetRemoteModality().GetHost()
+              << ":" << parameters.GetRemoteModality().GetPortNumber() 
+              << " (manufacturer: " << EnumerationToString(parameters.GetRemoteModality().GetManufacturer()) << ")";
+
+    CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_));
+    CheckConnecting(parameters, ASC_createAssociationParameters(&params_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU));
+
+    // Set this application's title and the called application's title in the params
+    CheckConnecting(parameters, ASC_setAPTitles(
+                      params_, parameters.GetLocalApplicationEntityTitle().c_str(),
+                      parameters.GetRemoteModality().GetApplicationEntityTitle().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",
+       parameters.GetRemoteModality().GetHost().c_str(),
+       parameters.GetRemoteModality().GetPortNumber());
+
+    CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort));
+
+    // Set various options
+    CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false));
+
+    // Setup the list of proposed presentation contexts
+    unsigned int presentationContextId = 1;
+    for (size_t i = 0; i < proposed_.size(); i++)
+    {
+      assert(presentationContextId <= 255);
+      const char* abstractSyntax = proposed_[i].abstractSyntax_.c_str();
+
+      const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_;
+          
+      std::vector<const char*> transferSyntaxes;
+      transferSyntaxes.reserve(source.size());
+          
+      for (std::set<DicomTransferSyntax>::const_iterator
+             it = source.begin(); it != source.end(); ++it)
+      {
+        transferSyntaxes.push_back(GetTransferSyntaxUid(*it));
+      }
+
+      assert(!transferSyntaxes.empty());
+      CheckConnecting(parameters, ASC_addPresentationContext(
+                        params_, presentationContextId, abstractSyntax,
+                        &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole));
+
+      presentationContextId += 2;
+    }
+
+    // Do the association
+    CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_));
+    isOpen_ = true;
+
+    // Inspect the accepted transfer syntaxes
+    LST_HEAD **l = &params_->DULparams.acceptedPresentationContext;
+    if (*l != NULL)
+    {
+      DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
+      LST_Position(l, (LST_NODE*)pc);
+      while (pc)
+      {
+        if (pc->result == ASC_P_ACCEPTANCE)
+        {
+          DicomTransferSyntax transferSyntax;
+          if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
+          {
+            AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID);
+          }
+          else
+          {
+            LOG(WARNING) << "Unknown transfer syntax received from AET \""
+                         << parameters.GetRemoteModality().GetApplicationEntityTitle()
+                         << "\": " << pc->acceptedTransferSyntax;
+          }
+        }
+            
+        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
+      }
+    }
+
+    if (accepted_.empty())
+    {
+      throw OrthancException(ErrorCode_NoPresentationContext,
+                             "Unable to negotiate a presentation context with AET \"" +
+                             parameters.GetRemoteModality().GetApplicationEntityTitle() + "\"");
+    }
+  }
+
+  void DicomAssociation::Close()
+  {
+    if (isOpen_)
+    {
+      CloseInternal();
+    }
+  }
+
+    
+  bool DicomAssociation::LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target,
+                                                           const std::string& abstractSyntax) const
+  {
+    if (!IsOpen())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened");
+    }
+      
+    AcceptedPresentationContexts::const_iterator found = accepted_.find(abstractSyntax);
+
+    if (found == accepted_.end())
+    {
+      return false;
+    }
+    else
+    {
+      target = found->second;
+      return true;
+    }
+  }
+
+    
+  void DicomAssociation::ProposeGenericPresentationContext(const std::string& abstractSyntax)
+  {
+    std::set<DicomTransferSyntax> ts;
+    ts.insert(DicomTransferSyntax_LittleEndianImplicit);
+    ts.insert(DicomTransferSyntax_LittleEndianExplicit);
+    ts.insert(DicomTransferSyntax_BigEndianExplicit);  // Retired
+    ProposePresentationContext(abstractSyntax, ts);
+  }
+
+    
+  void DicomAssociation::ProposePresentationContext(const std::string& abstractSyntax,
+                                                    DicomTransferSyntax transferSyntax)
+  {
+    std::set<DicomTransferSyntax> ts;
+    ts.insert(transferSyntax);
+    ProposePresentationContext(abstractSyntax, ts);
+  }
+
+    
+  size_t DicomAssociation::GetRemainingPropositions() const
+  {
+    assert(proposed_.size() <= MAX_PROPOSED_PRESENTATIONS);
+    return MAX_PROPOSED_PRESENTATIONS - proposed_.size();
+  }
+    
+
+  void DicomAssociation::ProposePresentationContext(
+    const std::string& abstractSyntax,
+    const std::set<DicomTransferSyntax>& transferSyntaxes)
+  {
+    if (transferSyntaxes.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "No transfer syntax provided");
+    }
+      
+    if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Too many proposed presentation contexts");
+    }
+      
+    if (IsOpen())
+    {
+      Close();
+    }
+
+    ProposedPresentationContext context;
+    context.abstractSyntax_ = abstractSyntax;
+    context.transferSyntaxes_ = transferSyntaxes;
+
+    proposed_.push_back(context);
+  }
+
+    
+  T_ASC_Association& DicomAssociation::GetDcmtkAssociation() const
+  {
+    if (isOpen_)
+    {
+      assert(assoc_ != NULL);
+      return *assoc_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "The connection is not open");
+    }
+  }
+
+    
+  T_ASC_Network& DicomAssociation::GetDcmtkNetwork() const
+  {
+    if (isOpen_)
+    {
+      assert(net_ != NULL);
+      return *net_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "The connection is not open");
+    }
+  }
+
+    
+  void DicomAssociation::CheckCondition(const OFCondition& cond,
+                                        const DicomAssociationParameters& parameters,
+                                        const std::string& command)
+  {
+    if (cond.bad())
+    {
+      // Reformat the error message from DCMTK by turning multiline
+      // errors into a single line
+      
+      std::string s(cond.text());
+      std::string info;
+      info.reserve(s.size());
+
+      bool isMultiline = false;
+      for (size_t i = 0; i < s.size(); i++)
+      {
+        if (s[i] == '\r')
+        {
+          // Ignore
+        }
+        else if (s[i] == '\n')
+        {
+          if (isMultiline)
+          {
+            info += "; ";
+          }
+          else
+          {
+            info += " (";
+            isMultiline = true;
+          }
+        }
+        else
+        {
+          info.push_back(s[i]);
+        }
+      }
+
+      if (isMultiline)
+      {
+        info += ")";
+      }
+
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "DicomAssociation - " + command + " to AET \"" +
+                             parameters.GetRemoteModality().GetApplicationEntityTitle() +
+                             "\": " + info);
+    }
+  }
+    
+
+  void DicomAssociation::ReportStorageCommitment(
+    const DicomAssociationParameters& parameters,
+    const std::string& transactionUid,
+    const std::vector<std::string>& sopClassUids,
+    const std::vector<std::string>& sopInstanceUids,
+    const std::vector<StorageCommitmentFailureReason>& failureReasons)
+  {
+    if (sopClassUids.size() != sopInstanceUids.size() ||
+        sopClassUids.size() != failureReasons.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+
+    std::vector<std::string> successSopClassUids, successSopInstanceUids, failedSopClassUids, failedSopInstanceUids;
+    std::vector<StorageCommitmentFailureReason> failedReasons;
+
+    successSopClassUids.reserve(sopClassUids.size());
+    successSopInstanceUids.reserve(sopClassUids.size());
+    failedSopClassUids.reserve(sopClassUids.size());
+    failedSopInstanceUids.reserve(sopClassUids.size());
+    failedReasons.reserve(sopClassUids.size());
+
+    for (size_t i = 0; i < sopClassUids.size(); i++)
+    {
+      switch (failureReasons[i])
+      {
+        case StorageCommitmentFailureReason_Success:
+          successSopClassUids.push_back(sopClassUids[i]);
+          successSopInstanceUids.push_back(sopInstanceUids[i]);
+          break;
+
+        case StorageCommitmentFailureReason_ProcessingFailure:
+        case StorageCommitmentFailureReason_NoSuchObjectInstance:
+        case StorageCommitmentFailureReason_ResourceLimitation:
+        case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+        case StorageCommitmentFailureReason_ClassInstanceConflict:
+        case StorageCommitmentFailureReason_DuplicateTransactionUID:
+          failedSopClassUids.push_back(sopClassUids[i]);
+          failedSopInstanceUids.push_back(sopInstanceUids[i]);
+          failedReasons.push_back(failureReasons[i]);
+          break;
+
+        default:
+        {
+          char buf[16];
+          sprintf(buf, "%04xH", failureReasons[i]);
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Unsupported failure reason for storage commitment: " + std::string(buf));
+        }
+      }
+    }
+    
+    DicomAssociation association;
+
+    {
+      std::set<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+
+      association.SetRole(DicomAssociationRole_Scp);
+      association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
+                                             transferSyntaxes);
+    }
+      
+    association.Open(parameters);
+
+    /**
+     * N-EVENT-REPORT
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
+     *
+     * Status code:
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
+     **/
+
+    /**
+     * Send the "EVENT_REPORT_RQ" request
+     **/
+
+    LOG(INFO) << "Reporting modality \""
+              << parameters.GetRemoteModality().GetApplicationEntityTitle()
+              << "\" about storage commitment transaction: " << transactionUid
+              << " (" << successSopClassUids.size() << " successes, " 
+              << failedSopClassUids.size() << " failures)";
+    const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
+      
+    {
+      T_DIMSE_Message message;
+      memset(&message, 0, sizeof(message));
+      message.CommandField = DIMSE_N_EVENT_REPORT_RQ;
+
+      T_DIMSE_N_EventReportRQ& content = message.msg.NEventReportRQ;
+      content.MessageID = messageId;
+      strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+      strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+      content.DataSetType = DIMSE_DATASET_PRESENT;
+
+      DcmDataset dataset;
+      if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        std::vector<StorageCommitmentFailureReason> empty;
+        FillSopSequence(dataset, DCM_ReferencedSOPSequence, successSopClassUids,
+                        successSopInstanceUids, empty, false);
+      }
+
+      // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+      if (failedSopClassUids.empty())
+      {
+        content.EventTypeID = 1;  // "Storage Commitment Request Successful"
+      }
+      else
+      {
+        content.EventTypeID = 2;  // "Storage Commitment Request Complete - Failures Exist"
+
+        // Failure reason
+        // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+        FillSopSequence(dataset, DCM_FailedSOPSequence, failedSopClassUids,
+                        failedSopInstanceUids, failedReasons, true);
+      }
+
+      int presID = ASC_findAcceptedPresentationContextID(
+        &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
+      if (presID == 0)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to send N-EVENT-REPORT request to AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      if (!DIMSE_sendMessageUsingMemoryData(
+            &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
+            &dataset, NULL /* callback */, NULL /* callback context */,
+            NULL /* commandSet */).good())
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol);
+      }
+    }
+
+    /**
+     * Read the "EVENT_REPORT_RSP" response
+     **/
+
+    {
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message message;
+
+      if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
+                                (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                parameters.GetTimeout(), &presID, &message,
+                                NULL /* no statusDetail */).good() ||
+          message.CommandField != DIMSE_N_EVENT_REPORT_RSP)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to read N-EVENT-REPORT response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      const T_DIMSE_N_EventReportRSP& content = message.msg.NEventReportRSP;
+      if (content.MessageIDBeingRespondedTo != messageId ||
+          !(content.opts & O_NEVENTREPORT_AFFECTEDSOPCLASSUID) ||
+          !(content.opts & O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID) ||
+          //(content.opts & O_NEVENTREPORT_EVENTTYPEID) ||  // Pedantic test - The "content.EventTypeID" is not used by Orthanc
+          std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
+          content.DataSetType != DIMSE_DATASET_NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Badly formatted N-EVENT-REPORT response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      if (content.DimseStatus != 0 /* success */)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "The request cannot be handled by remote AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+    }
+
+    association.Close();
+  }
+
+    
+  void DicomAssociation::RequestStorageCommitment(
+    const DicomAssociationParameters& parameters,
+    const std::string& transactionUid,
+    const std::vector<std::string>& sopClassUids,
+    const std::vector<std::string>& sopInstanceUids)
+  {
+    if (sopClassUids.size() != sopInstanceUids.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    for (size_t i = 0; i < sopClassUids.size(); i++)
+    {
+      if (sopClassUids[i].empty() ||
+          sopInstanceUids[i].empty())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "The SOP class/instance UIDs cannot be empty, found: \"" +
+                               sopClassUids[i] + "\" / \"" + sopInstanceUids[i] + "\"");
+      }
+    }
+
+    if (transactionUid.size() < 5 ||
+        transactionUid.substr(0, 5) != "2.25.")
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    DicomAssociation association;
+
+    {
+      std::set<DicomTransferSyntax> transferSyntaxes;
+      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+      transferSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      
+      association.SetRole(DicomAssociationRole_Default);
+      association.ProposePresentationContext(UID_StorageCommitmentPushModelSOPClass,
+                                             transferSyntaxes);
+    }
+      
+    association.Open(parameters);
+      
+    /**
+     * N-ACTION
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
+     *
+     * Status code:
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.1.1.8
+     **/
+
+    /**
+     * Send the "N_ACTION_RQ" request
+     **/
+
+    LOG(INFO) << "Request to modality \""
+              << parameters.GetRemoteModality().GetApplicationEntityTitle()
+              << "\" about storage commitment for " << sopClassUids.size()
+              << " instances, with transaction UID: " << transactionUid;
+    const DIC_US messageId = association.GetDcmtkAssociation().nextMsgID++;
+      
+    {
+      T_DIMSE_Message message;
+      memset(&message, 0, sizeof(message));
+      message.CommandField = DIMSE_N_ACTION_RQ;
+
+      T_DIMSE_N_ActionRQ& content = message.msg.NActionRQ;
+      content.MessageID = messageId;
+      strncpy(content.RequestedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+      strncpy(content.RequestedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+      content.ActionTypeID = 1;  // "Request Storage Commitment"
+      content.DataSetType = DIMSE_DATASET_PRESENT;
+
+      DcmDataset dataset;
+      if (!dataset.putAndInsertString(DCM_TransactionUID, transactionUid.c_str()).good())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        std::vector<StorageCommitmentFailureReason> empty;
+        FillSopSequence(dataset, DCM_ReferencedSOPSequence, sopClassUids, sopInstanceUids, empty, false);
+      }
+          
+      int presID = ASC_findAcceptedPresentationContextID(
+        &association.GetDcmtkAssociation(), UID_StorageCommitmentPushModelSOPClass);
+      if (presID == 0)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to send N-ACTION request to AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      if (!DIMSE_sendMessageUsingMemoryData(
+            &association.GetDcmtkAssociation(), presID, &message, NULL /* status detail */,
+            &dataset, NULL /* callback */, NULL /* callback context */,
+            NULL /* commandSet */).good())
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol);
+      }
+    }
+
+    /**
+     * Read the "N_ACTION_RSP" response
+     **/
+
+    {
+      T_ASC_PresentationContextID presID = 0;
+      T_DIMSE_Message message;
+        
+      if (!DIMSE_receiveCommand(&association.GetDcmtkAssociation(),
+                                (parameters.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                parameters.GetTimeout(), &presID, &message,
+                                NULL /* no statusDetail */).good() ||
+          message.CommandField != DIMSE_N_ACTION_RSP)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Unable to read N-ACTION response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      const T_DIMSE_N_ActionRSP& content = message.msg.NActionRSP;
+      if (content.MessageIDBeingRespondedTo != messageId ||
+          !(content.opts & O_NACTION_AFFECTEDSOPCLASSUID) ||
+          !(content.opts & O_NACTION_AFFECTEDSOPINSTANCEUID) ||
+          //(content.opts & O_NACTION_ACTIONTYPEID) ||  // Pedantic test - The "content.ActionTypeID" is not used by Orthanc
+          std::string(content.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(content.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance ||
+          content.DataSetType != DIMSE_DATASET_NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "Badly formatted N-ACTION response from AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+
+      if (content.DimseStatus != 0 /* success */)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "Storage commitment - "
+                               "The request cannot be handled by remote AET: " +
+                               parameters.GetRemoteModality().GetApplicationEntityTitle());
+      }
+    }
+
+    association.Close();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomAssociationParameters.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+#include <stdint.h>   // For uint8_t
+#include <boost/noncopyable.hpp>
+#include <set>
+
+namespace Orthanc
+{
+  class DicomAssociation : public boost::noncopyable
+  {
+  private:
+    // This is the maximum number of presentation context IDs (the
+    // number of odd integers between 1 and 255)
+    // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html
+    static const size_t MAX_PROPOSED_PRESENTATIONS = 128;
+    
+    struct ProposedPresentationContext
+    {
+      std::string                    abstractSyntax_;
+      std::set<DicomTransferSyntax>  transferSyntaxes_;
+    };
+
+    typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> >
+    AcceptedPresentationContexts;
+
+    DicomAssociationRole                      role_;
+    bool                                      isOpen_;
+    std::vector<ProposedPresentationContext>  proposed_;
+    AcceptedPresentationContexts              accepted_;
+    T_ASC_Network*                            net_;
+    T_ASC_Parameters*                         params_;
+    T_ASC_Association*                        assoc_;
+
+    void Initialize();
+
+    void CheckConnecting(const DicomAssociationParameters& parameters,
+                         const OFCondition& cond);
+    
+    void CloseInternal();
+
+    void AddAccepted(const std::string& abstractSyntax,
+                     DicomTransferSyntax syntax,
+                     uint8_t presentationContextId);
+
+  public:
+    DicomAssociation()
+    {
+      Initialize();
+    }
+
+    ~DicomAssociation();
+
+    bool IsOpen() const
+    {
+      return isOpen_;
+    }
+
+    void SetRole(DicomAssociationRole role);
+
+    void ClearPresentationContexts();
+
+    void Open(const DicomAssociationParameters& parameters);
+    
+    void Close();
+
+    bool LookupAcceptedPresentationContext(
+      std::map<DicomTransferSyntax, uint8_t>& target,
+      const std::string& abstractSyntax) const;
+
+    void ProposeGenericPresentationContext(const std::string& abstractSyntax);
+
+    void ProposePresentationContext(const std::string& abstractSyntax,
+                                    DicomTransferSyntax transferSyntax);
+
+    size_t GetRemainingPropositions() const;
+
+    void ProposePresentationContext(
+      const std::string& abstractSyntax,
+      const std::set<DicomTransferSyntax>& transferSyntaxes);
+    
+    T_ASC_Association& GetDcmtkAssociation() const;
+
+    T_ASC_Network& GetDcmtkNetwork() const;
+
+    static void CheckCondition(const OFCondition& cond,
+                               const DicomAssociationParameters& parameters,
+                               const std::string& command);
+
+    static void ReportStorageCommitment(
+      const DicomAssociationParameters& parameters,
+      const std::string& transactionUid,
+      const std::vector<std::string>& sopClassUids,
+      const std::vector<std::string>& sopInstanceUids,
+      const std::vector<StorageCommitmentFailureReason>& failureReasons);
+    
+    static void RequestStorageCommitment(
+      const DicomAssociationParameters& parameters,
+      const std::string& transactionUid,
+      const std::vector<std::string>& sopClassUids,
+      const std::vector<std::string>& sopInstanceUids);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,160 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomAssociationParameters.h"
+
+#include "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "NetworkingCompatibility.h"
+
+#include <boost/thread/mutex.hpp>
+
+// By default, the timeout for client DICOM connections is set to 10 seconds
+static boost::mutex  defaultTimeoutMutex_;
+static uint32_t defaultTimeout_ = 10;
+
+
+namespace Orthanc
+{
+  void DicomAssociationParameters::CheckHost(const std::string& host)
+  {
+    if (host.size() > HOST_NAME_MAX - 10)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid host name (too long): " + host);
+    }
+  }
+
+  
+  uint32_t DicomAssociationParameters::GetDefaultTimeout()
+  {
+    boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
+    return defaultTimeout_;
+  }
+
+
+  DicomAssociationParameters::DicomAssociationParameters() :
+    localAet_("ORTHANC"),
+    timeout_(GetDefaultTimeout())
+  {
+    remote_.SetApplicationEntityTitle("ANY-SCP");
+  }
+
+    
+  DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet,
+                                                         const RemoteModalityParameters& remote) :
+    localAet_(localAet),
+    timeout_(GetDefaultTimeout())
+  {
+    SetRemoteModality(remote);
+  }
+
+    
+  void DicomAssociationParameters::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    CheckHost(remote.GetHost());
+    remote_ = remote;
+  }
+
+
+  void DicomAssociationParameters::SetRemoteHost(const std::string& host)
+  {
+    CheckHost(host);
+    remote_.SetHost(host);
+  }
+
+
+  bool DicomAssociationParameters::IsEqual(const DicomAssociationParameters& other) const
+  {
+    return (localAet_ == other.localAet_ &&
+            remote_.GetApplicationEntityTitle() == other.remote_.GetApplicationEntityTitle() &&
+            remote_.GetHost() == other.remote_.GetHost() &&
+            remote_.GetPortNumber() == other.remote_.GetPortNumber() &&
+            remote_.GetManufacturer() == other.remote_.GetManufacturer() &&
+            timeout_ == other.timeout_);
+  }
+
+
+  static const char* const LOCAL_AET = "LocalAet";
+  static const char* const REMOTE = "Remote";
+  static const char* const TIMEOUT = "Timeout";  // New in Orthanc in 1.7.0
+
+  
+  void DicomAssociationParameters::SerializeJob(Json::Value& target) const
+  {
+    if (target.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      target[LOCAL_AET] = localAet_;
+      remote_.Serialize(target[REMOTE], true /* force advanced format */);
+      target[TIMEOUT] = timeout_;
+    }
+  }
+
+
+  DicomAssociationParameters DicomAssociationParameters::UnserializeJob(const Json::Value& serialized)
+  {
+    if (serialized.type() == Json::objectValue)
+    {
+      DicomAssociationParameters result;
+    
+      result.remote_ = RemoteModalityParameters(serialized[REMOTE]);
+      result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
+      result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout());
+
+      return result;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+    
+
+  void DicomAssociationParameters::SetDefaultTimeout(uint32_t seconds)
+  {
+    LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " 
+              << seconds << " seconds (0 = no timeout)";
+
+    {
+      boost::mutex::scoped_lock lock(defaultTimeoutMutex_);
+      defaultTimeout_ = seconds;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,119 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RemoteModalityParameters.h"
+
+#include <json/value.h>
+
+class OFCondition;  // From DCMTK
+
+namespace Orthanc
+{
+  class DicomAssociationParameters
+  {
+  private:
+    std::string               localAet_;
+    RemoteModalityParameters  remote_;
+    uint32_t                  timeout_;
+
+    static void CheckHost(const std::string& host);
+
+  public:
+    DicomAssociationParameters();
+    
+    DicomAssociationParameters(const std::string& localAet,
+                               const RemoteModalityParameters& remote);
+    
+    const std::string& GetLocalApplicationEntityTitle() const
+    {
+      return localAet_;
+    }
+
+    void SetLocalApplicationEntityTitle(const std::string& aet)
+    {
+      localAet_ = aet;
+    }
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return remote_;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& parameters);
+    
+    void SetRemoteApplicationEntityTitle(const std::string& aet)
+    {
+      remote_.SetApplicationEntityTitle(aet);
+    }
+
+    void SetRemoteHost(const std::string& host);
+
+    void SetRemotePort(uint16_t port)
+    {
+      remote_.SetPortNumber(port);
+    }
+
+    void SetRemoteManufacturer(ModalityManufacturer manufacturer)
+    {
+      remote_.SetManufacturer(manufacturer);
+    }
+
+    bool IsEqual(const DicomAssociationParameters& other) const;
+
+    // Setting it to "0" disables the timeout (infinite wait)
+    void SetTimeout(uint32_t seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    uint32_t GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    bool HasTimeout() const
+    {
+      return timeout_ != 0;
+    }
+
+    void SerializeJob(Json::Value& target) const;
+    
+    static DicomAssociationParameters UnserializeJob(const Json::Value& serialized);
+    
+    static void SetDefaultTimeout(uint32_t seconds);
+
+    static uint32_t GetDefaultTimeout();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,674 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomControlUserConnection.h"
+
+#include "../Compatibility.h"
+#include "../DicomFormat/DicomArray.h"
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "DicomAssociation.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmnet/diutil.h>
+
+namespace Orthanc
+{
+  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));
+    }
+  }
+
+
+  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 NormalizeFindQuery(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::unique_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, GetDefaultDicomEncoding(),
+                                   false /* be strict */);
+      }
+
+      default:
+        return new ParsedDicomFile(fields, GetDefaultDicomEncoding(),
+                                   false /* be strict */);
+    }
+  }
+
+
+
+  void DicomControlUserConnection::SetupPresentationContexts()
+  {
+    assert(association_.get() != NULL);
+    association_->ProposeGenericPresentationContext(UID_VerificationSOPClass);
+    association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel);
+    association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel);
+  }
+    
+
+  void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers,
+                                                DcmDataset* dataset,
+                                                const char* sopClass,
+                                                bool isWorklist,
+                                                const char* level)
+  {
+    assert(isWorklist ^ (level != NULL));
+    assert(association_.get() != NULL);
+
+    association_->Open(parameters_);
+
+    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_->GetDcmtkAssociation(), sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomFindUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_C_FindRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().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;
+
+#if DCMTK_VERSION_NUMBER >= 364
+    int responseCount;
+#endif
+
+    OFCondition cond = DIMSE_findUser(
+      &association_->GetDcmtkAssociation(), presID, &request, dataset,
+#if DCMTK_VERSION_NUMBER >= 364
+      responseCount,
+#endif
+      FindCallback, &payload,
+      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+      &response, &statusDetail);
+    
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    DicomAssociation::CheckCondition(cond, parameters_, "C-FIND");
+
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-FIND.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xFF00 &&  // Pending - Matches are continuing 
+        response.DimseStatus != 0xFF01)    // Pending - Matches are continuing 
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+
+      if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               HttpStatus_422_UnprocessableEntity,
+                               "C-FIND SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf +
+                               " (unable to process - invalid query ?)");
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf);
+      }
+    }
+  }
+
+    
+  void DicomControlUserConnection::MoveInternal(const std::string& targetAet,
+                                                ResourceType level,
+                                                const DicomMap& fields)
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    std::unique_ptr<ParsedDicomFile> query(
+      ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
+        break;
+
+      case ResourceType_Study:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
+        break;
+
+      case ResourceType_Series:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
+        break;
+
+      case ResourceType_Instance:
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Figure out which of the accepted presentation contexts should be used
+    int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass);
+    if (presID == 0)
+    {
+      throw OrthancException(ErrorCode_DicomMoveUnavailable,
+                             "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    T_DIMSE_C_MoveRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().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(
+      &association_->GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL,
+      /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+      /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+      &association_->GetDcmtkNetwork(), NULL, NULL,
+      &response, &statusDetail, &responseIdentifiers);
+
+    if (statusDetail)
+    {
+      delete statusDetail;
+    }
+
+    if (responseIdentifiers)
+    {
+      delete responseIdentifiers;
+    }
+
+    DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE");
+
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-MOVE.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xFF00)    // Pending - Sub-operations are continuing
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+
+      if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               HttpStatus_422_UnprocessableEntity,
+                               "C-MOVE SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf +
+                               " (unable to process - resource not found ?)");
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" +
+                               parameters_.GetRemoteModality().GetApplicationEntityTitle() +
+                               "\" has failed with DIMSE status 0x" + buf);
+      }
+    }
+  }
+    
+
+  DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) :
+    parameters_(params),
+    association_(new DicomAssociation)
+  {
+    SetupPresentationContexts();
+  }
+    
+
+  void DicomControlUserConnection::Close()
+  {
+    assert(association_.get() != NULL);
+    association_->Close();
+  }
+
+
+  bool DicomControlUserConnection::Echo()
+  {
+    assert(association_.get() != NULL);
+    association_->Open(parameters_);
+
+    DIC_US status;
+    DicomAssociation::CheckCondition(
+      DIMSE_echoUser(&association_->GetDcmtkAssociation(),
+                     association_->GetDcmtkAssociation().nextMsgID++, 
+                     /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                     /*opt_dimse_timeout*/ parameters_.GetTimeout(),
+                     &status, NULL),
+      parameters_, "C-ECHO");
+      
+    return status == STATUS_Success;
+  }
+
+
+  void DicomControlUserConnection::Find(DicomFindAnswers& result,
+                                        ResourceType level,
+                                        const DicomMap& originalFields,
+                                        bool normalize)
+  {
+    std::unique_ptr<ParsedDicomFile> query;
+
+    if (normalize)
+    {
+      DicomMap fields;
+      NormalizeFindQuery(fields, level, originalFields);
+      query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer()));
+    }
+    else
+    {
+      query.reset(new ParsedDicomFile(originalFields, GetDefaultDicomEncoding(),
+                                      false /* be strict */));
+    }
+    
+    DcmDataset* dataset = query->GetDcmtkObject().getDataset();
+
+    const char* clevel = NULL;
+    const char* sopClass = NULL;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        clevel = "PATIENT";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT");
+        sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Study:
+        clevel = "STUDY";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Series:
+        clevel = "SERIES";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      case ResourceType_Instance:
+        clevel = "IMAGE";
+        DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE");
+        sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+
+    const char* universal;
+    if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE)
+    {
+      universal = "*";
+    }
+    else
+    {
+      universal = "";
+    }      
+    
+
+    // Add the expected tags for this query level.
+    // WARNING: Do not reorder or add "break" in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        if (!dataset->tagExists(DCM_SOPInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal);
+        }
+
+      case ResourceType_Series:
+        if (!dataset->tagExists(DCM_SeriesInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal);
+        }
+
+      case ResourceType_Study:
+        if (!dataset->tagExists(DCM_AccessionNumber))
+        {
+          DU_putStringDOElement(dataset, DCM_AccessionNumber, universal);
+        }
+
+        if (!dataset->tagExists(DCM_StudyInstanceUID))
+        {
+          DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal);
+        }
+
+      case ResourceType_Patient:
+        if (!dataset->tagExists(DCM_PatientID))
+        {
+          DU_putStringDOElement(dataset, DCM_PatientID, universal);
+        }
+        
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(clevel != NULL && sopClass != NULL);
+    FindInternal(result, dataset, sopClass, false, clevel);
+  }
+    
+
+  void DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::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 DicomControlUserConnection::FindWorklist(DicomFindAnswers& result,
+                                                ParsedDicomFile& query)
+  {
+    DcmDataset* dataset = query.GetDcmtkObject().getDataset();
+    const char* sopClass = UID_FINDModalityWorklistInformationModel;
+
+    FindInternal(result, dataset, sopClass, true, NULL);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomAssociationParameters.h"
+#include "DicomFindAnswers.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DicomAssociation;  // Forward declaration for PImpl design pattern
+  
+  class DicomControlUserConnection : public boost::noncopyable
+  {
+  private:
+    DicomAssociationParameters           parameters_;
+    boost::shared_ptr<DicomAssociation>  association_;
+
+    void SetupPresentationContexts();
+
+    void FindInternal(DicomFindAnswers& answers,
+                      DcmDataset* dataset,
+                      const char* sopClass,
+                      bool isWorklist,
+                      const char* level);
+    
+    void MoveInternal(const std::string& targetAet,
+                      ResourceType level,
+                      const DicomMap& fields);
+    
+  public:
+    DicomControlUserConnection(const DicomAssociationParameters& params);
+    
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void Close();
+
+    bool Echo();
+
+    void Find(DicomFindAnswers& result,
+              ResourceType level,
+              const DicomMap& originalFields,
+              bool normalize);
+
+    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 FindWorklist(DicomFindAnswers& result,
+                      ParsedDicomFile& query);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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::unique_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)
+  {
+    // We use the permissive mode to be tolerant wrt. invalid DICOM
+    // files that contain some tags with out-of-range values (such
+    // tags are removed from the answers)
+    AddAnswerInternal(new ParsedDicomFile(map, encoding_, true /* permissive */));
+                                          //"" /* no private creator */));
+  }
+
+
+  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
+  {
+    // As "DicomFindAnswers" stores its content using class
+    // "ParsedDicomFile" (that internally uses "DcmFileFormat" from
+    // DCMTK), the dataset can contain tags that are reserved if
+    // storing the media on the disk, notably tag
+    // "MediaStorageSOPClassUID" (0002,0002). In this function, we
+    // remove all those tags whose group is below 0x0008. The
+    // resulting data set is clean for emission in the C-FIND SCP.
+
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
+    // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
+
+    DcmDataset& source = *GetAnswer(index).GetDcmtkObject().getDataset();
+
+    std::unique_ptr<DcmDataset> target(new DcmDataset);
+
+    for (unsigned long i = 0; i < source.card(); i++)
+    {
+      const DcmElement* element = source.getElement(i);
+      assert(element != NULL);
+
+      if (element != NULL &&
+          element->getTag().getGroup() >= 0x0008 &&
+          element->getTag().getElement() != 0x0000)
+      {
+        target->insert(dynamic_cast<DcmElement*>(element->clone()));
+      }
+    }
+    
+    return target.release();
+  }
+
+
+  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/OrthancFramework/Sources/DicomNetworking/DicomFindAnswers.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/DicomServer.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,429 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Logging.h"
+#include "../MultiThreading/RunnableWorkersPool.h"
+#include "../OrthancException.h"
+#include "../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::unique_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::unique_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;
+    getRequestHandlerFactory_ = NULL;
+    storeRequestHandlerFactory_ = NULL;
+    worklistRequestHandlerFactory_ = NULL;
+    storageCommitmentFactory_ = 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::SetGetRequestHandlerFactory(IGetRequestHandlerFactory& factory)
+  {
+    Stop();
+    getRequestHandlerFactory_ = &factory;
+  }
+
+  bool DicomServer::HasGetRequestHandlerFactory() const
+  {
+    return (getRequestHandlerFactory_ != NULL);
+  }
+
+  IGetRequestHandlerFactory& DicomServer::GetGetRequestHandlerFactory() const
+  {
+    if (HasGetRequestHandlerFactory())
+    {
+      return *getRequestHandlerFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoCGetHandler);
+    }
+  }
+
+  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::SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& factory)
+  {
+    Stop();
+    storageCommitmentFactory_ = &factory;
+  }
+
+  bool DicomServer::HasStorageCommitmentRequestHandlerFactory() const
+  {
+    return (storageCommitmentFactory_ != NULL);
+  }
+
+  IStorageCommitmentRequestHandlerFactory& DicomServer::GetStorageCommitmentRequestHandlerFactory() const
+  {
+    if (HasStorageCommitmentRequestHandlerFactory())
+    {
+      return *storageCommitmentFactory_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NoStorageCommitmentHandler);
+    }
+  }
+
+  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)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No list of modalities was provided to the DICOM server");
+    }
+    
+    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())
+    {
+      throw OrthancException(ErrorCode_DicomPortInUse,
+                             " (port = " + boost::lexical_cast<std::string>(port_) + ") cannot create network: " + std::string(cond.text()));
+    }
+
+    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/OrthancFramework/Sources/DicomNetworking/DicomServer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IGetRequestHandlerFactory.h"
+#include "IStoreRequestHandlerFactory.h"
+#include "IWorklistRequestHandlerFactory.h"
+#include "IStorageCommitmentRequestHandlerFactory.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_;
+    IGetRequestHandlerFactory* getRequestHandlerFactory_;
+    IStoreRequestHandlerFactory* storeRequestHandlerFactory_;
+    IWorklistRequestHandlerFactory* worklistRequestHandlerFactory_;
+    IStorageCommitmentRequestHandlerFactory* storageCommitmentFactory_;
+    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 SetGetRequestHandlerFactory(IGetRequestHandlerFactory& handler);
+    bool HasGetRequestHandlerFactory() const;
+    IGetRequestHandlerFactory& GetGetRequestHandlerFactory() const;
+
+    void SetStoreRequestHandlerFactory(IStoreRequestHandlerFactory& handler);
+    bool HasStoreRequestHandlerFactory() const;
+    IStoreRequestHandlerFactory& GetStoreRequestHandlerFactory() const;
+
+    void SetWorklistRequestHandlerFactory(IWorklistRequestHandlerFactory& handler);
+    bool HasWorklistRequestHandlerFactory() const;
+    IWorklistRequestHandlerFactory& GetWorklistRequestHandlerFactory() const;
+
+    void SetStorageCommitmentRequestHandlerFactory(IStorageCommitmentRequestHandlerFactory& handler);
+    bool HasStorageCommitmentRequestHandlerFactory() const;
+    IStorageCommitmentRequestHandlerFactory& GetStorageCommitmentRequestHandlerFactory() 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/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,527 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomStoreUserConnection.h"
+
+#include "../DicomParsing/FromDcmtkBridge.h"
+#include "../DicomParsing/ParsedDicomFile.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "DicomAssociation.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+
+namespace Orthanc
+{
+  bool DicomStoreUserConnection::ProposeStorageClass(const std::string& sopClassUid,
+                                                     const std::set<DicomTransferSyntax>& syntaxes)
+  {
+    // Default transfer syntax for DICOM
+    const bool addLittleEndianImplicit = (
+      proposeUncompressedSyntaxes_ &&
+      syntaxes.find(DicomTransferSyntax_LittleEndianImplicit) == syntaxes.end());
+    
+    const bool addLittleEndianExplicit = (
+      proposeUncompressedSyntaxes_ &&
+      syntaxes.find(DicomTransferSyntax_LittleEndianExplicit) == syntaxes.end());
+    
+    const bool addBigEndianExplicit = (
+      proposeUncompressedSyntaxes_ &&
+      proposeRetiredBigEndian_ &&
+      syntaxes.find(DicomTransferSyntax_BigEndianExplicit) == syntaxes.end());
+    
+    size_t requiredCount = syntaxes.size();
+    if (addLittleEndianImplicit)
+    {
+      requiredCount += 1;
+    }
+      
+    if (addLittleEndianExplicit ||
+        addBigEndianExplicit)
+    {
+      requiredCount += 1;
+    }
+      
+    if (association_->GetRemainingPropositions() <= requiredCount)
+    {
+      return false;  // Not enough room
+    }
+    else
+    {
+      for (std::set<DicomTransferSyntax>::const_iterator
+             it = syntaxes.begin(); it != syntaxes.end(); ++it)
+      {
+        association_->ProposePresentationContext(sopClassUid, *it);
+        proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *it));
+      }
+
+      if (addLittleEndianImplicit)
+      {
+        association_->ProposePresentationContext(sopClassUid, DicomTransferSyntax_LittleEndianImplicit);
+        proposedOriginalClasses_.insert(std::make_pair(sopClassUid, DicomTransferSyntax_LittleEndianImplicit));
+      }
+
+      if (addLittleEndianExplicit ||
+          addBigEndianExplicit)
+      {
+        std::set<DicomTransferSyntax> uncompressed;
+
+        if (addLittleEndianExplicit)
+        {
+          uncompressed.insert(DicomTransferSyntax_LittleEndianExplicit);
+        }
+
+        if (addBigEndianExplicit)
+        {
+          uncompressed.insert(DicomTransferSyntax_BigEndianExplicit);
+        }
+
+        association_->ProposePresentationContext(sopClassUid, uncompressed);
+
+        assert(!uncompressed.empty());
+        if (addLittleEndianExplicit ^ addBigEndianExplicit)
+        {
+          // Only one transfer syntax was proposed for this presentation context
+          assert(uncompressed.size() == 1);
+          proposedOriginalClasses_.insert(std::make_pair(sopClassUid, *uncompressed.begin()));
+        }
+      }
+
+      return true;
+    }
+  }
+
+
+  bool DicomStoreUserConnection::LookupPresentationContext(
+    uint8_t& presentationContextId,
+    const std::string& sopClassUid,
+    DicomTransferSyntax transferSyntax)
+  {
+    typedef std::map<DicomTransferSyntax, uint8_t>  PresentationContexts;
+
+    PresentationContexts pc;
+    if (association_->IsOpen() &&
+        association_->LookupAcceptedPresentationContext(pc, sopClassUid))
+    {
+      PresentationContexts::const_iterator found = pc.find(transferSyntax);
+      if (found != pc.end())
+      {
+        presentationContextId = found->second;
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  DicomStoreUserConnection::DicomStoreUserConnection(
+    const DicomAssociationParameters& params) :
+    parameters_(params),
+    association_(new DicomAssociation),
+    proposeCommonClasses_(true),
+    proposeUncompressedSyntaxes_(true),
+    proposeRetiredBigEndian_(false)
+  {
+  }
+    
+
+  void DicomStoreUserConnection::RegisterStorageClass(const std::string& sopClassUid,
+                                                      DicomTransferSyntax syntax)
+  {
+    RegisteredClasses::iterator found = registeredClasses_.find(sopClassUid);
+
+    if (found == registeredClasses_.end())
+    {
+      std::set<DicomTransferSyntax> ts;
+      ts.insert(syntax);
+      registeredClasses_[sopClassUid] = ts;
+    }
+    else
+    {
+      found->second.insert(syntax);
+    }
+  }
+
+
+  void DicomStoreUserConnection::LookupParameters(std::string& sopClassUid,
+                                                  std::string& sopInstanceUid,
+                                                  DicomTransferSyntax& transferSyntax,
+                                                  DcmFileFormat& dicom)
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    OFString a, b;
+    if (!dicom.getDataset()->findAndGetOFString(DCM_SOPClassUID, a).good() ||
+        !dicom.getDataset()->findAndGetOFString(DCM_SOPInstanceUID, b).good())
+    {
+      throw OrthancException(ErrorCode_NoSopClassOrInstance,
+                             "Unable to determine the SOP class/instance for C-STORE with AET " +
+                             parameters_.GetRemoteModality().GetApplicationEntityTitle());
+    }
+
+    sopClassUid.assign(a.c_str());
+    sopInstanceUid.assign(b.c_str());
+
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax, dicom))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unknown transfer syntax from DCMTK");
+    }
+  }
+  
+
+  bool DicomStoreUserConnection::NegotiatePresentationContext(
+    uint8_t& presentationContextId,
+    const std::string& sopClassUid,
+    DicomTransferSyntax transferSyntax)
+  {
+    /**
+     * Step 1: Check whether this presentation context is already
+     * available in the previously negotiated assocation.
+     **/
+
+    if (LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax))
+    {
+      return true;
+    }
+
+    // The association must be re-negotiated
+    if (association_->IsOpen())
+    {
+      LOG(INFO) << "Re-negotiating DICOM association with "
+                << parameters_.GetRemoteModality().GetApplicationEntityTitle();
+
+      if (proposedOriginalClasses_.find(std::make_pair(sopClassUid, transferSyntax)) !=
+          proposedOriginalClasses_.end())
+      {
+        LOG(INFO) << "The remote modality has already rejected SOP class UID \""
+                  << sopClassUid << "\" with transfer syntax \""
+                  << GetTransferSyntaxUid(transferSyntax) << "\", don't renegotiate";
+        return false;
+      }
+    }
+
+    association_->ClearPresentationContexts();
+    proposedOriginalClasses_.clear();
+    RegisterStorageClass(sopClassUid, transferSyntax);  // (*)
+
+    
+    /**
+     * Step 2: Propose at least the mandatory SOP class.
+     **/
+
+    {
+      RegisteredClasses::const_iterator mandatory = registeredClasses_.find(sopClassUid);
+
+      if (mandatory == registeredClasses_.end() ||
+          mandatory->second.find(transferSyntax) == mandatory->second.end())
+      {
+        // Should never fail because of (*)
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!ProposeStorageClass(sopClassUid, mandatory->second))
+      {
+        // Should never happen in real life: There are no more than
+        // 128 transfer syntaxes in DICOM!
+        throw OrthancException(ErrorCode_InternalError,
+                               "Too many transfer syntaxes for SOP class UID: " + sopClassUid);
+      }
+    }
+
+      
+    /**
+     * Step 3: Propose all the previously spotted SOP classes, as
+     * registered through the "RegisterStorageClass()" method.
+     **/
+      
+    for (RegisteredClasses::const_iterator it = registeredClasses_.begin();
+         it != registeredClasses_.end(); ++it)
+    {
+      if (it->first != sopClassUid)
+      {
+        ProposeStorageClass(it->first, it->second);
+      }
+    }
+      
+
+    /**
+     * Step 4: As long as there is room left in the proposed
+     * presentation contexts, propose the uncompressed transfer syntaxes
+     * for the most common SOP classes, as can be found in the
+     * "dcmShortSCUStorageSOPClassUIDs" array from DCMTK. The
+     * preferred transfer syntax is "LittleEndianImplicit".
+     **/
+
+    if (proposeCommonClasses_)
+    {
+      // The method "ProposeStorageClass()" will automatically add
+      // "LittleEndianImplicit"
+      std::set<DicomTransferSyntax> ts;
+        
+      for (int i = 0; i < numberOfDcmShortSCUStorageSOPClassUIDs; i++)
+      {
+        std::string c(dcmShortSCUStorageSOPClassUIDs[i]);
+          
+        if (c != sopClassUid &&
+            registeredClasses_.find(c) == registeredClasses_.end())
+        {
+          ProposeStorageClass(c, ts);
+        }
+      }
+    }
+
+
+    /**
+     * Step 5: Open the association, and check whether the pair (SOP
+     * class UID, transfer syntax) was accepted by the remote host.
+     **/
+
+    association_->Open(parameters_);
+    return LookupPresentationContext(presentationContextId, sopClassUid, transferSyntax);
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       DcmFileFormat& dicom,
+                                       bool hasMoveOriginator,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    DicomTransferSyntax transferSyntax;
+    LookupParameters(sopClassUid, sopInstanceUid, transferSyntax, dicom);
+
+    uint8_t presID;
+    if (!NegotiatePresentationContext(presID, sopClassUid, transferSyntax))
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "No valid presentation context was negotiated for "
+                             "SOP class UID [" + sopClassUid + "] and transfer "
+                             "syntax [" + GetTransferSyntaxUid(transferSyntax) + "] "
+                             "while sending to modality [" +
+                             parameters_.GetRemoteModality().GetApplicationEntityTitle() + "]");
+    }
+    
+    // Prepare the transmission of data
+    T_DIMSE_C_StoreRQ request;
+    memset(&request, 0, sizeof(request));
+    request.MessageID = association_->GetDcmtkAssociation().nextMsgID++;
+    strncpy(request.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
+    request.Priority = DIMSE_PRIORITY_MEDIUM;
+    request.DataSetType = DIMSE_DATASET_PRESENT;
+    strncpy(request.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
+
+    if (hasMoveOriginator)
+    {    
+      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;
+    }
+
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Finally conduct transmission of data
+    T_DIMSE_C_StoreRSP response;
+    DcmDataset* statusDetail = NULL;
+    DicomAssociation::CheckCondition(
+      DIMSE_storeUser(&association_->GetDcmtkAssociation(), presID, &request,
+                      NULL, dicom.getDataset(), /*progressCallback*/ NULL, NULL,
+                      /*opt_blockMode*/ (GetParameters().HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                      /*opt_dimse_timeout*/ GetParameters().GetTimeout(),
+                      &response, &statusDetail, NULL),
+      GetParameters(), "C-STORE");
+
+    if (statusDetail != NULL) 
+    {
+      delete statusDetail;
+    }
+    
+    /**
+     * New in Orthanc 1.6.0: Deal with failures during C-STORE.
+     * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_B.2.3.html#table_B.2-1
+     **/
+    
+    if (response.DimseStatus != 0x0000 &&  // Success
+        response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
+        response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
+        response.DimseStatus != 0xB006)    // Warning - Elements Discarded
+    {
+      char buf[16];
+      sprintf(buf, "%04X", response.DimseStatus);
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "C-STORE SCU to AET \"" +
+                             GetParameters().GetRemoteModality().GetApplicationEntityTitle() +
+                             "\" has failed with DIMSE status 0x" + buf);
+    }
+  }
+
+
+  void DicomStoreUserConnection::Store(std::string& sopClassUid,
+                                       std::string& sopInstanceUid,
+                                       const void* buffer,
+                                       size_t size,
+                                       bool hasMoveOriginator,
+                                       const std::string& moveOriginatorAET,
+                                       uint16_t moveOriginatorID)
+  {
+    std::unique_ptr<DcmFileFormat> dicom(
+      FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
+
+    if (dicom.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    Store(sopClassUid, sopInstanceUid, *dicom, hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+  }
+
+
+  void DicomStoreUserConnection::LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
+                                                   const std::string& sopClassUid,
+                                                   DicomTransferSyntax sourceSyntax)
+  {
+    acceptedSyntaxes.clear();
+
+    // Make sure a negotiation has already occurred for this transfer
+    // syntax. We don't use the return code: Transcoding is possible
+    // even if the "sourceSyntax" is not supported.
+    uint8_t presID;
+    NegotiatePresentationContext(presID, sopClassUid, sourceSyntax);
+
+    std::map<DicomTransferSyntax, uint8_t> contexts;
+    if (association_->LookupAcceptedPresentationContext(contexts, sopClassUid))
+    {
+      for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
+             it = contexts.begin(); it != contexts.end(); ++it)
+      {
+        acceptedSyntaxes.insert(it->first);
+      }
+    }
+  }
+
+
+  void DicomStoreUserConnection::Transcode(std::string& sopClassUid /* out */,
+                                           std::string& sopInstanceUid /* out */,
+                                           IDicomTranscoder& transcoder,
+                                           const void* buffer,
+                                           size_t size,
+                                           bool hasMoveOriginator,
+                                           const std::string& moveOriginatorAET,
+                                           uint16_t moveOriginatorID)
+  {
+    std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(buffer, size));
+    if (dicom.get() == NULL ||
+        dicom->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    DicomTransferSyntax inputSyntax;
+    LookupParameters(sopClassUid, sopInstanceUid, inputSyntax, *dicom);
+
+    std::set<DicomTransferSyntax> accepted;
+    LookupTranscoding(accepted, sopClassUid, inputSyntax);
+
+    if (accepted.find(inputSyntax) != accepted.end())
+    {
+      // No need for transcoding
+      Store(sopClassUid, sopInstanceUid, *dicom,
+            hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+    }
+    else
+    {
+      // Transcoding is needed
+      std::set<DicomTransferSyntax> uncompressedSyntaxes;
+
+      if (accepted.find(DicomTransferSyntax_LittleEndianImplicit) != accepted.end())
+      {
+        uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianImplicit);
+      }
+
+      if (accepted.find(DicomTransferSyntax_LittleEndianExplicit) != accepted.end())
+      {
+        uncompressedSyntaxes.insert(DicomTransferSyntax_LittleEndianExplicit);
+      }
+
+      if (accepted.find(DicomTransferSyntax_BigEndianExplicit) != accepted.end())
+      {
+        uncompressedSyntaxes.insert(DicomTransferSyntax_BigEndianExplicit);
+      }
+
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(dicom.release());
+      source.SetExternalBuffer(buffer, size);
+
+      const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(source.GetParsed());
+      
+      IDicomTranscoder::DicomImage transcoded;
+      if (transcoder.Transcode(transcoded, source, uncompressedSyntaxes, false))
+      {
+        if (sourceUid != IDicomTranscoder::GetSopInstanceUid(transcoded.GetParsed()))
+        {
+          throw OrthancException(ErrorCode_Plugin, "The transcoder has changed the SOP "
+                                 "instance UID while transcoding to an uncompressed transfer syntax");
+        }
+        else
+        {
+          DicomTransferSyntax transcodedSyntax;
+          
+          // Sanity check
+          if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transcodedSyntax, transcoded.GetParsed()) ||
+              accepted.find(transcodedSyntax) == accepted.end())
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          else
+          {
+            Store(sopClassUid, sopInstanceUid, transcoded.GetParsed(),
+                  hasMoveOriginator, moveOriginatorAET, moveOriginatorID);
+          }
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_TRANSCODING)
+#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
+#endif
+
+#include "DicomAssociationParameters.h"
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#  include "../DicomParsing/IDicomTranscoder.h"
+#endif
+
+#include <boost/shared_ptr.hpp>
+#include <boost/noncopyable.hpp>
+#include <set>
+#include <stdint.h>  // For uint8_t
+
+
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  /**
+
+     Orthanc < 1.7.0:
+
+     Input        | Output
+     -------------+---------------------------------------------
+     Compressed   | Same transfer syntax
+     Uncompressed | Same transfer syntax, or other uncompressed
+
+     Orthanc >= 1.7.0:
+
+     Input        | Output
+     -------------+---------------------------------------------
+     Compressed   | Same transfer syntax, or uncompressed
+     Uncompressed | Same transfer syntax, or other uncompressed
+
+  **/
+
+  class DicomAssociation;  // Forward declaration for PImpl design pattern
+
+  class DicomStoreUserConnection : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, std::set<DicomTransferSyntax> > RegisteredClasses;
+
+    // "ProposedOriginalClasses" keeps track of the storage classes
+    // that were proposed with a single transfer syntax
+    typedef std::set< std::pair<std::string, DicomTransferSyntax> > ProposedOriginalClasses;
+    
+    DicomAssociationParameters           parameters_;
+    boost::shared_ptr<DicomAssociation>  association_;  // "shared_ptr" is for PImpl
+    RegisteredClasses                    registeredClasses_;
+    ProposedOriginalClasses              proposedOriginalClasses_;
+    bool                                 proposeCommonClasses_;
+    bool                                 proposeUncompressedSyntaxes_;
+    bool                                 proposeRetiredBigEndian_;
+
+    // Return "false" if there is not enough room remaining in the association
+    bool ProposeStorageClass(const std::string& sopClassUid,
+                             const std::set<DicomTransferSyntax>& syntaxes);
+
+    bool LookupPresentationContext(uint8_t& presentationContextId,
+                                   const std::string& sopClassUid,
+                                   DicomTransferSyntax transferSyntax);
+    
+    bool NegotiatePresentationContext(uint8_t& presentationContextId,
+                                      const std::string& sopClassUid,
+                                      DicomTransferSyntax transferSyntax);
+
+    void LookupTranscoding(std::set<DicomTransferSyntax>& acceptedSyntaxes,
+                           const std::string& sopClassUid,
+                           DicomTransferSyntax sourceSyntax);
+
+  public:
+    DicomStoreUserConnection(const DicomAssociationParameters& params);
+    
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void SetCommonClassesProposed(bool proposed)
+    {
+      proposeCommonClasses_ = proposed;
+    }
+
+    bool IsCommonClassesProposed() const
+    {
+      return proposeCommonClasses_;
+    }
+
+    void SetUncompressedSyntaxesProposed(bool proposed)
+    {
+      proposeUncompressedSyntaxes_ = proposed;
+    }
+
+    bool IsUncompressedSyntaxesProposed() const
+    {
+      return proposeUncompressedSyntaxes_;
+    }
+
+    void SetRetiredBigEndianProposed(bool propose)
+    {
+      proposeRetiredBigEndian_ = propose;
+    }
+
+    bool IsRetiredBigEndianProposed() const
+    {
+      return proposeRetiredBigEndian_;
+    }      
+
+    void RegisterStorageClass(const std::string& sopClassUid,
+                              DicomTransferSyntax syntax);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               DcmFileFormat& dicom,
+               bool hasMoveOriginator,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void Store(std::string& sopClassUid,
+               std::string& sopInstanceUid,
+               const void* buffer,
+               size_t size,
+               bool hasMoveOriginator,
+               const std::string& moveOriginatorAET,
+               uint16_t moveOriginatorID);
+
+    void LookupParameters(std::string& sopClassUid,
+                          std::string& sopInstanceUid,
+                          DicomTransferSyntax& transferSyntax,
+                          DcmFileFormat& dicom);
+
+    void Transcode(std::string& sopClassUid /* out */,
+                   std::string& sopInstanceUid /* out */,
+                   IDicomTranscoder& transcoder,
+                   const void* buffer,
+                   size_t size,
+                   bool hasMoveOriginator,
+                   const std::string& moveOriginatorAET,
+                   uint16_t moveOriginatorID);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/IApplicationEntityFilter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/IFindRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/IFindRequestHandlerFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/IGetRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/dcmnet/assoc.h>
+
+#include "../DicomFormat/DicomMap.h"
+
+#include <string>
+
+
+namespace Orthanc
+{
+  class IGetRequestHandler : boost::noncopyable
+  {
+  public:
+    enum Status
+    {
+      Status_Success,
+      Status_Failure,
+      Status_Warning
+    };
+    
+    virtual ~IGetRequestHandler()
+    {
+    }
+
+    virtual bool Handle(const DicomMap& input,
+                        const std::string& originatorIp,
+                        const std::string& originatorAet,
+                        const std::string& calledAet,
+                        uint32_t timeout) = 0;
+    
+    virtual unsigned int GetSubOperationCount() const = 0;
+    
+    virtual Status DoNext(T_ASC_Association *) = 0;
+    
+    virtual unsigned int GetRemainingCount() const = 0;
+    
+    virtual unsigned int GetCompletedCount() const = 0;
+    
+    virtual unsigned int GetWarningCount() const = 0;
+    
+    virtual unsigned int GetFailedCount() const = 0;
+    
+    virtual const std::string& GetFailedUids() const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/IGetRequestHandlerFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IGetRequestHandler.h"
+
+namespace Orthanc
+{
+  class IGetRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IGetRequestHandlerFactory()
+    {
+    }
+
+    virtual IGetRequestHandler* ConstructGetRequestHandler() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/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/OrthancFramework/Sources/DicomNetworking/IMoveRequestHandlerFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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>
+#include <string>
+#include <vector>
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandler()
+    {
+    }
+
+    virtual void HandleRequest(const std::string& transactionUid,
+                               const std::vector<std::string>& sopClassUids,
+                               const std::vector<std::string>& sopInstanceUids,
+                               const std::string& remoteIp,
+                               const std::string& remoteAet,
+                               const std::string& calledAet) = 0;
+
+    virtual void HandleReport(const std::string& transactionUid,
+                              const std::vector<std::string>& successSopClassUids,
+                              const std::vector<std::string>& successSopInstanceUids,
+                              const std::vector<std::string>& failedSopClassUids,
+                              const std::vector<std::string>& failedSopInstanceUids,
+                              const std::vector<StorageCommitmentFailureReason>& failureReasons,
+                              const std::string& remoteIp,
+                              const std::string& remoteAet,
+                              const std::string& calledAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/IStorageCommitmentRequestHandlerFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IStorageCommitmentRequestHandler.h"
+
+namespace Orthanc
+{
+  class IStorageCommitmentRequestHandlerFactory : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentRequestHandlerFactory()
+    {
+    }
+
+    virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/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/OrthancFramework/Sources/DicomNetworking/IStoreRequestHandlerFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/IWorklistRequestHandlerFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1346 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "FindScp.h"
+#include "StoreScp.h"
+#include "MoveScp.h"
+#include "GetScp.h"
+#include "../../Compatibility.h"
+#include "../../Toolbox.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>     /* for storage commitment */
+#include <dcmtk/dcmdata/dcsequen.h>     /* for class DcmSequenceOfItems */
+#include <dcmtk/dcmdata/dcuid.h>        /* for variable dcmAllStorageSOPClassUIDs */
+#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)
+{
+  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
+  {
+    OFCondition AssociationCleanup(T_ASC_Association *assoc)
+    {
+      OFCondition cond = ASC_dropSCPAssociation(assoc);
+      if (cond.bad())
+      {
+        LOG(ERROR) << cond.text();
+        return cond;
+      }
+
+      cond = ASC_destroyAssociation(&assoc);
+      if (cond.bad())
+      {
+        LOG(ERROR) << 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;
+
+      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 (
+#if DCMTK_VERSION_NUMBER >= 364
+          ASC_getAPTitles(assoc->params, remoteAet_C, sizeof(remoteAet_C), calledAet_C, sizeof(calledAet_C), NULL, 0).bad() ||
+          ASC_getPresentationAddresses(assoc->params, remoteIp_C, sizeof(remoteIp_C), calledIP_C, sizeof(calledIP_C)).bad()
+#else
+          ASC_getAPTitles(assoc->params, remoteAet_C, calledAet_C, NULL).bad() ||
+          ASC_getPresentationAddresses(assoc->params, remoteIp_C, calledIP_C).bad()
+#endif
+          )
+        {
+          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;
+
+
+      {
+        /* accept the abstract syntaxes for C-ECHO, C-FIND, C-MOVE,
+           and storage commitment, if presented */
+
+        std::vector<const char*> genericTransferSyntaxes;
+        genericTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        genericTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+        std::vector<const char*> knownAbstractSyntaxes;
+
+        // For C-ECHO (always enabled since Orthanc 1.6.0; in earlier
+        // versions, only enabled if C-STORE was also enabled)
+        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);
+        }
+
+        // For C-GET
+        if (server.HasGetRequestHandlerFactory())
+        {
+          knownAbstractSyntaxes.push_back(UID_GETStudyRootQueryRetrieveInformationModel);
+          knownAbstractSyntaxes.push_back(UID_GETPatientRootQueryRetrieveInformationModel);
+        }
+
+        cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+          assoc->params,
+          &knownAbstractSyntaxes[0], knownAbstractSyntaxes.size(),
+          &genericTransferSyntaxes[0], genericTransferSyntaxes.size());
+        if (cond.bad())
+        {
+          LOG(INFO) << cond.text();
+          AssociationCleanup(assoc);
+          return NULL;
+        }
+
+      
+        /* storage commitment support, new in Orthanc 1.6.0 */
+        if (server.HasStorageCommitmentRequestHandlerFactory())
+        {
+          /**
+           * "ASC_SC_ROLE_SCUSCP": The "SCU" role is needed to accept
+           * remote storage commitment requests, and the "SCP" role is
+           * needed to receive storage commitments answers.
+           **/        
+          const char* as[1] = { UID_StorageCommitmentPushModelSOPClass }; 
+          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+            assoc->params, as, 1,
+            &genericTransferSyntaxes[0], genericTransferSyntaxes.size(), ASC_SC_ROLE_SCUSCP);
+          if (cond.bad())
+          {
+            LOG(INFO) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
+      }
+      
+
+      {
+        /* accept the abstract syntaxes for C-STORE, if presented */
+
+        std::vector<const char*> storageTransferSyntaxes;
+
+        // This is the list of the transfer syntaxes that were supported up to Orthanc 0.7.1
+        storageTransferSyntaxes.push_back(UID_LittleEndianExplicitTransferSyntax);
+        storageTransferSyntaxes.push_back(UID_BigEndianExplicitTransferSyntax);
+        storageTransferSyntaxes.push_back(UID_LittleEndianImplicitTransferSyntax);
+
+        // New transfer syntaxes supported since Orthanc 0.7.2
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Deflated))
+        {
+          storageTransferSyntaxes.push_back(UID_DeflatedExplicitVRLittleEndianTransferSyntax); 
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEGProcess1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess2_4TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess3_5TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess6_8TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess7_9TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess10_12TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess11_13TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess14TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess15TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess16_18TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess17_19TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess20_22TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess21_23TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess24_26TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess25_27TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess28TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess29TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGProcess14SV1TransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpeg2000))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000LosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_JpegLossless))
+        {
+          storageTransferSyntaxes.push_back(UID_JPEGLSLosslessTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPEGLSLossyTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Jpip))
+        {
+          storageTransferSyntaxes.push_back(UID_JPIPReferencedTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_JPIPReferencedDeflateTransferSyntax);
+        }
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg2))
+        {
+          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtMainLevelTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG2MainProfileAtHighLevelTransferSyntax);
+        }
+
+#if DCMTK_VERSION_NUMBER >= 361
+        // New in Orthanc 1.6.0
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Mpeg4))
+        {
+          storageTransferSyntaxes.push_back(UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_1TransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax);
+          storageTransferSyntaxes.push_back(UID_MPEG4StereoHighProfileLevel4_2TransferSyntax);
+        }
+#endif
+
+        if (!server.HasApplicationEntityFilter() ||
+            server.GetApplicationEntityFilter().IsAllowedTransferSyntax(remoteIp, remoteAet, calledAet, TransferSyntax_Rle))
+        {
+          storageTransferSyntaxes.push_back(UID_RLELosslessTransferSyntax);
+        }
+
+        /* the array of Storage SOP Class UIDs that is defined within "dcmdata/libsrc/dcuid.cc" */
+        size_t count = 0;
+        while (dcmAllStorageSOPClassUIDs[count] != NULL)
+        {
+          count++;
+        }
+        
+#if DCMTK_VERSION_NUMBER >= 362
+        // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
+        // only published if DCMTK >= 3.6.2:
+        // https://bitbucket.org/sjodogne/orthanc/issues/137
+        assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
+#endif
+      
+        if (!server.HasGetRequestHandlerFactory())    // dcmqrsrv.cc line 828
+        {
+          // This branch exactly corresponds to Orthanc <= 1.6.1 (in
+          // which C-GET SCP was not supported)
+          cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
+            assoc->params, dcmAllStorageSOPClassUIDs, count,
+            &storageTransferSyntaxes[0], storageTransferSyntaxes.size());
+          if (cond.bad())
+          {
+            LOG(INFO) << cond.text();
+            AssociationCleanup(assoc);
+            return NULL;
+          }
+        }
+        else                                         // see dcmqrsrv.cc lines 839 - 876
+        {
+          /* accept storage syntaxes with proposed role */
+          int npc = ASC_countPresentationContexts(assoc->params);
+          for (int i = 0; i < npc; i++)
+          {
+            T_ASC_PresentationContext pc;
+            ASC_getPresentationContext(assoc->params, i, &pc);
+            if (dcmIsaStorageSOPClassUID(pc.abstractSyntax))
+            {
+              /**
+               * We are prepared to accept whatever role the caller
+               * proposes.  Normally we can be the SCP of the Storage
+               * Service Class.  When processing the C-GET operation
+               * we can be the SCU of the Storage Service Class.
+               **/
+              const T_ASC_SC_ROLE role = pc.proposedRole;
+            
+              /**
+               * Accept in the order "least wanted" to "most wanted"
+               * transfer syntax.  Accepting a transfer syntax will
+               * override previously accepted transfer syntaxes.
+               **/
+              for (int k = static_cast<int>(storageTransferSyntaxes.size()) - 1; k >= 0; k--)
+              {
+                for (int j = 0; j < static_cast<int>(pc.transferSyntaxCount); j++)
+                {
+                  /**
+                   * If the transfer syntax was proposed then we can accept it
+                   * appears in our supported list of transfer syntaxes
+                   **/
+                  if (strcmp(pc.proposedTransferSyntaxes[j], storageTransferSyntaxes[k]) == 0)
+                  {
+                    cond = ASC_acceptPresentationContext(
+                      assoc->params, pc.presentationContextID, storageTransferSyntaxes[k], role);
+                    if (cond.bad())
+                    {
+                      LOG(INFO) << cond.text();
+                      AssociationCleanup(assoc);
+                      return NULL;
+                    }
+                  }
+                }
+              }
+            }
+          } /* for */
+        }
+
+        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, &storageTransferSyntaxes[0], storageTransferSyntaxes.size(), ASC_SC_ROLE_DEFAULT);
+          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 */
+#if DCMTK_VERSION_NUMBER >= 364
+      cond = ASC_getApplicationContextName(assoc->params, buf, sizeof(buf));
+#else
+      cond = ASC_getApplicationContextName(assoc->params, buf);
+#endif
+
+      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_GET_RQ:
+            request = DicomRequestType_Get;
+            supported = true;
+            break;
+
+          case DIMSE_C_FIND_RQ:
+            request = DicomRequestType_Find;
+            supported = true;
+            break;
+
+          case DIMSE_N_ACTION_RQ:
+            request = DicomRequestType_NAction;
+            supported = true;
+            break;
+
+          case DIMSE_N_EVENT_REPORT_RQ:
+            request = DicomRequestType_NEventReport;
+            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::unique_ptr<IStoreRequestHandler> handler
+                  (server_.GetStoreRequestHandlerFactory().ConstructStoreRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::storeScp(assoc_, &msg, presID, *handler, remoteIp_, associationTimeout_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Move:
+              if (server_.HasMoveRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr<IMoveRequestHandler> handler
+                  (server_.GetMoveRequestHandlerFactory().ConstructMoveRequestHandler());
+
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::moveScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+                }
+              }
+              break;
+              
+            case DicomRequestType_Get:
+              if (server_.HasGetRequestHandlerFactory()) // Should always be true
+              {
+                std::unique_ptr<IGetRequestHandler> handler
+                  (server_.GetGetRequestHandlerFactory().ConstructGetRequestHandler());
+                
+                if (handler.get() != NULL)
+                {
+                  cond = Internals::getScp(assoc_, &msg, presID, *handler, remoteIp_, remoteAet_, calledAet_, associationTimeout_);
+                }
+              }
+              break;
+
+            case DicomRequestType_Find:
+              if (server_.HasFindRequestHandlerFactory() || // Should always be true
+                  server_.HasWorklistRequestHandlerFactory())
+              {
+                std::unique_ptr<IFindRequestHandler> findHandler;
+                if (server_.HasFindRequestHandlerFactory())
+                {
+                  findHandler.reset(server_.GetFindRequestHandlerFactory().ConstructFindRequestHandler());
+                }
+
+                std::unique_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_, associationTimeout_);
+              }
+              break;
+
+            case DicomRequestType_NAction:
+              cond = NActionScp(&msg, presID);
+              break;              
+
+            case DicomRequestType_NEventReport:
+              cond = NEventReportScp(&msg, presID);
+              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;
+    }
+
+
+    static DcmDataset* ReadDataset(T_ASC_Association* assoc,
+                                   const char* errorMessage,
+                                   int timeout)
+    {
+      DcmDataset *tmp = NULL;
+      T_ASC_PresentationContextID presIdData;
+    
+      OFCondition cond = DIMSE_receiveDataSetInMemory(
+        assoc, (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout,
+        &presIdData, &tmp, NULL, NULL);
+      if (!cond.good() ||
+          tmp == NULL)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol, errorMessage);
+      }
+
+      return tmp;
+    }
+
+
+    static std::string ReadString(DcmDataset& dataset,
+                                  const DcmTagKey& tag)
+    {
+      const char* s = NULL;
+      if (!dataset.findAndGetString(tag, s).good() ||
+          s == NULL)
+      {
+        char buf[64];
+        sprintf(buf, "Missing mandatory tag in dataset: (%04X,%04X)",
+                tag.getGroup(), tag.getElement());
+        throw OrthancException(ErrorCode_NetworkProtocol, buf);
+      }
+
+      return std::string(s);
+    }
+
+
+    static void ReadSopSequence(
+      std::vector<std::string>& sopClassUids,
+      std::vector<std::string>& sopInstanceUids,
+      std::vector<StorageCommitmentFailureReason>* failureReasons, // Can be NULL
+      DcmDataset& dataset,
+      const DcmTagKey& tag,
+      bool mandatory)
+    {
+      sopClassUids.clear();
+      sopInstanceUids.clear();
+
+      if (failureReasons)
+      {
+        failureReasons->clear();
+      }
+
+      DcmSequenceOfItems* sequence = NULL;
+      if (!dataset.findAndGetSequence(tag, sequence).good() ||
+          sequence == NULL)
+      {
+        if (mandatory)
+        {        
+          char buf[64];
+          sprintf(buf, "Missing mandatory sequence in dataset: (%04X,%04X)",
+                  tag.getGroup(), tag.getElement());
+          throw OrthancException(ErrorCode_NetworkProtocol, buf);
+        }
+        else
+        {
+          return;
+        }
+      }
+
+      sopClassUids.reserve(sequence->card());
+      sopInstanceUids.reserve(sequence->card());
+
+      if (failureReasons)
+      {
+        failureReasons->reserve(sequence->card());
+      }
+
+      for (unsigned long i = 0; i < sequence->card(); i++)
+      {
+        const char* a = NULL;
+        const char* b = NULL;
+        if (!sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPClassUID, a).good() ||
+            !sequence->getItem(i)->findAndGetString(DCM_ReferencedSOPInstanceUID, b).good() ||
+            a == NULL ||
+            b == NULL)
+        {
+          throw OrthancException(ErrorCode_NetworkProtocol,
+                                 "Missing Referenced SOP Class/Instance UID "
+                                 "in storage commitment dataset");
+        }
+
+        sopClassUids.push_back(a);
+        sopInstanceUids.push_back(b);
+
+        if (failureReasons != NULL)
+        {
+          Uint16 reason;
+          if (!sequence->getItem(i)->findAndGetUint16(DCM_FailureReason, reason).good())
+          {
+            throw OrthancException(ErrorCode_NetworkProtocol,
+                                   "Missing Failure Reason (0008,1197) "
+                                   "in storage commitment dataset");
+          }
+
+          failureReasons->push_back(static_cast<StorageCommitmentFailureReason>(reason));
+        }
+      }
+    }
+
+    
+    OFCondition CommandDispatcher::NActionScp(T_DIMSE_Message* msg,
+                                              T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, only storage commitment is
+       * supported with DICOM N-ACTION. This corresponds to the case
+       * where "Action Type ID" equals "1".
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-4
+       **/
+      
+      if (msg->CommandField != DIMSE_N_ACTION_RQ /* value == 304 == 0x0130 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment request is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_ActionRQ& request = msg->msg.NActionRQ;
+
+      if (request.ActionTypeID != 1)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only storage commitment is implemented for DICOM N-ACTION SCP");
+      }
+
+      if (std::string(request.RequestedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(request.RequestedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (request.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment request without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-1. Storage Commitment Request - Action Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.2.html#table_J.3-1
+       **/
+      
+      std::unique_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-ACTION SCP", associationTimeout_));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> sopClassUid, sopInstanceUid;
+      ReadSopSequence(sopClassUid, sopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence, true /* mandatory */);
+
+      LOG(INFO) << "Incoming storage commitment request, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < sopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (" << (i + 1) << "/" << sopClassUid.size()
+                  << ") queried SOP Class/Instance UID: "
+                  << sopClassUid[i] << " / " << sopInstanceUid[i];
+      }
+
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+  
+      try
+      {
+        std::unique_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleRequest(transactionUid, sopClassUid, sopInstanceUid,
+                               remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing an incoming storage commitment request: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_ACTION_RSP;
+
+        T_DIMSE_N_ActionRSP& content = response.msg.NActionRSP;
+        content.MessageIDBeingRespondedTo = request.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.ActionTypeID = 0; // Not present, as "O_NACTION_ACTIONTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NACTION_AFFECTEDSOPCLASSUID | O_NACTION_AFFECTEDSOPINSTANCEUID;
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+
+
+    OFCondition CommandDispatcher::NEventReportScp(T_DIMSE_Message* msg,
+                                                   T_ASC_PresentationContextID presID)
+    {
+      /**
+       * Starting with Orthanc 1.6.0, handling N-EVENT-REPORT for
+       * storage commitment.
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#table_10.1-1
+       **/
+
+      if (msg->CommandField != DIMSE_N_EVENT_REPORT_RQ /* value == 256 == 0x0100 */ ||
+          !server_.HasStorageCommitmentRequestHandlerFactory())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+
+      /**
+       * Check that the storage commitment report is correctly formatted.
+       **/
+      
+      const T_DIMSE_N_EventReportRQ& report = msg->msg.NEventReportRQ;
+
+      if (report.EventTypeID != 1 /* successful */ &&
+          report.EventTypeID != 2 /* failures exist */)
+      {
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unknown event for DICOM N-EVENT-REPORT SCP");
+      }
+
+      if (std::string(report.AffectedSOPClassUID) != UID_StorageCommitmentPushModelSOPClass ||
+          std::string(report.AffectedSOPInstanceUID) != UID_StorageCommitmentPushModelSOPInstance)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Unexpected incoming SOP class or instance UID for storage commitment");
+      }
+
+      if (report.DataSetType != DIMSE_DATASET_PRESENT)
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Incoming storage commitment report without a dataset");
+      }
+
+
+      /**
+       * Extract the DICOM dataset that is associated with the DIMSE
+       * message. The content of this dataset is documented in "Table
+       * J.3-2. Storage Commitment Result - Event Information":
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html#table_J.3-2
+       **/
+      
+      std::unique_ptr<DcmDataset> dataset(
+        ReadDataset(assoc_, "Cannot read the dataset in N-EVENT-REPORT SCP", associationTimeout_));
+
+      std::string transactionUid = ReadString(*dataset, DCM_TransactionUID);
+
+      std::vector<std::string> successSopClassUid, successSopInstanceUid;
+      ReadSopSequence(successSopClassUid, successSopInstanceUid, NULL,
+                      *dataset, DCM_ReferencedSOPSequence,
+                      (report.EventTypeID == 1) /* mandatory in the case of success */);
+
+      std::vector<std::string> failedSopClassUid, failedSopInstanceUid;
+      std::vector<StorageCommitmentFailureReason> failureReasons;
+
+      if (report.EventTypeID == 2 /* failures exist */)
+      {
+        ReadSopSequence(failedSopClassUid, failedSopInstanceUid, &failureReasons,
+                        *dataset, DCM_FailedSOPSequence, true);
+      }
+
+      LOG(INFO) << "Incoming storage commitment report, with transaction UID: " << transactionUid;
+
+      for (size_t i = 0; i < successSopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (success " << (i + 1) << "/" << successSopClassUid.size()
+                  << ") SOP Class/Instance UID: "
+                  << successSopClassUid[i] << " / " << successSopInstanceUid[i];
+      }
+
+      for (size_t i = 0; i < failedSopClassUid.size(); i++)
+      {
+        LOG(INFO) << "  (failure " << (i + 1) << "/" << failedSopClassUid.size()
+                  << ") SOP Class/Instance UID: "
+                  << failedSopClassUid[i] << " / " << failedSopInstanceUid[i];
+      }
+
+      /**
+       * Call the Orthanc handler. The list of available DIMSE status
+       * codes can be found at:
+       * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part07/chapter_10.html#sect_10.1.4.1.10
+       **/
+
+      DIC_US dimseStatus;
+
+      try
+      {
+        std::unique_ptr<IStorageCommitmentRequestHandler> handler
+          (server_.GetStorageCommitmentRequestHandlerFactory().
+           ConstructStorageCommitmentRequestHandler());
+
+        handler->HandleReport(transactionUid, successSopClassUid, successSopInstanceUid,
+                              failedSopClassUid, failedSopInstanceUid, failureReasons,
+                              remoteIp_, remoteAet_, calledAet_);
+        
+        dimseStatus = 0;  // Success
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Error while processing an incoming storage commitment report: " << e.What();
+
+        // Code 0x0110 - "General failure in processing the operation was encountered"
+        dimseStatus = STATUS_N_ProcessingFailure;
+      }
+
+      
+      /**
+       * Send the DIMSE status back to the SCU.
+       **/
+
+      {
+        T_DIMSE_Message response;
+        memset(&response, 0, sizeof(response));
+        response.CommandField = DIMSE_N_EVENT_REPORT_RSP;
+
+        T_DIMSE_N_EventReportRSP& content = response.msg.NEventReportRSP;
+        content.MessageIDBeingRespondedTo = report.MessageID;
+        strncpy(content.AffectedSOPClassUID, UID_StorageCommitmentPushModelSOPClass, DIC_UI_LEN);
+        content.DimseStatus = dimseStatus;
+        strncpy(content.AffectedSOPInstanceUID, UID_StorageCommitmentPushModelSOPInstance, DIC_UI_LEN);
+        content.EventTypeID = 0; // Not present, as "O_NEVENTREPORT_EVENTTYPEID" not set in "opts"
+        content.DataSetType = DIMSE_DATASET_NULL;  // Dataset is absent in storage commitment response
+        content.opts = O_NEVENTREPORT_AFFECTEDSOPCLASSUID | O_NEVENTREPORT_AFFECTEDSOPINSTANCEUID;
+
+        return DIMSE_sendMessageUsingMemoryData(
+          assoc_, presID, &response, NULL /* no dataset */, NULL /* dataObject */,
+          NULL /* callback */, NULL /* callback context */, NULL /* commandSet */);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/CommandDispatcher.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_;
+
+      OFCondition NActionScp(T_DIMSE_Message* msg, 
+                             T_ASC_PresentationContextID presID);
+
+      OFCondition NEventReportScp(T_DIMSE_Message* msg, 
+                                  T_ASC_PresentationContextID presID);
+      
+    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();
+    };
+
+    CommandDispatcher* AcceptAssociation(const DicomServer& server, 
+                                         T_ASC_Network *net);
+
+    OFCondition EchoScp(T_ASC_Association* assoc, 
+                        T_DIMSE_Message* msg, 
+                        T_ASC_PresentationContextID presID);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../DicomFormat/DicomArray.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);
+    }
+
+
+    static void FixFindQuery(DicomMap& target,
+                             const DicomMap& source)
+    {
+      // "The definition of a Data Set in PS3.5 specifically excludes
+      // the range of groups below group 0008, and this includes in
+      // particular Meta Information Header elements such as Transfer
+      // Syntax UID (0002,0010)."
+      // http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#sect_C.4.1.1.3
+      // https://groups.google.com/d/msg/orthanc-users/D3kpPuX8yV0/_zgHOzkMEQAJ
+
+      DicomArray a(source);
+
+      for (size_t i = 0; i < a.GetSize(); i++)
+      {
+        if (a.GetElement(i).GetTag().GetGroup() >= 0x0008)
+        {
+          target.SetValue(a.GetElement(i).GetTag(), a.GetElement(i).GetValue());
+        }
+      }
+    }
+
+
+
+    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_))
+          {
+            throw OrthancException(ErrorCode_UnknownModality,
+                                   "Modality with AET \"" + (*data.remoteAet_) +
+                                   "\" is not defined in the \"DicomModalities\" configuration option");
+          }
+
+          
+          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);
+
+              DicomMap filtered;
+              FixFindQuery(filtered, input);
+
+              data.findHandler_->Handle(data.answers_, filtered, 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,
+                                 int timeout)
+  {
+    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*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                          /*opt_dimse_timeout*/ timeout);
+
+    // 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/OrthancFramework/Sources/DicomNetworking/Internals/FindScp.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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,
+                        int timeout);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,289 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <dcmtk/dcmnet/diutil.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include "GetScp.h"
+
+#include <memory>
+
+#include "../../DicomParsing/FromDcmtkBridge.h"
+#include "../../DicomParsing/ToDcmtkBridge.h"
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+#include <boost/lexical_cast.hpp>
+
+
+namespace Orthanc
+{
+  namespace
+  {
+    struct GetScpData
+    {
+      //  Handle returns void.
+      IGetRequestHandler* handler_;
+      DcmDataset* lastRequest_;
+      T_ASC_Association * assoc_;
+
+      std::string remoteIp_;
+      std::string remoteAet_;
+      std::string calledAet_;
+      int timeout_;
+
+      GetScpData()
+      {
+        handler_ = NULL;
+        lastRequest_ = NULL;
+        assoc_ = NULL;
+      };
+    };
+      
+    static DcmDataset *BuildFailedInstanceList(const std::string& failedUIDs)
+    {
+      if (failedUIDs.empty())
+      {
+        return NULL;
+      }
+      else
+      {
+        std::unique_ptr<DcmDataset> rspIds(new DcmDataset());
+        
+        if (!DU_putStringDOElement(rspIds.get(), DCM_FailedSOPInstanceUIDList, failedUIDs.c_str()))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "getSCP: failed to build DCM_FailedSOPInstanceUIDList");
+        }
+
+        return rspIds.release();
+      }
+    }
+
+    static void GetScpCallback(
+      /* in */ 
+      void *callbackData,  
+      OFBool cancelled, 
+      T_DIMSE_C_GetRQ *request, 
+      DcmDataset *requestIdentifiers, 
+      int responseCount,
+      /* out */
+      T_DIMSE_C_GetRSP *response,
+      DcmDataset **responseIdentifiers,
+      DcmDataset **statusDetail)
+    {
+      bzero(response, sizeof(T_DIMSE_C_GetRSP));
+      *statusDetail = NULL;
+      *responseIdentifiers = NULL;   
+
+      GetScpData& data = *reinterpret_cast<GetScpData*>(callbackData);
+      if (data.lastRequest_ == NULL)
+      {
+        DicomMap input;
+        FromDcmtkBridge::ExtractDicomSummary(input, *requestIdentifiers);
+
+        try
+        {
+          if (!data.handler_->Handle(
+                input, data.remoteIp_, data.remoteAet_, data.calledAet_,
+                data.timeout_ < 0 ? 0 : static_cast<uint32_t>(data.timeout_)))
+          {
+            response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+            return;
+          }
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          LOG(ERROR) << "IGetRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+          return;
+        }
+
+        data.lastRequest_ = requestIdentifiers;
+      }
+      else if (data.lastRequest_ != requestIdentifiers)
+      {
+        // Internal error!
+        LOG(ERROR) << "IGetRequestHandler Failed: Internal error lastRequestIdentifier";
+        response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+        return;
+      }
+
+      if (data.handler_->GetRemainingCount() == 0)
+      {
+        response->DimseStatus = STATUS_Success;
+      }
+      else
+      {
+        IGetRequestHandler::Status status;
+
+        try
+        {
+          status = data.handler_->DoNext(data.assoc_);
+        }
+        catch (OrthancException& e)
+        {
+          // Internal error!
+          LOG(ERROR) << "IGetRequestHandler Failed: " << e.What();
+          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+          return;
+        }
+        
+        if (status == STATUS_Success)
+        {
+          if (responseCount < static_cast<int>(data.handler_->GetRemainingCount()))
+          {
+            response->DimseStatus = STATUS_Pending;
+          }
+          else
+          {
+            response->DimseStatus = STATUS_Success;
+          }
+        }
+        else
+        {
+          response->DimseStatus = STATUS_GET_Failed_UnableToProcess;
+          
+          if (data.handler_->GetFailedCount() > 0 ||
+              data.handler_->GetWarningCount() > 0) 
+          {
+            response->DimseStatus = STATUS_GET_Warning_SubOperationsCompleteOneOrMoreFailures;
+          }
+          
+          /*
+           * if all the sub-operations failed then we need to generate
+           * a failed or refused status.  cf. DICOM part 4, C.4.3.3.1
+           * we choose to generate a "Refused - Out of Resources -
+           * Unable to perform suboperations" status.
+           */
+          if ((data.handler_->GetFailedCount() > 0) &&
+              ((data.handler_->GetCompletedCount() +
+                data.handler_->GetWarningCount()) == 0)) 
+          {
+            response->DimseStatus = STATUS_GET_Refused_OutOfResourcesSubOperations;
+          }
+          
+          *responseIdentifiers = BuildFailedInstanceList(data.handler_->GetFailedUids());
+        }
+      }
+      
+      response->NumberOfRemainingSubOperations = data.handler_->GetRemainingCount();
+      response->NumberOfCompletedSubOperations = data.handler_->GetCompletedCount();
+      response->NumberOfFailedSubOperations = data.handler_->GetFailedCount();
+      response->NumberOfWarningSubOperations = data.handler_->GetWarningCount();
+    }
+  }
+
+  OFCondition Internals::getScp(T_ASC_Association * assoc,
+                                T_DIMSE_Message * msg, 
+                                T_ASC_PresentationContextID presID,
+                                IGetRequestHandler& handler,
+                                std::string remoteIp,
+                                std::string remoteAet,
+                                std::string calledAet,
+                                int timeout)
+  {
+    GetScpData data;
+    data.lastRequest_ = NULL;
+    data.handler_ = &handler;
+    data.assoc_ = assoc;
+    data.remoteIp_ = remoteIp;
+    data.remoteAet_ = remoteAet;
+    data.calledAet_ = calledAet;
+    data.timeout_ = timeout;
+
+    OFCondition cond = DIMSE_getProvider(assoc, presID, &msg->msg.CGetRQ, 
+                                         GetScpCallback, &data,
+                                         /*opt_blockMode*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                         /*opt_dimse_timeout*/ timeout);
+    
+    // if some error occured, dump corresponding information and remove the outfile if necessary
+    if (cond.bad())
+    {
+      OFString temp_str;
+      LOG(ERROR) << "Get SCP Failed: " << cond.text();
+    }
+
+    return cond;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/GetScp.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../IGetRequestHandler.h"
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    OFCondition getScp(T_ASC_Association * assoc,
+                       T_DIMSE_Message * msg, 
+                       T_ASC_PresentationContextID presID,
+                       IGetRequestHandler& handler,
+                       std::string remoteIp,
+                       std::string remoteAet,
+                       std::string calledAet,
+                       int timeout);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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::unique_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,
+                                 int timeout)
+  {
+    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*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                                          /*opt_dimse_timeout*/ timeout);
+
+    // 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/OrthancFramework/Sources/DicomNetworking/Internals/MoveScp.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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,
+                        int timeout);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,311 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#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 DCMTK_VERSION_NUMBER >= 364
+	    if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sizeof(sopClass),
+						     sopInstance, sizeof(sopInstance), /*opt_correctUIDPadding*/ OFFalse))
+#else
+            if (!DU_findSOPClassAndInstanceInDataSet(*imageDataSet, sopClass, sopInstance, /*opt_correctUIDPadding*/ OFFalse))
+#endif
+            {
+		//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,
+                                  int timeout)
+  {
+    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*/ (timeout ? DIMSE_NONBLOCKING : DIMSE_BLOCKING),
+                               /*opt_dimse_timeout*/ timeout);
+
+    // 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/OrthancFramework/Sources/DicomNetworking/Internals/StoreScp.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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,
+                         int timeout);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/NetworkingCompatibility.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+
+#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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,378 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_ALLOW_N_ACTION = "AllowNAction";
+static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport";
+static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment";
+static const char* KEY_ALLOW_TRANSCODING = "AllowTranscoding";
+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;
+    allowNAction_ = true;  // For storage commitment
+    allowNEventReport_ = true;  // For storage commitment
+    allowTranscoding_ = 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)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "A TCP port number must be in range [1..65534], but found: " +
+                             boost::lexical_cast<std::string>(value));
+    }
+  }
+
+
+  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);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_N_ACTION))
+    {
+      allowNAction_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_ACTION);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_N_EVENT_REPORT))
+    {
+      allowNEventReport_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_N_EVENT_REPORT);
+    }
+
+    if (serialized.isMember(KEY_ALLOW_STORAGE_COMMITMENT))
+    {
+      bool allow = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_STORAGE_COMMITMENT);
+      allowNAction_ = allow;
+      allowNEventReport_ = allow;
+    }
+
+    if (serialized.isMember(KEY_ALLOW_TRANSCODING))
+    {
+      allowTranscoding_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_TRANSCODING);
+    }
+  }
+
+
+  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_;
+
+      case DicomRequestType_NAction:
+        return allowNAction_;
+
+      case DicomRequestType_NEventReport:
+        return allowNEventReport_;
+
+      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;
+
+      case DicomRequestType_NAction:
+        allowNAction_ = allowed;
+        break;
+
+      case DicomRequestType_NEventReport:
+        allowNEventReport_ = allowed;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool RemoteModalityParameters::IsAdvancedFormatNeeded() const
+  {
+    return (!allowEcho_ ||
+            !allowStore_ ||
+            !allowFind_ ||
+            !allowGet_ ||
+            !allowMove_ ||
+            !allowNAction_ ||
+            !allowNEventReport_ ||
+            !allowTranscoding_);
+  }
+
+  
+  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_;
+      target[KEY_ALLOW_N_ACTION] = allowNAction_;
+      target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_;
+      target[KEY_ALLOW_TRANSCODING] = allowTranscoding_;
+    }
+    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/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,146 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_;
+    bool                  allowNAction_;
+    bool                  allowNEventReport_;
+    bool                  allowTranscoding_;
+    
+    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;
+
+    bool IsTranscodingAllowed() const
+    {
+      return allowTranscoding_;
+    }
+
+    void SetTranscodingAllowed(bool allowed)
+    {
+      allowTranscoding_ = allowed;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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();
+  }
+
+
+  TimeoutDicomConnectionManager::Lock::Lock(TimeoutDicomConnectionManager& that,
+                                            const std::string& localAet,
+                                            const RemoteModalityParameters& remote) : 
+    that_(that),
+    lock_(that_.mutex_)
+  {
+    // Calling "Touch()" will be done by the "~Lock()" destructor
+    that_.OpenInternal(localAet, remote);
+  }
+
+  
+  TimeoutDicomConnectionManager::Lock::~Lock()
+  {
+    that_.TouchInternal();
+  }
+
+  
+  DicomStoreUserConnection& TimeoutDicomConnectionManager::Lock::GetConnection()
+  {
+    if (that_.connection_.get() == NULL)
+    {
+      // The allocation should have been done by "that_.Open()" in the constructor
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return *that_.connection_;
+    }
+  }
+
+
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::TouchInternal()
+  {
+    lastUse_ = GetNow();
+  }
+
+
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::OpenInternal(const std::string& localAet,
+                                                   const RemoteModalityParameters& remote)
+  {
+    DicomAssociationParameters other(localAet, remote);
+    
+    if (connection_.get() == NULL ||
+        !connection_->GetParameters().IsEqual(other))
+    {
+      connection_.reset(new DicomStoreUserConnection(other));
+    }
+  }
+
+
+  // Mutex must be locked
+  void TimeoutDicomConnectionManager::CloseInternal()
+  {
+    if (connection_.get() != NULL)
+    {
+      LOG(INFO) << "Closing inactive DICOM association with modality: "
+                << connection_->GetParameters().GetRemoteModality().GetApplicationEntityTitle();
+
+      connection_.reset(NULL);
+    }
+  }
+
+
+  void TimeoutDicomConnectionManager::SetInactivityTimeout(unsigned int milliseconds)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    timeout_ = boost::posix_time::milliseconds(milliseconds);
+    CloseInternal();
+  }
+
+
+  unsigned int TimeoutDicomConnectionManager::GetInactivityTimeout()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return static_cast<unsigned int>(timeout_.total_milliseconds());
+  }
+
+
+  void TimeoutDicomConnectionManager::CloseIfInactive()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (connection_.get() != NULL &&
+        (GetNow() - lastUse_) >= timeout_)
+    {
+      CloseInternal();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomNetworking/TimeoutDicomConnectionManager.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 != 1
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be 1 to use this file
+#endif
+
+
+#include "../Compatibility.h"
+#include "DicomStoreUserConnection.h"
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/thread/mutex.hpp>
+
+namespace Orthanc
+{
+  /**
+   * This class corresponds to a singleton to a DICOM SCU connection.
+   **/
+  class TimeoutDicomConnectionManager : public boost::noncopyable
+  {
+  private:
+    boost::mutex                               mutex_;
+    std::unique_ptr<DicomStoreUserConnection>  connection_;
+    boost::posix_time::ptime                   lastUse_;
+    boost::posix_time::time_duration           timeout_;
+
+    // Mutex must be locked
+    void TouchInternal();
+
+    // Mutex must be locked
+    void OpenInternal(const std::string& localAet,
+                      const RemoteModalityParameters& remote);
+
+    // Mutex must be locked
+    void CloseInternal();
+
+  public:
+    class Lock : public boost::noncopyable
+    {
+    private:
+      TimeoutDicomConnectionManager&  that_;
+      boost::mutex::scoped_lock       lock_;
+
+    public:
+      Lock(TimeoutDicomConnectionManager& that,
+           const std::string& localAet,
+           const RemoteModalityParameters& remote);
+      
+      ~Lock();
+
+      DicomStoreUserConnection& GetConnection();
+    };
+
+    TimeoutDicomConnectionManager() :
+      timeout_(boost::posix_time::milliseconds(1000))
+    {
+    }
+
+    void SetInactivityTimeout(unsigned int milliseconds);
+
+    unsigned int GetInactivityTimeout();  // In milliseconds
+
+    void Close();
+
+    void CloseIfInactive();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,352 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DcmtkTranscoder.h"
+
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG)
+#  error Macro ORTHANC_ENABLE_DCMTK_JPEG must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS)
+#  error Macro ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS must be defined
+#endif
+
+
+#include "FromDcmtkBridge.h"
+#include "../OrthancException.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmjpeg/djrploss.h>  // for DJ_RPLossy
+#include <dcmtk/dcmjpeg/djrplol.h>   // for DJ_RPLossless
+#include <dcmtk/dcmjpls/djrparam.h>  // for DJLSRepresentationParameter
+
+
+namespace Orthanc
+{
+  static bool GetBitsStored(uint16_t& bitsStored,
+                            DcmDataset& dataset)
+  {
+    return dataset.findAndGetUint16(DCM_BitsStored, bitsStored).good();
+  }
+
+  
+  void DcmtkTranscoder::SetLossyQuality(unsigned int quality)
+  {
+    if (quality <= 0 ||
+        quality > 100)
+    {
+      throw OrthancException(
+        ErrorCode_ParameterOutOfRange,
+        "The quality for lossy transcoding must be an integer between 1 and 100, received: " +
+        boost::lexical_cast<std::string>(quality));
+    }
+    else
+    {
+      LOG(INFO) << "Quality for lossy transcoding using DCMTK is set to: " << quality;
+      lossyQuality_ = quality;
+    }
+  }
+
+    
+  bool DcmtkTranscoder::InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                                         DcmFileFormat& dicom,
+                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid) 
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    DicomTransferSyntax syntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, dicom))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot determine the transfer syntax");
+    }
+
+    uint16_t bitsStored;
+    bool hasBitsStored = GetBitsStored(bitsStored, *dicom.getDataset());
+    
+    std::string sourceSopInstanceUid = IDicomTranscoder::GetSopInstanceUid(dicom);
+    
+    if (allowedSyntaxes.find(syntax) != allowedSyntaxes.end())
+    {
+      // No transcoding is needed
+      return true;
+    }
+      
+    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianImplicit) != allowedSyntaxes.end() &&
+        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
+    {
+      selectedSyntax = DicomTransferSyntax_LittleEndianImplicit;
+      return true;
+    }
+
+    if (allowedSyntaxes.find(DicomTransferSyntax_LittleEndianExplicit) != allowedSyntaxes.end() &&
+        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
+    {
+      selectedSyntax = DicomTransferSyntax_LittleEndianExplicit;
+      return true;
+    }
+      
+    if (allowedSyntaxes.find(DicomTransferSyntax_BigEndianExplicit) != allowedSyntaxes.end() &&
+        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
+    {
+      selectedSyntax = DicomTransferSyntax_BigEndianExplicit;
+      return true;
+    }
+
+    if (allowedSyntaxes.find(DicomTransferSyntax_DeflatedLittleEndianExplicit) != allowedSyntaxes.end() &&
+        FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
+    {
+      selectedSyntax = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+      return true;
+    }
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess1) != allowedSyntaxes.end() &&
+        allowNewSopInstanceUid &&
+        (!hasBitsStored || bitsStored == 8))
+    {
+      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+      DJ_RPLossy parameters(lossyQuality_);
+        
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess1, &parameters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGProcess1;
+        return true;
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess2_4) != allowedSyntaxes.end() &&
+        allowNewSopInstanceUid &&
+        (!hasBitsStored || bitsStored <= 12))
+    {
+      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+      DJ_RPLossy parameters(lossyQuality_);
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess2_4, &parameters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGProcess2_4;
+        return true;
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+      DJ_RPLossless parameters(6 /* opt_selection_value */,
+                               0 /* opt_point_transform */);
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14, &parameters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGProcess14;
+        return true;
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGProcess14SV1) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpeg/apps/dcmcjpeg.cc"
+      DJ_RPLossless parameters(6 /* opt_selection_value */,
+                               0 /* opt_point_transform */);
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGProcess14SV1, &parameters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGProcess14SV1;
+        return true;
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    if (allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossless) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpls/apps/dcmcjpls.cc"
+      DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */,
+                                             OFTrue /* opt_useLosslessProcess */);
+
+      /**
+       * WARNING: This call results in a segmentation fault if using
+       * the DCMTK package 3.6.2 from Ubuntu 18.04.
+       **/              
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossless, &parameters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGLSLossless;
+        return true;
+      }
+    }
+#endif
+      
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    if (allowNewSopInstanceUid &&
+        allowedSyntaxes.find(DicomTransferSyntax_JPEGLSLossy) != allowedSyntaxes.end())
+    {
+      // Check out "dcmjpls/apps/dcmcjpls.cc"
+      DJLSRepresentationParameter parameters(2 /* opt_nearlossless_deviation */,
+                                             OFFalse /* opt_useLosslessProcess */);
+
+      /**
+       * WARNING: This call results in a segmentation fault if using
+       * the DCMTK package 3.6.2 from Ubuntu 18.04.
+       **/              
+      if (FromDcmtkBridge::Transcode(dicom, DicomTransferSyntax_JPEGLSLossy, &parameters))
+      {
+        selectedSyntax = DicomTransferSyntax_JPEGLSLossy;
+        return true;
+      }
+    }
+#endif
+
+    return false;
+  }
+
+    
+  bool DcmtkTranscoder::IsSupported(DicomTransferSyntax syntax)
+  {
+    if (syntax == DicomTransferSyntax_LittleEndianImplicit ||
+        syntax == DicomTransferSyntax_LittleEndianExplicit ||
+        syntax == DicomTransferSyntax_BigEndianExplicit ||
+        syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit)
+    {
+      return true;
+    }
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    if (syntax == DicomTransferSyntax_JPEGProcess1 ||
+        syntax == DicomTransferSyntax_JPEGProcess2_4 ||
+        syntax == DicomTransferSyntax_JPEGProcess14 ||
+        syntax == DicomTransferSyntax_JPEGProcess14SV1)
+    {
+      return true;
+    }
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    if (syntax == DicomTransferSyntax_JPEGLSLossless ||
+        syntax == DicomTransferSyntax_JPEGLSLossy)
+    {
+      return true;
+    }
+#endif
+    
+    return false;
+  }
+
+
+  bool DcmtkTranscoder::Transcode(DicomImage& target,
+                                  DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                  const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                  bool allowNewSopInstanceUid)
+  {
+    target.Clear();
+    
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
+    {
+      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
+      return false;
+    }
+
+    {
+      std::string s;
+      for (std::set<DicomTransferSyntax>::const_iterator
+             it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
+      {
+        if (!s.empty())
+        {
+          s += ", ";
+        }
+
+        s += GetTransferSyntaxUid(*it);
+      }
+
+      if (s.empty())
+      {
+        s = "<none>";
+      }
+      
+      LOG(INFO) << "DCMTK transcoding from " << GetTransferSyntaxUid(sourceSyntax)
+                << " to one of: " << s;
+    }
+
+#if !defined(NDEBUG)
+    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
+#endif
+
+    DicomTransferSyntax targetSyntax;
+    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
+    {
+      // No transcoding is needed
+      target.AcquireParsed(source);
+      target.AcquireBuffer(source);
+      return true;
+    }
+    else if (InplaceTranscode(targetSyntax, source.GetParsed(),
+                              allowedSyntaxes, allowNewSopInstanceUid))
+    {   
+      // Sanity check
+      DicomTransferSyntax targetSyntax2;
+      if (FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax2, source.GetParsed()) &&
+          targetSyntax == targetSyntax2 &&
+          allowedSyntaxes.find(targetSyntax2) != allowedSyntaxes.end())
+      {
+        target.AcquireParsed(source);
+        source.Clear();
+        
+#if !defined(NDEBUG)
+        // Only run the sanity check in debug mode
+        CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                         allowedSyntaxes, allowNewSopInstanceUid);
+#endif
+        
+        return true;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }  
+    }
+    else
+    {
+      // Cannot transcode
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DcmtkTranscoder.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_TRANSCODING)
+#  error Macro ORTHANC_ENABLE_DCMTK_TRANSCODING must be defined to use this file
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING != 1
+#  error Transcoding is disabled, cannot compile this file
+#endif
+
+#include "IDicomTranscoder.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC DcmtkTranscoder : public IDicomTranscoder
+  {
+  private:
+    unsigned int  lossyQuality_;
+    
+    bool InplaceTranscode(DicomTransferSyntax& selectedSyntax /* out */,
+                          DcmFileFormat& dicom,
+                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                          bool allowNewSopInstanceUid);
+    
+  public:
+    DcmtkTranscoder() :
+      lossyQuality_(90)
+    {
+    }
+
+    void SetLossyQuality(unsigned int quality);
+
+    unsigned int GetLossyQuality() const
+    {
+      return lossyQuality_;
+    }
+    
+    static bool IsSupported(DicomTransferSyntax syntax);
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DicomDirWriter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,602 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.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::unique_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,
+                                bool hasCodeExtensions,
+                                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, hasCodeExtensions);
+          }
+          
+          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,
+                           bool hasCodeExtensions,
+                           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, hasCodeExtensions, 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,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, false, false);
+    }
+
+    static void CopyStringType1C(DcmDirectoryRecord& target,
+                                 DcmDataset& source,
+                                 Encoding encoding,
+                                 bool hasCodeExtensions,
+                                 const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, true, false);
+    }
+
+    static void CopyStringType2(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, key, false, true);
+    }
+
+    static void CopyStringType3(DcmDirectoryRecord& target,
+                                DcmDataset& source,
+                                Encoding encoding,
+                                bool hasCodeExtensions,
+                                const DcmTagKey& key)
+    {
+      CopyString(target, source, encoding, hasCodeExtensions, 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,
+                     bool hasCodeExtensions)
+    {
+      // cf. "DicomDirInterface::buildPatientRecord()"
+
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_PatientID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_PatientName);
+    }
+
+    void FillStudy(DcmDirectoryRecord& record,
+                   DcmDataset& dicom,
+                   Encoding encoding,
+                   bool hasCodeExtensions)
+    {
+      // cf. "DicomDirInterface::buildStudyRecord()"
+
+      std::string nowDate, nowTime;
+      SystemToolbox::GetNowDicom(nowDate, nowTime, utc_);
+
+      std::string studyDate;
+      if (!GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_StudyDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_SeriesDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_AcquisitionDate) &&
+          !GetUtf8TagValue(studyDate, dicom, encoding, hasCodeExtensions, DCM_ContentDate))
+      {
+        studyDate = nowDate;
+      }
+          
+      std::string studyTime;
+      if (!GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_StudyTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_SeriesTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, DCM_AcquisitionTime) &&
+          !GetUtf8TagValue(studyTime, dicom, encoding, hasCodeExtensions, 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, hasCodeExtensions, DCM_StudyDescription);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, DCM_StudyID);
+      CopyStringType2(record, dicom, encoding, hasCodeExtensions, DCM_AccessionNumber);
+    }
+
+    void FillSeries(DcmDirectoryRecord& record,
+                    DcmDataset& dicom,
+                    Encoding encoding,
+                    bool hasCodeExtensions)
+    {
+      // cf. "DicomDirInterface::buildSeriesRecord()"
+
+      /* copy attribute values from dataset to series record */
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_Modality);
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
+      /* use type 1C instead of 1 in order to avoid unwanted overwriting */
+      CopyStringType1C(record, dicom, encoding, hasCodeExtensions, 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, hasCodeExtensions, DCM_SeriesDescription);
+      }
+    }
+
+    void FillInstance(DcmDirectoryRecord& record,
+                      DcmDataset& dicom,
+                      Encoding encoding,
+                      bool hasCodeExtensions,
+                      DcmMetaInfo& metaInfo,
+                      const char* path)
+    {
+      // cf. "DicomDirInterface::buildImageRecord()"
+
+      /* copy attribute values from dataset to image record */
+      CopyStringType1(record, dicom, encoding, hasCodeExtensions, DCM_InstanceNumber);
+      //CopyElementType1C(record, dicom, encoding, hasCodeExtensions, DCM_ImageType);
+
+      // REMOVED since 0.9.7: copyElementType1C(dicom, DCM_ReferencedImageSequence, record);
+
+      std::string sopClassUid, sopInstanceUid, transferSyntaxUid;
+      if (!GetUtf8TagValue(sopClassUid, dicom, encoding, hasCodeExtensions, DCM_SOPClassUID) ||
+          !GetUtf8TagValue(sopInstanceUid, dicom, encoding, hasCodeExtensions, DCM_SOPInstanceUID) ||
+          !GetUtf8TagValue(transferSyntaxUid, metaInfo, encoding, hasCodeExtensions, 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();
+
+      bool hasCodeExtensions;
+      Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+
+      bool found;
+      std::string id;
+      E_DirRecType type;
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          if (!GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_PatientID))
+          {
+            // Be tolerant about missing patient ID. Fixes issue #124
+            // (GET /studies/ID/media fails for certain dicom file).
+            id = "";
+          }
+
+          found = true;
+          type = ERT_Patient;
+          break;
+
+        case ResourceType_Study:
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_StudyInstanceUID);
+          type = ERT_Study;
+          break;
+
+        case ResourceType_Series:
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, DCM_SeriesInstanceUID);
+          type = ERT_Series;
+          break;
+
+        case ResourceType_Instance:
+          found = GetUtf8TagValue(id, dataset, encoding, hasCodeExtensions, 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::unique_ptr<DcmDirectoryRecord> record(new DcmDirectoryRecord(type, NULL, filename));
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          FillPatient(*record, dataset, encoding, hasCodeExtensions);
+          break;
+
+        case ResourceType_Study:
+          FillStudy(*record, dataset, encoding, hasCodeExtensions);
+          break;
+
+        case ResourceType_Series:
+          FillSeries(*record, dataset, encoding, hasCodeExtensions);
+          break;
+
+        case ResourceType_Instance:
+          FillInstance(*record, dataset, encoding, hasCodeExtensions, *dicom.GetDcmtkObject().getMetaInfo(), path);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CopyStringType1C(*record, dataset, encoding, hasCodeExtensions, 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/OrthancFramework/Sources/DicomParsing/DicomDirWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomParsing/DicomModification.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1522 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+#include "FromDcmtkBridge.h"
+#include "ITagVisitor.h"
+
+#include <memory>   // For std::unique_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 VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr)
+    {
+    }
+
+    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag)
+    {
+    }
+
+    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 VisitIntegers(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::vector<int64_t>& values)
+    {
+    }
+
+    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              const std::vector<double>& value)
+    {
+    }
+
+    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 const std::vector<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 (parentTags.size() == 2 &&
+               parentTags[0] == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE &&
+               parentTags[1] == DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE &&
+               tag == DICOM_TAG_REFERENCED_SOP_INSTANCE_UID)
+      {
+        // in RT-STRUCT, this ReferencedSOPInstanceUID is actually referencing a StudyInstanceUID !!
+        // (observed in many data sets including: https://wiki.cancerimagingarchive.net/display/Public/Lung+CT+Segmentation+Challenge+2017)
+        // tested in test_anonymize_relationships_5
+        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Study);
+        return Action_Replace;
+      }
+      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 if (parentTags.size() == 3 &&
+               parentTags[0] == DICOM_TAG_REFERENCED_FRAME_OF_REFERENCE_SEQUENCE &&
+               parentTags[1] == DICOM_TAG_RT_REFERENCED_STUDY_SEQUENCE &&
+               parentTags[2] == DICOM_TAG_RT_REFERENCED_SERIES_SEQUENCE &&
+               tag == DICOM_TAG_SERIES_INSTANCE_UID)
+      {
+        newValue = that_.MapDicomIdentifier(Toolbox::StripSpaces(value), ResourceType_Series);
+        return Action_Replace;
+      }
+      else if (parentTags.size() == 1 &&
+               parentTags[0] == 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);
+    }
+  }
+
+  void DicomModification::RegisterMappedDicomIdentifier(const std::string& original,
+                                                        const std::string& mapped,
+                                                        ResourceType level)
+  {
+    UidMap::const_iterator previous = uidMap_.find(std::make_pair(level, original));
+
+    if (previous == uidMap_.end())
+    {
+      uidMap_.insert(std::make_pair(std::make_pair(level, original), mapped));
+    }
+  }
+
+  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_))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Unable to generate an anonymized ID");
+        }
+      }
+
+      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::unique_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, privateCreator_);
+  }
+
+  
+  DicomModification::DicomModification() :
+    removePrivateTags_(false),
+    level_(ResourceType_Instance),
+    allowManualIdentifiers_(true),
+    keepStudyInstanceUid_(false),
+    keepSeriesInstanceUid_(false),
+    keepSopInstanceUid_(false),
+    updateReferencedRelationships_(true),
+    isAnonymization_(false),
+    //privateCreator_("PrivateCreator"),
+    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 == DICOM_TAG_SOP_INSTANCE_UID)
+    {
+      keepSopInstanceUid_ = 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))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a patient, her PatientID is required to be modified");
+    }
+
+    if (!allowManualIdentifiers_)
+    {
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the StudyInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the SeriesInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Patient && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a patient, the SopInstanceUID cannot be manually modified");
+      }
+    }
+
+
+    // Sanity checks at the study level
+    if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a study, the parent PatientID cannot be manually modified");
+    }
+
+    if (!allowManualIdentifiers_)
+    {
+      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the SeriesInstanceUID cannot be manually modified");
+      }
+
+      if (level_ == ResourceType_Study && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a study, the SopInstanceUID cannot be manually modified");
+      }
+    }
+
+
+    // Sanity checks at the series level
+    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a series, the parent PatientID cannot be manually modified");
+    }
+
+    if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying a series, the parent StudyInstanceUID cannot be manually modified");
+    }
+
+    if (!allowManualIdentifiers_)
+    {
+      if (level_ == ResourceType_Series && IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "When modifying a series, the SopInstanceUID cannot be manually modified");
+      }
+    }
+
+
+    // Sanity checks at the instance level
+    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying an instance, the parent PatientID cannot be manually modified");
+    }
+
+    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying an instance, the parent StudyInstanceUID cannot be manually modified");
+    }
+
+    if (level_ == ResourceType_Instance && IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "When modifying an instance, the parent SeriesInstanceUID cannot be manually modified");
+    }
+
+    // (0) Create a summary of the source file, if a custom generator
+    // is provided
+    if (identifierGenerator_ != NULL)
+    {
+      toModify.ExtractDicomSummary(currentSource_);
+    }
+
+    // (1) Make sure the relationships are updated with the ids that we force too
+    // i.e: an RT-STRUCT is referencing its own StudyInstanceUID
+    if (isAnonymization_ && updateReferencedRelationships_)
+    {
+      if (IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+      {
+        std::string original;
+        std::string replacement = GetReplacementAsString(DICOM_TAG_STUDY_INSTANCE_UID);
+        toModify.GetTagValue(original, DICOM_TAG_STUDY_INSTANCE_UID);
+        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Study);
+      }
+
+      if (IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+      {
+        std::string original;
+        std::string replacement = GetReplacementAsString(DICOM_TAG_SERIES_INSTANCE_UID);
+        toModify.GetTagValue(original, DICOM_TAG_SERIES_INSTANCE_UID);
+        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Series);
+      }
+
+      if (IsReplaced(DICOM_TAG_SOP_INSTANCE_UID))
+      {
+        std::string original;
+        std::string replacement = GetReplacementAsString(DICOM_TAG_SOP_INSTANCE_UID);
+        toModify.GetTagValue(original, DICOM_TAG_SOP_INSTANCE_UID);
+        RegisterMappedDicomIdentifier(original, replacement, ResourceType_Instance);
+      }
+    }
+
+
+    // (2) Remove the private tags, if need be
+    if (removePrivateTags_)
+    {
+      toModify.RemovePrivateTags(privateTagsToKeep_);
+    }
+
+    // (3) 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 */);
+    }
+
+    // (4) Remove the tags specified by the user
+    for (SetOfTags::const_iterator it = removals_.begin(); 
+         it != removals_.end(); ++it)
+    {
+      toModify.Remove(*it);
+    }
+
+    // (5) 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, privateCreator_);
+    }
+
+    // (6) 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))
+    {
+      if (keepSopInstanceUid_)
+      {
+        LOG(WARNING) << "Modifying an instance while keeping its original SOPInstanceUID: This should be avoided!";
+      }
+      else
+      {
+        MapDicomTags(toModify, ResourceType_Instance);
+      }
+    }
+
+    // (7) 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))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Marking tag \"" + name + "\" as to be " +
+                               (operation == DicomModification::TagOperation_Keep ? "kept" : "removed") +
+                               " requires the \"Force\" option to be set to true");
+      }
+
+      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))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Marking tag \"" + name + "\" as to be replaced " +
+                               "requires the \"Force\" option to be set to true");
+      }
+
+      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
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Member \"" + member + "\" should be a Boolean value");
+    }
+  }
+
+
+  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);
+    }
+
+    // New in Orthanc 1.6.0
+    if (request.isMember("PrivateCreator"))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
+    }
+  }
+
+
+  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);
+
+    // New in Orthanc 1.6.0
+    if (request.isMember("PrivateCreator"))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(request, "PrivateCreator");
+    }
+  }
+
+
+
+
+  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* KEEP_SOP_INSTANCE_UID = "KeepSOPInstanceUID";
+  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";
+  static const char* PRIVATE_CREATOR = "PrivateCreator";  // New in Orthanc 1.6.0
+  
+  void DicomModification::Serialize(Json::Value& value) const
+  {
+    if (identifierGenerator_ != NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot serialize a DicomModification with a custom identifier generator");
+    }
+
+    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[KEEP_SOP_INSTANCE_UID] = keepSopInstanceUid_;
+    value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
+    value[IS_ANONYMIZATION] = isAnonymization_;
+    value[PRIVATE_CREATOR] = privateCreator_;
+
+    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);
+    keepSopInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOP_INSTANCE_UID);
+    updateReferencedRelationships_ = SerializationToolbox::ReadBoolean
+      (serialized, UPDATE_REFERENCED_RELATIONSHIPS);
+    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
+
+    if (serialized.isMember(PRIVATE_CREATOR))
+    {
+      privateCreator_ = SerializationToolbox::ReadString(serialized, PRIVATE_CREATOR);
+    }
+
+    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/OrthancFramework/Sources/DicomParsing/DicomModification.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,201 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 keepSopInstanceUid_;
+    bool updateReferencedRelationships_;
+    bool isAnonymization_;
+    DicomMap currentSource_;
+    std::string privateCreator_;
+
+    IDicomIdentifierGenerator* identifierGenerator_;
+
+    std::string MapDicomIdentifier(const std::string& original,
+                                   ResourceType level);
+
+    void RegisterMappedDicomIdentifier(const std::string& original,
+                                       const std::string& mapped,
+                                       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;
+
+    void SetPrivateCreator(const std::string& privateCreator)
+    {
+      privateCreator_ = privateCreator;
+    }
+
+    const std::string& GetPrivateCreator()
+    {
+      return privateCreator_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,673 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomWebJsonVisitor.h"
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "FromDcmtkBridge.h"
+
+#include <boost/math/special_functions/round.hpp>
+#include <boost/lexical_cast.hpp>
+
+
+static const char* const KEY_ALPHABETIC = "Alphabetic";
+static const char* const KEY_IDEOGRAPHIC = "Ideographic";
+static const char* const KEY_PHONETIC = "Phonetic";
+static const char* const KEY_BULK_DATA_URI = "BulkDataURI";
+static const char* const KEY_INLINE_BINARY = "InlineBinary";
+static const char* const KEY_SQ = "SQ";
+static const char* const KEY_TAG = "tag";
+static const char* const KEY_VALUE = "Value";
+static const char* const KEY_VR = "vr";
+
+
+namespace Orthanc
+{
+#if ORTHANC_ENABLE_PUGIXML == 1
+  static void DecomposeXmlPersonName(pugi::xml_node& target,
+                                     const std::string& source)
+  {
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, source, '^');
+
+    if (tokens.size() >= 1)
+    {
+      target.append_child("FamilyName").text() = tokens[0].c_str();
+    }
+            
+    if (tokens.size() >= 2)
+    {
+      target.append_child("GivenName").text() = tokens[1].c_str();
+    }
+            
+    if (tokens.size() >= 3)
+    {
+      target.append_child("MiddleName").text() = tokens[2].c_str();
+    }
+            
+    if (tokens.size() >= 4)
+    {
+      target.append_child("NamePrefix").text() = tokens[3].c_str();
+    }
+            
+    if (tokens.size() >= 5)
+    {
+      target.append_child("NameSuffix").text() = tokens[4].c_str();
+    }
+  }
+  
+  static void ExploreXmlDataset(pugi::xml_node& target,
+                                const Json::Value& source)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
+    assert(source.type() == Json::objectValue);
+
+    Json::Value::Members members = source.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const DicomTag tag = FromDcmtkBridge::ParseTag(members[i]);
+      const Json::Value& content = source[members[i]];
+
+      assert(content.type() == Json::objectValue &&
+             content.isMember(KEY_VR) &&
+             content[KEY_VR].type() == Json::stringValue);
+      const std::string vr = content[KEY_VR].asString();
+
+      const std::string keyword = FromDcmtkBridge::GetTagName(tag, "");
+    
+      pugi::xml_node node = target.append_child("DicomAttribute");
+      node.append_attribute(KEY_TAG).set_value(members[i].c_str());
+      node.append_attribute(KEY_VR).set_value(vr.c_str());
+
+      if (keyword != std::string(DcmTag_ERROR_TagName))
+      {
+        node.append_attribute("keyword").set_value(keyword.c_str());
+      }   
+
+      if (content.isMember(KEY_VALUE))
+      {
+        assert(content[KEY_VALUE].type() == Json::arrayValue);
+        
+        for (Json::Value::ArrayIndex j = 0; j < content[KEY_VALUE].size(); j++)
+        {
+          std::string number = boost::lexical_cast<std::string>(j + 1);
+
+          if (vr == "SQ")
+          {
+            if (content[KEY_VALUE][j].type() == Json::objectValue)
+            {
+              pugi::xml_node child = node.append_child("Item");
+              child.append_attribute("number").set_value(number.c_str());
+              ExploreXmlDataset(child, content[KEY_VALUE][j]);
+            }
+          }
+          if (vr == "PN")
+          {
+            bool hasAlphabetic = (content[KEY_VALUE][j].isMember(KEY_ALPHABETIC) &&
+                                  content[KEY_VALUE][j][KEY_ALPHABETIC].type() == Json::stringValue);
+
+            bool hasIdeographic = (content[KEY_VALUE][j].isMember(KEY_IDEOGRAPHIC) &&
+                                   content[KEY_VALUE][j][KEY_IDEOGRAPHIC].type() == Json::stringValue);
+
+            bool hasPhonetic = (content[KEY_VALUE][j].isMember(KEY_PHONETIC) &&
+                                content[KEY_VALUE][j][KEY_PHONETIC].type() == Json::stringValue);
+
+            if (hasAlphabetic ||
+                hasIdeographic ||
+                hasPhonetic)
+            {
+              pugi::xml_node child = node.append_child("PersonName");
+              child.append_attribute("number").set_value(number.c_str());
+
+              if (hasAlphabetic)
+              {
+                pugi::xml_node name = child.append_child(KEY_ALPHABETIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_ALPHABETIC].asString());
+              }
+
+              if (hasIdeographic)
+              {
+                pugi::xml_node name = child.append_child(KEY_IDEOGRAPHIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_IDEOGRAPHIC].asString());
+              }
+
+              if (hasPhonetic)
+              {
+                pugi::xml_node name = child.append_child(KEY_PHONETIC);
+                DecomposeXmlPersonName(name, content[KEY_VALUE][j][KEY_PHONETIC].asString());
+              }
+            }
+          }
+          else
+          {
+            pugi::xml_node child = node.append_child("Value");
+            child.append_attribute("number").set_value(number.c_str());
+
+            switch (content[KEY_VALUE][j].type())
+            {
+              case Json::stringValue:
+                child.text() = content[KEY_VALUE][j].asCString();
+                break;
+
+              case Json::realValue:
+                child.text() = content[KEY_VALUE][j].asFloat();
+                break;
+
+              case Json::intValue:
+                child.text() = content[KEY_VALUE][j].asInt();
+                break;
+
+              case Json::uintValue:
+                child.text() = content[KEY_VALUE][j].asUInt();
+                break;
+
+              default:
+                break;
+            }
+          }
+        }
+      }
+      else if (content.isMember(KEY_BULK_DATA_URI) &&
+               content[KEY_BULK_DATA_URI].type() == Json::stringValue)
+      {
+        pugi::xml_node child = node.append_child("BulkData");
+        child.append_attribute("URI").set_value(content[KEY_BULK_DATA_URI].asCString());
+      }
+      else if (content.isMember(KEY_INLINE_BINARY) &&
+               content[KEY_INLINE_BINARY].type() == Json::stringValue)
+      {
+        pugi::xml_node child = node.append_child("InlineBinary");
+        child.text() = content[KEY_INLINE_BINARY].asCString();
+      }
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  static void DicomWebJsonToXml(pugi::xml_document& target,
+                                const Json::Value& source)
+  {
+    pugi::xml_node root = target.append_child("NativeDicomModel");
+    root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
+
+    ExploreXmlDataset(root, source);
+
+    pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+  }
+#endif
+
+
+  std::string DicomWebJsonVisitor::FormatTag(const DicomTag& tag)
+  {
+    char buf[16];
+    sprintf(buf, "%04X%04X", tag.GetGroup(), tag.GetElement());
+    return std::string(buf);
+  }
+
+    
+  Json::Value& DicomWebJsonVisitor::CreateNode(const std::vector<DicomTag>& parentTags,
+                                               const std::vector<size_t>& parentIndexes,
+                                               const DicomTag& tag)
+  {
+    assert(parentTags.size() == parentIndexes.size());      
+
+    Json::Value* node = &result_;
+
+    for (size_t i = 0; i < parentTags.size(); i++)
+    {
+      std::string t = FormatTag(parentTags[i]);
+
+      if (!node->isMember(t))
+      {
+        Json::Value item = Json::objectValue;
+        item[KEY_VR] = KEY_SQ;
+        item[KEY_VALUE] = Json::arrayValue;
+        item[KEY_VALUE].append(Json::objectValue);
+        (*node) [t] = item;
+
+        node = &(*node)[t][KEY_VALUE][0];
+      }
+      else if ((*node)  [t].type() != Json::objectValue ||
+               !(*node) [t].isMember(KEY_VR) ||
+               (*node)  [t][KEY_VR].type() != Json::stringValue ||
+               (*node)  [t][KEY_VR].asString() != KEY_SQ ||
+               !(*node) [t].isMember(KEY_VALUE) ||
+               (*node)  [t][KEY_VALUE].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        size_t currentSize = (*node) [t][KEY_VALUE].size();
+
+        if (parentIndexes[i] < currentSize)
+        {
+          // The node already exists
+        }
+        else if (parentIndexes[i] == currentSize)
+        {
+          (*node) [t][KEY_VALUE].append(Json::objectValue);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+          
+        node = &(*node) [t][KEY_VALUE][Json::ArrayIndex(parentIndexes[i])];
+      }
+    }
+
+    assert(node->type() == Json::objectValue);
+
+    std::string t = FormatTag(tag);
+    if (node->isMember(t))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      (*node) [t] = Json::objectValue;
+      return (*node) [t];
+    }
+  }
+
+    
+  Json::Value DicomWebJsonVisitor::FormatInteger(int64_t value)
+  {
+    if (value < 0)
+    {
+      return Json::Value(static_cast<int32_t>(value));
+    }
+    else
+    {
+      return Json::Value(static_cast<uint32_t>(value));
+    }
+  }
+
+    
+  Json::Value DicomWebJsonVisitor::FormatDouble(double value)
+  {
+    try
+    {
+      long long a = boost::math::llround<double>(value);
+
+      double d = fabs(value - static_cast<double>(a));
+
+      if (d <= std::numeric_limits<double>::epsilon() * 100.0)
+      {
+        return FormatInteger(a);
+      }
+      else
+      {
+        return Json::Value(value);
+      }
+    }
+    catch (boost::math::rounding_error&)
+    {
+      // Can occur if "long long" is too small to receive this value
+      // (e.g. infinity)
+      return Json::Value(value);
+    }
+  }
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  void DicomWebJsonVisitor::FormatXml(std::string& target) const
+  {
+    pugi::xml_document doc;
+    DicomWebJsonToXml(doc, result_);
+    Toolbox::XmlToString(target, doc);
+  }
+#endif
+
+
+  void DicomWebJsonVisitor::VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                               const std::vector<size_t>& parentIndexes,
+                                               const DicomTag& tag)
+  {
+    if (tag.GetElement() != 0x0000)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(ValueRepresentation_Sequence);
+    }
+  }
+  
+
+  void DicomWebJsonVisitor::VisitBinary(const std::vector<DicomTag>& parentTags,
+                                        const std::vector<size_t>& parentIndexes,
+                                        const DicomTag& tag,
+                                        ValueRepresentation vr,
+                                        const void* data,
+                                        size_t size)
+  {
+    assert(vr == ValueRepresentation_OtherByte ||
+           vr == ValueRepresentation_OtherDouble ||
+           vr == ValueRepresentation_OtherFloat ||
+           vr == ValueRepresentation_OtherLong ||
+           vr == ValueRepresentation_OtherWord ||
+           vr == ValueRepresentation_Unknown);
+
+    if (tag.GetElement() != 0x0000)
+    {
+      BinaryMode mode;
+      std::string bulkDataUri;
+        
+      if (formatter_ == NULL)
+      {
+        mode = BinaryMode_InlineBinary;
+      }
+      else
+      {
+        mode = formatter_->Format(bulkDataUri, parentTags, parentIndexes, tag, vr);
+      }
+
+      if (mode != BinaryMode_Ignore)
+      {
+        Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+        node[KEY_VR] = EnumerationToString(vr);
+
+        switch (mode)
+        {
+          case BinaryMode_BulkDataUri:
+            node[KEY_BULK_DATA_URI] = bulkDataUri;
+            break;
+
+          case BinaryMode_InlineBinary:
+          {
+            std::string tmp(static_cast<const char*>(data), size);
+          
+            std::string base64;
+            Toolbox::EncodeBase64(base64, tmp);
+
+            node[KEY_INLINE_BINARY] = base64;
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+    }
+  }
+
+
+  void DicomWebJsonVisitor::VisitIntegers(const std::vector<DicomTag>& parentTags,
+                                          const std::vector<size_t>& parentIndexes,
+                                          const DicomTag& tag,
+                                          ValueRepresentation vr,
+                                          const std::vector<int64_t>& values)
+  {
+    if (tag.GetElement() != 0x0000 &&
+        vr != ValueRepresentation_NotSupported)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatInteger(values[i]));
+        }
+
+        node[KEY_VALUE] = content;
+      }
+    }
+  }
+
+  void DicomWebJsonVisitor::VisitDoubles(const std::vector<DicomTag>& parentTags,
+                                         const std::vector<size_t>& parentIndexes,
+                                         const DicomTag& tag,
+                                         ValueRepresentation vr,
+                                         const std::vector<double>& values)
+  {
+    if (tag.GetElement() != 0x0000 &&
+        vr != ValueRepresentation_NotSupported)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatDouble(values[i]));
+        }
+          
+        node[KEY_VALUE] = content;
+      }
+    }
+  }
+
+  
+  void DicomWebJsonVisitor::VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                            const std::vector<size_t>& parentIndexes,
+                                            const DicomTag& tag,
+                                            const std::vector<DicomTag>& values)
+  {
+    if (tag.GetElement() != 0x0000)
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(ValueRepresentation_AttributeTag);
+
+      if (!values.empty())
+      {
+        Json::Value content = Json::arrayValue;
+        for (size_t i = 0; i < values.size(); i++)
+        {
+          content.append(FormatTag(values[i]));
+        }
+          
+        node[KEY_VALUE] = content;
+      }
+    }
+  }
+
+  
+  ITagVisitor::Action
+  DicomWebJsonVisitor::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 (tag.GetElement() == 0x0000 ||
+        vr == ValueRepresentation_NotSupported)
+    {
+      return Action_None;
+    }
+    else
+    {
+      Json::Value& node = CreateNode(parentTags, parentIndexes, tag);
+      node[KEY_VR] = EnumerationToString(vr);
+
+#if 0
+      /**
+       * TODO - The JSON file has an UTF-8 encoding, thus DCMTK
+       * replaces the specific character set with "ISO_IR 192"
+       * (UNICODE UTF-8). On Google Cloud Healthcare, however, the
+       * source encoding is reported, which seems more logical. We
+       * thus choose the Google convention. Enabling this block will
+       * mimic the DCMTK behavior.
+       **/
+      if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        node[KEY_VALUE].append("ISO_IR 192");
+      }
+      else
+#endif
+      {
+        std::string truncated;
+        
+        if (!value.empty() &&
+            value[value.size() - 1] == '\0')
+        {
+          truncated = value.substr(0, value.size() - 1);
+        }
+        else
+        {
+          truncated = value;
+        }
+
+        if (!truncated.empty())
+        {
+          std::vector<std::string> tokens;
+          Toolbox::TokenizeString(tokens, truncated, '\\');
+
+          if (tag == DICOM_TAG_SPECIFIC_CHARACTER_SET &&
+              tokens.size() > 1 &&
+              tokens[0].empty())
+          {
+            // Specific character set with code extension: Remove the
+            // first element from the vector of encodings
+            tokens.erase(tokens.begin());
+          }
+
+          node[KEY_VALUE] = Json::arrayValue;
+          for (size_t i = 0; i < tokens.size(); i++)
+          {
+            try
+            {
+              switch (vr)
+              {
+                case ValueRepresentation_PersonName:
+                {
+                  Json::Value value = Json::objectValue;
+                  if (!tokens[i].empty())
+                  {
+                    std::vector<std::string> components;
+                    Toolbox::TokenizeString(components, tokens[i], '=');
+
+                    if (components.size() >= 1)
+                    {
+                      value[KEY_ALPHABETIC] = components[0];
+                    }
+
+                    if (components.size() >= 2)
+                    {
+                      value[KEY_IDEOGRAPHIC] = components[1];
+                    }
+
+                    if (components.size() >= 3)
+                    {
+                      value[KEY_PHONETIC] = components[2];
+                    }
+                  }
+                  
+                  node[KEY_VALUE].append(value);
+                  break;
+                }
+                  
+                case ValueRepresentation_IntegerString:
+                {
+                  /**
+                   * The calls to "StripSpaces()" below fix the
+                   * issue reported by Rana Asim Wajid on 2019-06-05
+                   * ("Error Exception while invoking plugin service
+                   * 32: Bad file format"):
+                   * https://groups.google.com/d/msg/orthanc-users/T32FovWPcCE/-hKFbfRJBgAJ
+                   **/
+
+                  std::string t = Orthanc::Toolbox::StripSpaces(tokens[i]);
+                  if (t.empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    int64_t value = boost::lexical_cast<int64_t>(t);
+                    node[KEY_VALUE].append(FormatInteger(value));
+                  }
+                 
+                  break;
+                }
+              
+                case ValueRepresentation_DecimalString:
+                {
+                  std::string t = Orthanc::Toolbox::StripSpaces(tokens[i]);
+                  if (t.empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    double value = boost::lexical_cast<double>(t);
+                    node[KEY_VALUE].append(FormatDouble(value));
+                  }
+
+                  break;
+                }
+
+                default:
+                  if (tokens[i].empty())
+                  {
+                    node[KEY_VALUE].append(Json::nullValue);
+                  }
+                  else
+                  {
+                    node[KEY_VALUE].append(tokens[i]);
+                  }
+                  
+                  break;
+              }
+            }
+            catch (boost::bad_lexical_cast&)
+            {
+              std::string tmp;
+              if (value.size() < 64 &&
+                  Toolbox::IsAsciiString(value))
+              {
+                tmp = ": " + value;
+              }
+              
+              LOG(WARNING) << "Ignoring DICOM tag (" << tag.Format()
+                           << ") with invalid content for VR " << EnumerationToString(vr) << tmp;
+            }
+          }
+        }
+      }
+    }
+      
+    return Action_None;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/DicomWebJsonVisitor.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,160 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_PUGIXML)
+#  error Macro ORTHANC_ENABLE_PUGIXML must be defined to use this file
+#endif
+
+#include "ITagVisitor.h"
+
+#include <json/value.h>
+
+
+namespace Orthanc
+{
+  class DicomWebJsonVisitor : public ITagVisitor
+  {
+  public:
+    enum BinaryMode
+    {
+      BinaryMode_Ignore,
+      BinaryMode_BulkDataUri,
+      BinaryMode_InlineBinary
+    };
+    
+    class IBinaryFormatter : public boost::noncopyable
+    {
+    public:
+      virtual ~IBinaryFormatter()
+      {
+      }
+
+      virtual BinaryMode Format(std::string& bulkDataUri,
+                                const std::vector<DicomTag>& parentTags,
+                                const std::vector<size_t>& parentIndexes,
+                                const DicomTag& tag,
+                                ValueRepresentation vr) = 0;
+    };
+    
+  private:
+    Json::Value        result_;
+    IBinaryFormatter  *formatter_;
+
+    static std::string FormatTag(const DicomTag& tag);
+    
+    Json::Value& CreateNode(const std::vector<DicomTag>& parentTags,
+                            const std::vector<size_t>& parentIndexes,
+                            const DicomTag& tag);
+
+    static Json::Value FormatInteger(int64_t value);
+
+    static Json::Value FormatDouble(double value);
+
+  public:
+    DicomWebJsonVisitor() :
+      formatter_(NULL)
+    {
+      Clear();
+    }
+
+    void SetFormatter(IBinaryFormatter& formatter)
+    {
+      formatter_ = &formatter;
+    }
+    
+    void Clear()
+    {
+      result_ = Json::objectValue;
+    }
+
+    const Json::Value& GetResult() const
+    {
+      return result_;
+    }
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+    void FormatXml(std::string& target) const;
+#endif
+
+    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr)
+      ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag)
+      ORTHANC_OVERRIDE;
+
+    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)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::vector<int64_t>& values)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              const std::vector<double>& values)
+      ORTHANC_OVERRIDE;
+
+    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 const std::vector<DicomTag>& values)
+      ORTHANC_OVERRIDE;
+
+    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)
+      ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2676 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+#include "FromDcmtkBridge.h"
+#include "ToDcmtkBridge.h"
+#include "../Compatibility.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/dcistrmb.h>
+#include <dcmtk/dcmdata/dcostrmb.h>
+#include <dcmtk/dcmdata/dcpixel.h>
+#include <dcmtk/dcmdata/dcuid.h>
+#include <dcmtk/dcmdata/dcxfer.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_VERSION_NUMBER >= 361
+#  include <dcmtk/dcmdata/dcvruc.h>
+#  include <dcmtk/dcmdata/dcvrur.h>
+#endif
+
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+#  include <OrthancFrameworkResources.h>
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+#  include <dcmtk/dcmjpeg/djdecode.h>
+#  if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#    include <dcmtk/dcmjpeg/djencode.h>
+#  endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+#  include <dcmtk/dcmjpls/djdecode.h>
+#  if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#    include <dcmtk/dcmjpls/djencode.h>
+#  endif
+#endif
+
+
+#include <dcmtk/dcmdata/dcrledrg.h>
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+#  include <dcmtk/dcmdata/dcrleerg.h>
+#  include <dcmtk/dcmimage/diregist.h>  // include to support color images
+#endif
+
+
+namespace Orthanc
+{
+  static bool IsBinaryTag(const DcmTag& key)
+  {
+    return (key.isUnknownVR() ||
+            key.getEVR() == EVR_OB ||
+            key.getEVR() == EVR_OW ||
+            key.getEVR() == EVR_UN ||
+            key.getEVR() == EVR_ox);
+  }
+
+
+#if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
+  static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
+                                     FrameworkResources::FileResourceId resource)
+  {
+    std::string content;
+    FrameworkResources::GetFileResource(content, resource);
+
+#if ORTHANC_SANDBOXED == 0
+    TemporaryFile tmp;
+    tmp.Write(content);
+
+    if (!dictionary.loadDictionary(tmp.GetPath().c_str()))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot read embedded dictionary. Under Windows, make sure that " 
+                             "your TEMP directory does not contain special characters.");
+    }
+#else
+    if (!dictionary.loadFromMemory(content))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot read embedded dictionary. Under Windows, make sure that " 
+                             "your TEMP directory does not contain special characters.");
+    }
+#endif
+  }
+#endif
+
+
+  namespace
+  {
+    class DictionaryLocker : public boost::noncopyable
+    {
+    private:
+      DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryLocker() : dictionary_(dcmDataDict.wrlock())
+      {
+      }
+
+      ~DictionaryLocker()
+      {
+#if DCMTK_VERSION_NUMBER >= 364
+        dcmDataDict.wrunlock();
+#else
+        dcmDataDict.unlock();
+#endif
+      }
+
+      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, FrameworkResources::DICTIONARY_DICONDE);
+
+      LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_DICOM);
+
+      if (loadPrivateDictionary)
+      {
+        LOG(INFO) << "Loading the embedded dictionary of private tags";
+        LoadEmbeddedDictionary(*locker, FrameworkResources::DICTIONARY_PRIVATE);
+      }
+      else
+      {
+        LOG(INFO) << "The dictionary of private tags has not been loaded";
+      }
+
+#else
+      std::vector<std::string> dictionaries;
+      
+      const char* env = std::getenv(DCM_DICT_ENVIRONMENT_VARIABLE);
+      if (env != NULL)
+      {
+        // This mimics the behavior of DCMTK:
+        // https://support.dcmtk.org/docs/file_envvars.html
+#if defined(_WIN32)
+        Toolbox::TokenizeString(dictionaries, std::string(env), ';');
+#else
+        Toolbox::TokenizeString(dictionaries, std::string(env), ':');
+#endif
+      }
+      else
+      {
+        boost::filesystem::path base = DCMTK_DICTIONARY_DIR;
+        dictionaries.push_back((base / "dicom.dic").string());
+        dictionaries.push_back((base / "private.dic").string());
+      }
+
+      for (size_t i = 0; i < dictionaries.size(); i++)
+      {
+        LOG(WARNING) << "Loading external DICOM dictionary: \"" << dictionaries[i] << "\"";
+        
+        if (!locker->loadDictionary(dictionaries[i].c_str()))
+        {
+          throw OrthancException(ErrorCode_InexistentFile);
+        }
+      }
+
+#endif
+    }
+
+    /* make sure data dictionary is loaded */
+    if (!dcmDataDict.isDictionaryLoaded())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "No DICOM dictionary loaded, check environment variable: " +
+                             std::string(DCM_DICT_ENVIRONMENT_VARIABLE));
+    }
+
+    {
+      // Test the dictionary with a simple DICOM tag
+      DcmTag key(0x0010, 0x1030); // This is PatientWeight
+      if (key.getEVR() != EVR_DS)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "The DICOM dictionary has not been correctly read");
+      }
+    }
+  }
+
+
+  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::unique_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());
+        throw OrthancException(ErrorCode_ParameterOutOfRange, std::string(buf));
+      }
+
+      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()))
+      {
+        throw OrthancException(ErrorCode_AlreadyExistingTag,
+                               "Cannot register two tags with the same symbolic name \"" + name + "\"");
+      }
+
+      locker->addEntry(entry.release());
+    }
+  }
+
+
+  Encoding FromDcmtkBridge::DetectEncoding(bool& hasCodeExtensions,
+                                           DcmItem& dataset,
+                                           Encoding defaultEncoding)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
+
+    OFString tmp;
+    if (dataset.findAndGetOFStringArray(DCM_SpecificCharacterSet, tmp).good())
+    {
+      std::vector<std::string> tokens;
+      Toolbox::TokenizeString(tokens, std::string(tmp.c_str()), '\\');
+
+      hasCodeExtensions = (tokens.size() > 1);
+
+      for (size_t i = 0; i < tokens.size(); i++)
+      {
+        std::string characterSet = Toolbox::StripSpaces(tokens[i]);
+
+        if (!characterSet.empty())
+        {
+          Encoding encoding;
+          
+          if (GetDicomEncoding(encoding, characterSet.c_str()))
+          {
+            // The specific character set is supported by the Orthanc core
+            return encoding;
+          }
+          else
+          {
+            LOG(WARNING) << "Value of Specific Character Set (0008,0005) is not supported: " << characterSet
+                         << ", fallback to ASCII (remove all special characters)";
+            return Encoding_Ascii;
+          }
+        }
+      }
+    }
+    else
+    {
+      hasCodeExtensions = false;
+    }
+    
+    // No specific character set tag: Use the default encoding
+    return defaultEncoding;
+  }
+
+
+  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, 
+                                            DcmItem& dataset,
+                                            unsigned int maxStringLength,
+                                            Encoding defaultEncoding,
+                                            const std::set<DicomTag>& ignoreTagLength)
+  {
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+
+    target.Clear();
+    for (unsigned long i = 0; i < dataset.card(); i++)
+    {
+      DcmElement* element = dataset.getElement(i);
+      if (element && element->isLeaf())
+      {
+        target.SetValueInternal(element->getTag().getGTag(),
+                                element->getTag().getETag(),
+                                ConvertLeafElement(*element, DicomToJsonFlags_Default,
+                                                   maxStringLength, encoding, hasCodeExtensions, 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,
+                                                  bool hasCodeExtensions,
+                                                  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, hasCodeExtensions);
+
+        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,
+                                      bool hasCodeExtensions,
+                                      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::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
+                                  (element, flags, 0, encoding, hasCodeExtensions, 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, hasCodeExtensions, ignoreTagLength);
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::DatasetToJson(Json::Value& parent,
+                                      DcmItem& item,
+                                      DicomToJsonFormat format,
+                                      DicomToJsonFlags flags,
+                                      unsigned int maxStringLength,
+                                      Encoding encoding,
+                                      bool hasCodeExtensions,
+                                      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(), element->getTag().getPrivateCreator()) == NULL)
+        {
+          continue;
+        }
+      }
+
+      if (IsBinaryTag(element->getTag()))
+      {
+        // 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, hasCodeExtensions, ignoreTagLength);
+    }
+  }
+
+
+  void FromDcmtkBridge::ExtractDicomAsJson(Json::Value& target, 
+                                           DcmDataset& dataset,
+                                           DicomToJsonFormat format,
+                                           DicomToJsonFlags flags,
+                                           unsigned int maxStringLength,
+                                           Encoding defaultEncoding,
+                                           const std::set<DicomTag>& ignoreTagLength)
+  {
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+
+    target = Json::objectValue;
+    DatasetToJson(target, dataset, format, flags, maxStringLength, encoding, hasCodeExtensions, 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, false, 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::Content::const_iterator 
+           it = values.content_.begin(); it != values.content_.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);
+    }
+  }
+
+
+  
+  static bool SaveToMemoryBufferInternal(std::string& buffer,
+                                         DcmFileFormat& dicom,
+                                         E_TransferSyntax xfer)
+  {
+    E_EncodingType encodingType = /*opt_sequenceType*/ EET_ExplicitLength;
+
+    // Create a memory buffer with the proper size
+    {
+      const uint32_t estimatedSize = dicom.calcElementLength(xfer, encodingType);  // (*)
+      buffer.resize(estimatedSize);
+    }
+
+    DcmOutputBufferStream ob(&buffer[0], buffer.size());
+
+    // Fill the memory buffer with the meta-header and the dataset
+    dicom.transferInit();
+    OFCondition c = dicom.write(ob, xfer, encodingType, NULL,
+                                /*opt_groupLength*/ EGL_recalcGL,
+                                /*opt_paddingType*/ EPD_noChange,
+                                /*padlen*/ 0, /*subPadlen*/ 0, /*instanceLength*/ 0,
+                                EWM_updateMeta /* creates new SOP instance UID on lossy */);
+    dicom.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;
+    }
+  }
+  
+
+  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
+                                           DcmDataset& dataSet)
+  {
+    // Determine the transfer syntax which shall be used to write the
+    // information to the file. If not possible, 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.getCurrentXfer();
+    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;
+    }
+
+    // Create the meta-header information
+    DcmFileFormat ff(&dataSet);
+    ff.validateMetaInfo(xfer);
+    ff.removeInvalidGroups();
+
+    return SaveToMemoryBufferInternal(buffer, ff, xfer);
+  }
+
+
+  bool FromDcmtkBridge::Transcode(DcmFileFormat& dicom,
+                                  DicomTransferSyntax syntax,
+                                  const DcmRepresentationParameter* representation)
+  {
+    E_TransferSyntax xfer;
+    if (!LookupDcmtkTransferSyntax(xfer, syntax))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      DicomTransferSyntax sourceSyntax;
+      bool known = LookupOrthancTransferSyntax(sourceSyntax, dicom);
+      
+      if (!dicom.chooseRepresentation(xfer, representation).good() ||
+          !dicom.canWriteXfer(xfer) ||
+          !dicom.validateMetaInfo(xfer, EWM_updateMeta).good())
+      {
+        return false;
+      }
+      else
+      {
+        dicom.removeInvalidGroups();
+
+        if (known)
+        {
+          LOG(INFO) << "Transcoded an image from transfer syntax "
+                    << GetTransferSyntaxUid(sourceSyntax) << " to "
+                    << GetTransferSyntaxUid(syntax);
+        }
+        else
+        {
+          LOG(INFO) << "Transcoded an image from unknown transfer syntax to "
+                    << GetTransferSyntaxUid(syntax);
+        }
+        
+        return true;
+      }
+    }
+  }
+
+
+  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;
+
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:
+          return ValueRepresentation_OtherDouble;
+#endif
+
+      case EVR_OF:
+        return ValueRepresentation_OtherFloat;
+
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:
+          return ValueRepresentation_OtherLong;
+#endif
+
+      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;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UC:
+        return ValueRepresentation_UnlimitedCharacters;
+#endif
+
+      case EVR_UI:
+        return ValueRepresentation_UniqueIdentifier;
+
+      case EVR_UL:
+        return ValueRepresentation_UnsignedLong;
+
+      case EVR_UN:
+        return ValueRepresentation_Unknown;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EVR_UR:
+        return ValueRepresentation_UniversalResource;
+#endif
+
+      case EVR_US:
+        return ValueRepresentation_UnsignedShort;
+
+      case EVR_UT:
+        return ValueRepresentation_UnlimitedText;
+
+      default:
+        return ValueRepresentation_NotSupported;
+    }
+  }
+
+
+  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag,
+                                                   const std::string& privateCreator)
+  {
+    if (tag.IsPrivate() &&
+        privateCreator.empty())
+    {
+      // This solves issue 140 (Modifying private tags with REST API
+      // changes VR from LO to UN)
+      // https://bitbucket.org/sjodogne/orthanc/issues/140
+      LOG(WARNING) << "Private creator should not be empty while creating a private tag: " << tag.Format();
+    }
+    
+#if DCMTK_VERSION_NUMBER >= 362
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    if (tag.IsPrivate())
+    {
+      return DcmItem::newDicomElement(key, privateCreator.c_str());
+    }
+    else
+    {
+      return DcmItem::newDicomElement(key, NULL);
+    }
+    
+#else
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    if (tag.IsPrivate())
+    {
+      // https://forum.dcmtk.org/viewtopic.php?t=4527
+      LOG(WARNING) << "You are using DCMTK <= 3.6.1: All the private tags "
+        "are considered as having a binary value representation";
+      key.setPrivateCreator(privateCreator.c_str());
+      return new DcmOtherByteOtherWord(key);
+    }
+    else
+    {
+      return newDicomElement(key);
+    }
+#endif      
+  }
+
+
+
+  void FromDcmtkBridge::FillElementWithString(DcmElement& element,
+                                              const std::string& utf8Value,
+                                              bool decodeDataUriScheme,
+                                              Encoding dicomEncoding)
+  {
+    std::string binary;
+    const std::string* decoded = &utf8Value;
+
+    if (decodeDataUriScheme &&
+        boost::starts_with(utf8Value, URI_SCHEME_PREFIX_BINARY))
+    {
+      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;
+    }
+
+    if (IsBinaryTag(element.getTag()))
+    {
+      bool ok;
+
+      switch (element.getTag().getEVR())
+      {
+        case EVR_OW:
+          if (decoded->size() % sizeof(Uint16) != 0)
+          {
+            LOG(ERROR) << "A tag with OW VR must have an even number of bytes";
+            ok = false;
+          }
+          else
+          {
+            ok = element.putUint16Array((const Uint16*) decoded->c_str(), decoded->size() / sizeof(Uint16)).good();
+          }
+          
+          break;
+      
+        default:
+          ok = element.putUint8Array((const Uint8*) decoded->c_str(), decoded->size()).good();
+          break;
+      }
+      
+      if (ok)
+      {
+        return;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    bool ok = false;
+    
+    try
+    {
+      switch (element.getTag().getEVR())
+      {
+        // http://support.dcmtk.org/docs/dcvr_8h-source.html
+
+        /**
+         * TODO.
+         **/
+
+        case EVR_OB:  // other byte
+        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
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_UC:  // unlimited characters
+        case EVR_UR:  // URI/URL
+#endif
+        {
+          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
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:  // other long (requires byte-swapping)
+#endif
+        {
+          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
+        case EVR_OF:  // other float (requires byte swapping)
+        {
+          ok = element.putFloat32(boost::lexical_cast<float>(*decoded)).good();
+          break;
+        }
+
+        case EVR_FD:  // float double-precision
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:  // other double (requires byte-swapping)
+#endif
+        {
+          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)
+    {
+      DicomTag tag(element.getTag().getGroup(), element.getTag().getElement());
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "While creating a DICOM instance, tag (" + tag.Format() +
+                             ") has out-of-range value: \"" + (*decoded) + "\"");
+    }
+  }
+
+
+  DcmElement* FromDcmtkBridge::FromJson(const DicomTag& tag,
+                                        const Json::Value& value,
+                                        bool decodeDataUriScheme,
+                                        Encoding dicomEncoding,
+                                        const std::string& privateCreator)
+  {
+    std::unique_ptr<DcmElement> element;
+
+    switch (value.type())
+    {
+      case Json::stringValue:
+        element.reset(CreateElementForTag(tag, privateCreator));
+        FillElementWithString(*element, value.asString(), decodeDataUriScheme, dicomEncoding);
+        break;
+
+      case Json::nullValue:
+        element.reset(CreateElementForTag(tag, privateCreator));
+        FillElementWithString(*element, "", decodeDataUriScheme, dicomEncoding);
+        break;
+
+      case Json::arrayValue:
+      {
+        const char* p = NULL;
+        if (tag.IsPrivate() &&
+            !privateCreator.empty())
+        {
+          p = privateCreator.c_str();
+        }
+        
+        DcmTag key(tag.GetGroup(), tag.GetElement(), p);
+        if (key.getEVR() != EVR_SQ)
+        {
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Bad Parameter type for tag " + tag.Format());
+        }
+
+        DcmSequenceOfItems* sequence = new DcmSequenceOfItems(key);
+        element.reset(sequence);
+        
+        for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+        {
+          std::unique_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, privateCreator));
+              }
+              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, "Bad Parameter type for tag " + tag.Format());
+    }
+
+    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);
+
+    E_TransferSyntax repType;
+    const DcmRepresentationParameter *repParam = NULL;
+    pixelData.getCurrentRepresentationKey(repType, repParam);
+    
+    DcmPixelSequence* pixelSequence = NULL;
+    if (!pixelData.getEncapsulatedRepresentation(repType, repParam, 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())))
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Unknown encoding while creating DICOM from JSON: " +
+                                 value.toStyledString());
+        }
+
+        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,
+                                        const std::string& privateCreator)
+  {
+    std::unique_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::unique_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
+        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::unique_ptr<DcmFileFormat> result(new DcmFileFormat);
+
+    result->transferInit();
+
+    /**
+     * New in Orthanc 1.6.0: The "size" is given as an argument to the
+     * "read()" method. This can avoid huge memory consumption if
+     * parsing an invalid DICOM file, which can notably been observed
+     * by executing the integration test "test_upload_compressed" on
+     * valgrind running Orthanc.
+     **/
+    if (!result->read(is, EXS_Unknown, EGL_noChange, size).good())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot parse an invalid DICOM file (size: " +
+                             boost::lexical_cast<std::string>(size) + " bytes)");
+    }
+
+    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,
+                                             bool hasSourceCodeExtensions,
+                                             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, hasSourceCodeExtensions);
+            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, hasSourceCodeExtensions, target);
+          }
+        }
+      }
+    }
+  }
+
+
+#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)
+    {
+      throw OrthancException(ErrorCode_LuaBadOutput,
+                             "Lua: The script must return a table");
+    }
+
+    Json::Value::Members members = output.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      if (output[members[i]].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_LuaBadOutput,
+                               "Lua: The script must return a table "
+                               "mapping names of DICOM tags to strings");
+      }
+
+      DicomTag tag(ParseTag(members[i]));
+      target.SetValue(tag, output[members[i]].asString(), false);
+    }
+  }
+#endif
+
+
+  void FromDcmtkBridge::ExtractDicomSummary(DicomMap& target, 
+                                            DcmItem& dataset,
+                                            const std::set<DicomTag>& ignoreTagLength)
+  {
+    ExtractDicomSummary(target, dataset,
+                        ORTHANC_MAXIMUM_TAG_LENGTH,
+                        GetDefaultDicomEncoding(), ignoreTagLength);
+  }
+
+  
+  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();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJLSEncoderRegistration::registerCodecs();
+# endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    LOG(INFO) << "Registering JPEG codecs in DCMTK";
+    DJDecoderRegistration::registerCodecs(); 
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJEncoderRegistration::registerCodecs();
+# endif
+#endif
+
+    LOG(INFO) << "Registering RLE codecs in DCMTK";
+    DcmRLEDecoderRegistration::registerCodecs(); 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DcmRLEEncoderRegistration::registerCodecs();
+#endif
+  }
+
+
+  void FromDcmtkBridge::FinalizeCodecs()
+  {
+#if ORTHANC_ENABLE_DCMTK_JPEG_LOSSLESS == 1
+    // Unregister JPEG-LS codecs
+    DJLSDecoderRegistration::cleanup();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJLSEncoderRegistration::cleanup();
+# endif
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_JPEG == 1
+    // Unregister JPEG codecs
+    DJDecoderRegistration::cleanup();
+# if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DJEncoderRegistration::cleanup();
+# endif
+#endif
+
+    DcmRLEDecoderRegistration::cleanup(); 
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+    DcmRLEEncoderRegistration::cleanup();
+#endif
+  }
+
+
+
+  // Forward declaration
+  static void ApplyVisitorToElement(DcmElement& element,
+                                    ITagVisitor& visitor,
+                                    const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    Encoding encoding,
+                                    bool hasCodeExtensions);
+ 
+  static void ApplyVisitorToDataset(DcmItem& dataset,
+                                    ITagVisitor& visitor,
+                                    const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    Encoding encoding,
+                                    bool hasCodeExtensions)
+  {
+    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, hasCodeExtensions);
+      }      
+    }
+  }
+
+
+  static void ApplyVisitorToLeaf(DcmElement& element,
+                                 ITagVisitor& visitor,
+                                 const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 Encoding encoding,
+                                 bool hasCodeExtensions)
+  {
+    // TODO - Merge this function, that is more recent, with ConvertLeafElement()
+
+    assert(element.isLeaf());
+
+    DcmEVR evr = element.getTag().getEVR();
+
+    
+    /**
+     * Fix the EVR for types internal to DCMTK 
+     **/
+
+    if (evr == EVR_ox)  // OB or OW depending on context
+    {
+      evr = EVR_OB;
+    }
+
+    if (evr == EVR_UNKNOWN ||  // used internally for elements with unknown VR (encoded with 4-byte length field in explicit VR)
+        evr == EVR_UNKNOWN2B)  // used internally for elements with unknown VR with 2-byte length field in explicit VR
+    {
+      evr = EVR_UN;
+    }
+
+    const ValueRepresentation vr = FromDcmtkBridge::Convert(evr);
+
+    
+    /**
+     * Deal with binary data (including PixelData).
+     **/
+
+    if (evr == EVR_OB ||  // other byte
+        evr == EVR_OW ||  // other word
+        evr == EVR_UN)    // unknown value representation
+    {
+      Uint16* data16 = NULL;
+      Uint8* data = NULL;
+
+      if (evr == EVR_OW &&
+          element.getUint16Array(data16) == EC_Normal)
+      {
+        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data16, element.getLength());
+      }
+      else if (evr != EVR_OW &&
+               element.getUint8Array(data) == EC_Normal)
+      {
+        visitor.VisitBinary(parentTags, parentIndexes, tag, vr, data, element.getLength());
+      }
+      else
+      {
+        visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+      }
+
+      return;  // We're done
+    }
+
+
+    /**
+     * Deal with plain strings (and convert them to UTF-8)
+     **/
+
+    char *c = NULL;
+    if (element.isaString() &&
+        element.getString(c).good())
+    {
+      std::string utf8;
+
+      if (c != NULL)  // This case corresponds to the empty string
+      {
+        if (element.getTag() == DCM_SpecificCharacterSet)
+        {
+          utf8.assign(c);
+        }
+        else
+        {
+          std::string s(c);
+          utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+        }
+      }
+
+      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)
+          {
+            throw OrthancException(ErrorCode_InternalError,
+                                   "Cannot replace value of tag: " + tag.Format());
+          }
+
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return;  // We're done
+    }
+
+
+    try
+    {
+      // http://support.dcmtk.org/docs/dcvr_8h-source.html
+      switch (evr)
+      {
+
+        /**
+         * Plain string values.
+         **/
+
+        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
+        {
+          Uint8* data = NULL;
+
+          if (element.getUint8Array(data) == EC_Normal)
+          {
+            const Uint32 length = element.getLength();
+            Uint32 l = 0;
+            while (l < length &&
+                   data[l] != 0)
+            {
+              l++;
+            }
+
+            if (l == length)
+            {
+              // Not a null-terminated plain string
+              visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+            }
+            else
+            {
+              std::string ignored;
+              std::string s(reinterpret_cast<const char*>(data), l);
+              ITagVisitor::Action action = visitor.VisitString
+                (ignored, parentTags, parentIndexes, tag, vr,
+                 Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions));
+
+              if (action != ITagVisitor::Action_None)
+              {
+                LOG(WARNING) << "Cannot replace this string tag: "
+                             << FromDcmtkBridge::GetTagName(element)
+                             << " (" << tag.Format() << ")";
+              }
+            }
+          }
+          else
+          {
+            visitor.VisitNotSupported(parentTags, parentIndexes, tag, vr);
+          }
+
+          return;
+        }
+    
+        /**
+         * Numeric types
+         **/ 
+      
+        case EVR_SL:  // signed long
+        {
+          DcmSignedLong& content = dynamic_cast<DcmSignedLong&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Sint32 f;
+            if (content.getSint32(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_SS:  // signed short
+        {
+          DcmSignedShort& content = dynamic_cast<DcmSignedShort&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Sint16 f;
+            if (content.getSint16(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_UL:  // unsigned long
+#if DCMTK_VERSION_NUMBER >= 362
+        case EVR_OL:
+#endif
+        {
+          DcmUnsignedLong& content = dynamic_cast<DcmUnsignedLong&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Uint32 f;
+            if (content.getUint32(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_US:  // unsigned short
+        {
+          DcmUnsignedShort& content = dynamic_cast<DcmUnsignedShort&>(element);
+
+          std::vector<int64_t> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Uint16 f;
+            if (content.getUint16(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          visitor.VisitIntegers(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_FL:  // float single-precision
+        case EVR_OF:
+        {
+          DcmFloatingPointSingle& content = dynamic_cast<DcmFloatingPointSingle&>(element);
+
+          std::vector<double> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Float32 f;
+            if (content.getFloat32(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+        case EVR_FD:  // float double-precision
+#if DCMTK_VERSION_NUMBER >= 361
+        case EVR_OD:
+#endif
+        {
+          DcmFloatingPointDouble& content = dynamic_cast<DcmFloatingPointDouble&>(element);
+
+          std::vector<double> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            Float64 f;
+            if (content.getFloat64(f, i).good())
+            {
+              values.push_back(f);
+            }
+          }
+
+          visitor.VisitDoubles(parentTags, parentIndexes, tag, vr, values);
+          break;
+        }
+
+
+        /**
+         * Attribute tag.
+         **/
+
+        case EVR_AT:
+        {
+          DcmAttributeTag& content = dynamic_cast<DcmAttributeTag&>(element);
+
+          std::vector<DicomTag> values;
+          values.reserve(content.getVM());
+
+          for (unsigned long i = 0; i < content.getVM(); i++)
+          {
+            DcmTagKey f;
+            if (content.getTagVal(f, i).good())
+            {
+              DicomTag t(f.getGroup(), f.getElement());
+              values.push_back(t);
+            }
+          }
+
+          assert(vr == ValueRepresentation_AttributeTag);
+          visitor.VisitAttributes(parentTags, parentIndexes, tag, values);
+          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.VisitNotSupported(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,
+                                    bool hasCodeExtensions)
+  {
+    assert(parentTags.size() == parentIndexes.size());
+
+    DicomTag tag(FromDcmtkBridge::Convert(element.getTag()));
+
+    if (element.isLeaf())
+    {
+      ApplyVisitorToLeaf(element, visitor, parentTags, parentIndexes, tag, encoding, hasCodeExtensions);
+    }
+    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);
+
+      if (sequence.card() == 0)
+      {
+        visitor.VisitEmptySequence(parentTags, parentIndexes, tag);
+      }
+      else
+      {
+        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, hasCodeExtensions);
+        }
+      }
+    }
+  }
+
+
+  void FromDcmtkBridge::Apply(DcmItem& dataset,
+                              ITagVisitor& visitor,
+                              Encoding defaultEncoding)
+  {
+    std::vector<DicomTag> parentTags;
+    std::vector<size_t> parentIndexes;
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions, dataset, defaultEncoding);
+    ApplyVisitorToDataset(dataset, visitor, parentTags, parentIndexes, encoding, hasCodeExtensions);
+  }
+
+
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    DcmFileFormat& dicom)
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+        
+    DcmDataset& dataset = *dicom.getDataset();
+
+    E_TransferSyntax xfer = dataset.getCurrentXfer();
+    if (xfer == EXS_Unknown)
+    {
+      dataset.updateOriginalXfer();
+      xfer = dataset.getOriginalXfer();
+      if (xfer == EXS_Unknown)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot determine the transfer syntax of the DICOM instance");
+      }
+    }
+
+    return FromDcmtkBridge::LookupOrthancTransferSyntax(target, xfer);
+  }
+}
+
+
+#include "./FromDcmtkBridge_TransferSyntaxes.impl.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,284 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 ORTHANC_PUBLIC 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,
+                                    const std::set<DicomTag>& ignoreTagLength);
+
+    static void DatasetToJson(Json::Value& parent,
+                              DcmItem& item,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength,
+                              Encoding encoding,
+                              bool hasCodeExtensions,
+                              const std::set<DicomTag>& ignoreTagLength);
+
+    static void ElementToJson(Json::Value& parent,
+                              DcmElement& element,
+                              DicomToJsonFormat format,
+                              DicomToJsonFlags flags,
+                              unsigned int maxStringLength,
+                              Encoding dicomEncoding,
+                              bool hasCodeExtensions,
+                              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,
+                                     bool hasSourceCodeExtensions,
+                                     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(bool& hasCodeExtensions,
+                                   DcmItem& dataset,
+                                   Encoding defaultEncoding);
+
+    static Encoding DetectEncoding(DcmItem& dataset,
+                                   Encoding defaultEncoding)
+    {
+      // Compatibility wrapper for Orthanc <= 1.5.4
+      bool hasCodeExtensions;  // ignored
+      return DetectEncoding(hasCodeExtensions, dataset, 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,
+                                          bool hasCodeExtensions,
+                                          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)
+    {
+      const DicomTag tag = ParseTag(tagName);
+      target.SetValueInternal(tag.GetGroup(), tag.GetElement(), 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 bool Transcode(DcmFileFormat& dicom,
+                          DicomTransferSyntax syntax,
+                          const DcmRepresentationParameter* representation);
+
+    static ValueRepresentation Convert(DcmEVR vr);
+
+    static ValueRepresentation LookupValueRepresentation(const DicomTag& tag);
+
+    static DcmElement* CreateElementForTag(const DicomTag& tag,
+                                           const std::string& privateCreator);
+    
+    static void FillElementWithString(DcmElement& element,
+                                      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,
+                                const std::string& privateCreator);
+
+    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,
+                                const std::string& privateCreator);
+
+    static DcmFileFormat* LoadFromMemoryBuffer(const void* buffer,
+                                               size_t size);
+
+    static void FromJson(DicomMap& values,
+                         const Json::Value& result);
+
+#if ORTHANC_ENABLE_LUA == 1
+    static void ExecuteToDicom(DicomMap& target,
+                               LuaFunctionCall& call);
+#endif
+
+    static void ExtractDicomSummary(DicomMap& target, 
+                                    DcmItem& dataset,
+                                    const std::set<DicomTag>& ignoreTagLength);
+
+    static void ExtractDicomSummary(DicomMap& target, 
+                                    DcmItem& dataset)
+    {
+      std::set<DicomTag> none;
+      ExtractDicomSummary(target, dataset, none);
+    }
+
+    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);
+
+    static bool LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                          DicomTransferSyntax source);
+
+    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                            E_TransferSyntax source);
+
+    static bool LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                            DcmFileFormat& dicom);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/FromDcmtkBridge_TransferSyntaxes.impl.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,549 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
+                                                  DicomTransferSyntax source)
+  {
+    switch (source)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        target = EXS_LittleEndianImplicit;
+        return true;
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        target = EXS_LittleEndianExplicit;
+        return true;
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        target = EXS_DeflatedLittleEndianExplicit;
+        return true;
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        target = EXS_BigEndianExplicit;
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess1:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess1TransferSyntax;
+#  else
+        target = EXS_JPEGProcess1;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess2_4TransferSyntax;
+#  else
+        target = EXS_JPEGProcess2_4;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess3_5TransferSyntax;
+#  else
+        target = EXS_JPEGProcess3_5;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess6_8TransferSyntax;
+#  else
+        target = EXS_JPEGProcess6_8;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess7_9TransferSyntax;
+#  else
+        target = EXS_JPEGProcess7_9;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess10_12TransferSyntax;
+#  else
+        target = EXS_JPEGProcess10_12;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess11_13TransferSyntax;
+#  else
+        target = EXS_JPEGProcess11_13;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess14TransferSyntax;
+#  else
+        target = EXS_JPEGProcess14;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess15:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess15TransferSyntax;
+#  else
+        target = EXS_JPEGProcess15;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess16_18TransferSyntax;
+#  else
+        target = EXS_JPEGProcess16_18;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess17_19TransferSyntax;
+#  else
+        target = EXS_JPEGProcess17_19;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess20_22TransferSyntax;
+#  else
+        target = EXS_JPEGProcess20_22;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess21_23TransferSyntax;
+#  else
+        target = EXS_JPEGProcess21_23;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess24_26TransferSyntax;
+#  else
+        target = EXS_JPEGProcess24_26;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess25_27TransferSyntax;
+#  else
+        target = EXS_JPEGProcess25_27;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess28:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess28TransferSyntax;
+#  else
+        target = EXS_JPEGProcess28;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess29:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess29TransferSyntax;
+#  else
+        target = EXS_JPEGProcess29;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+#  if DCMTK_VERSION_NUMBER <= 360
+        target = EXS_JPEGProcess14SV1TransferSyntax;
+#  else
+        target = EXS_JPEGProcess14SV1;
+#  endif
+        return true;
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        target = EXS_JPEGLSLossless;
+        return true;
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        target = EXS_JPEGLSLossy;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        target = EXS_JPEG2000LosslessOnly;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000:
+        target = EXS_JPEG2000;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        target = EXS_JPEG2000MulticomponentLosslessOnly;
+        return true;
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        target = EXS_JPEG2000Multicomponent;
+        return true;
+
+      case DicomTransferSyntax_JPIPReferenced:
+        target = EXS_JPIPReferenced;
+        return true;
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        target = EXS_JPIPReferencedDeflate;
+        return true;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        target = EXS_MPEG2MainProfileAtMainLevel;
+        return true;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        target = EXS_MPEG2MainProfileAtHighLevel;
+        return true;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        target = EXS_MPEG4HighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        target = EXS_MPEG4BDcompatibleHighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        target = EXS_MPEG4HighProfileLevel4_2_For2DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        target = EXS_MPEG4HighProfileLevel4_2_For3DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        target = EXS_MPEG4StereoHighProfileLevel4_2;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        target = EXS_HEVCMainProfileLevel5_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        target = EXS_HEVCMain10ProfileLevel5_1;
+        return true;
+#endif
+
+      case DicomTransferSyntax_RLELossless:
+        target = EXS_RLELossless;
+        return true;
+
+      default:
+        return false;
+    }
+  }
+  
+
+  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
+                                                    E_TransferSyntax source)
+  {
+    switch (source)
+    {
+      case EXS_LittleEndianImplicit:
+        target = DicomTransferSyntax_LittleEndianImplicit;
+        return true;
+
+      case EXS_LittleEndianExplicit:
+        target = DicomTransferSyntax_LittleEndianExplicit;
+        return true;
+
+      case EXS_DeflatedLittleEndianExplicit:
+        target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+        return true;
+
+      case EXS_BigEndianExplicit:
+        target = DicomTransferSyntax_BigEndianExplicit;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess1TransferSyntax:
+#  else
+      case EXS_JPEGProcess1:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess1;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess2_4TransferSyntax:
+#  else
+      case EXS_JPEGProcess2_4:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess2_4;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess3_5TransferSyntax:
+#  else
+      case EXS_JPEGProcess3_5:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess3_5;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess6_8TransferSyntax:
+#  else
+      case EXS_JPEGProcess6_8:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess6_8;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess7_9TransferSyntax:
+#  else
+      case EXS_JPEGProcess7_9:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess7_9;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess10_12TransferSyntax:
+#  else
+      case EXS_JPEGProcess10_12:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess10_12;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess11_13TransferSyntax:
+#  else
+      case EXS_JPEGProcess11_13:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess11_13;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess14TransferSyntax:
+#  else
+      case EXS_JPEGProcess14:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess14;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess15TransferSyntax:
+#  else
+      case EXS_JPEGProcess15:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess15;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess16_18TransferSyntax:
+#  else
+      case EXS_JPEGProcess16_18:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess16_18;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess17_19TransferSyntax:
+#  else
+      case EXS_JPEGProcess17_19:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess17_19;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess20_22TransferSyntax:
+#  else
+      case EXS_JPEGProcess20_22:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess20_22;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess21_23TransferSyntax:
+#  else
+      case EXS_JPEGProcess21_23:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess21_23;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess24_26TransferSyntax:
+#  else
+      case EXS_JPEGProcess24_26:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess24_26;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess25_27TransferSyntax:
+#  else
+      case EXS_JPEGProcess25_27:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess25_27;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess28TransferSyntax:
+#  else
+      case EXS_JPEGProcess28:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess28;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess29TransferSyntax:
+#  else
+      case EXS_JPEGProcess29:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess29;
+        return true;
+
+#  if DCMTK_VERSION_NUMBER <= 360
+      case EXS_JPEGProcess14SV1TransferSyntax:
+#  else
+      case EXS_JPEGProcess14SV1:
+#  endif
+        target = DicomTransferSyntax_JPEGProcess14SV1;
+        return true;
+
+      case EXS_JPEGLSLossless:
+        target = DicomTransferSyntax_JPEGLSLossless;
+        return true;
+
+      case EXS_JPEGLSLossy:
+        target = DicomTransferSyntax_JPEGLSLossy;
+        return true;
+
+      case EXS_JPEG2000LosslessOnly:
+        target = DicomTransferSyntax_JPEG2000LosslessOnly;
+        return true;
+
+      case EXS_JPEG2000:
+        target = DicomTransferSyntax_JPEG2000;
+        return true;
+
+      case EXS_JPEG2000MulticomponentLosslessOnly:
+        target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
+        return true;
+
+      case EXS_JPEG2000Multicomponent:
+        target = DicomTransferSyntax_JPEG2000Multicomponent;
+        return true;
+
+      case EXS_JPIPReferenced:
+        target = DicomTransferSyntax_JPIPReferenced;
+        return true;
+
+      case EXS_JPIPReferencedDeflate:
+        target = DicomTransferSyntax_JPIPReferencedDeflate;
+        return true;
+
+      case EXS_MPEG2MainProfileAtMainLevel:
+        target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
+        return true;
+
+      case EXS_MPEG2MainProfileAtHighLevel:
+        target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
+        return true;
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_1:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4BDcompatibleHighProfileLevel4_1:
+        target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_2_For2DVideo:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4HighProfileLevel4_2_For3DVideo:
+        target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+      case EXS_MPEG4StereoHighProfileLevel4_2:
+        target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EXS_HEVCMainProfileLevel5_1:
+        target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
+        return true;
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 362
+      case EXS_HEVCMain10ProfileLevel5_1:
+        target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
+        return true;
+#endif
+
+      case EXS_RLELossless:
+        target = DicomTransferSyntax_RLELossless;
+        return true;
+
+      default:
+        return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,438 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IDicomTranscoder.h"
+
+#include "../OrthancException.h"
+#include "FromDcmtkBridge.h"
+#include "ParsedDicomFile.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+namespace Orthanc
+{
+  IDicomTranscoder::TranscodingType IDicomTranscoder::GetTranscodingType(DicomTransferSyntax target,
+                                                                         DicomTransferSyntax source)
+  {
+    if (target == source)
+    {
+      return TranscodingType_Lossless;
+    }
+    else if (target == DicomTransferSyntax_LittleEndianImplicit ||
+             target == DicomTransferSyntax_LittleEndianExplicit ||
+             target == DicomTransferSyntax_BigEndianExplicit ||
+             target == DicomTransferSyntax_DeflatedLittleEndianExplicit ||
+             target == DicomTransferSyntax_JPEGProcess14 ||
+             target == DicomTransferSyntax_JPEGProcess14SV1 ||
+             target == DicomTransferSyntax_JPEGLSLossless ||
+             target == DicomTransferSyntax_JPEG2000LosslessOnly ||
+             target == DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly)
+    {
+      return TranscodingType_Lossless;
+    }
+    else if (target == DicomTransferSyntax_JPEGProcess1 ||
+             target == DicomTransferSyntax_JPEGProcess2_4 ||
+             target == DicomTransferSyntax_JPEGLSLossy ||
+             target == DicomTransferSyntax_JPEG2000 ||
+             target == DicomTransferSyntax_JPEG2000Multicomponent)
+    {
+      return TranscodingType_Lossy;
+    }
+    else
+    {
+      return TranscodingType_Unknown;
+    }
+  }
+
+
+  std::string IDicomTranscoder::GetSopInstanceUid(DcmFileFormat& dicom)
+  {
+    if (dicom.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    DcmDataset& dataset = *dicom.getDataset();
+    
+    const char* v = NULL;
+
+    if (dataset.findAndGetString(DCM_SOPInstanceUID, v).good() &&
+        v != NULL)
+    {
+      return std::string(v);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "File without SOP instance UID");
+    }
+  }
+
+
+  void IDicomTranscoder::CheckTranscoding(IDicomTranscoder::DicomImage& transcoded,
+                                          DicomTransferSyntax sourceSyntax,
+                                          const std::string& sourceSopInstanceUid,
+                                          const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                          bool allowNewSopInstanceUid)
+  {
+    DcmFileFormat& parsed = transcoded.GetParsed();
+    
+    if (parsed.getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::string targetSopInstanceUid = GetSopInstanceUid(parsed);
+
+    if (parsed.getDataset()->tagExists(DCM_PixelData))
+    {
+      if (!allowNewSopInstanceUid && (targetSopInstanceUid != sourceSopInstanceUid))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      if (targetSopInstanceUid != sourceSopInstanceUid)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "No pixel data: Transcoding must not change the SOP instance UID");
+      }
+    }
+
+    DicomTransferSyntax targetSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, parsed))
+    {
+      return;  // Unknown transfer syntax, cannot do further test
+    }
+
+    if (allowedSyntaxes.find(sourceSyntax) != allowedSyntaxes.end())
+    {
+      // No transcoding should have happened
+      if (targetSopInstanceUid != sourceSopInstanceUid)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+        
+    if (allowedSyntaxes.find(targetSyntax) == allowedSyntaxes.end())
+    {
+      throw OrthancException(ErrorCode_InternalError, "An incorrect output transfer syntax was chosen");
+    }
+    
+    if (parsed.getDataset()->tagExists(DCM_PixelData))
+    {
+      switch (GetTranscodingType(targetSyntax, sourceSyntax))
+      {
+        case TranscodingType_Lossy:
+          if (targetSopInstanceUid == sourceSopInstanceUid)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        case TranscodingType_Lossless:
+          if (targetSopInstanceUid != sourceSopInstanceUid)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+          break;
+
+        default:
+          break;
+      }
+    }
+  }
+    
+
+  void IDicomTranscoder::DicomImage::Parse()
+  {
+    if (parsed_.get() != NULL)
+    {
+      // Already parsed
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (buffer_.get() != NULL)
+    {
+      if (isExternalBuffer_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      else
+      {
+        parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(
+                        buffer_->empty() ? NULL : buffer_->c_str(), buffer_->size()));
+        
+        if (parsed_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }      
+      }
+    }
+    else if (isExternalBuffer_)
+    {
+      parsed_.reset(FromDcmtkBridge::LoadFromMemoryBuffer(externalBuffer_, externalSize_));
+      
+      if (parsed_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }      
+    }
+    else
+    {
+      // No buffer is available
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+  
+  void IDicomTranscoder::DicomImage::Serialize()
+  {
+    if (parsed_.get() == NULL ||
+        buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (parsed_->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      buffer_.reset(new std::string);
+      FromDcmtkBridge::SaveToMemoryBuffer(*buffer_, *parsed_->getDataset());
+    }
+  }
+
+  
+  IDicomTranscoder::DicomImage::DicomImage() :
+    isExternalBuffer_(false)
+  {
+  }
+
+
+  void IDicomTranscoder::DicomImage::Clear()
+  {
+    parsed_.reset(NULL);
+    buffer_.reset(NULL);
+    isExternalBuffer_ = false;
+  }
+
+  
+  void IDicomTranscoder::DicomImage::AcquireParsed(ParsedDicomFile& parsed)
+  {
+    AcquireParsed(parsed.ReleaseDcmtkObject());
+  }
+  
+      
+  void IDicomTranscoder::DicomImage::AcquireParsed(DcmFileFormat* parsed)
+  {
+    if (parsed == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (parsed->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (parsed_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parsed_.reset(parsed);
+    }
+  }
+  
+
+  void IDicomTranscoder::DicomImage::AcquireParsed(DicomImage& other)
+  {
+    AcquireParsed(other.ReleaseParsed());
+  }
+  
+
+  void IDicomTranscoder::DicomImage::AcquireBuffer(std::string& buffer /* will be swapped */)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      buffer_.reset(new std::string);
+      buffer_->swap(buffer);
+    }
+  }
+
+
+  void IDicomTranscoder::DicomImage::AcquireBuffer(DicomImage& other)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (other.isExternalBuffer_)
+    {
+      assert(other.buffer_.get() == NULL);
+      isExternalBuffer_ = true;
+      externalBuffer_ = other.externalBuffer_;
+      externalSize_ = other.externalSize_;
+    }
+    else if (other.buffer_.get() != NULL)
+    {
+      buffer_.reset(other.buffer_.release());
+    }
+    else
+    {
+      buffer_.reset(NULL);
+    }    
+  }
+
+  
+  void IDicomTranscoder::DicomImage::SetExternalBuffer(const void* buffer,
+                                                       size_t size)
+  {
+    if (buffer_.get() != NULL ||
+        isExternalBuffer_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isExternalBuffer_ = true;
+      externalBuffer_ = buffer;
+      externalSize_ = size;
+    }
+  }
+
+
+  void IDicomTranscoder::DicomImage::SetExternalBuffer(const std::string& buffer)
+  {
+    SetExternalBuffer(buffer.empty() ? NULL : buffer.c_str(), buffer.size());
+  }
+
+
+  DcmFileFormat& IDicomTranscoder::DicomImage::GetParsed()
+  {
+    if (parsed_.get() != NULL)
+    {
+      return *parsed_;
+    }
+    else if (buffer_.get() != NULL ||
+             isExternalBuffer_)
+    {
+      Parse();
+      return *parsed_;
+    }
+    else
+    {
+      throw OrthancException(
+        ErrorCode_BadSequenceOfCalls,
+        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
+    }
+  }
+  
+
+  DcmFileFormat* IDicomTranscoder::DicomImage::ReleaseParsed()
+  {
+    if (parsed_.get() != NULL)
+    {
+      buffer_.reset(NULL);
+      return parsed_.release();
+    }
+    else if (buffer_.get() != NULL ||
+             isExternalBuffer_)
+    {
+      Parse();
+      buffer_.reset(NULL);
+      return parsed_.release();
+    }
+    else
+    {
+      throw OrthancException(
+        ErrorCode_BadSequenceOfCalls,
+        "AcquireParsed(), AcquireBuffer() or SetExternalBuffer() should have been called");
+    }
+  }
+
+
+  ParsedDicomFile* IDicomTranscoder::DicomImage::ReleaseAsParsedDicomFile()
+  {
+    return ParsedDicomFile::AcquireDcmtkObject(ReleaseParsed());
+  }
+
+  
+  const void* IDicomTranscoder::DicomImage::GetBufferData()
+  {
+    if (isExternalBuffer_)
+    {
+      assert(buffer_.get() == NULL);
+      return externalBuffer_;
+    }
+    else
+    {    
+      if (buffer_.get() == NULL)
+      {
+        Serialize();
+      }
+
+      assert(buffer_.get() != NULL);
+      return buffer_->empty() ? NULL : buffer_->c_str();
+    }
+  }
+
+  
+  size_t IDicomTranscoder::DicomImage::GetBufferSize()
+  {
+    if (isExternalBuffer_)
+    {
+      assert(buffer_.get() == NULL);
+      return externalSize_;
+    }
+    else
+    {    
+      if (buffer_.get() == NULL)
+      {
+        Serialize();
+      }
+
+      assert(buffer_.get() != NULL);
+      return buffer_->size();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/IDicomTranscoder.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,130 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.h"
+#include "../Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <set>
+
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  class ParsedDicomFile;
+  
+  /**
+   * WARNING: This class might be called from several threads at
+   * once. Make sure to implement proper locking.
+   **/
+  class ORTHANC_PUBLIC IDicomTranscoder : public boost::noncopyable
+  {
+  public:
+    class DicomImage : public boost::noncopyable
+    {
+    private:
+      std::unique_ptr<DcmFileFormat>  parsed_;
+      std::unique_ptr<std::string>    buffer_;
+      bool                            isExternalBuffer_;
+      const void*                     externalBuffer_;
+      size_t                          externalSize_;
+
+      void Parse();
+
+      void Serialize();
+
+      DcmFileFormat* ReleaseParsed();
+
+    public:
+      DicomImage();
+      
+      void Clear();
+      
+      // Calling this method will invalidate the "ParsedDicomFile" object
+      void AcquireParsed(ParsedDicomFile& parsed);
+      
+      void AcquireParsed(DcmFileFormat* parsed);
+
+      void AcquireParsed(DicomImage& other);
+
+      void AcquireBuffer(std::string& buffer /* will be swapped */);
+
+      void AcquireBuffer(DicomImage& other);
+
+      void SetExternalBuffer(const void* buffer,
+                             size_t size);
+
+      void SetExternalBuffer(const std::string& buffer);
+
+      DcmFileFormat& GetParsed();
+
+      ParsedDicomFile* ReleaseAsParsedDicomFile();
+
+      const void* GetBufferData();
+
+      size_t GetBufferSize();
+    };
+
+
+  protected:
+    enum TranscodingType
+    {
+      TranscodingType_Lossy,
+      TranscodingType_Lossless,
+      TranscodingType_Unknown
+    };
+
+    static TranscodingType GetTranscodingType(DicomTransferSyntax target,
+                                              DicomTransferSyntax source);
+
+    static void CheckTranscoding(DicomImage& transcoded,
+                                 DicomTransferSyntax sourceSyntax,
+                                 const std::string& sourceSopInstanceUid,
+                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid);
+    
+  public:    
+    virtual ~IDicomTranscoder()
+    {
+    }
+
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) = 0;
+
+    static std::string GetSopInstanceUid(DcmFileFormat& dicom);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/ITagVisitor.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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()
+    {
+    }
+
+    // Visiting a DICOM element that is internal to DCMTK
+    virtual void VisitNotSupported(const std::vector<DicomTag>& parentTags,
+                                   const std::vector<size_t>& parentIndexes,
+                                   const DicomTag& tag,
+                                   ValueRepresentation vr) = 0;
+
+    // SQ
+    virtual void VisitEmptySequence(const std::vector<DicomTag>& parentTags,
+                                    const std::vector<size_t>& parentIndexes,
+                                    const DicomTag& tag) = 0;
+
+    // SL, SS, UL, US
+    virtual void VisitIntegers(const std::vector<DicomTag>& parentTags,
+                               const std::vector<size_t>& parentIndexes,
+                               const DicomTag& tag,
+                               ValueRepresentation vr,
+                               const std::vector<int64_t>& values) = 0;
+
+    // FL, FD, OD, OF
+    virtual void VisitDoubles(const std::vector<DicomTag>& parentTags,
+                              const std::vector<size_t>& parentIndexes,
+                              const DicomTag& tag,
+                              ValueRepresentation vr,
+                              const std::vector<double>& values) = 0;
+
+    // AT
+    virtual void VisitAttributes(const std::vector<DicomTag>& parentTags,
+                                 const std::vector<size_t>& parentIndexes,
+                                 const DicomTag& tag,
+                                 const std::vector<DicomTag>& values) = 0;
+
+    // OB, OL, OW, UN
+    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;
+
+    // Visiting an UTF-8 string
+    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/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,403 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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)
+      {
+        // Degenerate case: Empty offset table means only one frame
+        // that overlaps all the fragments
+        table.resize(1);
+        table[0] = 0;
+        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_);
+      }
+    }
+  };
+
+
+  unsigned int DicomFrameIndex::GetFramesCount(DcmDataset& dicom)
+  {
+    const char* tmp = NULL;
+    if (!dicom.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 static_cast<unsigned int>(count);
+    }
+  }
+
+
+  DicomFrameIndex::DicomFrameIndex(DcmDataset& dicom)
+  {
+    countFrames_ = GetFramesCount(dicom);
+    if (countFrames_ == 0)
+    {
+      // The image has no frame. No index is to be built.
+      return;
+    }
+
+    // Test whether this image is composed of a sequence of fragments
+    DcmPixelSequence* pixelSequence = FromDcmtkBridge::GetPixelSequence(dicom);
+    if (pixelSequence != NULL)
+    {
+      index_.reset(new FragmentIndex(pixelSequence, countFrames_));
+      return;
+    }
+
+    // Extract information about the image structure
+    DicomMap tags;
+    FromDcmtkBridge::ExtractDicomSummary(tags, dicom);
+
+    DicomImageInformation information(tags);
+
+    // Access to the raw pixel data
+    if (DicomImageDecoder::IsPsmctRle1(dicom))
+    {
+      index_.reset(new PsmctRle1Index(dicom, countFrames_, information.GetFrameSize()));
+    }
+    else
+    {
+      index_.reset(new UncompressedIndex(dicom, 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/OrthancFramework/Sources/DicomParsing/Internals/DicomFrameIndex.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../Compatibility.h"
+#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::unique_ptr<IIndex>  index_;
+    unsigned int             countFrames_;
+
+  public:
+    DicomFrameIndex(DcmDataset& dicom);
+
+    unsigned int GetFramesCount() const
+    {
+      return countFrames_;
+    }
+
+    void GetRawFrame(std::string& frame,
+                     unsigned int index) const;
+
+    static unsigned int GetFramesCount(DcmDataset& dicom);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1042 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#include "../ParsedDicomFile.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"
+
+#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/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::unique_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)
+  {
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    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::unique_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::unique_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::unique_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())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot decode a palette image");
+      }
+
+      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())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Cannot decode a non-palette image");
+      }
+
+      return target.release();
+    }
+  }
+
+
+  ImageAccessor* DicomImageDecoder::Decode(ParsedDicomFile& dicom,
+                                           unsigned int frame)
+  {
+    if (dicom.GetDcmtkObject().getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return Decode(*dicom.GetDcmtkObject().getDataset(), frame);
+    }
+  }
+
+
+  ImageAccessor* DicomImageDecoder::Decode(DcmDataset& dataset,
+                                           unsigned int frame)
+  {
+    E_TransferSyntax syntax = dataset.getCurrentXfer();
+
+    /**
+     * 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::unique_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::unique_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) << "Trying to decode a compressed image by transcoding it to Little Endian Explicit";
+
+      std::unique_ptr<DcmDataset> converted(dynamic_cast<DcmDataset*>(dataset.clone()));
+      converted->chooseRepresentation(EXS_LittleEndianExplicit, NULL);
+
+      if (converted->canWriteXfer(EXS_LittleEndianExplicit))
+      {
+        return DecodeUncompressedImage(*converted, frame);
+      }
+    }
+
+    DicomTransferSyntax s;
+    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, dataset.getCurrentXfer()))
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The built-in DCMTK decoder cannot decode some DICOM instance "
+                             "whose transfer syntax is: " + std::string(GetTransferSyntaxUid(s)));
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "The built-in DCMTK decoder cannot decode some DICOM instance");
+    }
+  }
+
+
+  static bool IsColorImage(PixelFormat format)
+  {
+    return (format == PixelFormat_RGB24 ||
+            format == PixelFormat_RGBA32);
+  }
+
+
+  bool DicomImageDecoder::TruncateDecodedImage(std::unique_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::unique_ptr<ImageAccessor> target
+        (new Image(format, image->GetWidth(), image->GetHeight(), false));
+      ImageProcessing::Convert(*target, *image);
+
+#if __cplusplus < 201103L
+      image.reset(target.release());
+#else
+      image = std::move(target);
+#endif
+    }
+
+    return true;
+  }
+
+
+  bool DicomImageDecoder::PreviewDecodedImage(std::unique_ptr<ImageAccessor>& image)
+  {
+    switch (image->GetFormat())
+    {
+      case PixelFormat_RGB24:
+      {
+        // Directly return color images without modification (RGB)
+        return true;
+      }
+
+      case PixelFormat_RGB48:
+      {
+        std::unique_ptr<ImageAccessor> target
+          (new Image(PixelFormat_RGB24, image->GetWidth(), image->GetHeight(), false));
+        ImageProcessing::Convert(*target, *image);
+
+#if __cplusplus < 201103L
+        image.reset(target.release());
+#else
+        image = std::move(target);
+#endif
+
+        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::unique_ptr<ImageAccessor> target
+            (new Image(PixelFormat_Grayscale8, image->GetWidth(), image->GetHeight(), false));
+          ImageProcessing::Convert(*target, *image);
+
+#if __cplusplus < 201103L
+          image.reset(target.release());
+#else
+          image = std::move(target);
+#endif
+        }
+
+        return true;
+      }
+      
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void DicomImageDecoder::ApplyExtractionMode(std::unique_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::unique_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::unique_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::unique_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/OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,130 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../Compatibility.h"
+#include "../../Images/ImageAccessor.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 ParsedDicomFile;
+  
+  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::unique_ptr<ImageAccessor>& image,
+                                     PixelFormat format,
+                                     bool allowColorConversion);
+
+    static bool PreviewDecodedImage(std::unique_ptr<ImageAccessor>& image);
+
+    static void ApplyExtractionMode(std::unique_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 ImageAccessor *Decode(DcmDataset& dataset,
+                                 unsigned int frame);
+
+    static void ExtractPamImage(std::string& result,
+                                std::unique_ptr<ImageAccessor>& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+
+#if ORTHANC_ENABLE_PNG == 1
+    static void ExtractPngImage(std::string& result,
+                                std::unique_ptr<ImageAccessor>& image,
+                                ImageExtractionMode mode,
+                                bool invert);
+#endif
+
+#if ORTHANC_ENABLE_JPEG == 1
+    static void ExtractJpegImage(std::string& result,
+                                 std::unique_ptr<ImageAccessor>& image,
+                                 ImageExtractionMode mode,
+                                 bool invert,
+                                 uint8_t quality);
+#endif
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MemoryBufferTranscoder.h"
+
+#include "../OrthancException.h"
+#include "FromDcmtkBridge.h"
+
+#if !defined(NDEBUG)  // For debugging
+#  include "ParsedDicomFile.h"
+#endif
+
+namespace Orthanc
+{
+  static void CheckTargetSyntax(const std::string& transcoded,
+                                const std::set<DicomTransferSyntax>& allowedSyntaxes)
+  {
+#if !defined(NDEBUG)
+    // Debug mode
+    ParsedDicomFile parsed(transcoded);
+
+    std::string s;
+    DicomTransferSyntax a, b;
+    if (!parsed.LookupTransferSyntax(s) ||
+        !FromDcmtkBridge::LookupOrthancTransferSyntax(a, parsed.GetDcmtkObject()) ||
+        !LookupTransferSyntax(b, s) ||
+        a != b ||
+        allowedSyntaxes.find(a) == allowedSyntaxes.end())
+    {
+      throw OrthancException(
+        ErrorCode_Plugin,
+        "DEBUG - The transcoding plugin has not written to one of the allowed transfer syntaxes");
+    }
+#endif
+  }
+    
+
+  bool MemoryBufferTranscoder::Transcode(DicomImage& target,
+                                         DicomImage& source,
+                                         const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                         bool allowNewSopInstanceUid)
+  {
+    target.Clear();
+    
+#if !defined(NDEBUG)
+    // Don't run this code in release mode, as it implies parsing the DICOM file
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, source.GetParsed()))
+    {
+      LOG(ERROR) << "Unsupport transfer syntax for transcoding";
+      return false;
+    }
+    
+    const std::string sourceSopInstanceUid = GetSopInstanceUid(source.GetParsed());
+#endif
+
+    std::string buffer;
+    if (TranscodeBuffer(buffer, source.GetBufferData(), source.GetBufferSize(),
+                        allowedSyntaxes, allowNewSopInstanceUid))
+    {
+      CheckTargetSyntax(buffer, allowedSyntaxes);  // For debug only
+
+      target.AcquireBuffer(buffer);
+      
+#if !defined(NDEBUG)
+      // Only run the sanity check in debug mode
+      CheckTranscoding(target, sourceSyntax, sourceSopInstanceUid,
+                       allowedSyntaxes, allowNewSopInstanceUid);
+#endif
+
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/MemoryBufferTranscoder.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IDicomTranscoder.h"
+
+namespace Orthanc
+{
+  // This is the basis class for transcoding plugins
+  class MemoryBufferTranscoder : public IDicomTranscoder
+  {
+  protected:
+    virtual bool TranscodeBuffer(std::string& target,
+                                 const void* buffer,
+                                 size_t size,
+                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid) = 0;
+    
+  public:
+    virtual bool Transcode(DicomImage& target /* out */,
+                           DicomImage& source,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,195 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ParsedDicomDir.h"
+
+#include "../Compatibility.h"
+#include "../OrthancException.h"
+#include "ParsedDicomFile.h"
+#include "FromDcmtkBridge.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+
+namespace Orthanc
+{
+  void ParsedDicomDir::Clear()
+  {
+    for (size_t i = 0; i < content_.size(); i++)
+    {
+      assert(content_[i] != NULL);
+      delete content_[i];
+    }
+  }
+
+  
+  bool ParsedDicomDir::LookupIndexOfOffset(size_t& target,
+                                           unsigned int offset) const
+  {
+    if (offset == 0)
+    {
+      return false;
+    }
+
+    OffsetToIndex::const_iterator found = offsetToIndex_.find(offset);
+    if (found == offsetToIndex_.end())
+    {
+      // Error in the algorithm that computes the offsets
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      target = found->second;
+      return true;
+    }
+  }
+
+
+  ParsedDicomDir::ParsedDicomDir(const std::string content)
+  {
+    ParsedDicomFile dicom(content);
+
+    DcmSequenceOfItems* sequence = NULL;
+    if (dicom.GetDcmtkObject().getDataset() == NULL ||
+        !dicom.GetDcmtkObject().getDataset()->findAndGetSequence(DCM_DirectoryRecordSequence, sequence).good() ||
+        sequence == NULL)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Not a DICOMDIR");
+    }
+
+    content_.resize(sequence->card());
+    nextOffsets_.resize(content_.size());
+    lowerOffsets_.resize(content_.size());
+
+    // Manually reconstruct the list of all the available offsets of
+    // "DcmItem", as "fStartPosition" is a protected member in DCMTK
+    // API
+    std::set<uint32_t> availableOffsets;
+    availableOffsets.insert(0);
+
+
+    for (unsigned long i = 0; i < sequence->card(); i++)
+    {
+      DcmItem* item = sequence->getItem(i);
+      if (item == NULL)
+      {
+        Clear();
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      Uint32 next, lower;
+      if (!item->findAndGetUint32(DCM_OffsetOfTheNextDirectoryRecord, next).good() ||
+          !item->findAndGetUint32(DCM_OffsetOfReferencedLowerLevelDirectoryEntity, lower).good())
+      {
+        item->writeXML(std::cout);
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing offsets in DICOMDIR");
+      }          
+
+      nextOffsets_[i] = next;
+      lowerOffsets_[i] = lower;
+
+      std::unique_ptr<DicomMap> entry(new DicomMap);
+      FromDcmtkBridge::ExtractDicomSummary(*entry, *item);
+
+      if (next != 0)
+      {
+        availableOffsets.insert(next);
+      }
+
+      if (lower != 0)
+      {
+        availableOffsets.insert(lower);
+      }
+
+      content_[i] = entry.release();
+    }
+
+    if (content_.size() != availableOffsets.size())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Inconsistent offsets in DICOMDIR");
+    }
+
+    unsigned int index = 0;
+    for (std::set<uint32_t>::const_iterator it = availableOffsets.begin();
+         it != availableOffsets.end(); ++it)
+    {
+      offsetToIndex_[*it] = index;
+      index ++;
+    }    
+  }
+
+
+  const DicomMap& ParsedDicomDir::GetItem(size_t i) const
+  {
+    if (i >= content_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(content_[i] != NULL);
+      return *content_[i];
+    }
+  }
+
+
+  bool ParsedDicomDir::LookupNext(size_t& target,
+                                  size_t index) const
+  {
+    if (index >= nextOffsets_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return LookupIndexOfOffset(target, nextOffsets_[index]);
+    }
+  }
+
+
+  bool ParsedDicomDir::LookupLower(size_t& target,
+                                   size_t index) const
+  {
+    if (index >= lowerOffsets_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return LookupIndexOfOffset(target, lowerOffsets_[index]);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomDir.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 to use this file
+#endif
+
+#include "../DicomFormat/DicomMap.h"
+
+namespace Orthanc
+{
+  class ParsedDicomDir : public boost::noncopyable
+  {
+  private:
+    typedef std::map<uint32_t, size_t>  OffsetToIndex;
+
+    std::vector<DicomMap*>  content_;
+    std::vector<size_t>     nextOffsets_;
+    std::vector<size_t>     lowerOffsets_;
+    OffsetToIndex           offsetToIndex_;
+
+    void Clear();
+
+    bool LookupIndexOfOffset(size_t& target,
+                             unsigned int offset) const;
+
+  public:
+    ParsedDicomDir(const std::string content);
+
+    ~ParsedDicomDir()
+    {
+      Clear();
+    }
+
+    size_t GetSize() const
+    {
+      return content_.size();
+    }
+
+    const DicomMap& GetItem(size_t i) const;
+
+    bool LookupNext(size_t& target,
+                    size_t index) const;
+
+    bool LookupLower(size_t& target,
+                     size_t index) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1742 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Internals/DicomFrameIndex.h"
+#include "ToDcmtkBridge.h"
+
+#include "../Images/PamReader.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::unique_ptr<DcmFileFormat> file_;
+    std::unique_ptr<DicomFrameIndex>  frameIndex_;
+  };
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  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*/)
+        ORTHANC_OVERRIDE
+      {
+        // No support for compression
+        return HttpCompression_None;
+      }
+
+      virtual bool HasContentFilename(std::string& filename) ORTHANC_OVERRIDE
+      {
+        return false;
+      }
+
+      virtual std::string GetContentType() ORTHANC_OVERRIDE
+      {
+        return EnumerationToString(MimeType_Binary);
+      }
+
+      virtual uint64_t  GetContentLength() ORTHANC_OVERRIDE
+      {
+        return length_;
+      }
+ 
+      virtual bool ReadNextChunk() ORTHANC_OVERRIDE
+      {
+        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())
+          {
+            throw OrthancException(ErrorCode_InternalError,
+                                   "Error while sending a DICOM field: " +
+                                   std::string(cond.text()));
+          }
+
+          return true;
+        }
+      }
+ 
+      virtual const char *GetChunkContent() ORTHANC_OVERRIDE
+      {
+        return chunk_.c_str();
+      }
+ 
+      virtual size_t GetChunkSize() ORTHANC_OVERRIDE
+      {
+        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, MimeType_Binary);
+                return true;
+              }
+
+              Uint8* buffer = NULL;
+              if (pixelItem->getUint8Array(buffer).good() && buffer)
+              {
+                output.AnswerBuffer(buffer, pixelItem->getLength(), MimeType_Binary);
+                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 = GetDcmtkObject().getDataset();
+    E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer();
+
+    // 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 = GetDcmtkObject().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 = GetDcmtkObject().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 = *GetDcmtkObject().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,
+                               const std::string& privateCreator)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    if (GetDcmtkObject().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();
+
+    bool hasCodeExtensions;
+    Encoding encoding = DetectEncoding(hasCodeExtensions);
+    std::unique_ptr<DcmElement> element(FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
+    InsertInternal(*GetDcmtkObject().getDataset(), element.release());
+  }
+
+
+  void ParsedDicomFile::ReplacePlainString(const DicomTag& tag,
+                                           const std::string& utf8Value)
+  {
+    if (tag.IsPrivate())
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot apply this function to private tags: " + tag.Format());
+    }
+    else
+    {
+      Replace(tag, utf8Value, false, DicomReplaceMode_InsertIfAbsent,
+              "" /* not a private tag, so no private creator */);
+    }
+  }
+
+
+  void ParsedDicomFile::SetIfAbsent(const DicomTag& tag,
+                                    const std::string& utf8Value)
+  {
+    std::string currentValue;
+    if (!GetTagValue(currentValue, tag))
+    {
+      ReplacePlainString(tag, utf8Value);
+    }
+  }
+
+
+  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, URI_SCHEME_PREFIX_BINARY))
+    {
+      std::string mime;
+      if (!Toolbox::DecodeDataUriScheme(mime, binary, utf8Value))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      decoded = &binary;
+    }
+    else
+    {
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      if (encoding != 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,
+                                const std::string& privateCreator)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    InvalidateCache();
+
+    DcmDataset& dicom = *GetDcmtkObject().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::unique_ptr<DcmElement> element(FromDcmtkBridge::CreateElementForTag(tag, privateCreator));
+
+      if (!utf8Value.empty())
+      {
+        bool hasCodeExtensions;
+        Encoding encoding = DetectEncoding(hasCodeExtensions);
+        FromDcmtkBridge::FillElementWithString(*element, utf8Value, decodeDataUriScheme, encoding);
+      }
+
+      InsertInternal(dicom, element.release());
+      UpdateStorageUid(tag, utf8Value, false);
+    }
+  }
+
+    
+  void ParsedDicomFile::Replace(const DicomTag& tag,
+                                const Json::Value& value,
+                                bool decodeDataUriScheme,
+                                DicomReplaceMode mode,
+                                const std::string& privateCreator)
+  {
+    if (tag.GetElement() == 0x0000)
+    {
+      // Prevent manually modifying generic group length tags: This is
+      // handled by DCMTK serialization
+      return;
+    }
+
+    InvalidateCache();
+
+    DcmDataset& dicom = *GetDcmtkObject().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;
+        }
+      }
+
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      InsertInternal(dicom, FromDcmtkBridge::FromJson(tag, value, decodeDataUriScheme, encoding, privateCreator));
+
+      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, *GetDcmtkObject().getDataset()))
+    {
+      output.AnswerBuffer(serialized, MimeType_Dicom);
+    }
+  }
+#endif
+
+
+  bool ParsedDicomFile::GetTagValue(std::string& value,
+                                    const DicomTag& tag)
+  {
+    DcmTagKey k(tag.GetGroup(), tag.GetElement());
+    DcmDataset& dataset = *GetDcmtkObject().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;
+      }
+
+      bool hasCodeExtensions;
+      Encoding encoding = DetectEncoding(hasCodeExtensions);
+      
+      std::set<DicomTag> tmp;
+      std::unique_ptr<DicomValue> v(FromDcmtkBridge::ConvertLeafElement
+                                    (*element, DicomToJsonFlags_Default, 
+                                     0, encoding, hasCodeExtensions, 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))
+    {
+      /**
+       * If "PatientID" is absent, be tolerant by considering it
+       * equals the empty string, then proceed. In Orthanc <= 1.5.6,
+       * an exception "Bad file format" was generated.
+       * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ
+       * https://hg.orthanc-server.com/orthanc/rev/4c45e018bd3de3cfa21d6efc6734673aaaee4435
+       **/
+      patientId.clear();
+    }        
+    
+    if (!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, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
+    }
+
+    return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid);
+  }
+
+
+  void ParsedDicomFile::SaveToMemoryBuffer(std::string& buffer)
+  {
+    FromDcmtkBridge::SaveToMemoryBuffer(buffer, *GetDcmtkObject().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,
+                                           bool permissive,
+                                           const std::string& defaultPrivateCreator,
+                                           const std::map<uint16_t, std::string>& privateCreators)
+  {
+    pimpl_->file_.reset(new DcmFileFormat);
+    InvalidateCache();
+
+    const DicomValue* tmp = source.TestAndGetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    if (tmp == NULL)
+    {
+      SetEncoding(defaultEncoding);
+    }
+    else if (tmp->IsBinary())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Invalid binary string in the SpecificCharacterSet (0008,0005) tag");
+    }
+    else if (tmp->IsNull() ||
+             tmp->GetContent().empty())
+    {
+      SetEncoding(defaultEncoding);
+    }
+    else
+    {
+      Encoding encoding;
+
+      if (GetDicomEncoding(encoding, tmp->GetContent().c_str()))
+      {
+        SetEncoding(encoding);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Unsupported value for the SpecificCharacterSet (0008,0005) tag: \"" +
+                               tmp->GetContent() + "\"");
+      }
+    }
+
+    for (DicomMap::Content::const_iterator 
+           it = source.content_.begin(); it != source.content_.end(); ++it)
+    {
+      if (it->first != DICOM_TAG_SPECIFIC_CHARACTER_SET &&
+          !it->second->IsNull())
+      {
+        try
+        {
+          // Same as "ReplacePlainString()", but with support for private creator
+          const std::string& utf8Value = it->second->GetContent();
+
+          std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(it->first.GetGroup());
+          
+          if (it->first.IsPrivate() &&
+              found != privateCreators.end())
+          {
+            Replace(it->first, utf8Value, false, DicomReplaceMode_InsertIfAbsent, found->second);
+          }
+          else
+          {
+            Replace(it->first, utf8Value, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
+          }
+        }
+        catch (OrthancException&)
+        {
+          if (!permissive)
+          {
+            throw;
+          }
+        }
+      }
+    }
+  }
+
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
+                                   Encoding defaultEncoding,
+                                   bool permissive) :
+    pimpl_(new PImpl)
+  {
+    std::map<uint16_t, std::string> noPrivateCreators;
+    CreateFromDicomMap(map, defaultEncoding, permissive, "" /* no default private creator */, noPrivateCreators);
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(const DicomMap& map,
+                                   Encoding defaultEncoding,
+                                   bool permissive,
+                                   const std::string& defaultPrivateCreator,
+                                   const std::map<uint16_t, std::string>& privateCreators) :
+    pimpl_(new PImpl)
+  {
+    CreateFromDicomMap(map, defaultEncoding, permissive, defaultPrivateCreator, privateCreators);
+  }
+
+
+  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.GetDcmtkObject().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));
+  }
+
+
+  ParsedDicomFile::ParsedDicomFile(DcmFileFormat* dicom) : pimpl_(new PImpl)
+  {
+    pimpl_->file_.reset(dicom);  // No cloning
+  }
+
+
+  DcmFileFormat& ParsedDicomFile::GetDcmtkObject() const
+  {
+    if (pimpl_->file_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "ReleaseDcmtkObject() was called");
+    }
+    else
+    {
+      return *pimpl_->file_;
+    }
+  }
+
+
+  DcmFileFormat* ParsedDicomFile::ReleaseDcmtkObject()
+  {
+    if (pimpl_->file_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "ReleaseDcmtkObject() was called");
+    }
+    else
+    {
+      pimpl_->frameIndex_.reset(NULL);
+      return pimpl_->file_.release();
+    }
+  }
+
+
+  ParsedDicomFile* ParsedDicomFile::Clone(bool keepSopInstanceUid)
+  {
+    return new ParsedDicomFile(*this, keepSopInstanceUid);
+  }
+
+
+  bool ParsedDicomFile::EmbedContentInternal(const std::string& dataUriScheme)
+  {
+    std::string mimeString, content;
+    if (!Toolbox::DecodeDataUriScheme(mimeString, content, dataUriScheme))
+    {
+      return false;
+    }
+
+    Toolbox::ToLowerCase(mimeString);
+    MimeType mime = StringToMimeType(mimeString);
+
+    switch (mime)
+    {
+      case MimeType_Png:
+#if ORTHANC_ENABLE_PNG == 1
+        EmbedImage(mime, content);
+        break;
+#else
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Orthanc was compiled without support of PNG");
+#endif
+
+      case MimeType_Jpeg:
+#if ORTHANC_ENABLE_JPEG == 1
+        EmbedImage(mime, content);
+        break;
+#else
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Orthanc was compiled without support of JPEG");
+#endif
+
+      case MimeType_Pam:
+        EmbedImage(mime, content);
+        break;
+
+      case MimeType_Pdf:
+        EmbedPdf(content);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Unsupported MIME type for the content of a new DICOM file: " +
+                               std::string(EnumerationToString(mime)));
+    }
+
+    return true;
+  }
+
+
+  void ParsedDicomFile::EmbedContent(const std::string& dataUriScheme)
+  {
+    if (!EmbedContentInternal(dataUriScheme))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void ParsedDicomFile::EmbedImage(MimeType mime,
+                                   const std::string& content)
+  {
+    switch (mime)
+    {
+    
+#if ORTHANC_ENABLE_JPEG == 1
+      case MimeType_Jpeg:
+      {
+        JpegReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+#endif
+    
+#if ORTHANC_ENABLE_PNG == 1
+      case MimeType_Png:
+      {
+        PngReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+#endif
+
+      case MimeType_Pam:
+      {
+        PamReader reader;
+        reader.ReadFromMemory(content);
+        EmbedImage(reader);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  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");
+
+    // The "Number of frames" must only be present in multi-frame images
+    //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
+    }
+
+    unsigned int bytesPerPixel = 0;
+
+    switch (accessor.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        // By default, grayscale images are MONOCHROME2
+        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
+        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;
+
+        // "Planar configuration" must only present if "Samples per
+        // Pixel" is greater than 1
+        ReplacePlainString(DICOM_TAG_PLANAR_CONFIGURATION, "0");  // Color channels are interleaved
+
+        break;
+
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+        // By default, grayscale images are MONOCHROME2
+        SetIfAbsent(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2");
+
+        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::unique_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 (!GetDcmtkObject().getDataset()->insert(pixels.release(), false, false).good())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }    
+  }
+
+  
+  Encoding ParsedDicomFile::DetectEncoding(bool& hasCodeExtensions) const
+  {
+    return FromDcmtkBridge::DetectEncoding(hasCodeExtensions,
+                                           *GetDcmtkObject().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, *GetDcmtkObject().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, *GetDcmtkObject().getDataset(),
+                                        format, flags, maxStringLength,
+                                        GetDefaultDicomEncoding(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target,
+                                      const std::set<DicomTag>& ignoreTagLength)
+  {
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::DatasetToJson(Json::Value& target)
+  {
+    const std::set<DicomTag> ignoreTagLength;
+    FromDcmtkBridge::ExtractDicomAsJson(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
+  }
+
+
+  void ParsedDicomFile::HeaderToJson(Json::Value& target, 
+                                     DicomToJsonFormat format)
+  {
+    FromDcmtkBridge::ExtractHeaderAsJson(target, *GetDcmtkObject().getMetaInfo(), format, DicomToJsonFlags_None, 0);
+  }
+
+
+  bool ParsedDicomFile::HasTag(const DicomTag& tag) const
+  {
+    DcmTag key(tag.GetGroup(), tag.GetElement());
+    return GetDcmtkObject().getDataset()->tagExists(key);
+  }
+
+
+  void ParsedDicomFile::EmbedPdf(const std::string& pdf)
+  {
+    if (pdf.size() < 5 ||  // (*)
+        strncmp("%PDF-", pdf.c_str(), 5) != 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Not a PDF file");
+    }
+
+    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), MIME_PDF);
+    //ReplacePlainString(FromDcmtkBridge::Convert(DCM_SeriesNumber), "1");
+
+    std::unique_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 = GetDcmtkObject().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 != MIME_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 std::string& privateCreator)
+  {
+    const bool generateIdentifiers = (flags & DicomFromJsonFlags_GenerateIdentifiers) ? true : false;
+    const bool decodeDataUriScheme = (flags & DicomFromJsonFlags_DecodeDataUriScheme) ? true : false;
+
+    std::unique_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, privateCreator);
+      }
+    }
+
+    return result.release();
+  }
+
+
+  void ParsedDicomFile::GetRawFrame(std::string& target,
+                                    MimeType& mime,
+                                    unsigned int frameId)
+  {
+    if (pimpl_->frameIndex_.get() == NULL)
+    {
+      assert(pimpl_->file_ != NULL &&
+             GetDcmtkObject().getDataset() != NULL);
+      pimpl_->frameIndex_.reset(new DicomFrameIndex(*GetDcmtkObject().getDataset()));
+    }
+
+    pimpl_->frameIndex_->GetRawFrame(target, frameId);
+
+    E_TransferSyntax transferSyntax = GetDcmtkObject().getDataset()->getCurrentXfer();
+    switch (transferSyntax)
+    {
+      case EXS_JPEGProcess1:
+        mime = MimeType_Jpeg;
+        break;
+       
+      case EXS_JPEG2000LosslessOnly:
+      case EXS_JPEG2000:
+        mime = MimeType_Jpeg2000;
+        break;
+
+      default:
+        mime = MimeType_Binary;
+        break;
+    }
+  }
+
+
+  void ParsedDicomFile::InvalidateCache()
+  {
+    pimpl_->frameIndex_.reset(NULL);
+  }
+
+
+  unsigned int ParsedDicomFile::GetFramesCount() const
+  {
+    assert(pimpl_->file_ != NULL &&
+           GetDcmtkObject().getDataset() != NULL);
+    return DicomFrameIndex::GetFramesCount(*GetDcmtkObject().getDataset());
+  }
+
+
+  void ParsedDicomFile::ChangeEncoding(Encoding target)
+  {
+    bool hasCodeExtensions;
+    Encoding source = DetectEncoding(hasCodeExtensions);
+
+    if (source != target)  // Avoid unnecessary conversion
+    {
+      ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, GetDicomSpecificCharacterSet(target));
+      FromDcmtkBridge::ChangeStringEncoding(*GetDcmtkObject().getDataset(), source, hasCodeExtensions, target);
+    }
+  }
+
+
+  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target) const
+  {
+    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset());
+  }
+
+
+  void ParsedDicomFile::ExtractDicomSummary(DicomMap& target,
+                                            const std::set<DicomTag>& ignoreTagLength) const
+  {
+    FromDcmtkBridge::ExtractDicomSummary(target, *GetDcmtkObject().getDataset(), ignoreTagLength);
+  }
+
+
+  bool ParsedDicomFile::LookupTransferSyntax(std::string& result)
+  {
+#if 0
+    // This was the implementation in Orthanc <= 1.6.1
+
+    // TODO - Shouldn't "dataset.getCurrentXfer()" be used instead of
+    // using the meta header?
+    const char* value = NULL;
+
+    if (GetDcmtkObject().getMetaInfo() != NULL &&
+        GetDcmtkObject().getMetaInfo()->findAndGetString(DCM_TransferSyntaxUID, value).good() &&
+        value != NULL)
+    {
+      result.assign(value);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+#else
+    DicomTransferSyntax s;
+    if (FromDcmtkBridge::LookupOrthancTransferSyntax(s, GetDcmtkObject()))
+    {
+      result.assign(GetTransferSyntaxUid(s));
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+#endif
+  }
+
+
+  bool ParsedDicomFile::LookupPhotometricInterpretation(PhotometricInterpretation& result) const
+  {
+    DcmTagKey k(DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetGroup(),
+                DICOM_TAG_PHOTOMETRIC_INTERPRETATION.GetElement());
+
+    DcmDataset& dataset = *GetDcmtkObject().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(*GetDcmtkObject().getDataset(), visitor, GetDefaultDicomEncoding());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,270 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+
+#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
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error The macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK != 1
+#  error The macro ORTHANC_ENABLE_DCMTK must be set to 1 to use this file
+#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 ORTHANC_PUBLIC ParsedDicomFile : public IDynamicObject
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    ParsedDicomFile(ParsedDicomFile& other,
+                    bool keepSopInstanceUid);
+
+    void CreateFromDicomMap(const DicomMap& source,
+                            Encoding defaultEncoding,
+                            bool permissive,
+                            const std::string& defaultPrivateCreator,
+                            const std::map<uint16_t, std::string>& privateCreators);
+
+    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);
+
+    ParsedDicomFile(DcmFileFormat* dicom);  // This takes ownership (no clone)
+
+  public:
+    ParsedDicomFile(bool createIdentifiers);  // Create a minimal DICOM instance
+
+    ParsedDicomFile(const DicomMap& map,
+                    Encoding defaultEncoding,
+                    bool permissive
+                    );
+
+    ParsedDicomFile(const DicomMap& map,
+                    Encoding defaultEncoding,
+                    bool permissive,
+                    const std::string& defaultPrivateCreator,
+                    const std::map<uint16_t, std::string>& privateCreators
+                    );
+
+    ParsedDicomFile(const void* content,
+                    size_t size);
+
+    ParsedDicomFile(const std::string& content);
+
+    ParsedDicomFile(DcmDataset& dicom);  // This clones the DCMTK object
+
+    ParsedDicomFile(DcmFileFormat& dicom);  // This clones the DCMTK object
+
+    static ParsedDicomFile* AcquireDcmtkObject(DcmFileFormat* dicom)  // No clone here
+    {
+      return new ParsedDicomFile(dicom);
+    }
+
+    DcmFileFormat& GetDcmtkObject() const;
+
+    // The "ParsedDicomFile" object cannot be used after calling this method
+    DcmFileFormat* ReleaseDcmtkObject();
+
+    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,
+                 const std::string& privateCreator /* used only for private tags */);
+
+    void Replace(const DicomTag& tag,
+                 const Json::Value& value,  // Assumed to be encoded with UTF-8
+                 bool decodeDataUriScheme,
+                 DicomReplaceMode mode,
+                 const std::string& privateCreator /* used only for private tags */);
+
+    void Insert(const DicomTag& tag,
+                const Json::Value& value,   // Assumed to be encoded with UTF-8
+                bool decodeDataUriScheme,
+                const std::string& privateCreator /* used only for private tags */);
+
+    // Cannot be applied to private tags
+    void ReplacePlainString(const DicomTag& tag,
+                            const std::string& utf8Value);
+
+    // Cannot be applied to private tags
+    void SetIfAbsent(const DicomTag& tag,
+                     const std::string& utf8Value);
+
+    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);
+
+    void EmbedImage(MimeType mime,
+                    const std::string& content);
+
+    Encoding DetectEncoding(bool& hasCodeExtensions) 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
+                     MimeType& mime,   // OUT
+                     unsigned int frameId);  // IN
+
+    unsigned int GetFramesCount() const;
+
+    static ParsedDicomFile* CreateFromJson(const Json::Value& value,
+                                           DicomFromJsonFlags flags,
+                                           const std::string& privateCreator);
+
+    void ChangeEncoding(Encoding target);
+
+    void ExtractDicomSummary(DicomMap& target) const;
+
+    void ExtractDicomSummary(DicomMap& target,
+                             const std::set<DicomTag>& ignoreTagLength) 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/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/DicomParsing/ToDcmtkBridge.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/Endianness.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/EnumerationDictionary.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,119 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancException.h"
+
+#include "Toolbox.h"
+#include <boost/lexical_cast.hpp>
+#include <string>
+#include <map>
+
+namespace Orthanc
+{
+  template <typename Enumeration>
+  class EnumerationDictionary
+  {
+  private:
+    typedef std::map<Enumeration, std::string>  EnumerationToString;
+    typedef std::map<std::string, Enumeration>  StringToEnumeration;
+
+    EnumerationToString enumerationToString_;
+    StringToEnumeration stringToEnumeration_;
+
+  public:
+    void Clear()
+    {
+      enumerationToString_.clear();
+      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
+      if (enumerationToString_.find(value) != enumerationToString_.end() ||
+          stringToEnumeration_.find(str) != stringToEnumeration_.end() ||
+          Toolbox::IsInteger(str) /* Prevent the registration of a number */)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      // OK, the string is free and is not a number
+      enumerationToString_[value] = str;
+      stringToEnumeration_[str] = value;
+      stringToEnumeration_[boost::lexical_cast<std::string>(static_cast<int>(value))] = value;
+    }
+
+    Enumeration Translate(const std::string& str) const
+    {
+      if (Toolbox::IsInteger(str))
+      {
+        return static_cast<Enumeration>(boost::lexical_cast<int>(str));
+      }
+
+      typename StringToEnumeration::const_iterator
+        found = stringToEnumeration_.find(str);
+
+      if (found == stringToEnumeration_.end())
+      {
+        throw OrthancException(ErrorCode_InexistentItem);
+      }
+      else
+      {
+        return found->second;
+      }
+    }
+
+    std::string Translate(Enumeration e) const
+    {
+      typename EnumerationToString::const_iterator
+        found = enumerationToString_.find(e);
+
+      if (found == enumerationToString_.end())
+      {
+        // No name for this item
+        return boost::lexical_cast<std::string>(static_cast<int>(e));
+      }
+      else
+      {
+        return found->second;
+      }
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Enumerations.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2255 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Enumerations.h"
+
+#include "OrthancException.h"
+#include "Toolbox.h"
+#include "Logging.h"
+
+#include <boost/thread/mutex.hpp>
+#include <string.h>
+#include <cassert>
+
+namespace Orthanc
+{
+  static const char* const MIME_CSS = "text/css";
+  static const char* const MIME_DICOM = "application/dicom";
+  static const char* const MIME_GIF = "image/gif";
+  static const char* const MIME_GZIP = "application/gzip";
+  static const char* const MIME_HTML = "text/html";
+  static const char* const MIME_JAVASCRIPT = "application/javascript";
+  static const char* const MIME_JPEG2000 = "image/jp2";
+  static const char* const MIME_NACL = "application/x-nacl";
+  static const char* const MIME_PLAIN_TEXT = "text/plain";
+  static const char* const MIME_PNACL = "application/x-pnacl";
+  static const char* const MIME_SVG = "image/svg+xml";
+  static const char* const MIME_WEB_ASSEMBLY = "application/wasm";
+  static const char* const MIME_WOFF = "application/x-font-woff";
+  static const char* const MIME_WOFF2 = "font/woff2";
+  static const char* const MIME_XML_2 = "text/xml";
+  static const char* const MIME_ZIP = "application/zip";
+  static const char* const MIME_DICOM_WEB_JSON = "application/dicom+json";
+  static const char* const MIME_DICOM_WEB_XML = "application/dicom+xml";
+
+  // This function is autogenerated by the script
+  // "Resources/GenerateErrorCodes.py"
+  const char* EnumerationToString(ErrorCode error)
+  {
+    switch (error)
+    {
+      case ErrorCode_InternalError:
+        return "Internal error";
+
+      case ErrorCode_Success:
+        return "Success";
+
+      case ErrorCode_Plugin:
+        return "Error encountered within the plugin engine";
+
+      case ErrorCode_NotImplemented:
+        return "Not implemented yet";
+
+      case ErrorCode_ParameterOutOfRange:
+        return "Parameter out of range";
+
+      case ErrorCode_NotEnoughMemory:
+        return "The server hosting Orthanc is running out of memory";
+
+      case ErrorCode_BadParameterType:
+        return "Bad type for a parameter";
+
+      case ErrorCode_BadSequenceOfCalls:
+        return "Bad sequence of calls";
+
+      case ErrorCode_InexistentItem:
+        return "Accessing an inexistent item";
+
+      case ErrorCode_BadRequest:
+        return "Bad request";
+
+      case ErrorCode_NetworkProtocol:
+        return "Error in the network protocol";
+
+      case ErrorCode_SystemCommand:
+        return "Error while calling a system command";
+
+      case ErrorCode_Database:
+        return "Error with the database engine";
+
+      case ErrorCode_UriSyntax:
+        return "Badly formatted URI";
+
+      case ErrorCode_InexistentFile:
+        return "Inexistent file";
+
+      case ErrorCode_CannotWriteFile:
+        return "Cannot write to file";
+
+      case ErrorCode_BadFileFormat:
+        return "Bad file format";
+
+      case ErrorCode_Timeout:
+        return "Timeout";
+
+      case ErrorCode_UnknownResource:
+        return "Unknown resource";
+
+      case ErrorCode_IncompatibleDatabaseVersion:
+        return "Incompatible version of the database";
+
+      case ErrorCode_FullStorage:
+        return "The file storage is full";
+
+      case ErrorCode_CorruptedFile:
+        return "Corrupted file (e.g. inconsistent MD5 hash)";
+
+      case ErrorCode_InexistentTag:
+        return "Inexistent tag";
+
+      case ErrorCode_ReadOnly:
+        return "Cannot modify a read-only data structure";
+
+      case ErrorCode_IncompatibleImageFormat:
+        return "Incompatible format of the images";
+
+      case ErrorCode_IncompatibleImageSize:
+        return "Incompatible size of the images";
+
+      case ErrorCode_SharedLibrary:
+        return "Error while using a shared library (plugin)";
+
+      case ErrorCode_UnknownPluginService:
+        return "Plugin invoking an unknown service";
+
+      case ErrorCode_UnknownDicomTag:
+        return "Unknown DICOM tag";
+
+      case ErrorCode_BadJson:
+        return "Cannot parse a JSON document";
+
+      case ErrorCode_Unauthorized:
+        return "Bad credentials were provided to an HTTP request";
+
+      case ErrorCode_BadFont:
+        return "Badly formatted font file";
+
+      case ErrorCode_DatabasePlugin:
+        return "The plugin implementing a custom database back-end does not fulfill the proper interface";
+
+      case ErrorCode_StorageAreaPlugin:
+        return "Error in the plugin implementing a custom storage area";
+
+      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_BadGeometry:
+        return "Geometry error encountered in Stone";
+
+      case ErrorCode_SslInitialization:
+        return "Cannot initialize SSL encryption, check out your certificates";
+
+      case ErrorCode_SQLiteNotOpened:
+        return "SQLite: The database is not opened";
+
+      case ErrorCode_SQLiteAlreadyOpened:
+        return "SQLite: Connection is already open";
+
+      case ErrorCode_SQLiteCannotOpen:
+        return "SQLite: Unable to open the database";
+
+      case ErrorCode_SQLiteStatementAlreadyUsed:
+        return "SQLite: This cached statement is already being referred to";
+
+      case ErrorCode_SQLiteExecute:
+        return "SQLite: Cannot execute a command";
+
+      case ErrorCode_SQLiteRollbackWithoutTransaction:
+        return "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)";
+
+      case ErrorCode_SQLiteCommitWithoutTransaction:
+        return "SQLite: Committing a nonexistent transaction";
+
+      case ErrorCode_SQLiteRegisterFunction:
+        return "SQLite: Unable to register a function";
+
+      case ErrorCode_SQLiteFlush:
+        return "SQLite: Unable to flush the database";
+
+      case ErrorCode_SQLiteCannotRun:
+        return "SQLite: Cannot run a cached statement";
+
+      case ErrorCode_SQLiteCannotStep:
+        return "SQLite: Cannot step over a cached statement";
+
+      case ErrorCode_SQLiteBindOutOfRange:
+        return "SQLite: Bing a value while out of range (serious error)";
+
+      case ErrorCode_SQLitePrepareStatement:
+        return "SQLite: Cannot prepare a cached statement";
+
+      case ErrorCode_SQLiteTransactionAlreadyStarted:
+        return "SQLite: Beginning the same transaction twice";
+
+      case ErrorCode_SQLiteTransactionCommit:
+        return "SQLite: Failure when committing the transaction";
+
+      case ErrorCode_SQLiteTransactionBegin:
+        return "SQLite: Cannot start a transaction";
+
+      case ErrorCode_DirectoryOverFile:
+        return "The directory to be created is already occupied by a regular file";
+
+      case ErrorCode_FileStorageCannotWrite:
+        return "Unable to create a subdirectory or a file in the file storage";
+
+      case ErrorCode_DirectoryExpected:
+        return "The specified path does not point to a directory";
+
+      case ErrorCode_HttpPortInUse:
+        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 privileged or already in use";
+
+      case ErrorCode_BadHttpStatusInRest:
+        return "This HTTP status is not allowed in a REST API";
+
+      case ErrorCode_RegularFileExpected:
+        return "The specified path does not point to a regular file";
+
+      case ErrorCode_PathToExecutable:
+        return "Unable to get the path to the executable";
+
+      case ErrorCode_MakeDirectory:
+        return "Cannot create a directory";
+
+      case ErrorCode_BadApplicationEntityTitle:
+        return "An application entity title (AET) cannot be empty or be longer than 16 characters";
+
+      case ErrorCode_NoCFindHandler:
+        return "No request handler factory for DICOM C-FIND SCP";
+
+      case ErrorCode_NoCMoveHandler:
+        return "No request handler factory for DICOM C-MOVE SCP";
+
+      case ErrorCode_NoCStoreHandler:
+        return "No request handler factory for DICOM C-STORE SCP";
+
+      case ErrorCode_NoApplicationEntityFilter:
+        return "No application entity filter";
+
+      case ErrorCode_NoSopClassOrInstance:
+        return "DicomUserConnection: Unable to find the SOP class and instance";
+
+      case ErrorCode_NoPresentationContext:
+        return "DicomUserConnection: No acceptable presentation context for modality";
+
+      case ErrorCode_DicomFindUnavailable:
+        return "DicomUserConnection: The C-FIND command is not supported by the remote SCP";
+
+      case ErrorCode_DicomMoveUnavailable:
+        return "DicomUserConnection: The C-MOVE command is not supported by the remote SCP";
+
+      case ErrorCode_CannotStoreInstance:
+        return "Cannot store an instance";
+
+      case ErrorCode_CreateDicomNotString:
+        return "Only string values are supported when creating DICOM instances";
+
+      case ErrorCode_CreateDicomOverrideTag:
+        return "Trying to override a value inherited from a parent module";
+
+      case ErrorCode_CreateDicomUseContent:
+        return "Use \"Content\" to inject an image into a new DICOM instance";
+
+      case ErrorCode_CreateDicomNoPayload:
+        return "No payload is present for one instance in the series";
+
+      case ErrorCode_CreateDicomUseDataUriScheme:
+        return "The payload of the DICOM instance must be specified according to Data URI scheme";
+
+      case ErrorCode_CreateDicomBadParent:
+        return "Trying to attach a new DICOM instance to an inexistent resource";
+
+      case ErrorCode_CreateDicomParentIsInstance:
+        return "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)";
+
+      case ErrorCode_CreateDicomParentEncoding:
+        return "Unable to get the encoding of the parent resource";
+
+      case ErrorCode_UnknownModality:
+        return "Unknown modality";
+
+      case ErrorCode_BadJobOrdering:
+        return "Bad ordering of filters in a job";
+
+      case ErrorCode_JsonToLuaTable:
+        return "Cannot convert the given JSON object to a Lua table";
+
+      case ErrorCode_CannotCreateLua:
+        return "Cannot create the Lua context";
+
+      case ErrorCode_CannotExecuteLua:
+        return "Cannot execute a Lua command";
+
+      case ErrorCode_LuaAlreadyExecuted:
+        return "Arguments cannot be pushed after the Lua function is executed";
+
+      case ErrorCode_LuaBadOutput:
+        return "The Lua function does not give the expected number of outputs";
+
+      case ErrorCode_NotLuaPredicate:
+        return "The Lua function is not a predicate (only true/false outputs allowed)";
+
+      case ErrorCode_LuaReturnsNoString:
+        return "The Lua function does not return a string";
+
+      case ErrorCode_StorageAreaAlreadyRegistered:
+        return "Another plugin has already registered a custom storage area";
+
+      case ErrorCode_DatabaseBackendAlreadyRegistered:
+        return "Another plugin has already registered a custom database back-end";
+
+      case ErrorCode_DatabaseNotInitialized:
+        return "Plugin trying to call the database during its initialization";
+
+      case ErrorCode_SslDisabled:
+        return "Orthanc has been built without SSL support";
+
+      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";
+
+      case ErrorCode_NoCGetHandler:
+        return "No request handler factory for DICOM C-GET SCP";
+
+      case ErrorCode_NoStorageCommitmentHandler:
+        return "No request handler factory for DICOM N-ACTION SCP (storage commitment)";
+
+      case ErrorCode_UnsupportedMediaType:
+        return "Unsupported media type";
+
+      default:
+        if (error >= ErrorCode_START_PLUGINS)
+        {
+          return "Error encountered within some plugin";
+        }
+        else
+        {
+          return "Unknown error code";
+        }
+    }
+  }
+
+
+  const char* EnumerationToString(HttpMethod method)
+  {
+    switch (method)
+    {
+      case HttpMethod_Get:
+        return "GET";
+
+      case HttpMethod_Post:
+        return "POST";
+
+      case HttpMethod_Delete:
+        return "DELETE";
+
+      case HttpMethod_Put:
+        return "PUT";
+
+      default:
+        return "?";
+    }
+  }
+
+
+  const char* EnumerationToString(HttpStatus status)
+  {
+    switch (status)
+    {
+    case HttpStatus_100_Continue:
+      return "Continue";
+
+    case HttpStatus_101_SwitchingProtocols:
+      return "Switching Protocols";
+
+    case HttpStatus_102_Processing:
+      return "Processing";
+
+    case HttpStatus_200_Ok:
+      return "OK";
+
+    case HttpStatus_201_Created:
+      return "Created";
+
+    case HttpStatus_202_Accepted:
+      return "Accepted";
+
+    case HttpStatus_203_NonAuthoritativeInformation:
+      return "Non-Authoritative Information";
+
+    case HttpStatus_204_NoContent:
+      return "No Content";
+
+    case HttpStatus_205_ResetContent:
+      return "Reset Content";
+
+    case HttpStatus_206_PartialContent:
+      return "Partial Content";
+
+    case HttpStatus_207_MultiStatus:
+      return "Multi-Status";
+
+    case HttpStatus_208_AlreadyReported:
+      return "Already Reported";
+
+    case HttpStatus_226_IMUsed:
+      return "IM Used";
+
+    case HttpStatus_300_MultipleChoices:
+      return "Multiple Choices";
+
+    case HttpStatus_301_MovedPermanently:
+      return "Moved Permanently";
+
+    case HttpStatus_302_Found:
+      return "Found";
+
+    case HttpStatus_303_SeeOther:
+      return "See Other";
+
+    case HttpStatus_304_NotModified:
+      return "Not Modified";
+
+    case HttpStatus_305_UseProxy:
+      return "Use Proxy";
+
+    case HttpStatus_307_TemporaryRedirect:
+      return "Temporary Redirect";
+
+    case HttpStatus_400_BadRequest:
+      return "Bad Request";
+
+    case HttpStatus_401_Unauthorized:
+      return "Unauthorized";
+
+    case HttpStatus_402_PaymentRequired:
+      return "Payment Required";
+
+    case HttpStatus_403_Forbidden:
+      return "Forbidden";
+
+    case HttpStatus_404_NotFound:
+      return "Not Found";
+
+    case HttpStatus_405_MethodNotAllowed:
+      return "Method Not Allowed";
+
+    case HttpStatus_406_NotAcceptable:
+      return "Not Acceptable";
+
+    case HttpStatus_407_ProxyAuthenticationRequired:
+      return "Proxy Authentication Required";
+
+    case HttpStatus_408_RequestTimeout:
+      return "Request Timeout";
+
+    case HttpStatus_409_Conflict:
+      return "Conflict";
+
+    case HttpStatus_410_Gone:
+      return "Gone";
+
+    case HttpStatus_411_LengthRequired:
+      return "Length Required";
+
+    case HttpStatus_412_PreconditionFailed:
+      return "Precondition Failed";
+
+    case HttpStatus_413_RequestEntityTooLarge:
+      return "Request Entity Too Large";
+
+    case HttpStatus_414_RequestUriTooLong:
+      return "Request-URI Too Long";
+
+    case HttpStatus_415_UnsupportedMediaType:
+      return "Unsupported Media Type";
+
+    case HttpStatus_416_RequestedRangeNotSatisfiable:
+      return "Requested Range Not Satisfiable";
+
+    case HttpStatus_417_ExpectationFailed:
+      return "Expectation Failed";
+
+    case HttpStatus_422_UnprocessableEntity:
+      return "Unprocessable Entity";
+
+    case HttpStatus_423_Locked:
+      return "Locked";
+
+    case HttpStatus_424_FailedDependency:
+      return "Failed Dependency";
+
+    case HttpStatus_426_UpgradeRequired:
+      return "Upgrade Required";
+
+    case HttpStatus_500_InternalServerError:
+      return "Internal Server Error";
+
+    case HttpStatus_501_NotImplemented:
+      return "Not Implemented";
+
+    case HttpStatus_502_BadGateway:
+      return "Bad Gateway";
+
+    case HttpStatus_503_ServiceUnavailable:
+      return "Service Unavailable";
+
+    case HttpStatus_504_GatewayTimeout:
+      return "Gateway Timeout";
+
+    case HttpStatus_505_HttpVersionNotSupported:
+      return "HTTP Version Not Supported";
+
+    case HttpStatus_506_VariantAlsoNegotiates:
+      return "Variant Also Negotiates";
+
+    case HttpStatus_507_InsufficientStorage:
+      return "Insufficient Storage";
+
+    case HttpStatus_509_BandwidthLimitExceeded:
+      return "Bandwidth Limit Exceeded";
+
+    case HttpStatus_510_NotExtended:
+      return "Not Extended";
+
+    default:
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return "Patient";
+
+      case ResourceType_Study:
+        return "Study";
+
+      case ResourceType_Series:
+        return "Series";
+
+      case ResourceType_Instance:
+        return "Instance";
+      
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ImageFormat format)
+  {
+    switch (format)
+    {
+      case ImageFormat_Png:
+        return "Png";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(Encoding encoding)
+  {
+    switch (encoding)
+    {
+      case Encoding_Ascii:
+        return "Ascii";
+
+      case Encoding_Utf8:
+        return "Utf8";
+
+      case Encoding_Latin1:
+        return "Latin1";
+
+      case Encoding_Latin2:
+        return "Latin2";
+
+      case Encoding_Latin3:
+        return "Latin3";
+
+      case Encoding_Latin4:
+        return "Latin4";
+
+      case Encoding_Latin5:
+        return "Latin5";
+
+      case Encoding_Cyrillic:
+        return "Cyrillic";
+
+      case Encoding_Windows1251:
+        return "Windows1251";
+
+      case Encoding_Arabic:
+        return "Arabic";
+
+      case Encoding_Greek:
+        return "Greek";
+
+      case Encoding_Hebrew:
+        return "Hebrew";
+
+      case Encoding_Thai:
+        return "Thai";
+
+      case Encoding_Japanese:
+        return "Japanese";
+
+      case Encoding_Chinese:
+        return "Chinese";
+
+      case Encoding_Korean:
+        return "Korean";
+
+      case Encoding_JapaneseKanji:
+        return "JapaneseKanji";
+
+      case Encoding_SimplifiedChinese:
+        return "SimplifiedChinese";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(PhotometricInterpretation photometric)
+  {
+    switch (photometric)
+    {
+      case PhotometricInterpretation_RGB:
+        return "RGB";
+
+      case PhotometricInterpretation_Monochrome1:
+        return "MONOCHROME1";
+
+      case PhotometricInterpretation_Monochrome2:
+        return "MONOCHROME2";
+
+      case PhotometricInterpretation_ARGB:
+        return "ARGB";
+
+      case PhotometricInterpretation_CMYK:
+        return "CMYK";
+
+      case PhotometricInterpretation_HSV:
+        return "HSV";
+
+      case PhotometricInterpretation_Palette:
+        return "PALETTE COLOR";
+
+      case PhotometricInterpretation_YBRFull:
+        return "YBR_FULL";
+
+      case PhotometricInterpretation_YBRFull422:
+        return "YBR_FULL_422";
+
+      case PhotometricInterpretation_YBRPartial420:
+        return "YBR_PARTIAL_420"; 
+
+      case PhotometricInterpretation_YBRPartial422:
+        return "YBR_PARTIAL_422"; 
+
+      case PhotometricInterpretation_YBR_ICT:
+        return "YBR_ICT"; 
+
+      case PhotometricInterpretation_YBR_RCT:
+        return "YBR_RCT"; 
+
+      case PhotometricInterpretation_Unknown:
+        return "Unknown";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(RequestOrigin origin)
+  {
+    switch (origin)
+    {
+      case RequestOrigin_Unknown:
+        return "Unknown";
+
+      case RequestOrigin_DicomProtocol:
+        return "DicomProtocol";
+
+      case RequestOrigin_RestApi:
+        return "RestApi";
+
+      case RequestOrigin_Plugins:
+        return "Plugins";
+
+      case RequestOrigin_Lua:
+        return "Lua";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  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_Vitrea:
+        return "Vitrea";
+      
+      case ModalityManufacturer_GE:
+        return "GE";
+      
+      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;
+
+      case DicomRequestType_NAction:
+        return "N-ACTION";
+        break;
+
+      case DicomRequestType_NEventReport:
+        return "N-EVENT-REPORT";
+        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_Mpeg4:
+        return "MPEG4";
+
+      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);
+    }
+  }
+
+
+  const char* EnumerationToString(MimeType mime)
+  {
+    switch (mime)
+    {
+      case MimeType_Binary:
+        return MIME_BINARY;
+        
+      case MimeType_Dicom:
+        return MIME_DICOM;
+        
+      case MimeType_Jpeg:
+        return MIME_JPEG;
+        
+      case MimeType_Jpeg2000:
+        return MIME_JPEG2000;
+        
+      case MimeType_Json:
+        return MIME_JSON;
+        
+      case MimeType_Pdf:
+        return MIME_PDF;
+        
+      case MimeType_Png:
+        return MIME_PNG;
+        
+      case MimeType_Xml:
+        return MIME_XML;
+        
+      case MimeType_PlainText:
+        return MIME_PLAIN_TEXT;
+                
+      case MimeType_Pam:
+        return MIME_PAM;
+                
+      case MimeType_Html:
+        return MIME_HTML;
+                
+      case MimeType_Gzip:
+        return MIME_GZIP;
+                
+      case MimeType_JavaScript:
+        return MIME_JAVASCRIPT;
+                
+      case MimeType_Css:
+        return MIME_CSS;
+                
+      case MimeType_WebAssembly:
+        return MIME_WEB_ASSEMBLY;
+                
+      case MimeType_Gif:
+        return MIME_GIF;
+                
+      case MimeType_Zip:
+        return MIME_ZIP;
+                
+      case MimeType_NaCl:
+        return MIME_NACL;
+                
+      case MimeType_PNaCl:
+        return MIME_PNACL;
+                
+      case MimeType_Svg:
+        return MIME_SVG;
+                
+      case MimeType_Woff:
+        return MIME_WOFF;
+
+      case MimeType_Woff2:
+        return MIME_WOFF2;
+
+      case MimeType_PrometheusText:
+        // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
+        return "text/plain; version=0.0.4";
+
+      case MimeType_DicomWebJson:
+        return MIME_DICOM_WEB_JSON;
+                
+      case MimeType_DicomWebXml:
+        return MIME_DICOM_WEB_XML;
+                
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
+  const char* EnumerationToString(Endianness endianness)
+  {
+    switch (endianness)
+    {
+      case Endianness_Little:
+        return "Little-endian";
+
+      case Endianness_Big:
+        return "Big-endian";
+
+      case Endianness_Unknown:
+        return "Unknown endianness";
+                
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(StorageCommitmentFailureReason reason)
+  {
+    switch (reason)
+    {
+      case StorageCommitmentFailureReason_Success:
+        return "Success";
+
+      case StorageCommitmentFailureReason_ProcessingFailure:
+        return "A general failure in processing the operation was encountered";
+
+      case StorageCommitmentFailureReason_NoSuchObjectInstance:
+        return "One or more of the elements in the Referenced SOP "
+          "Instance Sequence was not available";
+        
+      case StorageCommitmentFailureReason_ResourceLimitation:
+        return "The SCP does not currently have enough resources to "
+          "store the requested SOP Instance(s)";
+
+      case StorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+        return "Storage Commitment has been requested for a SOP Instance "
+          "with a SOP Class that is not supported by the SCP";
+
+      case StorageCommitmentFailureReason_ClassInstanceConflict:
+        return "The SOP Class of an element in the Referenced SOP Instance Sequence "
+          "did not correspond to the SOP class registered for this SOP Instance at the SCP";
+
+      case StorageCommitmentFailureReason_DuplicateTransactionUID:
+        return "The Transaction UID of the Storage Commitment Request is already in use";
+
+      default:
+        return "Unknown failure reason";
+    }
+  }
+
+
+  Encoding StringToEncoding(const char* encoding)
+  {
+    std::string s(encoding);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "UTF8")
+    {
+      return Encoding_Utf8;
+    }
+
+    if (s == "ASCII")
+    {
+      return Encoding_Ascii;
+    }
+
+    if (s == "LATIN1")
+    {
+      return Encoding_Latin1;
+    }
+
+    if (s == "LATIN2")
+    {
+      return Encoding_Latin2;
+    }
+
+    if (s == "LATIN3")
+    {
+      return Encoding_Latin3;
+    }
+
+    if (s == "LATIN4")
+    {
+      return Encoding_Latin4;
+    }
+
+    if (s == "LATIN5")
+    {
+      return Encoding_Latin5;
+    }
+
+    if (s == "CYRILLIC")
+    {
+      return Encoding_Cyrillic;
+    }
+
+    if (s == "WINDOWS1251")
+    {
+      return Encoding_Windows1251;
+    }
+
+    if (s == "ARABIC")
+    {
+      return Encoding_Arabic;
+    }
+
+    if (s == "GREEK")
+    {
+      return Encoding_Greek;
+    }
+
+    if (s == "HEBREW")
+    {
+      return Encoding_Hebrew;
+    }
+
+    if (s == "THAI")
+    {
+      return Encoding_Thai;
+    }
+
+    if (s == "JAPANESE")
+    {
+      return Encoding_Japanese;
+    }
+
+    if (s == "CHINESE")
+    {
+      return Encoding_Chinese;
+    }
+
+    if (s == "KOREAN")
+    {
+      return Encoding_Korean;
+    }
+
+    if (s == "JAPANESEKANJI")
+    {
+      return Encoding_JapaneseKanji;
+    }
+
+    if (s == "SIMPLIFIEDCHINESE")
+    {
+      return Encoding_SimplifiedChinese;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  ResourceType StringToResourceType(const char* type)
+  {
+    std::string s(type);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "PATIENT" || s == "PATIENTS")
+    {
+      return ResourceType_Patient;
+    }
+    else if (s == "STUDY" || s == "STUDIES")
+    {
+      return ResourceType_Study;
+    }
+    else if (s == "SERIES")
+    {
+      return ResourceType_Series;
+    }
+    else if (s == "INSTANCE"  || s == "IMAGE" || 
+             s == "INSTANCES" || s == "IMAGES")
+    {
+      return ResourceType_Instance;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  ImageFormat StringToImageFormat(const char* format)
+  {
+    std::string s(format);
+    Toolbox::ToUpperCase(s);
+
+    if (s == "PNG")
+    {
+      return ImageFormat_Png;
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  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)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, s);
+      }
+      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 == "StoreScp")
+    {
+      return ModalityManufacturer_StoreScp;
+    }
+    else if (manufacturer == "Vitrea")
+    {
+      return ModalityManufacturer_Vitrea;
+    }
+    else if (manufacturer == "GE")
+    {
+      return ModalityManufacturer_GE;
+    }
+    else if (manufacturer == "AgfaImpax" ||
+             manufacturer == "SyngoVia")
+    {
+      result = ModalityManufacturer_GenericNoWildcardInDates;
+      obsolete = true;
+    }
+    else if (manufacturer == "EFilm2" ||
+             manufacturer == "MedInria" ||
+             manufacturer == "ClearCanvas" ||
+             manufacturer == "Dcm4Chee"
+             )
+    {
+      result = ModalityManufacturer_Generic;
+      obsolete = true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown modality manufacturer: \"" + manufacturer + "\"");
+    }
+
+    if (obsolete)
+    {
+      LOG(WARNING) << "The \"" << manufacturer << "\" manufacturer is now obsolete. "
+                   << "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);
+    }
+  }
+
+
+  MimeType StringToMimeType(const std::string& mime)
+  {
+    if (mime == MIME_BINARY)
+    {
+      return MimeType_Binary;
+    }
+    else if (mime == MIME_DICOM)
+    {
+      return MimeType_Dicom;
+    }
+    else if (mime == MIME_JPEG)
+    {
+      return MimeType_Jpeg;
+    }
+    else if (mime == MIME_JPEG2000)
+    {
+      return MimeType_Jpeg2000;
+    }
+    else if (mime == MIME_JSON)
+    {
+      return MimeType_Json;
+    }
+    else if (mime == MIME_PDF)
+    {
+      return MimeType_Pdf;
+    }
+    else if (mime == MIME_PNG)
+    {
+      return MimeType_Png;
+    }
+    else if (mime == MIME_XML ||
+             mime == MIME_XML_2)
+    {
+      return MimeType_Xml;
+    }
+    else if (mime == MIME_PLAIN_TEXT)
+    {
+      return MimeType_PlainText;
+    }
+    else if (mime == MIME_PAM)
+    {
+      return MimeType_Pam;
+    }
+    else if (mime == MIME_HTML)
+    {
+      return MimeType_Html;
+    }
+    else if (mime == MIME_GZIP)
+    {
+      return MimeType_Gzip;
+    }
+    else if (mime == MIME_JAVASCRIPT)
+    {
+      return MimeType_JavaScript;
+    }
+    else if (mime == MIME_CSS)
+    {
+      return MimeType_Css;
+    }
+    else if (mime == MIME_WEB_ASSEMBLY)
+    {
+      return MimeType_WebAssembly;
+    }
+    else if (mime == MIME_GIF)
+    {
+      return MimeType_Gif;
+    }
+    else if (mime == MIME_ZIP)
+    {
+      return MimeType_Zip;
+    }
+    else if (mime == MIME_NACL)
+    {
+      return MimeType_NaCl;
+    }
+    else if (mime == MIME_PNACL)
+    {
+      return MimeType_PNaCl;
+    }
+    else if (mime == MIME_SVG)
+    {
+      return MimeType_Svg;
+    }
+    else if (mime == MIME_WOFF)
+    {
+      return MimeType_Woff;
+    }
+    else if (mime == MIME_WOFF2)
+    {
+      return MimeType_Woff2;
+    }
+    else if (mime == MIME_DICOM_WEB_JSON)
+    {
+      return MimeType_DicomWebJson;
+    }
+    else if (mime == MIME_DICOM_WEB_XML)
+    {
+      return MimeType_DicomWebXml;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
+  unsigned int GetBytesPerPixel(PixelFormat format)
+  {
+    switch (format)
+    {
+      case PixelFormat_Grayscale8:
+        return 1;
+
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+        return 2;
+
+      case PixelFormat_RGB24:
+        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);
+    }
+  }
+
+
+  bool GetDicomEncoding(Encoding& encoding,
+                        const char* specificCharacterSet)
+  {
+    std::string s = Toolbox::StripSpaces(specificCharacterSet);
+    Toolbox::ToUpperCase(s);
+
+    // 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 2022 IR 6")
+    {
+      encoding = Encoding_Ascii;
+    }
+    else if (s == "ISO_IR 192")
+    {
+      encoding = Encoding_Utf8;
+    }
+    else if (s == "ISO_IR 100" ||
+             s == "ISO 2022 IR 100")
+    {
+      encoding = Encoding_Latin1;
+    }
+    else if (s == "ISO_IR 101" ||
+             s == "ISO 2022 IR 101")
+    {
+      encoding = Encoding_Latin2;
+    }
+    else if (s == "ISO_IR 109" ||
+             s == "ISO 2022 IR 109")
+    {
+      encoding = Encoding_Latin3;
+    }
+    else if (s == "ISO_IR 110" ||
+             s == "ISO 2022 IR 110")
+    {
+      encoding = Encoding_Latin4;
+    }
+    else if (s == "ISO_IR 148" ||
+             s == "ISO 2022 IR 148")
+    {
+      encoding = Encoding_Latin5;
+    }
+    else if (s == "ISO_IR 144" ||
+             s == "ISO 2022 IR 144")
+    {
+      encoding = Encoding_Cyrillic;
+    }
+    else if (s == "ISO_IR 127" ||
+             s == "ISO 2022 IR 127")
+    {
+      encoding = Encoding_Arabic;
+    }
+    else if (s == "ISO_IR 126" ||
+             s == "ISO 2022 IR 126")
+    {
+      encoding = Encoding_Greek;
+    }
+    else if (s == "ISO_IR 138" ||
+             s == "ISO 2022 IR 138")
+    {
+      encoding = Encoding_Hebrew;
+    }
+    else if (s == "ISO_IR 166" ||
+             s == "ISO 2022 IR 166")
+    {
+      encoding = Encoding_Thai;
+    }
+    else if (s == "ISO_IR 13" ||
+             s == "ISO 2022 IR 13")
+    {
+      encoding = Encoding_Japanese;
+    }
+    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;
+    }
+    else if (s == "ISO 2022 IR 149")
+    {
+      encoding = Encoding_Korean;
+    }
+    else if (s == "ISO 2022 IR 87")
+    {
+      encoding = Encoding_JapaneseKanji;
+    }
+    else if (s == "ISO 2022 IR 58")
+    {
+      encoding = Encoding_SimplifiedChinese;
+    }
+    /*
+      else if (s == "ISO 2022 IR 159")
+      {
+      TODO - Supplementary Kanji set
+      }
+    */
+    else
+    {
+      return false;
+    }
+
+    // The encoding was properly detected
+    return true;
+  }
+
+
+  ResourceType GetChildResourceType(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return ResourceType_Study;
+
+      case ResourceType_Study:
+        return ResourceType_Series;
+        
+      case ResourceType_Series:
+        return ResourceType_Instance;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  ResourceType GetParentResourceType(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Study:
+        return ResourceType_Patient;
+        
+      case ResourceType_Series:
+        return ResourceType_Study;
+
+      case ResourceType_Instance:
+        return ResourceType_Series;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference)
+  {
+    switch (reference)
+    {
+      case ResourceType_Patient:
+        return (level == ResourceType_Patient);
+
+      case ResourceType_Study:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study);
+
+      case ResourceType_Series:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series);
+
+      case ResourceType_Instance:
+        return (level == ResourceType_Patient ||
+                level == ResourceType_Study ||
+                level == ResourceType_Series ||
+                level == ResourceType_Instance);
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DicomModule GetModule(ResourceType type)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return DicomModule_Patient;
+
+      case ResourceType_Study:
+        return DicomModule_Study;
+        
+      case ResourceType_Series:
+        return DicomModule_Series;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+
+  const char* GetDicomSpecificCharacterSet(Encoding encoding)
+  {
+    // 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:
+        return "ISO_IR 192";
+
+      case Encoding_Latin1:
+        return "ISO_IR 100";
+
+      case Encoding_Latin2:
+        return "ISO_IR 101";
+
+      case Encoding_Latin3:
+        return "ISO_IR 109";
+
+      case Encoding_Latin4:
+        return "ISO_IR 110";
+
+      case Encoding_Latin5:
+        return "ISO_IR 148";
+
+      case Encoding_Cyrillic:
+        return "ISO_IR 144";
+
+      case Encoding_Arabic:
+        return "ISO_IR 127";
+
+      case Encoding_Greek:
+        return "ISO_IR 126";
+
+      case Encoding_Hebrew:
+        return "ISO_IR 138";
+
+      case Encoding_Japanese:
+        return "ISO_IR 13";
+
+      case Encoding_Chinese:
+        return "GB18030";
+
+      case Encoding_Thai:
+        return "ISO_IR 166";
+
+      case Encoding_Korean:
+        return "ISO 2022 IR 149";
+
+      case Encoding_JapaneseKanji:
+        return "ISO 2022 IR 87";
+
+      case Encoding_SimplifiedChinese:
+        return "ISO 2022 IR 58";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  // This function is autogenerated by the script
+  // "Resources/GenerateErrorCodes.py"
+  HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error)
+  {
+    switch (error)
+    {
+      case ErrorCode_Success:
+        return HttpStatus_200_Ok;
+
+      case ErrorCode_ParameterOutOfRange:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_BadParameterType:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_InexistentItem:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_BadRequest:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_UriSyntax:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_InexistentFile:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_BadFileFormat:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_UnknownResource:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_InexistentTag:
+        return HttpStatus_404_NotFound;
+
+      case ErrorCode_BadJson:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_Unauthorized:
+        return HttpStatus_401_Unauthorized;
+
+      case ErrorCode_NotAcceptable:
+        return HttpStatus_406_NotAcceptable;
+
+      case ErrorCode_DatabaseUnavailable:
+        return HttpStatus_503_ServiceUnavailable;
+
+      case ErrorCode_CreateDicomNotString:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomOverrideTag:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomUseContent:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomNoPayload:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomUseDataUriScheme:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomBadParent:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_CreateDicomParentIsInstance:
+        return HttpStatus_400_BadRequest;
+
+      case ErrorCode_UnsupportedMediaType:
+        return HttpStatus_415_UnsupportedMediaType;
+
+      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;
+  }
+}
+
+
+#include "./Enumerations_TransferSyntaxes.impl.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Enumerations.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#include <string>
+
+
+// Macro "ORTHANC_FORCE_INLINE" forces a function/method to be inlined
+#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
+
+
+// Macros "ORTHANC_OVERRIDE" and "ORTHANC_FINAL" wrap the "override"
+// and "final" keywords introduced in C++11, to do compile-time
+// checking of virtual methods
+// The __cplusplus macro is broken in Visual Studio up to 15.6 and, in
+// later versions, require the usage of the /Zc:__cplusplus flag
+// We thus use an alternate way of checking for 'override' support
+#ifdef ORTHANC_OVERRIDE_SUPPORTED
+#error ORTHANC_OVERRIDE_SUPPORTED cannot be defined at this point
+#endif 
+
+#if __cplusplus >= 201103L
+#  define ORTHANC_OVERRIDE_SUPPORTED 1
+#else
+#  ifdef _MSC_VER
+#    if _MSC_VER >= 1600
+#      define ORTHANC_OVERRIDE_SUPPORTED 1
+#    endif
+#  endif
+#endif
+
+#if ORTHANC_OVERRIDE_SUPPORTED
+// The override keyword (C++11) is enabled
+#  define ORTHANC_OVERRIDE  override 
+#  define ORTHANC_FINAL     final
+#else
+// The override keyword (C++11) is not available
+#  define ORTHANC_OVERRIDE
+#  define ORTHANC_FINAL
+#endif
+
+namespace Orthanc
+{
+  static const char* const URI_SCHEME_PREFIX_BINARY = "data:application/octet-stream;base64,";
+
+  static const char* const MIME_BINARY = "application/octet-stream";
+  static const char* const MIME_JPEG = "image/jpeg";
+  static const char* const MIME_JSON = "application/json";
+  static const char* const MIME_JSON_UTF8 = "application/json; charset=utf-8";
+  static const char* const MIME_PDF = "application/pdf";
+  static const char* const MIME_PNG = "image/png";
+  static const char* const MIME_XML = "application/xml";
+  static const char* const MIME_XML_UTF8 = "application/xml; charset=utf-8";
+
+  /**
+   * "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
+   **/
+  static const char* const MIME_PAM = "image/x-portable-arbitrarymap";
+
+
+  enum MimeType
+  {
+    MimeType_Binary,
+    MimeType_Css,
+    MimeType_Dicom,
+    MimeType_Gif,
+    MimeType_Gzip,
+    MimeType_Html,
+    MimeType_JavaScript,
+    MimeType_Jpeg,
+    MimeType_Jpeg2000,
+    MimeType_Json,
+    MimeType_NaCl,
+    MimeType_PNaCl,
+    MimeType_Pam,
+    MimeType_Pdf,
+    MimeType_PlainText,
+    MimeType_Png,
+    MimeType_Svg,
+    MimeType_WebAssembly,
+    MimeType_Xml,
+    MimeType_Woff,            // Web Open Font Format
+    MimeType_Woff2,
+    MimeType_Zip,
+    MimeType_PrometheusText,  // Prometheus text-based exposition format (for metrics)
+    MimeType_DicomWebJson,
+    MimeType_DicomWebXml
+  };
+
+  
+  enum Endianness
+  {
+    Endianness_Unknown,
+    Endianness_Big,
+    Endianness_Little
+  };
+
+  // This enumeration is autogenerated by the script
+  // "Resources/GenerateErrorCodes.py"
+  enum ErrorCode
+  {
+    ErrorCode_InternalError = -1    /*!< Internal error */,
+    ErrorCode_Success = 0    /*!< Success */,
+    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    /*!< 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 */,
+    ErrorCode_BadRequest = 8    /*!< Bad request */,
+    ErrorCode_NetworkProtocol = 9    /*!< Error in the network protocol */,
+    ErrorCode_SystemCommand = 10    /*!< Error while calling a system command */,
+    ErrorCode_Database = 11    /*!< Error with the database engine */,
+    ErrorCode_UriSyntax = 12    /*!< Badly formatted URI */,
+    ErrorCode_InexistentFile = 13    /*!< Inexistent file */,
+    ErrorCode_CannotWriteFile = 14    /*!< Cannot write to file */,
+    ErrorCode_BadFileFormat = 15    /*!< Bad file format */,
+    ErrorCode_Timeout = 16    /*!< Timeout */,
+    ErrorCode_UnknownResource = 17    /*!< Unknown resource */,
+    ErrorCode_IncompatibleDatabaseVersion = 18    /*!< Incompatible version of the database */,
+    ErrorCode_FullStorage = 19    /*!< The file storage is full */,
+    ErrorCode_CorruptedFile = 20    /*!< Corrupted file (e.g. inconsistent MD5 hash) */,
+    ErrorCode_InexistentTag = 21    /*!< Inexistent tag */,
+    ErrorCode_ReadOnly = 22    /*!< Cannot modify a read-only data structure */,
+    ErrorCode_IncompatibleImageFormat = 23    /*!< Incompatible format of the images */,
+    ErrorCode_IncompatibleImageSize = 24    /*!< Incompatible size of the images */,
+    ErrorCode_SharedLibrary = 25    /*!< Error while using a shared library (plugin) */,
+    ErrorCode_UnknownPluginService = 26    /*!< Plugin invoking an unknown service */,
+    ErrorCode_UnknownDicomTag = 27    /*!< Unknown DICOM tag */,
+    ErrorCode_BadJson = 28    /*!< Cannot parse a JSON document */,
+    ErrorCode_Unauthorized = 29    /*!< Bad credentials were provided to an HTTP request */,
+    ErrorCode_BadFont = 30    /*!< Badly formatted font file */,
+    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_BadGeometry = 38    /*!< Geometry error encountered in Stone */,
+    ErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
+    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 */,
+    ErrorCode_SQLiteStatementAlreadyUsed = 1003    /*!< SQLite: This cached statement is already being referred to */,
+    ErrorCode_SQLiteExecute = 1004    /*!< SQLite: Cannot execute a command */,
+    ErrorCode_SQLiteRollbackWithoutTransaction = 1005    /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */,
+    ErrorCode_SQLiteCommitWithoutTransaction = 1006    /*!< SQLite: Committing a nonexistent transaction */,
+    ErrorCode_SQLiteRegisterFunction = 1007    /*!< SQLite: Unable to register a function */,
+    ErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
+    ErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
+    ErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
+    ErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    ErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
+    ErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
+    ErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
+    ErrorCode_SQLiteTransactionBegin = 1015    /*!< SQLite: Cannot start a transaction */,
+    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 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 */,
+    ErrorCode_MakeDirectory = 2008    /*!< Cannot create a directory */,
+    ErrorCode_BadApplicationEntityTitle = 2009    /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */,
+    ErrorCode_NoCFindHandler = 2010    /*!< No request handler factory for DICOM C-FIND SCP */,
+    ErrorCode_NoCMoveHandler = 2011    /*!< No request handler factory for DICOM C-MOVE SCP */,
+    ErrorCode_NoCStoreHandler = 2012    /*!< No request handler factory for DICOM C-STORE SCP */,
+    ErrorCode_NoApplicationEntityFilter = 2013    /*!< No application entity filter */,
+    ErrorCode_NoSopClassOrInstance = 2014    /*!< DicomUserConnection: Unable to find the SOP class and instance */,
+    ErrorCode_NoPresentationContext = 2015    /*!< DicomUserConnection: No acceptable presentation context for modality */,
+    ErrorCode_DicomFindUnavailable = 2016    /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */,
+    ErrorCode_DicomMoveUnavailable = 2017    /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */,
+    ErrorCode_CannotStoreInstance = 2018    /*!< Cannot store an instance */,
+    ErrorCode_CreateDicomNotString = 2019    /*!< Only string values are supported when creating DICOM instances */,
+    ErrorCode_CreateDicomOverrideTag = 2020    /*!< Trying to override a value inherited from a parent module */,
+    ErrorCode_CreateDicomUseContent = 2021    /*!< Use \"Content\" to inject an image into a new DICOM instance */,
+    ErrorCode_CreateDicomNoPayload = 2022    /*!< No payload is present for one instance in the series */,
+    ErrorCode_CreateDicomUseDataUriScheme = 2023    /*!< The payload of the DICOM instance must be specified according to Data URI scheme */,
+    ErrorCode_CreateDicomBadParent = 2024    /*!< Trying to attach a new DICOM instance to an inexistent resource */,
+    ErrorCode_CreateDicomParentIsInstance = 2025    /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */,
+    ErrorCode_CreateDicomParentEncoding = 2026    /*!< Unable to get the encoding of the parent resource */,
+    ErrorCode_UnknownModality = 2027    /*!< Unknown modality */,
+    ErrorCode_BadJobOrdering = 2028    /*!< Bad ordering of filters in a job */,
+    ErrorCode_JsonToLuaTable = 2029    /*!< Cannot convert the given JSON object to a Lua table */,
+    ErrorCode_CannotCreateLua = 2030    /*!< Cannot create the Lua context */,
+    ErrorCode_CannotExecuteLua = 2031    /*!< Cannot execute a Lua command */,
+    ErrorCode_LuaAlreadyExecuted = 2032    /*!< Arguments cannot be pushed after the Lua function is executed */,
+    ErrorCode_LuaBadOutput = 2033    /*!< The Lua function does not give the expected number of outputs */,
+    ErrorCode_NotLuaPredicate = 2034    /*!< The Lua function is not a predicate (only true/false outputs allowed) */,
+    ErrorCode_LuaReturnsNoString = 2035    /*!< The Lua function does not return a string */,
+    ErrorCode_StorageAreaAlreadyRegistered = 2036    /*!< Another plugin has already registered a custom storage area */,
+    ErrorCode_DatabaseBackendAlreadyRegistered = 2037    /*!< Another plugin has already registered a custom database back-end */,
+    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_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
+    ErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    ErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
+    ErrorCode_START_PLUGINS = 1000000
+  };
+
+  // This enumeration is autogenerated by the script
+  // "Resources/GenerateTransferSyntaxes.py"
+  enum DicomTransferSyntax
+  {
+    DicomTransferSyntax_LittleEndianImplicit    /*!< Implicit VR Little Endian */,
+    DicomTransferSyntax_LittleEndianExplicit    /*!< Explicit VR Little Endian */,
+    DicomTransferSyntax_DeflatedLittleEndianExplicit    /*!< Deflated Explicit VR Little Endian */,
+    DicomTransferSyntax_BigEndianExplicit    /*!< Explicit VR Big Endian */,
+    DicomTransferSyntax_JPEGProcess1    /*!< JPEG Baseline (process 1, lossy) */,
+    DicomTransferSyntax_JPEGProcess2_4    /*!< JPEG Extended Sequential (processes 2 & 4) */,
+    DicomTransferSyntax_JPEGProcess3_5    /*!< JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess6_8    /*!< JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess7_9    /*!< JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess10_12    /*!< JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess11_13    /*!< JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess14    /*!< JPEG Lossless, Nonhierarchical with any selection value (process 14) */,
+    DicomTransferSyntax_JPEGProcess15    /*!< JPEG Lossless with any selection value, arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess16_18    /*!< JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess17_19    /*!< JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess20_22    /*!< JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess21_23    /*!< JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess24_26    /*!< JPEG Full Progression, Hierarchical (lossy, 8/12 bit) */,
+    DicomTransferSyntax_JPEGProcess25_27    /*!< JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess28    /*!< JPEG Lossless, Hierarchical */,
+    DicomTransferSyntax_JPEGProcess29    /*!< JPEG Lossless, Hierarchical, arithmetic coding */,
+    DicomTransferSyntax_JPEGProcess14SV1    /*!< JPEG Lossless, Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1]) */,
+    DicomTransferSyntax_JPEGLSLossless    /*!< JPEG-LS (lossless) */,
+    DicomTransferSyntax_JPEGLSLossy    /*!< JPEG-LS (lossy or near-lossless) */,
+    DicomTransferSyntax_JPEG2000LosslessOnly    /*!< JPEG 2000 (lossless) */,
+    DicomTransferSyntax_JPEG2000    /*!< JPEG 2000 (lossless or lossy) */,
+    DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly    /*!< JPEG 2000 part 2 multicomponent extensions (lossless) */,
+    DicomTransferSyntax_JPEG2000Multicomponent    /*!< JPEG 2000 part 2 multicomponent extensions (lossless or lossy) */,
+    DicomTransferSyntax_JPIPReferenced    /*!< JPIP Referenced */,
+    DicomTransferSyntax_JPIPReferencedDeflate    /*!< JPIP Referenced Deflate */,
+    DicomTransferSyntax_MPEG2MainProfileAtMainLevel    /*!< MPEG2 Main Profile / Main Level */,
+    DicomTransferSyntax_MPEG2MainProfileAtHighLevel    /*!< MPEG2 Main Profile / High Level */,
+    DicomTransferSyntax_MPEG4HighProfileLevel4_1    /*!< MPEG4 AVC/H.264 High Profile / Level 4.1 */,
+    DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1    /*!< MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1 */,
+    DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo    /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video */,
+    DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo    /*!< MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video */,
+    DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2    /*!< MPEG4 AVC/H.264 Stereo High Profile / Level 4.2 */,
+    DicomTransferSyntax_HEVCMainProfileLevel5_1    /*!< HEVC/H.265 Main Profile / Level 5.1 */,
+    DicomTransferSyntax_HEVCMain10ProfileLevel5_1    /*!< HEVC/H.265 Main 10 Profile / Level 5.1 */,
+    DicomTransferSyntax_RLELossless    /*!< RLE - Run Length Encoding (lossless) */,
+    DicomTransferSyntax_RFC2557MimeEncapsulation    /*!< RFC 2557 MIME Encapsulation */,
+    DicomTransferSyntax_XML    /*!< XML Encoding */
+  };
+
+
+  /**
+   * {summary}{The memory layout of the pixels (resp. voxels) of a 2D (resp. 3D) image.}
+   **/
+  enum PixelFormat
+  {
+    /**
+     * {summary}{Color image in RGB24 format.}
+     * {description}{This format describes a color image. The pixels are stored in 3
+     * consecutive bytes. The memory layout is RGB.}
+     **/
+    PixelFormat_RGB24 = 1,
+
+    /**
+     * {summary}{Color image in RGBA32 format.}
+     * {description}{This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is RGBA.}
+     **/
+    PixelFormat_RGBA32 = 2,
+
+    /**
+     * {summary}{Graylevel 8bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in one byte.}
+     **/
+    PixelFormat_Grayscale8 = 3,
+      
+    /**
+     * {summary}{Graylevel, unsigned 16bpp image.}
+     * {description}{The image is graylevel. Each pixel is unsigned and stored in two bytes.}
+     **/
+    PixelFormat_Grayscale16 = 4,
+      
+    /**
+     * {summary}{Graylevel, signed 16bpp image.}
+     * {description}{The image is graylevel. Each pixel is signed and stored in two bytes.}
+     **/
+    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 8 bytes.}
+     **/
+    PixelFormat_Grayscale64 = 10
+  };
+
+
+  /**
+   * {summary}{The extraction mode specifies the way the values of the pixels are scaled when downloading a 2D image.}
+   **/
+  enum ImageExtractionMode
+  {
+    /**
+     * {summary}{Rescaled to 8bpp.}
+     * {description}{The minimum value of the image is set to 0, and its maximum value is set to 255.}
+     **/
+    ImageExtractionMode_Preview = 1,
+
+    /**
+     * {summary}{Truncation to the [0, 255] range.}
+     **/
+    ImageExtractionMode_UInt8 = 2,
+
+    /**
+     * {summary}{Truncation to the [0, 65535] range.}
+     **/
+    ImageExtractionMode_UInt16 = 3,
+
+    /**
+     * {summary}{Truncation to the [-32768, 32767] range.}
+     **/
+    ImageExtractionMode_Int16 = 4
+  };
+
+
+  /**
+   * Most common, non-joke and non-experimental HTTP status codes
+   * http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+   **/
+  enum HttpStatus
+  {
+    HttpStatus_None = -1,
+
+    // 1xx Informational
+    HttpStatus_100_Continue = 100,
+    HttpStatus_101_SwitchingProtocols = 101,
+    HttpStatus_102_Processing = 102,
+
+    // 2xx Success
+    HttpStatus_200_Ok = 200,
+    HttpStatus_201_Created = 201,
+    HttpStatus_202_Accepted = 202,
+    HttpStatus_203_NonAuthoritativeInformation = 203,
+    HttpStatus_204_NoContent = 204,
+    HttpStatus_205_ResetContent = 205,
+    HttpStatus_206_PartialContent = 206,
+    HttpStatus_207_MultiStatus = 207,
+    HttpStatus_208_AlreadyReported = 208,
+    HttpStatus_226_IMUsed = 226,
+
+    // 3xx Redirection
+    HttpStatus_300_MultipleChoices = 300,
+    HttpStatus_301_MovedPermanently = 301,
+    HttpStatus_302_Found = 302,
+    HttpStatus_303_SeeOther = 303,
+    HttpStatus_304_NotModified = 304,
+    HttpStatus_305_UseProxy = 305,
+    HttpStatus_307_TemporaryRedirect = 307,
+
+    // 4xx Client Error
+    HttpStatus_400_BadRequest = 400,
+    HttpStatus_401_Unauthorized = 401,
+    HttpStatus_402_PaymentRequired = 402,
+    HttpStatus_403_Forbidden = 403,
+    HttpStatus_404_NotFound = 404,
+    HttpStatus_405_MethodNotAllowed = 405,
+    HttpStatus_406_NotAcceptable = 406,
+    HttpStatus_407_ProxyAuthenticationRequired = 407,
+    HttpStatus_408_RequestTimeout = 408,
+    HttpStatus_409_Conflict = 409,
+    HttpStatus_410_Gone = 410,
+    HttpStatus_411_LengthRequired = 411,
+    HttpStatus_412_PreconditionFailed = 412,
+    HttpStatus_413_RequestEntityTooLarge = 413,
+    HttpStatus_414_RequestUriTooLong = 414,
+    HttpStatus_415_UnsupportedMediaType = 415,
+    HttpStatus_416_RequestedRangeNotSatisfiable = 416,
+    HttpStatus_417_ExpectationFailed = 417,
+    HttpStatus_422_UnprocessableEntity = 422,
+    HttpStatus_423_Locked = 423,
+    HttpStatus_424_FailedDependency = 424,
+    HttpStatus_426_UpgradeRequired = 426,
+
+    // 5xx Server Error
+    HttpStatus_500_InternalServerError = 500,
+    HttpStatus_501_NotImplemented = 501,
+    HttpStatus_502_BadGateway = 502,
+    HttpStatus_503_ServiceUnavailable = 503,
+    HttpStatus_504_GatewayTimeout = 504,
+    HttpStatus_505_HttpVersionNotSupported = 505,
+    HttpStatus_506_VariantAlsoNegotiates = 506,
+    HttpStatus_507_InsufficientStorage = 507,
+    HttpStatus_509_BandwidthLimitExceeded = 509,
+    HttpStatus_510_NotExtended = 510
+  };
+
+
+  enum HttpMethod
+  {
+    HttpMethod_Get = 0,
+    HttpMethod_Post = 1,
+    HttpMethod_Delete = 2,
+    HttpMethod_Put = 3
+  };
+
+
+  enum ImageFormat
+  {
+    ImageFormat_Png = 1
+  };
+
+
+  // https://en.wikipedia.org/wiki/HTTP_compression
+  enum HttpCompression
+  {
+    HttpCompression_None,
+    HttpCompression_Deflate,
+    HttpCompression_Gzip
+  };
+
+
+  // Specific Character Sets
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
+  enum Encoding
+  {
+    Encoding_Ascii,
+    Encoding_Utf8,
+    Encoding_Latin1,
+    Encoding_Latin2,
+    Encoding_Latin3,
+    Encoding_Latin4,
+    Encoding_Latin5,                        // Turkish
+    Encoding_Cyrillic,
+    Encoding_Windows1251,                   // Windows-1251 (commonly used for Cyrillic)
+    Encoding_Arabic,
+    Encoding_Greek,
+    Encoding_Hebrew,
+    Encoding_Thai,                          // TIS 620-2533
+    Encoding_Japanese,                      // JIS X 0201 (Shift JIS): Katakana
+    Encoding_Chinese,                       // GB18030 - Chinese simplified
+    Encoding_JapaneseKanji,                 // Multibyte - JIS X 0208: Kanji
+    //Encoding_JapaneseSupplementaryKanji,  // Multibyte - JIS X 0212: Supplementary Kanji set
+    Encoding_Korean,                        // Multibyte - KS X 1001: Hangul and Hanja
+    Encoding_SimplifiedChinese              // ISO 2022 IR 58
+  };
+
+
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.2
+  enum PhotometricInterpretation
+  {
+    PhotometricInterpretation_ARGB,  // Retired
+    PhotometricInterpretation_CMYK,  // Retired
+    PhotometricInterpretation_HSV,   // Retired
+    PhotometricInterpretation_Monochrome1,
+    PhotometricInterpretation_Monochrome2,
+    PhotometricInterpretation_Palette,
+    PhotometricInterpretation_RGB,
+    PhotometricInterpretation_YBRFull,
+    PhotometricInterpretation_YBRFull422,
+    PhotometricInterpretation_YBRPartial420,
+    PhotometricInterpretation_YBRPartial422,
+    PhotometricInterpretation_YBR_ICT,
+    PhotometricInterpretation_YBR_RCT,
+    PhotometricInterpretation_Unknown
+  };
+
+  enum DicomModule
+  {
+    DicomModule_Patient,
+    DicomModule_Study,
+    DicomModule_Series,
+    DicomModule_Instance,
+    DicomModule_Image
+  };
+
+  enum RequestOrigin
+  {
+    RequestOrigin_Unknown,
+    RequestOrigin_DicomProtocol,
+    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_Vitrea,
+    ModalityManufacturer_GE
+  };
+
+  enum DicomRequestType
+  {
+    DicomRequestType_Echo,
+    DicomRequestType_Find,
+    DicomRequestType_Get,
+    DicomRequestType_Move,
+    DicomRequestType_Store,
+    DicomRequestType_NAction,
+    DicomRequestType_NEventReport
+  };
+
+  enum TransferSyntax
+  {
+    TransferSyntax_Deflated,
+    TransferSyntax_Jpeg,
+    TransferSyntax_Jpeg2000,
+    TransferSyntax_JpegLossless,
+    TransferSyntax_Jpip,
+    TransferSyntax_Mpeg2,
+    TransferSyntax_Mpeg4,  // New in Orthanc 1.6.0
+    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
+  };
+
+  
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+  enum StorageCommitmentFailureReason
+  {
+    StorageCommitmentFailureReason_Success = 0,
+
+    // A general failure in processing the operation was encountered
+    StorageCommitmentFailureReason_ProcessingFailure = 0x0110,
+
+    // One or more of the elements in the Referenced SOP Instance
+    // Sequence was not available
+    StorageCommitmentFailureReason_NoSuchObjectInstance = 0x0112,
+
+    // The SCP does not currently have enough resources to store the
+    // requested SOP Instance(s)
+    StorageCommitmentFailureReason_ResourceLimitation = 0x0213,
+
+    // Storage Commitment has been requested for a SOP Instance with a
+    // SOP Class that is not supported by the SCP
+    StorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 0x0122,
+
+    // The SOP Class of an element in the Referenced SOP Instance
+    // Sequence did not correspond to the SOP class registered for
+    // this SOP Instance at the SCP
+    StorageCommitmentFailureReason_ClassInstanceConflict = 0x0119,
+
+    // The Transaction UID of the Storage Commitment Request is already in use
+    StorageCommitmentFailureReason_DuplicateTransactionUID = 0x0131
+  };
+
+
+  enum DicomAssociationRole
+  {
+    DicomAssociationRole_Default,
+    DicomAssociationRole_Scu,
+    DicomAssociationRole_Scp
+  };
+
+
+  /**
+   * WARNING: Do not change the explicit values in the enumerations
+   * below this point. This would result in incompatible databases
+   * between versions of Orthanc!
+   **/
+
+  enum CompressionType
+  {
+    /**
+     * Buffer/file that is stored as-is, in a raw fashion, without
+     * compression.
+     **/
+    CompressionType_None = 1,
+
+    /**
+     * Buffer that is compressed using the "deflate" algorithm (RFC
+     * 1951), wrapped inside the zlib data format (RFC 1950), prefixed
+     * with a "uint64_t" (8 bytes) that encodes the size of the
+     * uncompressed buffer. If the compressed buffer is empty, its
+     * represents an empty uncompressed buffer. This format is
+     * internal to Orthanc. If the 8 first bytes are skipped AND the
+     * buffer is non-empty, the buffer is compatible with the
+     * "deflate" HTTP compression.
+     **/
+    CompressionType_ZlibWithSize = 2
+  };
+
+  enum FileContentType
+  {
+    // If you add a value below, insert it in "PluginStorageArea" in
+    // the file "Plugins/Engine/OrthancPlugins.cpp"
+    FileContentType_Unknown = 0,
+    FileContentType_Dicom = 1,
+    FileContentType_DicomAsJson = 2,
+
+    // Make sure that the value "65535" can be stored into this enumeration
+    FileContentType_StartUser = 1024,
+    FileContentType_EndUser = 65535
+  };
+
+  enum ResourceType
+  {
+    ResourceType_Patient = 1,
+    ResourceType_Study = 2,
+    ResourceType_Series = 3,
+    ResourceType_Instance = 4
+  };
+
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ErrorCode code);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(HttpMethod method);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(HttpStatus status);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ResourceType type);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ImageFormat format);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(Encoding encoding);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(PhotometricInterpretation photometric);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(RequestOrigin origin);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(PixelFormat format);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ModalityManufacturer manufacturer);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(DicomRequestType type);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(TransferSyntax syntax);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(DicomVersion version);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(ValueRepresentation vr);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(JobState state);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(MimeType mime);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(Endianness endianness);
+
+  ORTHANC_PUBLIC
+  const char* EnumerationToString(StorageCommitmentFailureReason reason);
+
+  ORTHANC_PUBLIC
+  Encoding StringToEncoding(const char* encoding);
+
+  ORTHANC_PUBLIC
+  ResourceType StringToResourceType(const char* type);
+
+  ORTHANC_PUBLIC
+  ImageFormat StringToImageFormat(const char* format);
+
+  ORTHANC_PUBLIC
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported);
+
+  ORTHANC_PUBLIC
+  PhotometricInterpretation StringToPhotometricInterpretation(const char* value);
+
+  ORTHANC_PUBLIC
+  ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
+
+  ORTHANC_PUBLIC
+  DicomVersion StringToDicomVersion(const std::string& version);
+
+  ORTHANC_PUBLIC
+  JobState StringToJobState(const std::string& state);
+  
+  ORTHANC_PUBLIC
+  RequestOrigin StringToRequestOrigin(const std::string& origin);
+
+  ORTHANC_PUBLIC
+  MimeType StringToMimeType(const std::string& mime);
+  
+  ORTHANC_PUBLIC
+  unsigned int GetBytesPerPixel(PixelFormat format);
+
+  ORTHANC_PUBLIC
+  bool GetDicomEncoding(Encoding& encoding,
+                        const char* specificCharacterSet);
+
+  ORTHANC_PUBLIC
+  ResourceType GetChildResourceType(ResourceType type);
+
+  ORTHANC_PUBLIC
+  ResourceType GetParentResourceType(ResourceType type);
+
+  ORTHANC_PUBLIC
+  bool IsResourceLevelAboveOrEqual(ResourceType level,
+                                   ResourceType reference);
+
+  ORTHANC_PUBLIC
+  DicomModule GetModule(ResourceType type);
+
+  ORTHANC_PUBLIC
+  const char* GetDicomSpecificCharacterSet(Encoding encoding);
+
+  ORTHANC_PUBLIC
+  HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error);
+
+  ORTHANC_PUBLIC
+  bool IsUserContentType(FileContentType type);
+
+  ORTHANC_PUBLIC
+  bool IsBinaryValueRepresentation(ValueRepresentation vr);
+  
+  ORTHANC_PUBLIC
+  Encoding GetDefaultDicomEncoding();
+
+  ORTHANC_PUBLIC
+  void SetDefaultDicomEncoding(Encoding encoding);
+
+  ORTHANC_PUBLIC
+  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax);
+
+  ORTHANC_PUBLIC
+  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax);
+
+  ORTHANC_PUBLIC
+  bool LookupTransferSyntax(DicomTransferSyntax& target,
+                            const std::string& uid);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Enumerations_TransferSyntaxes.impl.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,566 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
+
+namespace Orthanc
+{
+  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        return "1.2.840.10008.1.2";
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        return "1.2.840.10008.1.2.1";
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        return "1.2.840.10008.1.2.1.99";
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        return "1.2.840.10008.1.2.2";
+
+      case DicomTransferSyntax_JPEGProcess1:
+        return "1.2.840.10008.1.2.4.50";
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+        return "1.2.840.10008.1.2.4.51";
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+        return "1.2.840.10008.1.2.4.52";
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+        return "1.2.840.10008.1.2.4.53";
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+        return "1.2.840.10008.1.2.4.54";
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+        return "1.2.840.10008.1.2.4.55";
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+        return "1.2.840.10008.1.2.4.56";
+
+      case DicomTransferSyntax_JPEGProcess14:
+        return "1.2.840.10008.1.2.4.57";
+
+      case DicomTransferSyntax_JPEGProcess15:
+        return "1.2.840.10008.1.2.4.58";
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+        return "1.2.840.10008.1.2.4.59";
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+        return "1.2.840.10008.1.2.4.60";
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+        return "1.2.840.10008.1.2.4.61";
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+        return "1.2.840.10008.1.2.4.62";
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+        return "1.2.840.10008.1.2.4.63";
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+        return "1.2.840.10008.1.2.4.64";
+
+      case DicomTransferSyntax_JPEGProcess28:
+        return "1.2.840.10008.1.2.4.65";
+
+      case DicomTransferSyntax_JPEGProcess29:
+        return "1.2.840.10008.1.2.4.66";
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+        return "1.2.840.10008.1.2.4.70";
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        return "1.2.840.10008.1.2.4.80";
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        return "1.2.840.10008.1.2.4.81";
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        return "1.2.840.10008.1.2.4.90";
+
+      case DicomTransferSyntax_JPEG2000:
+        return "1.2.840.10008.1.2.4.91";
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        return "1.2.840.10008.1.2.4.92";
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        return "1.2.840.10008.1.2.4.93";
+
+      case DicomTransferSyntax_JPIPReferenced:
+        return "1.2.840.10008.1.2.4.94";
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        return "1.2.840.10008.1.2.4.95";
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        return "1.2.840.10008.1.2.4.100";
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        return "1.2.840.10008.1.2.4.101";
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        return "1.2.840.10008.1.2.4.102";
+
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        return "1.2.840.10008.1.2.4.103";
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        return "1.2.840.10008.1.2.4.104";
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        return "1.2.840.10008.1.2.4.105";
+
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        return "1.2.840.10008.1.2.4.106";
+
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        return "1.2.840.10008.1.2.4.107";
+
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        return "1.2.840.10008.1.2.4.108";
+
+      case DicomTransferSyntax_RLELossless:
+        return "1.2.840.10008.1.2.5";
+
+      case DicomTransferSyntax_RFC2557MimeEncapsulation:
+        return "1.2.840.10008.1.2.6.1";
+
+      case DicomTransferSyntax_XML:
+        return "1.2.840.10008.1.2.6.2";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax)
+  {
+    switch (syntax)
+    {
+      case DicomTransferSyntax_LittleEndianImplicit:
+        return false;
+
+      case DicomTransferSyntax_LittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_DeflatedLittleEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_BigEndianExplicit:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess1:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess2_4:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess3_5:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess6_8:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess7_9:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess10_12:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess11_13:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14:
+        return false;
+
+      case DicomTransferSyntax_JPEGProcess15:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess16_18:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess17_19:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess20_22:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess21_23:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess24_26:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess25_27:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess28:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess29:
+        return true;
+
+      case DicomTransferSyntax_JPEGProcess14SV1:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossless:
+        return false;
+
+      case DicomTransferSyntax_JPEGLSLossy:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000LosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly:
+        return false;
+
+      case DicomTransferSyntax_JPEG2000Multicomponent:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferenced:
+        return false;
+
+      case DicomTransferSyntax_JPIPReferencedDeflate:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtMainLevel:
+        return false;
+
+      case DicomTransferSyntax_MPEG2MainProfileAtHighLevel:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_1:
+        return false;
+
+      case DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo:
+        return false;
+
+      case DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo:
+        return false;
+
+      case DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2:
+        return false;
+
+      case DicomTransferSyntax_HEVCMainProfileLevel5_1:
+        return false;
+
+      case DicomTransferSyntax_HEVCMain10ProfileLevel5_1:
+        return false;
+
+      case DicomTransferSyntax_RLELossless:
+        return false;
+
+      case DicomTransferSyntax_RFC2557MimeEncapsulation:
+        return true;
+
+      case DicomTransferSyntax_XML:
+        return true;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool LookupTransferSyntax(DicomTransferSyntax& target,
+                            const std::string& uid)
+  {
+    if (uid == "1.2.840.10008.1.2")
+    {
+      target = DicomTransferSyntax_LittleEndianImplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.1")
+    {
+      target = DicomTransferSyntax_LittleEndianExplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.1.99")
+    {
+      target = DicomTransferSyntax_DeflatedLittleEndianExplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.2")
+    {
+      target = DicomTransferSyntax_BigEndianExplicit;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.50")
+    {
+      target = DicomTransferSyntax_JPEGProcess1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.51")
+    {
+      target = DicomTransferSyntax_JPEGProcess2_4;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.52")
+    {
+      target = DicomTransferSyntax_JPEGProcess3_5;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.53")
+    {
+      target = DicomTransferSyntax_JPEGProcess6_8;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.54")
+    {
+      target = DicomTransferSyntax_JPEGProcess7_9;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.55")
+    {
+      target = DicomTransferSyntax_JPEGProcess10_12;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.56")
+    {
+      target = DicomTransferSyntax_JPEGProcess11_13;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.57")
+    {
+      target = DicomTransferSyntax_JPEGProcess14;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.58")
+    {
+      target = DicomTransferSyntax_JPEGProcess15;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.59")
+    {
+      target = DicomTransferSyntax_JPEGProcess16_18;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.60")
+    {
+      target = DicomTransferSyntax_JPEGProcess17_19;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.61")
+    {
+      target = DicomTransferSyntax_JPEGProcess20_22;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.62")
+    {
+      target = DicomTransferSyntax_JPEGProcess21_23;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.63")
+    {
+      target = DicomTransferSyntax_JPEGProcess24_26;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.64")
+    {
+      target = DicomTransferSyntax_JPEGProcess25_27;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.65")
+    {
+      target = DicomTransferSyntax_JPEGProcess28;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.66")
+    {
+      target = DicomTransferSyntax_JPEGProcess29;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.70")
+    {
+      target = DicomTransferSyntax_JPEGProcess14SV1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.80")
+    {
+      target = DicomTransferSyntax_JPEGLSLossless;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.81")
+    {
+      target = DicomTransferSyntax_JPEGLSLossy;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.90")
+    {
+      target = DicomTransferSyntax_JPEG2000LosslessOnly;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.91")
+    {
+      target = DicomTransferSyntax_JPEG2000;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.92")
+    {
+      target = DicomTransferSyntax_JPEG2000MulticomponentLosslessOnly;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.93")
+    {
+      target = DicomTransferSyntax_JPEG2000Multicomponent;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.94")
+    {
+      target = DicomTransferSyntax_JPIPReferenced;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.95")
+    {
+      target = DicomTransferSyntax_JPIPReferencedDeflate;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.100")
+    {
+      target = DicomTransferSyntax_MPEG2MainProfileAtMainLevel;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.101")
+    {
+      target = DicomTransferSyntax_MPEG2MainProfileAtHighLevel;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.102")
+    {
+      target = DicomTransferSyntax_MPEG4HighProfileLevel4_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.103")
+    {
+      target = DicomTransferSyntax_MPEG4BDcompatibleHighProfileLevel4_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.104")
+    {
+      target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For2DVideo;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.105")
+    {
+      target = DicomTransferSyntax_MPEG4HighProfileLevel4_2_For3DVideo;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.106")
+    {
+      target = DicomTransferSyntax_MPEG4StereoHighProfileLevel4_2;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.107")
+    {
+      target = DicomTransferSyntax_HEVCMainProfileLevel5_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.4.108")
+    {
+      target = DicomTransferSyntax_HEVCMain10ProfileLevel5_1;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.5")
+    {
+      target = DicomTransferSyntax_RLELossless;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.6.1")
+    {
+      target = DicomTransferSyntax_RFC2557MimeEncapsulation;
+      return true;
+    }
+    
+    if (uid == "1.2.840.10008.1.2.6.2")
+    {
+      target = DicomTransferSyntax_XML;
+      return true;
+    }
+    
+    return false;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileBuffer.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "FileBuffer.h"
+
+#include "TemporaryFile.h"
+#include "OrthancException.h"
+
+#include <boost/filesystem/fstream.hpp>
+
+
+namespace Orthanc
+{
+  class FileBuffer::PImpl
+  {
+  private:
+    TemporaryFile                file_;
+    boost::filesystem::ofstream  stream_;
+    bool                         isWriting_;
+
+  public:
+    PImpl() :
+      isWriting_(true)
+    {
+      stream_.open(file_.GetPath(), std::ofstream::out | std::ofstream::binary);
+      if (!stream_.good())
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile);
+      }
+    }
+
+    ~PImpl()
+    {
+      if (isWriting_)
+      {
+        stream_.close();
+      }
+    }
+
+    void Append(const char* buffer,
+                size_t size)
+    {
+      if (!isWriting_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (size > 0)
+      {
+        stream_.write(buffer, size);
+        if (!stream_.good())
+        {
+          stream_.close();
+          throw OrthancException(ErrorCode_FileStorageCannotWrite);
+        }
+      }
+    }
+
+    void Read(std::string& target)
+    {
+      if (isWriting_)
+      {
+        stream_.close();
+        isWriting_ = false;
+      }
+
+      file_.Read(target);
+    }
+  };
+
+    
+  FileBuffer::FileBuffer() :
+    pimpl_(new PImpl)
+  {
+  }
+
+
+  void FileBuffer::Append(const char* buffer,
+                          size_t size)
+  {
+    assert(pimpl_.get() != NULL);
+    pimpl_->Append(buffer, size);
+  }
+
+
+  void FileBuffer::Read(std::string& target)
+  {
+    assert(pimpl_.get() != NULL);
+    pimpl_->Read(target);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileBuffer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class FileBuffer cannot be used in sandboxed environments
+#endif
+
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FileBuffer : public boost::noncopyable
+  {
+  private:
+    class PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+  public:
+    FileBuffer();
+
+    void Append(const char* buffer,
+                size_t size);
+
+    void Read(std::string& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/FileInfo.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,132 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <stdint.h>
+#include "../Enumerations.h"
+
+namespace Orthanc
+{
+  struct FileInfo
+  {
+  private:
+    std::string uuid_;
+    FileContentType contentType_;
+
+    uint64_t uncompressedSize_;
+    std::string uncompressedMD5_;
+
+    CompressionType compressionType_;
+    uint64_t compressedSize_;
+    std::string compressedMD5_;
+
+  public:
+    FileInfo()
+    {
+    }
+
+    /**
+     * Constructor for an uncompressed attachment.
+     **/
+    FileInfo(const std::string& uuid,
+             FileContentType contentType,
+             uint64_t size,
+             const std::string& md5) :
+      uuid_(uuid),
+      contentType_(contentType),
+      uncompressedSize_(size),
+      uncompressedMD5_(md5),
+      compressionType_(CompressionType_None),
+      compressedSize_(size),
+      compressedMD5_(md5)
+    {
+    }
+
+    /**
+     * Constructor for a compressed attachment.
+     **/
+    FileInfo(const std::string& uuid,
+             FileContentType contentType,
+             uint64_t uncompressedSize,
+             const std::string& uncompressedMD5,
+             CompressionType compressionType,
+             uint64_t compressedSize,
+             const std::string& compressedMD5) :
+      uuid_(uuid),
+      contentType_(contentType),
+      uncompressedSize_(uncompressedSize),
+      uncompressedMD5_(uncompressedMD5),
+      compressionType_(compressionType),
+      compressedSize_(compressedSize),
+      compressedMD5_(compressedMD5)
+    {
+    }
+
+    const std::string& GetUuid() const
+    {
+      return uuid_;
+    }
+
+    FileContentType GetContentType() const
+    {
+      return contentType_;
+    }
+
+    uint64_t GetUncompressedSize() const
+    {
+      return uncompressedSize_;
+    }
+
+    CompressionType GetCompressionType() const
+    {
+      return compressionType_;
+    }
+
+    uint64_t GetCompressedSize() const
+    {
+      return compressedSize_;
+    }
+
+    const std::string& GetCompressedMD5() const
+    {
+      return compressedMD5_;
+    }
+
+    const std::string& GetUncompressedMD5() const
+    {
+      return uncompressedMD5_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,274 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "FilesystemStorage.h"
+
+// http://stackoverflow.com/questions/1576272/storing-large-number-of-files-in-file-system
+// http://stackoverflow.com/questions/446358/storing-a-large-number-of-images
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "../SystemToolbox.h"
+
+#include <boost/filesystem/fstream.hpp>
+
+
+static std::string ToString(const boost::filesystem::path& p)
+{
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+  return p.filename().string();
+#else
+  return p.filename();
+#endif
+}
+
+
+namespace Orthanc
+{
+  boost::filesystem::path FilesystemStorage::GetPath(const std::string& uuid) const
+  {
+    namespace fs = boost::filesystem;
+
+    if (!Toolbox::IsUuid(uuid))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    fs::path path = root_;
+
+    path /= std::string(&uuid[0], &uuid[2]);
+    path /= std::string(&uuid[2], &uuid[4]);
+    path /= uuid;
+
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+    path.make_preferred();
+#endif
+
+    return path;
+  }
+
+  FilesystemStorage::FilesystemStorage(std::string root)
+  {
+    //root_ = boost::filesystem::absolute(root).string();
+    root_ = 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)
+  {
+    LOG(INFO) << "Creating attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" type (size: " << (size / (1024 * 1024) + 1) << "MB)";
+
+    boost::filesystem::path path;
+    
+    path = GetPath(uuid);
+
+    if (boost::filesystem::exists(path))
+    {
+      // Extremely unlikely case: This Uuid has already been created
+      // in the past.
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (boost::filesystem::exists(path.parent_path()))
+    {
+      if (!boost::filesystem::is_directory(path.parent_path()))
+      {
+        throw OrthancException(ErrorCode_DirectoryOverFile);
+      }
+    }
+    else
+    {
+      if (!boost::filesystem::create_directories(path.parent_path()))
+      {
+        throw OrthancException(ErrorCode_FileStorageCannotWrite);
+      }
+    }
+
+    SystemToolbox::WriteFile(content, size, path.string());
+  }
+
+
+  void FilesystemStorage::Read(std::string& content,
+                               const std::string& uuid,
+                               FileContentType type)
+  {
+    LOG(INFO) << "Reading attachment \"" << uuid << "\" of \"" << GetDescriptionInternal(type) 
+              << "\" content type";
+
+    content.clear();
+    SystemToolbox::ReadFile(content, GetPath(uuid).string());
+  }
+
+
+  uintmax_t FilesystemStorage::GetSize(const std::string& uuid) const
+  {
+    boost::filesystem::path path = GetPath(uuid);
+    return boost::filesystem::file_size(path);
+  }
+
+
+
+  void FilesystemStorage::ListAllFiles(std::set<std::string>& result) const
+  {
+    namespace fs = boost::filesystem;
+
+    result.clear();
+
+    if (fs::exists(root_) && fs::is_directory(root_))
+    {
+      for (fs::recursive_directory_iterator current(root_), end; current != end ; ++current)
+      {
+        if (SystemToolbox::IsRegularFile(current->path().string()))
+        {
+          try
+          {
+            fs::path d = current->path();
+            std::string uuid = ToString(d);
+            if (Toolbox::IsUuid(uuid))
+            {
+              fs::path p0 = d.parent_path().parent_path().parent_path();
+              std::string p1 = ToString(d.parent_path().parent_path());
+              std::string p2 = ToString(d.parent_path());
+              if (p1.length() == 2 &&
+                  p2.length() == 2 &&
+                  p1 == uuid.substr(0, 2) &&
+                  p2 == uuid.substr(2, 2) &&
+                  p0 == root_)
+              {
+                result.insert(uuid);
+              }
+            }
+          }
+          catch (fs::filesystem_error&)
+          {
+          }
+        }
+      }
+    }
+  }
+
+
+  void FilesystemStorage::Clear()
+  {
+    namespace fs = boost::filesystem;
+    typedef std::set<std::string> List;
+
+    List result;
+    ListAllFiles(result);
+
+    for (List::const_iterator it = result.begin(); it != result.end(); ++it)
+    {
+      Remove(*it, FileContentType_Unknown /*ignored in this class*/);
+    }
+  }
+
+
+  void FilesystemStorage::Remove(const std::string& uuid,
+                                 FileContentType type)
+  {
+    LOG(INFO) << "Deleting attachment \"" << uuid << "\" of type " << static_cast<int>(type);
+
+    namespace fs = boost::filesystem;
+
+    fs::path p = GetPath(uuid);
+
+    try
+    {
+      fs::remove(p);
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+
+    // Remove the two parent directories, ignoring the error code if
+    // these directories are not empty
+
+    try
+    {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+      boost::system::error_code err;
+      fs::remove(p.parent_path(), err);
+      fs::remove(p.parent_path().parent_path(), err);
+#else
+      fs::remove(p.parent_path());
+      fs::remove(p.parent_path().parent_path());
+#endif
+    }
+    catch (...)
+    {
+      // Ignore the error
+    }
+  }
+
+
+  uintmax_t FilesystemStorage::GetCapacity() const
+  {
+    return boost::filesystem::space(root_).capacity;
+  }
+
+  uintmax_t FilesystemStorage::GetAvailableSpace() const
+  {
+    return boost::filesystem::space(root_).available;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/FilesystemStorage.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,90 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+
+#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>
+#include <boost/filesystem.hpp>
+#include <set>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FilesystemStorage : public IStorageArea
+  {
+    // TODO REMOVE THIS
+    friend class FilesystemHttpSender;
+    friend class FileStorageAccessor;
+
+  private:
+    boost::filesystem::path root_;
+
+    boost::filesystem::path GetPath(const std::string& uuid) const;
+
+  public:
+    explicit FilesystemStorage(std::string root);
+
+    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);
+
+    void ListAllFiles(std::set<std::string>& result) const;
+
+    uintmax_t GetSize(const std::string& uuid) const;
+
+    void Clear();
+
+    uintmax_t GetCapacity() const;
+
+    uintmax_t GetAvailableSpace() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/IStorageArea.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IStorageArea : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageArea()
+    {
+    }
+
+    virtual void Create(const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type) = 0;
+
+    virtual void Read(std::string& content,
+                      const std::string& uuid,
+                      FileContentType type) = 0;
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/MemoryStorageArea.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/FileStorage/MemoryStorageArea.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,246 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "StorageAccessor.h"
+
+#include "../Compatibility.h"
+#include "../Compression/ZlibCompressor.h"
+#include "../MetricsRegistry.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../HttpServer/HttpStreamTranscoder.h"
+#endif
+
+
+static const std::string METRICS_CREATE = "orthanc_storage_create_duration_ms";
+static const std::string METRICS_READ = "orthanc_storage_read_duration_ms";
+static const std::string METRICS_REMOVE = "orthanc_storage_remove_duration_ms";
+
+
+namespace Orthanc
+{
+  class StorageAccessor::MetricsTimer : public boost::noncopyable
+  {
+  private:
+    std::unique_ptr<MetricsRegistry::Timer>  timer_;
+
+  public:
+    MetricsTimer(StorageAccessor& that,
+                 const std::string& name)
+    {
+      if (that.metrics_ != NULL)
+      {
+        timer_.reset(new MetricsRegistry::Timer(*that.metrics_, name));
+      }
+    }
+  };
+
+
+  FileInfo StorageAccessor::Write(const void* data,
+                                  size_t size,
+                                  FileContentType type,
+                                  CompressionType compression,
+                                  bool storeMd5)
+  {
+    std::string uuid = Toolbox::GenerateUuid();
+
+    std::string md5;
+
+    if (storeMd5)
+    {
+      Toolbox::ComputeMD5(md5, data, size);
+    }
+
+    switch (compression)
+    {
+      case CompressionType_None:
+      {
+        MetricsTimer timer(*this, METRICS_CREATE);
+
+        area_.Create(uuid, data, size, type);
+        return FileInfo(uuid, type, size, md5);
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::string compressed;
+        zlib.Compress(compressed, data, size);
+
+        std::string compressedMD5;
+      
+        if (storeMd5)
+        {
+          Toolbox::ComputeMD5(compressedMD5, compressed);
+        }
+
+        {
+          MetricsTimer timer(*this, METRICS_CREATE);
+
+          if (compressed.size() > 0)
+          {
+            area_.Create(uuid, &compressed[0], compressed.size(), type);
+          }
+          else
+          {
+            area_.Create(uuid, NULL, 0, type);
+          }
+        }
+
+        return FileInfo(uuid, type, size, md5,
+                        CompressionType_ZlibWithSize, compressed.size(), compressedMD5);
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void StorageAccessor::Read(std::string& content,
+                             const FileInfo& info)
+  {
+    switch (info.GetCompressionType())
+    {
+      case CompressionType_None:
+      {
+        MetricsTimer timer(*this, METRICS_READ);
+        area_.Read(content, info.GetUuid(), info.GetContentType());
+        break;
+      }
+
+      case CompressionType_ZlibWithSize:
+      {
+        ZlibCompressor zlib;
+
+        std::string compressed;
+
+        {
+          MetricsTimer timer(*this, METRICS_READ);
+          area_.Read(compressed, info.GetUuid(), info.GetContentType());
+        }
+
+        IBufferCompressor::Uncompress(content, zlib, compressed);
+        break;
+      }
+
+      default:
+      {
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+    // TODO Check the validity of the uncompressed MD5?
+  }
+
+
+  void StorageAccessor::ReadRaw(std::string& content,
+                                const FileInfo& info)
+  {
+    MetricsTimer timer(*this, METRICS_READ);
+    area_.Read(content, info.GetUuid(), info.GetContentType());
+  }
+
+
+  void StorageAccessor::Remove(const std::string& fileUuid,
+                               FileContentType type)
+  {
+    MetricsTimer timer(*this, METRICS_REMOVE);
+    area_.Remove(fileUuid, type);
+  }
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+  void StorageAccessor::SetupSender(BufferHttpSender& sender,
+                                    const FileInfo& info,
+                                    const std::string& mime)
+  {
+    {
+      MetricsTimer timer(*this, METRICS_READ);
+      area_.Read(sender.GetBuffer(), info.GetUuid(), 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 std::string& mime)
+  {
+    BufferHttpSender sender;
+    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 std::string& mime)
+  {
+    BufferHttpSender sender;
+    SetupSender(sender, info, mime);
+  
+    HttpStreamTranscoder transcoder(sender, info.GetCompressionType());
+    output.AnswerStream(transcoder);
+  }
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/FileStorage/StorageAccessor.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,155 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 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"
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+#  include "../HttpServer/BufferHttpSender.h"
+#  include "../RestApi/RestApiOutput.h"
+#endif
+
+#include <vector>
+#include <string>
+#include <boost/noncopyable.hpp>
+#include <stdint.h>
+
+namespace Orthanc
+{
+  class MetricsRegistry;
+
+  /**
+   * This class handles the compression/decompression of the raw files
+   * contained in the storage area, and monitors timing metrics (if
+   * enabled).
+   **/
+  class StorageAccessor : boost::noncopyable
+  {
+  private:
+    class MetricsTimer;
+
+    IStorageArea&     area_;
+    MetricsRegistry*  metrics_;
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+    void SetupSender(BufferHttpSender& sender,
+                     const FileInfo& info,
+                     const std::string& mime);
+#endif
+
+  public:
+    StorageAccessor(IStorageArea& area) : 
+      area_(area),
+      metrics_(NULL)
+    {
+    }
+
+    StorageAccessor(IStorageArea& area,
+                    MetricsRegistry& metrics) : 
+      area_(area),
+      metrics_(&metrics)
+    {
+    }
+
+    FileInfo Write(const void* data,
+                   size_t size,
+                   FileContentType type,
+                   CompressionType compression,
+                   bool storeMd5);
+
+    FileInfo Write(const std::string& data, 
+                   FileContentType type,
+                   CompressionType compression,
+                   bool storeMd5)
+    {
+      return Write((data.size() == 0 ? NULL : data.c_str()),
+                   data.size(), type, compression, storeMd5);
+    }
+
+    void Read(std::string& content,
+              const FileInfo& info);
+
+    void ReadRaw(std::string& content,
+                 const FileInfo& info);
+
+    void Remove(const std::string& fileUuid,
+                FileContentType type);
+
+    void Remove(const FileInfo& info)
+    {
+      Remove(info.GetUuid(), info.GetContentType());
+    }
+
+#if ORTHANC_ENABLE_CIVETWEB == 1 || ORTHANC_ENABLE_MONGOOSE == 1
+    void AnswerFile(HttpOutput& output,
+                    const FileInfo& info,
+                    MimeType mime)
+    {
+      AnswerFile(output, info, EnumerationToString(mime));
+    }
+
+    void AnswerFile(HttpOutput& output,
+                    const FileInfo& info,
+                    const std::string& mime);
+
+    void AnswerFile(RestApiOutput& output,
+                    const FileInfo& info,
+                    MimeType mime)
+    {
+      AnswerFile(output, info, EnumerationToString(mime));
+    }
+
+    void AnswerFile(RestApiOutput& output,
+                    const FileInfo& info,
+                    const std::string& mime);
+#endif
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpClient.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1214 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpClient.h"
+
+#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>
+
+// Default timeout = 60 seconds (in Orthanc <= 1.5.6, it was 10 seconds)
+static const unsigned int DEFAULT_HTTP_TIMEOUT = 60;
+
+
+#if ORTHANC_ENABLE_PKCS11 == 1
+#  include "Pkcs11.h"
+#endif
+
+
+extern "C"
+{
+  static CURLcode GetHttpStatus(CURLcode code, CURL* curl, long* status)
+  {
+    if (code == CURLE_OK)
+    {
+      code = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status);
+      return code;
+    }
+    else
+    {
+      LOG(ERROR) << "Error code " << static_cast<int>(code)
+                 << " in libcurl: " << curl_easy_strerror(code);
+      *status = 0;
+      return code;
+    }
+  }
+}
+
+// This is a dummy wrapper function to suppress any OpenSSL-related
+// problem in valgrind. Inlining is prevented.
+#if defined(__GNUC__) || defined(__clang__)
+  __attribute__((noinline)) 
+#endif
+static CURLcode OrthancHttpClientPerformSSL(CURL* curl, long* status)
+{
+#if ORTHANC_ENABLE_SSL == 1
+  return GetHttpStatus(curl_easy_perform(curl), curl, status);
+#else
+  throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError,
+                                  "Orthanc was compiled without SSL support, "
+                                  "cannot make HTTPS request");
+#endif
+}
+
+
+
+namespace Orthanc
+{
+  static CURLcode CheckCode(CURLcode code)
+  {
+    if (code == CURLE_NOT_BUILT_IN)
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Your libcurl does not contain a required feature, "
+                             "please recompile Orthanc with -DUSE_SYSTEM_CURL=OFF");
+    }
+
+    if (code != CURLE_OK)
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol,
+                             "libCURL error: " + std::string(curl_easy_strerror(code)));
+    }
+
+    return code;
+  }
+
+
+  // RAII pattern around a "curl_slist"
+  class HttpClient::CurlHeaders : public boost::noncopyable
+  {
+  private:
+    struct curl_slist *content_;
+    bool               isChunkedTransfer_;
+    bool               hasExpect_;
+
+  public:
+    CurlHeaders() :
+      content_(NULL),
+      isChunkedTransfer_(false),
+      hasExpect_(false)
+    {
+    }
+
+    CurlHeaders(const HttpClient::HttpHeaders& headers)
+    {
+      for (HttpClient::HttpHeaders::const_iterator
+             it = headers.begin(); it != headers.end(); ++it)
+      {
+        AddHeader(it->first, it->second);
+      }
+    }
+
+    ~CurlHeaders()
+    {
+      Clear();
+    }
+
+    bool IsEmpty() const
+    {
+      return content_ == NULL;
+    }
+
+    void Clear()
+    {
+      if (content_ != NULL)
+      {
+        curl_slist_free_all(content_);
+        content_ = NULL;
+      }
+
+      isChunkedTransfer_ = false;
+      hasExpect_ = false;
+    }
+
+    void AddHeader(const std::string& key,
+                   const std::string& value)
+    {
+      if (boost::iequals(key, "Expect"))
+      {
+        hasExpect_ = true;
+      }
+
+      if (boost::iequals(key, "Transfer-Encoding") &&
+          value == "chunked")
+      {
+        isChunkedTransfer_ = true;
+      }
+        
+      std::string item = key + ": " + value;
+
+      struct curl_slist *tmp = curl_slist_append(content_, item.c_str());
+        
+      if (tmp == NULL)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+      else
+      {
+        content_ = tmp;
+      }
+    }
+
+    void Assign(CURL* curl) const
+    {
+      CheckCode(curl_easy_setopt(curl, CURLOPT_HTTPHEADER, content_));
+    }
+
+    bool HasExpect() const
+    {
+      return hasExpect_;
+    }
+
+    bool IsChunkedTransfer() const
+    {
+      return isChunkedTransfer_;
+    }
+  };
+
+
+  class HttpClient::CurlRequestBody : public boost::noncopyable
+  {
+  private:
+    HttpClient::IRequestBody*  body_;
+    std::string                sourceBuffer_;
+    size_t                     sourceBufferTransmittedSize_;
+
+    size_t CallbackInternal(char* curlBuffer,
+                            size_t curlBufferSize)
+    {
+      if (body_ == NULL)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (curlBufferSize == 0)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      // Read chunks from the body stream so as to fill the target buffer
+      size_t curlBufferFilledSize = 0;
+      size_t sourceRemainingSize = sourceBuffer_.size() - sourceBufferTransmittedSize_;
+      bool hasMore = true;
+      
+      while (sourceRemainingSize < curlBufferSize && hasMore)
+      {
+        if (sourceRemainingSize > 0)
+        {
+          // transmit the end of current source buffer
+          memcpy(curlBuffer + curlBufferFilledSize,
+                 sourceBuffer_.data() + sourceBufferTransmittedSize_, sourceRemainingSize);
+
+          curlBufferFilledSize += sourceRemainingSize;
+        }
+
+        // start filling a new source buffer
+        sourceBufferTransmittedSize_ = 0;
+        sourceBuffer_.clear();
+
+        hasMore = body_->ReadNextChunk(sourceBuffer_);
+
+        sourceRemainingSize = sourceBuffer_.size();
+      }
+
+      if (sourceRemainingSize > 0 &&
+          curlBufferSize > curlBufferFilledSize)
+      {
+        size_t s = std::min(sourceRemainingSize, curlBufferSize - curlBufferFilledSize);
+
+        memcpy(curlBuffer + curlBufferFilledSize,
+               sourceBuffer_.data() + sourceBufferTransmittedSize_, s);
+
+        sourceBufferTransmittedSize_ += s;
+        curlBufferFilledSize += s;
+      }
+
+      return curlBufferFilledSize;
+    }
+    
+  public:
+    CurlRequestBody() :
+      body_(NULL),
+      sourceBufferTransmittedSize_(0)
+    {
+    }
+
+    void SetBody(HttpClient::IRequestBody& body)
+    {
+      body_ = &body;
+      sourceBufferTransmittedSize_ = 0;
+      sourceBuffer_.clear();
+    }
+
+    void Clear()
+    {
+      body_ = NULL;
+      sourceBufferTransmittedSize_ = 0;
+      sourceBuffer_.clear();
+    }
+
+    bool IsValid() const
+    {
+      return body_ != NULL;
+    }
+
+    static size_t Callback(char *buffer, size_t size, size_t nitems, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+        return reinterpret_cast<HttpClient::CurlRequestBody*>(userdata)->
+          CallbackInternal(buffer, size * nitems);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+  };
+
+
+  class HttpClient::CurlAnswer : public boost::noncopyable
+  {
+  private:
+    HttpClient::IAnswer&  answer_;
+    bool                  headersLowerCase_;
+
+  public:
+    CurlAnswer(HttpClient::IAnswer& answer,
+               bool headersLowerCase) :
+      answer_(answer),
+      headersLowerCase_(headersLowerCase)
+    {
+    }
+
+    static size_t HeaderCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+        CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
+
+        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 (that.headersLowerCase_)
+            {
+              Toolbox::ToLowerCase(tmp);
+            }
+
+            std::string key = Toolbox::StripSpaces(tmp);
+
+            if (!key.empty())
+            {
+              std::string value = Toolbox::StripSpaces(s.substr(colon + 1, eol));
+              that.answer_.AddHeader(key, value);
+            }
+          }
+
+          return length;
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+
+    static size_t BodyCallback(void *buffer, size_t size, size_t nmemb, void *userdata)
+    {
+      try
+      {
+        assert(userdata != NULL);
+        CurlAnswer& that = *(static_cast<CurlAnswer*>(userdata));
+
+        size_t length = size * nmemb;
+        if (length == 0)
+        {
+          return 0;
+        }
+        else
+        {
+          that.answer_.AddChunk(buffer, length);
+          return length;
+        }
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while streaming HTTP body: " << e.What();
+        return CURL_READFUNC_ABORT;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while streaming HTTP body";
+        return CURL_READFUNC_ABORT;
+      }
+    }
+  };
+
+
+  class HttpClient::DefaultAnswer : public HttpClient::IAnswer
+  {
+  private:
+    ChunkedBuffer   answer_;
+    HttpHeaders*    headers_;
+
+  public:
+    DefaultAnswer() : headers_(NULL)
+    {
+    }
+
+    void SetHeaders(HttpHeaders& headers)
+    {
+      headers_ = &headers;
+      headers_->clear();
+    }
+
+    void FlattenBody(std::string& target)
+    {
+      answer_.Flatten(target);
+    }
+
+    virtual void AddHeader(const std::string& key,
+                           const std::string& value)
+    {
+      if (headers_ != NULL)
+      {
+        (*headers_) [key] = value;
+      }
+    }
+      
+    virtual void AddChunk(const void* data,
+                          size_t size)
+    {
+      answer_.AddChunk(data, size);
+    }
+  };
+
+
+  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_;
+    CurlHeaders defaultPostHeaders_;
+    CurlHeaders defaultChunkedHeaders_;
+    CurlHeaders userHeaders_;
+    CurlRequestBody requestBody_;
+  };
+
+
+  void HttpClient::ThrowException(HttpStatus status)
+  {
+    switch (status)
+    {
+      case HttpStatus_400_BadRequest:
+        throw OrthancException(ErrorCode_BadRequest);
+
+      case HttpStatus_401_Unauthorized:
+      case HttpStatus_403_Forbidden:
+        throw OrthancException(ErrorCode_Unauthorized);
+
+      case HttpStatus_404_NotFound:
+        throw OrthancException(ErrorCode_UnknownResource);
+
+      default:
+        throw OrthancException(ErrorCode_NetworkProtocol);
+    }
+  }
+
+
+  /*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;
+    }*/
+
+
+  void HttpClient::Setup()
+  {
+    pimpl_->defaultPostHeaders_.AddHeader("Expect", "");
+    pimpl_->defaultChunkedHeaders_.AddHeader("Expect", "");
+    pimpl_->defaultChunkedHeaders_.AddHeader("Transfer-Encoding", "chunked");
+
+    pimpl_->curl_ = curl_easy_init();
+
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERFUNCTION, &CurlAnswer::HeaderCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEFUNCTION, &CurlAnswer::BodyCallback));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1));
+
+    // This fixes the "longjmp causes uninitialized stack frame" crash
+    // that happens on modern Linux versions.
+    // http://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOSIGNAL, 1));
+
+    url_ = "";
+    method_ = HttpMethod_Get;
+    lastStatus_ = HttpStatus_None;
+    SetVerbose(GlobalParameters::GetInstance().IsDefaultVerbose());
+    timeout_ = GlobalParameters::GetInstance().GetDefaultTimeout();
+    GlobalParameters::GetInstance().GetDefaultProxy(proxy_);
+    GlobalParameters::GetInstance().GetSslConfiguration(verifyPeers_, caCertificates_);    
+  }
+
+
+  HttpClient::HttpClient() : 
+    pimpl_(new PImpl),
+    verifyPeers_(true),
+    pkcs11Enabled_(false),
+    headersToLowerCase_(true),
+    redirectionFollowed_(true)
+  {
+    Setup();
+  }
+
+
+  HttpClient::HttpClient(const WebServiceParameters& service,
+                         const std::string& uri) : 
+    pimpl_(new PImpl),
+    verifyPeers_(true),
+    headersToLowerCase_(true),
+    redirectionFollowed_(true)
+  {
+    Setup();
+
+    if (service.GetUsername().size() != 0 && 
+        service.GetPassword().size() != 0)
+    {
+      SetCredentials(service.GetUsername().c_str(), 
+                     service.GetPassword().c_str());
+    }
+
+    if (!service.GetCertificateFile().empty())
+    {
+      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);
+    }
+  }
+
+
+  HttpClient::~HttpClient()
+  {
+    curl_easy_cleanup(pimpl_->curl_);
+  }
+
+
+  void HttpClient::SetBody(const std::string& data)
+  {
+    body_ = data;
+    pimpl_->requestBody_.Clear();
+  }
+
+
+  void HttpClient::SetBody(IRequestBody& body)
+  {
+    body_.clear();
+    pimpl_->requestBody_.SetBody(body);
+  }
+
+  
+  void HttpClient::ClearBody()
+  {
+    body_.clear();
+    pimpl_->requestBody_.Clear();
+  }
+
+
+  void HttpClient::SetVerbose(bool isVerbose)
+  {
+    isVerbose_ = isVerbose;
+
+    if (isVerbose_)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 1));
+      //CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_DEBUGFUNCTION, &CurlDebugCallback));
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_VERBOSE, 0));
+    }
+  }
+
+
+  void HttpClient::AddHeader(const std::string& key,
+                             const std::string& value)
+  {
+    if (key.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      pimpl_->userHeaders_.AddHeader(key, value);
+    }
+  }
+
+
+  void HttpClient::ClearHeaders()
+  {
+    pimpl_->userHeaders_.Clear();
+  }
+
+
+  bool HttpClient::ApplyInternal(CurlAnswer& answer)
+  {
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str()));
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADERDATA, &answer));
+
+#if ORTHANC_ENABLE_SSL == 1
+    // Setup HTTPS-related options
+
+    if (verifyPeers_)
+    {
+      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_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Cannot enable both client certificates and PKCS#11 authentication");
+    }
+
+    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
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot use PKCS#11 for a HTTPS request, "
+                               "because it has not been initialized");
+      }
+#else
+      throw OrthancException(ErrorCode_InternalError,
+                             "This version of Orthanc is compiled without support for PKCS#11");
+#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
+      throw OrthancException(ErrorCode_InternalError,
+                             "This version of Orthanc is compiled without OpenSSL support, "
+                             "cannot use HTTPS client authentication");
+#endif
+    }
+
+    // Reset the parameters from previous calls to Apply()
+    pimpl_->userHeaders_.Assign(pimpl_->curl_);
+    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, 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)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, DEFAULT_HTTP_TIMEOUT));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, DEFAULT_HTTP_TIMEOUT));
+    }
+    else
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_TIMEOUT, timeout_));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CONNECTTIMEOUT, timeout_));
+    }
+
+    if (credentials_.size() != 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_USERPWD, credentials_.c_str()));
+    }
+
+    if (proxy_.size() != 0)
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str()));
+    }
+
+    switch (method_)
+    {
+    case HttpMethod_Get:
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 1L));
+      break;
+
+    case HttpMethod_Post:
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
+
+      break;
+
+    case HttpMethod_Delete:
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_NOBODY, 1L));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "DELETE"));
+      break;
+
+    case HttpMethod_Put:
+      // http://stackoverflow.com/a/7570281/881731: Don't use
+      // CURLOPT_PUT if there is a body
+
+      // CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PUT, 1L));
+
+      curl_easy_setopt(pimpl_->curl_, CURLOPT_CUSTOMREQUEST, "PUT"); /* !!! */
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (method_ == HttpMethod_Post ||
+        method_ == HttpMethod_Put)
+    {
+      if (!pimpl_->userHeaders_.IsEmpty() &&
+          !pimpl_->userHeaders_.HasExpect())
+      {
+        LOG(INFO) << "For performance, the HTTP header \"Expect\" should be set to empty string in POST/PUT requests";
+      }
+
+      if (pimpl_->requestBody_.IsValid())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, CurlRequestBody::Callback));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READDATA, &pimpl_->requestBody_));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POST, 1L));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, -1L));
+    
+        if (pimpl_->userHeaders_.IsEmpty())
+        {
+          pimpl_->defaultChunkedHeaders_.Assign(pimpl_->curl_);
+        }
+        else if (!pimpl_->userHeaders_.IsChunkedTransfer())
+        {
+          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must be set to \"chunked\" "
+                       << "if streaming a chunked body in POST/PUT requests";
+        }
+      }
+      else
+      {
+        // Disable possible previous stream transfers
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_READFUNCTION, NULL));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_UPLOAD, 0));
+
+        if (pimpl_->userHeaders_.IsChunkedTransfer())
+        {
+          LOG(WARNING) << "The HTTP header \"Transfer-Encoding\" must only be set "
+                       << "if streaming a chunked body in POST/PUT requests";
+        }
+
+        if (pimpl_->userHeaders_.IsEmpty())
+        {
+          pimpl_->defaultPostHeaders_.Assign(pimpl_->curl_);
+        }
+
+        if (body_.size() > 0)
+        {
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, body_.c_str()));
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, body_.size()));
+        }
+        else
+        {
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDS, NULL));
+          CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_POSTFIELDSIZE, 0));
+        }
+      }
+    }
+
+
+    // Do the actual request
+    CURLcode code;
+    long status = 0;
+
+    CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer));
+
+    const boost::posix_time::ptime start = boost::posix_time::microsec_clock::universal_time();
+    
+    if (boost::starts_with(url_, "https://"))
+    {
+      code = OrthancHttpClientPerformSSL(pimpl_->curl_, &status);
+    }
+    else
+    {
+      code = GetHttpStatus(curl_easy_perform(pimpl_->curl_), pimpl_->curl_, &status);
+    }
+
+    const boost::posix_time::ptime end = boost::posix_time::microsec_clock::universal_time();
+    
+    LOG(INFO) << "HTTP status code " << status << " in "
+              << ((end - start).total_milliseconds()) << " ms after "
+              << EnumerationToString(method_) << " request on: " << url_;
+
+    if (isVerbose_)
+    {
+      LOG(INFO) << "cURL status code: " << code;
+    }
+
+    CheckCode(code);
+
+    if (status == 0)
+    {
+      // This corresponds to a call to an inexistent host
+      lastStatus_ = HttpStatus_500_InternalServerError;
+    }
+    else
+    {
+      lastStatus_ = static_cast<HttpStatus>(status);
+    }
+
+    if (status >= 200 && status < 300)
+    {
+      return true;   // Success
+    }
+    else
+    {
+      LOG(ERROR) << "Error in HTTP request, received HTTP status " << status 
+                 << " (" << EnumerationToString(lastStatus_) << ")";
+      return false;
+    }
+  }
+
+
+  bool HttpClient::ApplyInternal(std::string& answerBody,
+                                 HttpHeaders* answerHeaders)
+  {
+    answerBody.clear();
+
+    DefaultAnswer answer;
+
+    if (answerHeaders != NULL)
+    {
+      answer.SetHeaders(*answerHeaders);
+    }
+
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+
+    if (ApplyInternal(wrapper))
+    {
+      answer.FlattenBody(answerBody);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool HttpClient::ApplyInternal(Json::Value& answerBody,
+                                 HttpClient::HttpHeaders* answerHeaders)
+  {
+    std::string s;
+    if (ApplyInternal(s, answerHeaders))
+    {
+      Json::Reader reader;
+      return reader.parse(s, answerBody);
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void HttpClient::SetCredentials(const char* username,
+                                  const char* password)
+  {
+    credentials_ = std::string(username) + ":" + std::string(password);
+  }
+
+
+  void HttpClient::ConfigureSsl(bool httpsVerifyPeers,
+                                const std::string& httpsVerifyCertificates)
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    if (httpsVerifyPeers)
+    {
+      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: " << httpsVerifyCertificates;
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "The verification of the peers in HTTPS requests is disabled";
+    }
+#endif
+
+    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);
+  }
+
+
+  bool HttpClient::Apply(IAnswer& answer)
+  {
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+    return ApplyInternal(wrapper);
+  }
+
+
+  void HttpClient::ApplyAndThrowException(IAnswer& answer)
+  {
+    CurlAnswer wrapper(answer, headersToLowerCase_);
+
+    if (!ApplyInternal(wrapper))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+
+  void HttpClient::ApplyAndThrowException(std::string& answerBody)
+  {
+    if (!Apply(answerBody))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+  
+  void HttpClient::ApplyAndThrowException(Json::Value& answerBody)
+  {
+    if (!Apply(answerBody))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+
+
+  void HttpClient::ApplyAndThrowException(std::string& answerBody,
+                                          HttpHeaders& answerHeaders)
+  {
+    if (!Apply(answerBody, answerHeaders))
+    {
+      ThrowException(GetLastStatus());
+    }
+  }
+  
+
+  void HttpClient::ApplyAndThrowException(Json::Value& answerBody,
+                                          HttpHeaders& answerHeaders)
+  {
+    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))
+    {
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "Cannot open certificate file: " + certificateFile);
+    }
+
+    if (!certificateKeyFile.empty() && 
+        !SystemToolbox::IsRegularFile(certificateKeyFile))
+    {
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "Cannot open key file: " + certificateKeyFile);
+    }
+
+    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
+    throw OrthancException(ErrorCode_InternalError,
+                           "This version of Orthanc is compiled without support for PKCS#11");
+#endif
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpClient.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,341 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+#include "WebServiceParameters.h"
+
+#include <string>
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <json/json.h>
+
+#if !defined(ORTHANC_ENABLE_CURL)
+#  error The macro ORTHANC_ENABLE_CURL must be defined
+#endif
+
+#if ORTHANC_ENABLE_CURL != 1
+#  error Support for curl is disabled, cannot use this file
+#endif
+
+#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 ORTHANC_PUBLIC HttpClient : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
+    class IRequestBody : public boost::noncopyable
+    {
+    public:
+      virtual ~IRequestBody()
+      {
+      }
+      
+      virtual bool ReadNextChunk(std::string& chunk) = 0;
+    };
+
+    class IAnswer : public boost::noncopyable
+    {
+    public:
+      virtual ~IAnswer()
+      {
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value) = 0;
+      
+      virtual void AddChunk(const void* data,
+                            size_t size) = 0;
+    };
+
+  private:
+    class CurlHeaders;
+    class CurlRequestBody;
+    class CurlAnswer;
+    class DefaultAnswer;
+    class GlobalParameters;
+
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    std::string url_;
+    std::string credentials_;
+    HttpMethod method_;
+    HttpStatus lastStatus_;
+    std::string body_;  // This only makes sense for POST and PUT requests
+    bool isVerbose_;
+    long timeout_;
+    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&);  // Assignment forbidden
+    HttpClient(const HttpClient& base);  // Copy forbidden
+
+    bool ApplyInternal(CurlAnswer& answer);
+
+    bool ApplyInternal(std::string& answerBody,
+                       HttpHeaders* answerHeaders);
+
+    bool ApplyInternal(Json::Value& answerBody,
+                       HttpHeaders* answerHeaders);
+
+  public:
+    HttpClient();
+
+    HttpClient(const WebServiceParameters& service,
+               const std::string& uri);
+
+    ~HttpClient();
+
+    void SetUrl(const char* url)
+    {
+      url_ = std::string(url);
+    }
+
+    void SetUrl(const std::string& url)
+    {
+      url_ = url;
+    }
+
+    const std::string& GetUrl() const
+    {
+      return url_;
+    }
+
+    void SetMethod(HttpMethod method)
+    {
+      method_ = method;
+    }
+
+    HttpMethod GetMethod() const
+    {
+      return method_;
+    }
+
+    void SetTimeout(long seconds)
+    {
+      timeout_ = seconds;
+    }
+
+    long GetTimeout() const
+    {
+      return timeout_;
+    }
+
+    void SetBody(const std::string& data);
+
+    std::string& GetBody()
+    {
+      return body_;
+    }
+
+    const std::string& GetBody() const
+    {
+      return body_;
+    }
+
+    void SetBody(IRequestBody& body);
+
+    void ClearBody();
+
+    void SetVerbose(bool isVerbose);
+
+    bool IsVerbose() const
+    {
+      return isVerbose_;
+    }
+
+    void AddHeader(const std::string& key,
+                   const std::string& value);
+
+    void ClearHeaders();
+
+    bool Apply(IAnswer& answer);
+
+    bool Apply(std::string& answerBody)
+    {
+      return ApplyInternal(answerBody, NULL);
+    }
+
+    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
+    {
+      return lastStatus_;
+    }
+
+    void SetCredentials(const char* username,
+                        const char* password);
+
+    void SetProxy(const std::string& proxy)
+    {
+      proxy_ = proxy;
+    }
+
+    void SetHttpsVerifyPeers(bool verify)
+    {
+      verifyPeers_ = verify;
+    }
+
+    bool IsHttpsVerifyPeers() const
+    {
+      return verifyPeers_;
+    }
+
+    void SetHttpsCACertificates(const std::string& certificates)
+    {
+      caCertificates_ = certificates;
+    }
+
+    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_;
+    }
+
+    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(IAnswer& answer);
+
+    void ApplyAndThrowException(std::string& answerBody);
+
+    void ApplyAndThrowException(Json::Value& answerBody);
+
+    void ApplyAndThrowException(std::string& answerBody,
+                                HttpHeaders& answerHeaders);
+
+    void ApplyAndThrowException(Json::Value& answerBody,
+                                HttpHeaders& answerHeaders);
+
+    static void ThrowException(HttpStatus status);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "BufferHttpSender.h"
+
+#include "../OrthancException.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  BufferHttpSender::BufferHttpSender() :
+    position_(0), 
+    chunkSize_(0),
+    currentChunkSize_(0)
+  {
+  }
+
+
+  bool BufferHttpSender::ReadNextChunk()
+  {
+    assert(position_ + currentChunkSize_ <= buffer_.size());
+
+    position_ += currentChunkSize_;
+
+    if (position_ == buffer_.size())
+    {
+      return false;
+    }
+    else
+    {
+      currentChunkSize_ = buffer_.size() - position_;
+
+      if (chunkSize_ != 0 &&
+          currentChunkSize_ > chunkSize_)
+      {
+        currentChunkSize_ = chunkSize_;
+      }
+
+      return true;
+    }
+  }
+
+
+  const char* BufferHttpSender::GetChunkContent()
+  {
+    return buffer_.c_str() + position_;
+  }
+
+
+  size_t BufferHttpSender::GetChunkSize()
+  {
+    return currentChunkSize_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/BufferHttpSender.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpFileSender.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC BufferHttpSender : public HttpFileSender
+  {
+  private:
+    std::string  buffer_;
+    size_t       position_;
+    size_t       chunkSize_;
+    size_t       currentChunkSize_;
+
+  public:
+    BufferHttpSender();
+
+    std::string& GetBuffer() 
+    {
+      return buffer_;
+    }
+
+    const std::string& GetBuffer() const
+    {
+      return buffer_;
+    }
+
+    // This is for test purpose. If "chunkSize" is set to "0" (the
+    // default), the entire buffer is consumed at once.
+    void SetChunkSize(size_t chunkSize)
+    {
+      chunkSize_ = chunkSize;
+    }
+
+
+    /**
+     * Implementation of the IHttpStreamAnswer interface.
+     **/
+
+    virtual uint64_t GetContentLength()
+    {
+      return buffer_.size();
+    }
+
+    virtual bool ReadNextChunk();
+
+    virtual const char* GetChunkContent();
+
+    virtual size_t GetChunkSize();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,182 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "FilesystemHttpHandler.h"
+
+#include "../OrthancException.h"
+#include "../SystemToolbox.h"
+#include "FilesystemHttpSender.h"
+
+#include <boost/filesystem.hpp>
+
+
+namespace Orthanc
+{
+  struct FilesystemHttpHandler::PImpl
+  {
+    UriComponents baseUri_;
+    boost::filesystem::path root_;
+  };
+
+
+
+  static void OutputDirectoryContent(HttpOutput& output,
+                                     const IHttpHandler::Arguments& headers,
+                                     const UriComponents& uri,
+                                     const boost::filesystem::path& p)
+  {
+    namespace fs = boost::filesystem;
+
+    std::string s;
+    s += "<html>";
+    s += "  <body>";
+    s += "    <h1>Subdirectories</h1>";
+    s += "    <ul>";
+
+    if (uri.size() > 0)
+    {
+      std::string h = Toolbox::FlattenUri(uri) + "/..";
+      s += "<li><a href=\"" + h + "\">..</a></li>";
+    }
+
+    fs::directory_iterator end;
+    for (fs::directory_iterator it(p) ; it != end; ++it)
+    {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+      std::string f = it->path().filename().string();
+#else
+      std::string f = it->path().filename();
+#endif
+
+      std::string h = Toolbox::FlattenUri(uri) + "/" + f;
+      if (fs::is_directory(it->status()))
+        s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
+    }      
+
+    s += "    </ul>";      
+    s += "    <h1>Files</h1>";
+    s += "    <ul>";
+
+    for (fs::directory_iterator it(p) ; it != end; ++it)
+    {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+      std::string f = it->path().filename().string();
+#else
+      std::string f = it->path().filename();
+#endif
+
+      std::string h = Toolbox::FlattenUri(uri) + "/" + f;
+      if (SystemToolbox::IsRegularFile(it->path().string()))
+      {
+        s += "<li><a href=\"" + h + "\">" + f + "</a></li>";
+      }
+    }      
+
+    s += "    </ul>";
+    s += "  </body>";
+    s += "</html>";
+
+    output.SetContentType(MimeType_Html);
+    output.Answer(s);
+  }
+
+
+  FilesystemHttpHandler::FilesystemHttpHandler(const std::string& baseUri,
+                                               const std::string& root) : pimpl_(new PImpl)
+  {
+    Toolbox::SplitUriComponents(pimpl_->baseUri_, baseUri);
+    pimpl_->root_ = root;
+    listDirectoryContent_ = false;
+    
+    namespace fs = boost::filesystem;
+    if (!fs::exists(pimpl_->root_) || 
+        !fs::is_directory(pimpl_->root_))
+    {
+      throw OrthancException(ErrorCode_DirectoryExpected);
+    }
+  }
+
+
+  bool FilesystemHttpHandler::Handle(
+    HttpOutput& output,
+    RequestOrigin /*origin*/,
+    const char* /*remoteIp*/,
+    const char* /*username*/,
+    HttpMethod method,
+    const UriComponents& uri,
+    const Arguments& headers,
+    const GetArguments& arguments,
+    const void* /*bodyData*/,
+    size_t /*bodySize*/)
+  {
+    if (!Toolbox::IsChildUri(pimpl_->baseUri_, uri))
+    {
+      // This URI is not served by this handler
+      return false;
+    }
+
+    if (method != HttpMethod_Get)
+    {
+      output.SendMethodNotAllowed("GET");
+      return true;
+    }
+
+    namespace fs = boost::filesystem;
+
+    fs::path p = pimpl_->root_;
+    for (size_t i = pimpl_->baseUri_.size(); i < uri.size(); i++)
+    {
+      p /= uri[i];
+    }
+
+    if (SystemToolbox::IsRegularFile(p.string()))
+    {
+      FilesystemHttpSender sender(p);
+      sender.SetContentType(SystemToolbox::AutodetectMimeType(p.string()));
+      output.Answer(sender);   // TODO COMPRESSION
+    }
+    else if (listDirectoryContent_ &&
+             fs::exists(p) && 
+             fs::is_directory(p))
+    {
+      OutputDirectoryContent(output, headers, uri, p);
+    }
+    else
+    {
+      output.SendStatus(HttpStatus_404_NotFound);
+    }
+
+    return true;
+  } 
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class FilesystemHttpHandler : public IHttpHandler
+  {
+  private:
+    // PImpl idiom to avoid the inclusion of boost::filesystem
+    // throughout the software
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    bool listDirectoryContent_;
+
+  public:
+    FilesystemHttpHandler(const std::string& baseUri,
+                          const std::string& root);
+
+    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                            RequestOrigin origin,
+                                            const char* remoteIp,
+                                            const char* username,
+                                            HttpMethod method,
+                                            const UriComponents& uri,
+                                            const Arguments& headers)
+    {
+      return false;
+    }
+
+    virtual bool Handle(
+      HttpOutput& output,
+      RequestOrigin origin,
+      const char* remoteIp,
+      const char* username,
+      HttpMethod method,
+      const UriComponents& uri,
+      const Arguments& headers,
+      const GetArguments& arguments,
+      const void* /*bodyData*/,
+      size_t /*bodySize*/);
+
+    bool IsListDirectoryContent() const
+    {
+      return listDirectoryContent_;
+    }
+
+    void SetListDirectoryContent(bool enabled)
+    {
+      listDirectoryContent_ = enabled;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "FilesystemHttpSender.h"
+
+#include "../OrthancException.h"
+
+static const size_t  CHUNK_SIZE = 64 * 1024;   // Use 64KB chunks
+
+namespace Orthanc
+{
+  void FilesystemHttpSender::Initialize(const boost::filesystem::path& path)
+  {
+    SetContentFilename(path.filename().string());
+    file_.open(path.string().c_str(), std::ifstream::binary);
+
+    if (!file_.is_open())
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    file_.seekg(0, file_.end);
+    size_ = file_.tellg();
+    file_.seekg(0, file_.beg);
+  }
+
+
+  bool FilesystemHttpSender::ReadNextChunk()
+  {
+    if (chunk_.size() == 0)
+    {
+      chunk_.resize(CHUNK_SIZE);
+    }
+
+    file_.read(&chunk_[0], chunk_.size());
+
+    if ((file_.flags() & std::istream::failbit) ||
+        file_.gcount() < 0)
+    {
+      throw OrthancException(ErrorCode_CorruptedFile);
+    }
+
+    chunkSize_ = static_cast<size_t>(file_.gcount());
+
+    return chunkSize_ > 0;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/FilesystemHttpSender.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpFileSender.h"
+#include "BufferHttpSender.h"
+#include "../FileStorage/FilesystemStorage.h"
+
+#include <fstream>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FilesystemHttpSender : public HttpFileSender
+  {
+  private:
+    std::ifstream    file_;
+    uint64_t         size_;
+    std::string      chunk_;
+    size_t           chunkSize_;
+
+    void Initialize(const boost::filesystem::path& path);
+
+  public:
+    explicit FilesystemHttpSender(const std::string& path)
+    {
+      Initialize(path);
+    }
+
+    explicit FilesystemHttpSender(const boost::filesystem::path& path)
+    {
+      Initialize(path);
+    }
+
+    FilesystemHttpSender(const std::string& path,
+                         MimeType contentType)
+    {
+      SetContentType(contentType);
+      Initialize(path);
+    }
+
+    FilesystemHttpSender(const FilesystemStorage& storage,
+                         const std::string& uuid)
+    {
+      Initialize(storage.GetPath(uuid));
+    }
+
+    /**
+     * Implementation of the IHttpStreamAnswer interface.
+     **/
+
+    virtual uint64_t GetContentLength()
+    {
+      return size_;
+    }
+
+    virtual bool ReadNextChunk();
+
+    virtual const char* GetChunkContent()
+    {
+      return chunk_.c_str();
+    }
+
+    virtual size_t GetChunkSize()
+    {
+      return chunkSize_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpContentNegociation.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+        {
+          throw OrthancException(
+            ErrorCode_BadRequest,
+            "Quality parameter out of range in a HTTP request (must be between 0 and 1): " + value);
+        }
+      }
+    }
+
+    return 1.0f;  // Default quality
+  }
+
+
+  void HttpContentNegociation::SelectBestMatch(std::unique_ptr<Reference>& best,
+                                               const Handler& handler,
+                                               const std::string& type,
+                                               const std::string& subtype,
+                                               float quality)
+  {
+    std::unique_ptr<Reference> match(new Reference(handler, type, subtype, quality));
+
+    if (best.get() == NULL ||
+        *best < *match)
+    {
+#if __cplusplus < 201103L
+      best.reset(match.release());
+#else
+      best = std::move(match);
+#endif
+    }
+  }
+
+
+  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::unique_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/OrthancFramework/Sources/HttpServer/HttpContentNegociation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+#include "../Compatibility.h"
+
+#include <memory>
+#include <boost/noncopyable.hpp>
+#include <map>
+#include <list>
+#include <string>
+#include <vector>
+#include <stdint.h>
+
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC 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::unique_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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpFileSender.h"
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+#include "../SystemToolbox.h"
+
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  void HttpFileSender::SetContentFilename(const std::string& filename)
+  {
+    filename_ = filename;
+
+    if (contentType_.empty())
+    {
+      contentType_ = SystemToolbox::AutodetectMimeType(filename);
+    }
+  }
+
+
+  bool HttpFileSender::HasContentFilename(std::string& filename)
+  {
+    if (filename_.empty())
+    {
+      return false;
+    }
+    else
+    {
+      filename = filename_;
+      return true;
+    }
+  }
+    
+  std::string HttpFileSender::GetContentType()
+  {
+    if (contentType_.empty())
+    {
+      return MIME_BINARY;
+    }
+    else
+    {
+      return contentType_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpFileSender.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpOutput.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HttpFileSender : public IHttpStreamAnswer
+  {
+  private:
+    std::string contentType_;
+    std::string filename_;
+
+  public:
+    void SetContentType(MimeType contentType)
+    {
+      contentType_ = EnumerationToString(contentType);
+    }
+
+    void SetContentType(const std::string& contentType)
+    {
+      contentType_ = contentType;
+    }
+
+    const std::string& GetContentType() const
+    {
+      return contentType_;
+    }
+
+    void SetContentFilename(const std::string& filename);
+
+    const std::string& GetContentFilename() const
+    {
+      return filename_;
+    }
+
+
+    /**
+     * Implementation of the IHttpStreamAnswer interface.
+     **/
+
+    virtual HttpCompression SetupHttpCompression(bool /*gzipAllowed*/, 
+                                                 bool /*deflateAllowed*/)
+    {
+      return HttpCompression_None;
+    }
+
+    virtual bool HasContentFilename(std::string& filename);
+    
+    virtual std::string GetContentType();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,772 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpOutput.h"
+
+#include "../ChunkedBuffer.h"
+#include "../Compression/GzipCompressor.h"
+#include "../Compression/ZlibCompressor.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <iostream>
+#include <vector>
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
+#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
+#  endif
+#endif
+
+
+namespace Orthanc
+{
+  HttpOutput::StateMachine::StateMachine(IHttpOutputStream& stream,
+                                         bool isKeepAlive) : 
+    stream_(stream),
+    state_(State_WritingHeader),
+    status_(HttpStatus_200_Ok),
+    hasContentLength_(false),
+    contentPosition_(0),
+    keepAlive_(isKeepAlive)
+  {
+  }
+
+  HttpOutput::StateMachine::~StateMachine()
+  {
+    if (state_ != State_Done)
+    {
+      //asm volatile ("int3;");
+      //LOG(ERROR) << "This HTTP answer does not contain any body";
+    }
+
+    if (hasContentLength_ && contentPosition_ != contentLength_)
+    {
+      LOG(ERROR) << "This HTTP answer has not sent the proper number of bytes in its body";
+    }
+  }
+
+
+  void HttpOutput::StateMachine::SetHttpStatus(HttpStatus status)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    status_ = status;
+  }
+
+
+  void HttpOutput::StateMachine::SetContentLength(uint64_t length)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    hasContentLength_ = true;
+    contentLength_ = length;
+  }
+
+  void HttpOutput::StateMachine::SetContentType(const char* contentType)
+  {
+    AddHeader("Content-Type", contentType);
+  }
+
+  void HttpOutput::StateMachine::SetContentFilename(const char* filename)
+  {
+    // TODO Escape double quotes
+    AddHeader("Content-Disposition", "filename=\"" + std::string(filename) + "\"");
+  }
+
+  void HttpOutput::StateMachine::SetCookie(const std::string& cookie,
+                                           const std::string& value)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    // TODO Escape "=" characters
+    AddHeader("Set-Cookie", cookie + "=" + value);
+  }
+
+
+  void HttpOutput::StateMachine::AddHeader(const std::string& header,
+                                           const std::string& value)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    headers_.push_back(header + ": " + value + "\r\n");
+  }
+
+  void HttpOutput::StateMachine::ClearHeaders()
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    headers_.clear();
+  }
+
+  void HttpOutput::StateMachine::SendBody(const void* buffer, size_t length)
+  {
+    if (state_ == State_Done)
+    {
+      if (length == 0)
+      {
+        return;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Because of keep-alive connections, the entire body must "
+                               "be sent at once or Content-Length must be given");
+      }
+    }
+
+    if (state_ == State_WritingMultipart)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (state_ == State_WritingHeader)
+    {
+      // Send the HTTP header before writing the body
+
+      stream_.OnHttpStatusReceived(status_);
+
+      std::string s = "HTTP/1.1 " + 
+        boost::lexical_cast<std::string>(status_) +
+        " " + std::string(EnumerationToString(status_)) +
+        "\r\n";
+
+      if (keepAlive_)
+      {
+        s += "Connection: keep-alive\r\n";
+      }
+      else
+      {
+        s += "Connection: close\r\n";
+      }
+
+      for (std::list<std::string>::const_iterator
+             it = headers_.begin(); it != headers_.end(); ++it)
+      {
+        s += *it;
+      }
+
+      if (status_ != HttpStatus_200_Ok)
+      {
+        hasContentLength_ = false;
+      }
+
+      uint64_t contentLength = (hasContentLength_ ? contentLength_ : length);
+      s += "Content-Length: " + boost::lexical_cast<std::string>(contentLength) + "\r\n\r\n";
+
+      stream_.Send(true, s.c_str(), s.size());
+      state_ = State_WritingBody;
+    }
+
+    if (hasContentLength_ &&
+        contentPosition_ + length > contentLength_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "The body size exceeds what was declared with SetContentSize()");
+    }
+
+    if (length > 0)
+    {
+      stream_.Send(false, buffer, length);
+      contentPosition_ += length;
+    }
+
+    if (!hasContentLength_ ||
+        contentPosition_ == contentLength_)
+    {
+      state_ = State_Done;
+    }
+  }
+
+
+  void HttpOutput::StateMachine::CloseBody()
+  {
+    switch (state_)
+    {
+      case State_WritingHeader:
+        SetContentLength(0);
+        SendBody(NULL, 0);
+        break;
+
+      case State_WritingBody:
+        if (!hasContentLength_ ||
+            contentPosition_ == contentLength_)
+        {
+          state_ = State_Done;
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                                 "The body size has not reached what was declared with SetContentSize()");
+        }
+
+        break;
+
+      case State_WritingMultipart:
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot invoke CloseBody() with multipart outputs");
+
+      case State_Done:
+        return;  // Ignore
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }      
+  }
+
+
+  HttpCompression HttpOutput::GetPreferredCompression(size_t bodySize) const
+  {
+#if 0
+    // TODO Do not compress small files?
+    if (bodySize < 512)
+    {
+      return HttpCompression_None;
+    }
+#endif
+
+    // Prefer "gzip" over "deflate" if the choice is offered
+
+    if (isGzipAllowed_)
+    {
+      return HttpCompression_Gzip;
+    }
+    else if (isDeflateAllowed_)
+    {
+      return HttpCompression_Deflate;
+    }
+    else
+    {
+      return HttpCompression_None;
+    }
+  }
+
+
+  void HttpOutput::SendMethodNotAllowed(const std::string& allowed)
+  {
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(HttpStatus_405_MethodNotAllowed);
+    stateMachine_.AddHeader("Allow", allowed);
+    stateMachine_.SendBody(NULL, 0);
+  }
+
+
+  void HttpOutput::SendStatus(HttpStatus status,
+			      const char* message,
+			      size_t messageSize)
+  {
+    if (status == HttpStatus_301_MovedPermanently ||
+        //status == HttpStatus_401_Unauthorized ||
+        status == HttpStatus_405_MethodNotAllowed)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Please use the dedicated methods to this HTTP status code in HttpOutput");
+    }
+    
+    stateMachine_.SetHttpStatus(status);
+    stateMachine_.SendBody(message, messageSize);
+  }
+
+
+  void HttpOutput::Redirect(const std::string& path)
+  {
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(HttpStatus_301_MovedPermanently);
+    stateMachine_.AddHeader("Location", path);
+    stateMachine_.SendBody(NULL, 0);
+  }
+
+
+  void HttpOutput::SendUnauthorized(const std::string& realm)
+  {
+    stateMachine_.ClearHeaders();
+    stateMachine_.SetHttpStatus(HttpStatus_401_Unauthorized);
+    stateMachine_.AddHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\"");
+    stateMachine_.SendBody(NULL, 0);
+  }
+
+  
+  void HttpOutput::Answer(const void* buffer, 
+                          size_t length)
+  {
+    if (length == 0)
+    {
+      AnswerEmpty();
+      return;
+    }
+
+    HttpCompression compression = GetPreferredCompression(length);
+
+    if (compression == HttpCompression_None)
+    {
+      stateMachine_.SetContentLength(length);
+      stateMachine_.SendBody(buffer, length);
+      return;
+    }
+
+    std::string compressed, encoding;
+
+    switch (compression)
+    {
+      case HttpCompression_Deflate:
+      {
+        encoding = "deflate";
+        ZlibCompressor compressor;
+        // Do not prefix the buffer with its uncompressed size, to be compatible with "deflate"
+        compressor.SetPrefixWithUncompressedSize(false);  
+        compressor.Compress(compressed, buffer, length);
+        break;
+      }
+
+      case HttpCompression_Gzip:
+      {
+        encoding = "gzip";
+        GzipCompressor compressor;
+        compressor.Compress(compressed, buffer, length);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    LOG(TRACE) << "Compressing a HTTP answer using " << encoding;
+
+    // The body is empty, do not use HTTP compression
+    if (compressed.size() == 0)
+    {
+      AnswerEmpty();
+    }
+    else
+    {
+      stateMachine_.AddHeader("Content-Encoding", encoding);
+      stateMachine_.SetContentLength(compressed.size());
+      stateMachine_.SendBody(compressed.c_str(), compressed.size());
+    }
+
+    stateMachine_.CloseBody();
+  }
+
+
+  void HttpOutput::Answer(const std::string& str)
+  {
+    Answer(str.size() == 0 ? NULL : str.c_str(), str.size());
+  }
+
+
+  void HttpOutput::AnswerEmpty()
+  {
+    stateMachine_.CloseBody();
+  }
+
+
+  void HttpOutput::StateMachine::CheckHeadersCompatibilityWithMultipart() const
+  {
+    for (std::list<std::string>::const_iterator
+           it = headers_.begin(); it != headers_.end(); ++it)
+    {
+      if (!Toolbox::StartsWith(*it, "Set-Cookie: "))
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "The only headers that can be set in multipart answers "
+                               "are Set-Cookie (here: " + *it + " is set)");
+      }
+    }
+  }
+
+
+  static void PrepareMultipartMainHeader(std::string& boundary,
+                                         std::string& contentTypeHeader,
+                                         const std::string& subType,
+                                         const std::string& contentType)
+  {
+    if (subType != "mixed" &&
+        subType != "related")
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    /**
+     * 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 + "\"";
+    }
+
+    boundary = Toolbox::GenerateUuid() + "-" + Toolbox::GenerateUuid();
+
+    /**
+     * Fix for issue #165: "Encapsulation boundaries must not appear
+     * within the encapsulations, and must be no longer than 70
+     * characters, not counting the two leading hyphens."
+     * https://tools.ietf.org/html/rfc1521
+     * https://bitbucket.org/sjodogne/orthanc/issues/165/
+     **/
+    if (boundary.size() != 36 + 1 + 36)  // one UUID contains 36 characters
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    boundary = boundary.substr(0, 70);
+    
+    contentTypeHeader = ("multipart/" + subType + "; type=" + tmp + "; boundary=" + boundary);
+  }
+
+
+  void HttpOutput::StateMachine::StartMultipart(const std::string& subType,
+                                                const std::string& contentType)
+  {
+    if (state_ != State_WritingHeader)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (status_ != HttpStatus_200_Ok)
+    {
+      SendBody(NULL, 0);
+      return;
+    }
+
+    stream_.OnHttpStatusReceived(status_);
+
+    std::string header = "HTTP/1.1 200 OK\r\n";
+
+    if (keepAlive_)
+    {
+#if ORTHANC_ENABLE_MONGOOSE == 1
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "Multipart answers are not implemented together "
+                             "with keep-alive connections if using Mongoose");
+      
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
+      // Turn off Keep-Alive for multipart answers
+      // https://github.com/civetweb/civetweb/issues/727
+      stream_.DisableKeepAlive();
+      header += "Connection: close\r\n";
+#  else
+      // The function "mg_disable_keep_alive()" is not available,
+      // let's continue with Keep-Alive. Performance of WADO-RS will
+      // decrease.
+      header += "Connection: keep-alive\r\n";
+#  endif   
+
+#else
+#  error Please support your embedded Web server here
+#endif
+    }
+    else
+    {
+      header += "Connection: close\r\n";
+    }
+
+    // Possibly add the cookies
+    CheckHeadersCompatibilityWithMultipart();
+
+    for (std::list<std::string>::const_iterator
+           it = headers_.begin(); it != headers_.end(); ++it)
+    {
+      header += *it;
+    }
+
+    std::string contentTypeHeader;
+    PrepareMultipartMainHeader(multipartBoundary_, contentTypeHeader, subType, contentType);
+    multipartContentType_ = contentType;
+    header += ("Content-Type: " + contentTypeHeader + "\r\n\r\n");
+
+    stream_.Send(true, header.c_str(), header.size());
+    state_ = State_WritingMultipart;
+  }
+
+
+  static void PrepareMultipartItemHeader(std::string& target,
+                                         size_t length,
+                                         const std::map<std::string, std::string>& headers,
+                                         const std::string& boundary,
+                                         const std::string& contentType)
+  {
+    target = "--" + boundary + "\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)
+    {
+      target += 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)
+    {
+      target += "Content-Type: " + contentType + "\r\n";
+    }
+
+    if (!hasContentLength)
+    {
+      target += "Content-Length: " + boost::lexical_cast<std::string>(length) + "\r\n";
+    }
+
+    if (!hasMimeVersion)
+    {
+      target += "MIME-Version: 1.0\r\n\r\n";
+    }
+  }
+
+
+  void HttpOutput::StateMachine::SendMultipartItem(const void* item,
+                                                   size_t length,
+                                                   const std::map<std::string, std::string>& headers)
+  {
+    if (state_ != State_WritingMultipart)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    std::string header;
+    PrepareMultipartItemHeader(header, length, headers, multipartBoundary_, multipartContentType_);
+    stream_.Send(false, header.c_str(), header.size());
+
+    if (length > 0)
+    {
+      stream_.Send(false, item, length);
+    }
+
+    stream_.Send(false, "\r\n", 2);    
+  }
+
+
+  void HttpOutput::StateMachine::CloseMultipart()
+  {
+    if (state_ != State_WritingMultipart)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    // The two lines below might throw an exception, if the client has
+    // closed the connection. Such an error is ignored.
+    try
+    {
+      std::string header = "--" + multipartBoundary_ + "--\r\n";
+      stream_.Send(false, header.c_str(), header.size());
+    }
+    catch (OrthancException&)
+    {
+    }
+
+    state_ = State_Done;
+  }
+
+
+  static void AnswerStreamAsBuffer(HttpOutput& output,
+                                   IHttpStreamAnswer& stream)
+  {
+    ChunkedBuffer buffer;
+
+    while (stream.ReadNextChunk())
+    {
+      if (stream.GetChunkSize() > 0)
+      {
+        buffer.AddChunk(stream.GetChunkContent(), stream.GetChunkSize());
+      }
+    }
+
+    std::string s;
+    buffer.Flatten(s);
+
+    output.SetContentType(stream.GetContentType());
+    
+    std::string filename;
+    if (stream.HasContentFilename(filename))
+    {
+      output.SetContentFilename(filename.c_str());
+    }
+
+    output.Answer(s);
+  }
+
+
+  void HttpOutput::Answer(IHttpStreamAnswer& stream)
+  {
+    HttpCompression compression = stream.SetupHttpCompression(isGzipAllowed_, isDeflateAllowed_);
+
+    switch (compression)
+    {
+      case HttpCompression_None:
+      {
+        if (isGzipAllowed_ || isDeflateAllowed_)
+        {
+          // New in Orthanc 1.5.7: Compress streams without built-in
+          // compression, if requested by the "Accept-Encoding" HTTP
+          // header
+          AnswerStreamAsBuffer(*this, stream);
+          return;
+        }
+        
+        break;
+      }
+
+      case HttpCompression_Gzip:
+        stateMachine_.AddHeader("Content-Encoding", "gzip");
+        break;
+
+      case HttpCompression_Deflate:
+        stateMachine_.AddHeader("Content-Encoding", "deflate");
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    stateMachine_.SetContentLength(stream.GetContentLength());
+
+    std::string contentType = stream.GetContentType();
+    if (contentType.empty())
+    {
+      contentType = MIME_BINARY;
+    }
+
+    stateMachine_.SetContentType(contentType.c_str());
+
+    std::string filename;
+    if (stream.HasContentFilename(filename))
+    {
+      SetContentFilename(filename.c_str());
+    }
+
+    while (stream.ReadNextChunk())
+    {
+      stateMachine_.SendBody(stream.GetChunkContent(),
+                             stream.GetChunkSize());
+    }
+
+    stateMachine_.CloseBody();
+  }
+
+
+  void HttpOutput::AnswerMultipartWithoutChunkedTransfer(
+    const std::string& subType,
+    const std::string& contentType,
+    const std::vector<const void*>& parts,
+    const std::vector<size_t>& sizes,
+    const std::vector<const std::map<std::string, std::string>*>& headers)
+  {
+    if (parts.size() != sizes.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    stateMachine_.CheckHeadersCompatibilityWithMultipart();
+
+    std::string boundary, contentTypeHeader;
+    PrepareMultipartMainHeader(boundary, contentTypeHeader, subType, contentType);
+    SetContentType(contentTypeHeader);
+
+    std::map<std::string, std::string> empty;
+
+    ChunkedBuffer chunked;
+    for (size_t i = 0; i < parts.size(); i++)
+    {
+      std::string partHeader;
+      PrepareMultipartItemHeader(partHeader, sizes[i], headers[i] == NULL ? empty : *headers[i], 
+                                 boundary, contentType);
+
+      chunked.AddChunk(partHeader);
+      chunked.AddChunk(parts[i], sizes[i]);
+      chunked.AddChunk("\r\n");    
+    }
+
+    chunked.AddChunk("--" + boundary + "--\r\n");
+
+    std::string body;
+    chunked.Flatten(body);
+    Answer(body);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpOutput.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,250 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IHttpOutputStream.h"
+#include "IHttpStreamAnswer.h"
+
+#include <list>
+#include <string>
+#include <stdint.h>
+#include <map>
+#include <vector>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HttpOutput : public boost::noncopyable
+  {
+  private:
+    typedef std::list< std::pair<std::string, std::string> >  Header;
+
+    class StateMachine : public boost::noncopyable
+    {
+    public:
+      enum State
+      {
+        State_WritingHeader,      
+        State_WritingBody,
+        State_WritingMultipart,
+        State_Done
+      };
+
+    private:
+      IHttpOutputStream& stream_;
+      State state_;
+
+      HttpStatus status_;
+      bool hasContentLength_;
+      uint64_t contentLength_;
+      uint64_t contentPosition_;
+      bool keepAlive_;
+      std::list<std::string> headers_;
+
+      std::string multipartBoundary_;
+      std::string multipartContentType_;
+
+    public:
+      StateMachine(IHttpOutputStream& stream,
+                   bool isKeepAlive);
+
+      ~StateMachine();
+
+      void SetHttpStatus(HttpStatus status);
+
+      void SetContentLength(uint64_t length);
+
+      void SetContentType(const char* contentType);
+
+      void SetContentFilename(const char* filename);
+
+      void SetCookie(const std::string& cookie,
+                     const std::string& value);
+
+      void AddHeader(const std::string& header,
+                     const std::string& value);
+
+      void ClearHeaders();
+
+      void SendBody(const void* buffer, size_t length);
+
+      void StartMultipart(const std::string& subType,
+                          const std::string& contentType);
+
+      void SendMultipartItem(const void* item, 
+                             size_t length,
+                             const std::map<std::string, std::string>& headers);
+
+      void CloseMultipart();
+
+      void CloseBody();
+
+      State GetState() const
+      {
+        return state_;
+      }
+
+      void CheckHeadersCompatibilityWithMultipart() const;
+    };
+
+    StateMachine stateMachine_;
+    bool         isDeflateAllowed_;
+    bool         isGzipAllowed_;
+
+    HttpCompression GetPreferredCompression(size_t bodySize) const;
+
+  public:
+    HttpOutput(IHttpOutputStream& stream,
+               bool isKeepAlive) : 
+      stateMachine_(stream, isKeepAlive),
+      isDeflateAllowed_(false),
+      isGzipAllowed_(false)
+    {
+    }
+
+    void SetDeflateAllowed(bool allowed)
+    {
+      isDeflateAllowed_ = allowed;
+    }
+
+    bool IsDeflateAllowed() const
+    {
+      return isDeflateAllowed_;
+    }
+
+    void SetGzipAllowed(bool allowed)
+    {
+      isGzipAllowed_ = allowed;
+    }
+
+    bool IsGzipAllowed() const
+    {
+      return isGzipAllowed_;
+    }
+
+    void SendStatus(HttpStatus status,
+		    const char* message,
+		    size_t messageSize);
+
+    void SendStatus(HttpStatus status)
+    {
+      SendStatus(status, NULL, 0);
+    }
+
+    void SendStatus(HttpStatus status,
+		    const std::string& message)
+    {
+      SendStatus(status, message.c_str(), message.size());
+    }
+
+    void SetContentType(MimeType contentType)
+    {
+      stateMachine_.SetContentType(EnumerationToString(contentType));
+    }
+    
+    void SetContentType(const std::string& contentType)
+    {
+      stateMachine_.SetContentType(contentType.c_str());
+    }
+
+    void SetContentFilename(const char* filename)
+    {
+      stateMachine_.SetContentFilename(filename);
+    }
+
+    void SetCookie(const std::string& cookie,
+                   const std::string& value)
+    {
+      stateMachine_.SetCookie(cookie, value);
+    }
+
+    void AddHeader(const std::string& key,
+                   const std::string& value)
+    {
+      stateMachine_.AddHeader(key, value);
+    }
+
+    void Answer(const void* buffer, 
+                size_t length);
+
+    void Answer(const std::string& str);
+
+    void AnswerEmpty();
+
+    void SendMethodNotAllowed(const std::string& allowed);
+
+    void Redirect(const std::string& path);
+
+    void SendUnauthorized(const std::string& realm);
+
+    void StartMultipart(const std::string& subType,
+                        const std::string& contentType)
+    {
+      stateMachine_.StartMultipart(subType, contentType);
+    }
+
+    void SendMultipartItem(const void* item, 
+                           size_t size,
+                           const std::map<std::string, std::string>& headers)
+    {
+      stateMachine_.SendMultipartItem(item, size, headers);
+    }
+
+    void CloseMultipart()
+    {
+      stateMachine_.CloseMultipart();
+    }
+
+    bool IsWritingMultipart() const
+    {
+      return stateMachine_.GetState() == StateMachine::State_WritingMultipart;
+    }
+
+    void Answer(IHttpStreamAnswer& stream);
+
+    /**
+     * This method is a replacement to the combination
+     * "StartMultipart()" + "SendMultipartItem()". It generates the
+     * same answer, but it gives a chance to compress the body if
+     * "Accept-Encoding: gzip" is provided by the client, which is not
+     * possible in chunked transfers.
+     **/
+    void AnswerMultipartWithoutChunkedTransfer(
+      const std::string& subType,
+      const std::string& contentType,
+      const std::vector<const void*>& parts,
+      const std::vector<size_t>& sizes,
+      const std::vector<const std::map<std::string, std::string>*>& headers);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1385 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+// http://en.highscore.de/cpp/boost/stringhandling.html
+
+#include "../PrecompiledHeaders.h"
+#include "HttpServer.h"
+
+#include "../ChunkedBuffer.h"
+#include "../FileBuffer.h"
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../TemporaryFile.h"
+#include "HttpToolbox.h"
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+#  include <mongoose.h>
+
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  include <civetweb.h>
+#  define MONGOOSE_USE_CALLBACKS 1
+#  if !defined(CIVETWEB_HAS_DISABLE_KEEP_ALIVE)
+#    error Macro CIVETWEB_HAS_DISABLE_KEEP_ALIVE must be defined
+#  endif
+
+#else
+#  error "Either Mongoose or Civetweb must be enabled to compile this file"
+#endif
+
+#include <algorithm>
+#include <string.h>
+#include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/filesystem.hpp>
+#include <iostream>
+#include <string.h>
+#include <stdio.h>
+#include <boost/thread.hpp>
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+#  include <openssl/opensslv.h>
+#  include <openssl/err.h>
+#endif
+
+#define ORTHANC_REALM "Orthanc Secure Area"
+
+
+namespace Orthanc
+{
+  static const char MULTIPART_FORM[] = "multipart/form-data; boundary=";
+  static unsigned int MULTIPART_FORM_LENGTH = sizeof(MULTIPART_FORM) / sizeof(char) - 1;
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    class MongooseOutputStream : public IHttpOutputStream
+    {
+    private:
+      struct mg_connection* connection_;
+
+    public:
+      MongooseOutputStream(struct mg_connection* connection) : connection_(connection)
+      {
+      }
+
+      virtual void Send(bool isHeader, const void* buffer, size_t length)
+      {
+        if (length > 0)
+        {
+          int status = mg_write(connection_, buffer, length);
+          if (status != static_cast<int>(length))
+          {
+            // status == 0 when the connection has been closed, -1 on error
+            throw OrthancException(ErrorCode_NetworkProtocol);
+          }
+        }
+      }
+
+      virtual void OnHttpStatusReceived(HttpStatus status)
+      {
+        // Ignore this
+      }
+
+      virtual void DisableKeepAlive()
+      {
+#if ORTHANC_ENABLE_MONGOOSE == 1
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only available if using CivetWeb");
+
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+#  if CIVETWEB_HAS_DISABLE_KEEP_ALIVE == 1
+        mg_disable_keep_alive(connection_);
+#  else
+#       warning The function "mg_disable_keep_alive()" is not available, DICOMweb might run slowly
+        throw OrthancException(ErrorCode_NotImplemented,
+                               "Only available if using a patched version of CivetWeb");
+#  endif
+
+#else
+#  error Please support your embedded Web server here
+#endif
+      }
+    };
+
+
+    enum PostDataStatus
+    {
+      PostDataStatus_Success,
+      PostDataStatus_NoLength,
+      PostDataStatus_Pending,
+      PostDataStatus_Failure
+    };
+  }
+
+
+// TODO Move this to external file
+
+
+  class ChunkedFile : public ChunkedBuffer
+  {
+  private:
+    std::string filename_;
+
+  public:
+    ChunkedFile(const std::string& filename) :
+      filename_(filename)
+    {
+    }
+
+    const std::string& GetFilename() const
+    {
+      return filename_;
+    }
+  };
+
+
+
+  class ChunkStore : public boost::noncopyable
+  {
+  private:
+    typedef std::list<ChunkedFile*>  Content;
+    Content  content_;
+    unsigned int numPlaces_;
+
+    boost::mutex mutex_;
+    std::set<std::string> discardedFiles_;
+
+    void Clear()
+    {
+      for (Content::iterator it = content_.begin();
+           it != content_.end(); ++it)
+      {
+        delete *it;
+      }
+    }
+
+    Content::iterator Find(const std::string& filename)
+    {
+      for (Content::iterator it = content_.begin();
+           it != content_.end(); ++it)
+      {
+        if ((*it)->GetFilename() == filename)
+        {
+          return it;
+        }
+      }
+
+      return content_.end();
+    }
+
+    void Remove(const std::string& filename)
+    {
+      Content::iterator it = Find(filename);
+      if (it != content_.end())
+      {
+        delete *it;
+        content_.erase(it);
+      }
+    }
+
+  public:
+    ChunkStore()
+    {
+      numPlaces_ = 10;
+    }
+
+    ~ChunkStore()
+    {
+      Clear();
+    }
+
+    PostDataStatus Store(std::string& completed,
+                         const char* chunkData,
+                         size_t chunkSize,
+                         const std::string& filename,
+                         size_t filesize)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      std::set<std::string>::iterator wasDiscarded = discardedFiles_.find(filename);
+      if (wasDiscarded != discardedFiles_.end())
+      {
+        discardedFiles_.erase(wasDiscarded);
+        return PostDataStatus_Failure;
+      }
+
+      ChunkedFile* f;
+      Content::iterator it = Find(filename);
+      if (it == content_.end())
+      {
+        f = new ChunkedFile(filename);
+
+        // Make some room
+        if (content_.size() >= numPlaces_)
+        {
+          discardedFiles_.insert(content_.front()->GetFilename());
+          delete content_.front();
+          content_.pop_front();
+        }
+
+        content_.push_back(f);
+      }
+      else
+      {
+        f = *it;
+      }
+
+      f->AddChunk(chunkData, chunkSize);
+
+      if (f->GetNumBytes() > filesize)
+      {
+        Remove(filename);
+      }
+      else if (f->GetNumBytes() == filesize)
+      {
+        f->Flatten(completed);
+        Remove(filename);
+        return PostDataStatus_Success;
+      }
+
+      return PostDataStatus_Pending;
+    }
+
+    /*void Print() 
+      {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      printf("ChunkStore status:\n");
+      for (Content::const_iterator i = content_.begin();
+      i != content_.end(); i++)
+      {
+      printf("  [%s]: %d\n", (*i)->GetFilename().c_str(), (*i)->GetNumBytes());
+      }
+      printf("-----\n");
+      }*/
+  };
+
+
+  struct HttpServer::PImpl
+  {
+    struct mg_context *context_;
+    ChunkStore chunkStore_;
+  };
+
+
+  ChunkStore& HttpServer::GetChunkStore()
+  {
+    return pimpl_->chunkStore_;
+  }
+
+
+  static PostDataStatus ReadBodyWithContentLength(std::string& body,
+                                                  struct mg_connection *connection,
+                                                  const std::string& contentLength)
+  {
+    int length;
+    try
+    {
+      length = boost::lexical_cast<int>(contentLength);
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return PostDataStatus_NoLength;
+    }
+
+    if (length < 0)
+    {
+      length = 0;
+    }
+
+    body.resize(length);
+
+    size_t pos = 0;
+    while (length > 0)
+    {
+      int r = mg_read(connection, &body[pos], length);
+      if (r <= 0)
+      {
+        return PostDataStatus_Failure;
+      }
+
+      assert(r <= length);
+      length -= r;
+      pos += r;
+    }
+
+    return PostDataStatus_Success;
+  }
+                                                  
+
+  static PostDataStatus ReadBodyToString(std::string& body,
+                                         struct mg_connection *connection,
+                                         const IHttpHandler::Arguments& headers)
+  {
+    IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length");
+
+    if (contentLength != headers.end())
+    {
+      // "Content-Length" is available
+      return ReadBodyWithContentLength(body, connection, contentLength->second);
+    }
+    else
+    {
+      // No Content-Length. Store the individual chunks in a temporary
+      // file, then read it back into the memory buffer "body"
+      FileBuffer buffer;
+
+      std::string tmp(1024 * 1024, 0);
+      
+      for (;;)
+      {
+        int r = mg_read(connection, &tmp[0], tmp.size());
+        if (r < 0)
+        {
+          return PostDataStatus_Failure;
+        }
+        else if (r == 0)
+        {
+          break;
+        }
+        else
+        {
+          buffer.Append(tmp.c_str(), r);
+        }
+      }
+
+      buffer.Read(body);
+
+      return PostDataStatus_Success;
+    }
+  }
+
+
+  static PostDataStatus ReadBodyToStream(IHttpHandler::IChunkedRequestReader& stream,
+                                         struct mg_connection *connection,
+                                         const IHttpHandler::Arguments& headers)
+  {
+    IHttpHandler::Arguments::const_iterator contentLength = headers.find("content-length");
+
+    if (contentLength != headers.end())
+    {
+      // "Content-Length" is available
+      std::string body;
+      PostDataStatus status = ReadBodyWithContentLength(body, connection, contentLength->second);
+
+      if (status == PostDataStatus_Success &&
+          !body.empty())
+      {
+        stream.AddBodyChunk(body.c_str(), body.size());
+      }
+
+      return status;
+    }
+    else
+    {
+      // No Content-Length: This is a chunked transfer. Stream the HTTP connection.
+      std::string tmp(1024 * 1024, 0);
+      
+      for (;;)
+      {
+        int r = mg_read(connection, &tmp[0], tmp.size());
+        if (r < 0)
+        {
+          return PostDataStatus_Failure;
+        }
+        else if (r == 0)
+        {
+          break;
+        }
+        else
+        {
+          stream.AddBodyChunk(tmp.c_str(), r);
+        }
+      }
+
+      return PostDataStatus_Success;
+    }
+  }
+
+
+  static PostDataStatus ParseMultipartForm(std::string &completedFile,
+                                           struct mg_connection *connection,
+                                           const IHttpHandler::Arguments& headers,
+                                           const std::string& contentType,
+                                           ChunkStore& chunkStore)
+  {
+    std::string boundary = "--" + contentType.substr(MULTIPART_FORM_LENGTH);
+
+    std::string body;
+    PostDataStatus status = ReadBodyToString(body, connection, headers);
+
+    if (status != PostDataStatus_Success)
+    {
+      return status;
+    }
+
+    /*for (IHttpHandler::Arguments::const_iterator i = headers.begin(); i != headers.end(); i++)
+      {
+      std::cout << "Header [" << i->first << "] = " << i->second << "\n";
+      }
+      printf("CHUNK\n");*/
+
+    typedef IHttpHandler::Arguments::const_iterator ArgumentIterator;
+
+    ArgumentIterator requestedWith = headers.find("x-requested-with");
+    ArgumentIterator fileName = headers.find("x-file-name");
+    ArgumentIterator fileSizeStr = headers.find("x-file-size");
+
+    if (requestedWith != headers.end() &&
+        requestedWith->second != "XMLHttpRequest")
+    {
+      return PostDataStatus_Failure; 
+    }
+
+    size_t fileSize = 0;
+    if (fileSizeStr != headers.end())
+    {
+      try
+      {
+        fileSize = boost::lexical_cast<size_t>(fileSizeStr->second);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return PostDataStatus_Failure;
+      }
+    }
+
+    typedef boost::find_iterator<std::string::iterator> FindIterator;
+    typedef boost::iterator_range<char*> Range;
+
+    //chunkStore.Print();
+
+    // TODO - Refactor using class "MultipartStreamReader"
+    try
+    {
+      FindIterator last;
+      for (FindIterator it =
+             make_find_iterator(body, boost::first_finder(boundary));
+           it!=FindIterator();
+           ++it)
+      {
+        if (last != FindIterator())
+        {
+          Range part(&last->back(), &it->front());
+          Range content = boost::find_first(part, "\r\n\r\n");
+          if (/*content != Range()*/!content.empty())
+          {
+            Range c(&content.back() + 1, &it->front() - 2);
+            size_t chunkSize = c.size();
+
+            if (chunkSize > 0)
+            {
+              const char* chunkData = &c.front();
+
+              if (fileName == headers.end())
+              {
+                // This file is stored in a single chunk
+                completedFile.resize(chunkSize);
+                if (chunkSize > 0)
+                {
+                  memcpy(&completedFile[0], chunkData, chunkSize);
+                }
+                return PostDataStatus_Success;
+              }
+              else
+              {
+                return chunkStore.Store(completedFile, chunkData, chunkSize, fileName->second, fileSize);
+              }
+            }
+          }
+        }
+
+        last = it;
+      }
+    }
+    catch (std::length_error&)
+    {
+      return PostDataStatus_Failure;
+    }
+
+    return PostDataStatus_Pending;
+  }
+
+
+  static bool IsAccessGranted(const HttpServer& that,
+                              const IHttpHandler::Arguments& headers)
+  {
+    bool granted = false;
+
+    IHttpHandler::Arguments::const_iterator auth = headers.find("authorization");
+    if (auth != headers.end())
+    {
+      std::string s = auth->second;
+      if (s.size() > 6 &&
+          s.substr(0, 6) == "Basic ")
+      {
+        std::string b64 = s.substr(6);
+        granted = that.IsValidBasicHttpAuthentication(b64);
+      }
+    }
+
+    return granted;
+  }
+
+
+  static std::string GetAuthenticatedUsername(const IHttpHandler::Arguments& headers)
+  {
+    IHttpHandler::Arguments::const_iterator auth = headers.find("authorization");
+
+    if (auth == headers.end())
+    {
+      return "";
+    }
+
+    std::string s = auth->second;
+    if (s.size() <= 6 ||
+        s.substr(0, 6) != "Basic ")
+    {
+      return "";
+    }
+
+    std::string b64 = s.substr(6);
+    std::string decoded;
+    Toolbox::DecodeBase64(decoded, b64);
+    size_t semicolons = decoded.find(':');
+
+    if (semicolons == std::string::npos)
+    {
+      // Bad-formatted request
+      return "";
+    }
+    else
+    {
+      return decoded.substr(0, semicolons);
+    }
+  }
+
+
+  static bool ExtractMethod(HttpMethod& method,
+                            const struct mg_request_info *request,
+                            const IHttpHandler::Arguments& headers,
+                            const IHttpHandler::GetArguments& argumentsGET)
+  {
+    std::string overriden;
+
+    // Check whether some PUT/DELETE faking is done
+
+    // 1. Faking with Google's approach
+    IHttpHandler::Arguments::const_iterator methodOverride =
+      headers.find("x-http-method-override");
+
+    if (methodOverride != headers.end())
+    {
+      overriden = methodOverride->second;
+    }
+    else if (!strcmp(request->request_method, "GET"))
+    {
+      // 2. Faking with Ruby on Rail's approach
+      // GET /my/resource?_method=delete <=> DELETE /my/resource
+      for (size_t i = 0; i < argumentsGET.size(); i++)
+      {
+        if (argumentsGET[i].first == "_method")
+        {
+          overriden = argumentsGET[i].second;
+          break;
+        }
+      }
+    }
+
+    if (overriden.size() > 0)
+    {
+      // A faking has been done within this request
+      Toolbox::ToUpperCase(overriden);
+
+      LOG(INFO) << "HTTP method faking has been detected for " << overriden;
+
+      if (overriden == "PUT")
+      {
+        method = HttpMethod_Put;
+        return true;
+      }
+      else if (overriden == "DELETE")
+      {
+        method = HttpMethod_Delete;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    // No PUT/DELETE faking was present
+    if (!strcmp(request->request_method, "GET"))
+    {
+      method = HttpMethod_Get;
+    }
+    else if (!strcmp(request->request_method, "POST"))
+    {
+      method = HttpMethod_Post;
+    }
+    else if (!strcmp(request->request_method, "DELETE"))
+    {
+      method = HttpMethod_Delete;
+    }
+    else if (!strcmp(request->request_method, "PUT"))
+    {
+      method = HttpMethod_Put;
+    }
+    else
+    {
+      return false;
+    }    
+
+    return true;
+  }
+
+
+  static void ConfigureHttpCompression(HttpOutput& output,
+                                       const IHttpHandler::Arguments& headers)
+  {
+    // Look if the client wishes HTTP compression
+    // https://en.wikipedia.org/wiki/HTTP_compression
+    IHttpHandler::Arguments::const_iterator it = headers.find("accept-encoding");
+    if (it != headers.end())
+    {
+      std::vector<std::string> encodings;
+      Toolbox::TokenizeString(encodings, it->second, ',');
+
+      for (size_t i = 0; i < encodings.size(); i++)
+      {
+        std::string s = Toolbox::StripSpaces(encodings[i]);
+
+        if (s == "deflate")
+        {
+          output.SetDeflateAllowed(true);
+        }
+        else if (s == "gzip")
+        {
+          output.SetGzipAllowed(true);
+        }
+      }
+    }
+  }
+
+
+  static void InternalCallback(HttpOutput& output /* out */,
+                               HttpMethod& method /* out */,
+                               HttpServer& server,
+                               struct mg_connection *connection,
+                               const struct mg_request_info *request)
+  {
+    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 (!server.IsRemoteAccessAllowed() &&
+        !localhost)
+    {
+      output.SendUnauthorized(server.GetRealm());
+      return;
+    }
+
+
+    // Extract the HTTP headers
+    IHttpHandler::Arguments headers;
+    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, value));
+      VLOG(1) << "HTTP header: [" << name << "]: [" << value << "]";
+    }
+
+    if (server.IsHttpCompressionEnabled())
+    {
+      ConfigureHttpCompression(output, headers);
+    }
+
+
+    // Extract the GET arguments
+    IHttpHandler::GetArguments argumentsGET;
+    if (!strcmp(request->request_method, "GET"))
+    {
+      HttpToolbox::ParseGetArguments(argumentsGET, request->query_string);
+    }
+
+
+    // Compute the HTTP method, taking method faking into consideration
+    method = HttpMethod_Get;
+    if (!ExtractMethod(method, request, headers, argumentsGET))
+    {
+      output.SendStatus(HttpStatus_400_BadRequest);
+      return;
+    }
+
+
+    // Authenticate this connection
+    if (server.IsAuthenticationEnabled() && 
+        !IsAccessGranted(server, headers))
+    {
+      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", 
+            reinterpret_cast<const uint8_t*>(&request->remote_ip) [3], 
+            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]);
+
+    const char* requestUri = request->uri;
+      
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+    const char* remoteIp = request->remote_addr;
+    const char* requestUri = request->local_uri;
+#else
+#  error
+#endif
+
+    if (requestUri == NULL)
+    {
+      requestUri = "";
+    }
+      
+    std::string username = GetAuthenticatedUsername(headers);
+
+    IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter();
+    if (filter != NULL)
+    {
+      if (!filter->IsAllowed(method, requestUri, remoteIp,
+                             username.c_str(), headers, argumentsGET))
+      {
+        //output.SendUnauthorized(server.GetRealm());
+        output.SendStatus(HttpStatus_403_Forbidden);
+        return;
+      }
+    }
+
+
+    // Decompose the URI into its components
+    UriComponents uri;
+    try
+    {
+      Toolbox::SplitUriComponents(uri, requestUri);
+    }
+    catch (OrthancException&)
+    {
+      output.SendStatus(HttpStatus_400_BadRequest);
+      return;
+    }
+
+    LOG(INFO) << EnumerationToString(method) << " " << Toolbox::FlattenUri(uri);
+
+
+    bool found = false;
+
+    // Extract the body of the request for PUT and POST, or process
+    // the body as a stream
+
+    // TODO Avoid unneccessary memcopy of the body
+
+    std::string body;
+    if (method == HttpMethod_Post ||
+        method == HttpMethod_Put)
+    {
+      PostDataStatus status;
+
+      bool isMultipartForm = false;
+
+      IHttpHandler::Arguments::const_iterator ct = headers.find("content-type");
+      if (ct != headers.end() &&
+          ct->second.size() >= MULTIPART_FORM_LENGTH &&
+          !memcmp(ct->second.c_str(), MULTIPART_FORM, MULTIPART_FORM_LENGTH))
+      {
+        /** 
+         * The user uses the "upload" form of Orthanc Explorer, for
+         * file uploads through a HTML form.
+         **/
+        status = ParseMultipartForm(body, connection, headers, ct->second, server.GetChunkStore());
+        isMultipartForm = true;
+      }
+
+      if (!isMultipartForm)
+      {
+        std::unique_ptr<IHttpHandler::IChunkedRequestReader> stream;
+
+        if (server.HasHandler())
+        {
+          found = server.GetHandler().CreateChunkedRequestReader
+            (stream, RequestOrigin_RestApi, remoteIp, username.c_str(), method, uri, headers);
+        }
+        
+        if (found)
+        {
+          if (stream.get() == NULL)
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          status = ReadBodyToStream(*stream, connection, headers);
+
+          if (status == PostDataStatus_Success)
+          {
+            stream->Execute(output);
+          }
+        }
+        else
+        {
+          status = ReadBodyToString(body, connection, headers);
+        }
+      }
+
+      switch (status)
+      {
+        case PostDataStatus_NoLength:
+          output.SendStatus(HttpStatus_411_LengthRequired);
+          return;
+
+        case PostDataStatus_Failure:
+          output.SendStatus(HttpStatus_400_BadRequest);
+          return;
+
+        case PostDataStatus_Pending:
+          output.AnswerEmpty();
+          return;
+
+        case PostDataStatus_Success:
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    if (!found && 
+        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
+    {
+#if ORTHANC_ENABLE_MONGOOSE == 1
+      void *that = request->user_data;
+      const char* requestUri = request->uri;
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+      // https://github.com/civetweb/civetweb/issues/409
+      void *that = mg_get_user_data(mg_get_context(connection));
+      const char* requestUri = request->local_uri;
+#else
+#  error
+#endif
+
+      if (requestUri == NULL)
+      {
+        requestUri = "";
+      }
+      
+      HttpServer* server = reinterpret_cast<HttpServer*>(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
+      {
+        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&)
+        {
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Syntax error in some user-supplied data");
+        }
+        catch (boost::filesystem::filesystem_error& e)
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Error while accessing the filesystem: " + e.path1().string());
+        }
+        catch (std::runtime_error&)
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Presumably an error while parsing the JSON body");
+        }
+        catch (std::bad_alloc&)
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory,
+                                 "The server hosting Orthanc is running out of memory");
+        }
+        catch (...)
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "An unhandled exception was generated inside the HTTP server");
+        }
+      }
+      catch (OrthancException& e)
+      {
+        assert(server != NULL);
+
+        // 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, requestUri);
+          }
+        }
+        catch (OrthancException&)
+        {
+          // An exception here reflects the fact that the status code
+          // was already set by the HTTP handler.
+        }
+      }
+    }
+    catch (...)
+    {
+      // 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";
+    }
+  }
+
+
+#if MONGOOSE_USE_CALLBACKS == 0
+  static void* Callback(enum mg_event event,
+                        struct mg_connection *connection,
+                        const struct mg_request_info *request)
+  {
+    if (event == MG_NEW_REQUEST) 
+    {
+      ProtectedCallback(connection, request);
+
+      // Mark as processed
+      return (void*) "";
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+#elif MONGOOSE_USE_CALLBACKS == 1
+  static int Callback(struct mg_connection *connection)
+  {
+    const struct mg_request_info *request = mg_get_request_info(connection);
+
+    ProtectedCallback(connection, request);
+
+    return 1;  // Do not let Mongoose handle the request by itself
+  }
+
+#else
+#  error Please set MONGOOSE_USE_CALLBACKS
+#endif
+
+
+
+
+
+  bool HttpServer::IsRunning() const
+  {
+    return (pimpl_->context_ != NULL);
+  }
+
+
+  HttpServer::HttpServer() : pimpl_(new PImpl)
+  {
+    pimpl_->context_ = NULL;
+    handler_ = NULL;
+    remoteAllowed_ = false;
+    authentication_ = false;
+    ssl_ = false;
+    port_ = 8000;
+    filter_ = NULL;
+    keepAlive_ = false;
+    httpCompression_ = true;
+    exceptionFormatter_ = NULL;
+    realm_ = ORTHANC_REALM;
+    threadsCount_ = 50;  // Default value in mongoose
+    tcpNoDelay_ = true;
+    requestTimeout_ = 30;  // Default value in mongoose/civetweb (30 seconds)
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+    LOG(INFO) << "This Orthanc server uses Mongoose as its embedded HTTP server";
+#endif
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+    LOG(INFO) << "This Orthanc server uses CivetWeb as its embedded HTTP server";
+#endif
+
+#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 */ &&
+        OPENSSL_VERSION_NUMBER >= 0x1000100fL  /* openssl-1.0.1 */) 
+    {
+      LOG(WARNING) << "This version of OpenSSL is vulnerable to the Heartbleed exploit";
+    }
+#endif
+  }
+
+
+  HttpServer::~HttpServer()
+  {
+    Stop();
+  }
+
+
+  void HttpServer::SetPortNumber(uint16_t port)
+  {
+    Stop();
+    port_ = port;
+  }
+
+  void HttpServer::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_);
+      std::string numThreads = boost::lexical_cast<std::string>(threadsCount_);
+      std::string requestTimeoutMilliseconds = boost::lexical_cast<std::string>(requestTimeout_ * 1000);
+
+      if (ssl_)
+      {
+        port += "s";
+      }
+
+      const char *options[] = {
+        // Set the TCP port for the HTTP server
+        "listening_ports", port.c_str(), 
+        
+        // Optimization reported by Chris Hafey
+        // 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
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+        // Disable TCP Nagle's algorithm to maximize speed (this
+        // option is not available in Mongoose).
+        // https://groups.google.com/d/topic/civetweb/35HBR9seFjU/discussion
+        // https://eklitzke.org/the-caveats-of-tcp-nodelay
+        "tcp_nodelay", (tcpNoDelay_ ? "1" : "0"),
+#endif
+
+        // Set the number of threads
+        "num_threads", numThreads.c_str(),
+        
+        // Set the timeout for the HTTP server
+        "request_timeout_ms", requestTimeoutMilliseconds.c_str(),
+
+        // Set the SSL certificate, if any. This must be the last option.
+        ssl_ ? "ssl_certificate" : NULL,
+        certificate_.c_str(),
+        NULL
+      };
+
+#if MONGOOSE_USE_CALLBACKS == 0
+      pimpl_->context_ = mg_start(&Callback, this, options);
+
+#elif MONGOOSE_USE_CALLBACKS == 1
+      struct mg_callbacks callbacks;
+      memset(&callbacks, 0, sizeof(callbacks));
+      callbacks.begin_request = Callback;
+      pimpl_->context_ = mg_start(&callbacks, this, options);
+
+#else
+#  error Please set MONGOOSE_USE_CALLBACKS
+#endif
+
+      if (!pimpl_->context_)
+      {
+        bool isSslError = false;
+
+#if ORTHANC_ENABLE_SSL == 1
+        for (;;)
+        {
+          unsigned long code = ERR_get_error();
+          if (code == 0)
+          {
+            break;
+          }
+          else
+          {
+            isSslError = true;
+            char message[1024];
+            ERR_error_string_n(code, message, sizeof(message) - 1);
+            LOG(ERROR) << "OpenSSL error: " << message;
+          }
+        }        
+#endif
+
+        if (isSslError)
+        {
+          throw OrthancException(ErrorCode_SslInitialization);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_HttpPortInUse,
+                                 " (port = " + boost::lexical_cast<std::string>(port_) + ")");
+        }
+      }
+
+      LOG(WARNING) << "HTTP server listening on port: " << GetPortNumber()
+                   << " (HTTPS encryption is "
+                   << (IsSslEnabled() ? "enabled" : "disabled")
+                   << ", remote access is "
+                   << (IsRemoteAccessAllowed() ? "" : "not ")
+                   << "allowed)";
+    }
+  }
+
+  void HttpServer::Stop()
+  {
+    if (IsRunning())
+    {
+      mg_stop(pimpl_->context_);
+      pimpl_->context_ = NULL;
+    }
+  }
+
+
+  void HttpServer::ClearUsers()
+  {
+    Stop();
+    registeredUsers_.clear();
+  }
+
+
+  void HttpServer::RegisterUser(const char* username,
+                                const char* password)
+  {
+    Stop();
+
+    std::string tag = std::string(username) + ":" + std::string(password);
+    std::string encoded;
+    Toolbox::EncodeBase64(encoded, tag);
+    registeredUsers_.insert(encoded);
+  }
+
+  void HttpServer::SetSslEnabled(bool enabled)
+  {
+    Stop();
+
+#if ORTHANC_ENABLE_SSL == 0
+    if (enabled)
+    {
+      throw OrthancException(ErrorCode_SslDisabled);
+    }
+    else
+    {
+      ssl_ = false;
+    }
+#else
+    ssl_ = enabled;
+#endif
+  }
+
+
+  void HttpServer::SetKeepAliveEnabled(bool enabled)
+  {
+    Stop();
+    keepAlive_ = enabled;
+    LOG(INFO) << "HTTP keep alive is " << (enabled ? "enabled" : "disabled");
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+    if (enabled)
+    {
+      LOG(WARNING) << "You should disable HTTP keep alive, as you are using Mongoose";
+    }
+#endif
+  }
+
+
+  void HttpServer::SetAuthenticationEnabled(bool enabled)
+  {
+    Stop();
+    authentication_ = enabled;
+  }
+
+  void HttpServer::SetSslCertificate(const char* path)
+  {
+    Stop();
+    certificate_ = path;
+  }
+
+  void HttpServer::SetRemoteAccessAllowed(bool allowed)
+  {
+    Stop();
+    remoteAllowed_ = allowed;
+  }
+
+  void HttpServer::SetHttpCompressionEnabled(bool enabled)
+  {
+    Stop();
+    httpCompression_ = enabled;
+    LOG(WARNING) << "HTTP compression is " << (enabled ? "enabled" : "disabled");
+  }
+  
+  void HttpServer::SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter)
+  {
+    Stop();
+    filter_ = &filter;
+  }
+
+
+  void HttpServer::SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter)
+  {
+    Stop();
+    exceptionFormatter_ = &formatter;
+  }
+
+
+  bool HttpServer::IsValidBasicHttpAuthentication(const std::string& basic) const
+  {
+    return registeredUsers_.find(basic) != registeredUsers_.end();
+  }
+
+
+  void HttpServer::Register(IHttpHandler& handler)
+  {
+    Stop();
+    handler_ = &handler;
+  }
+
+
+  IHttpHandler& HttpServer::GetHandler() const
+  {
+    if (handler_ == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return *handler_;
+  }
+
+
+  void HttpServer::SetThreadsCount(unsigned int threads)
+  {
+    if (threads <= 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    Stop();
+    threadsCount_ = threads;
+
+    LOG(INFO) << "The embedded HTTP server will use " << threads << " threads";
+  }
+
+  
+  void HttpServer::SetTcpNoDelay(bool tcpNoDelay)
+  {
+    Stop();
+    tcpNoDelay_ = tcpNoDelay;
+    LOG(INFO) << "TCP_NODELAY for the HTTP sockets is set to "
+              << (tcpNoDelay ? "true" : "false");
+  }
+
+
+  void HttpServer::SetRequestTimeout(unsigned int seconds)
+  {
+    if (seconds <= 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Request timeout must be a stricly positive integer");
+    }
+
+    Stop();
+    requestTimeout_ = seconds;
+    LOG(INFO) << "Request timeout in the HTTP server is set to " << seconds << " seconds";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpServer.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,230 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+// To have ORTHANC_ENABLE_xxx defined if using the shared library
+#include "../OrthancFramework.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 <list>
+#include <map>
+#include <set>
+#include <stdint.h>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class ChunkStore;
+  class OrthancException;
+
+  class IHttpExceptionFormatter : public boost::noncopyable
+  {
+  public:
+    virtual ~IHttpExceptionFormatter()
+    {
+    }
+
+    virtual void Format(HttpOutput& output,
+                        const OrthancException& exception,
+                        HttpMethod method,
+                        const char* uri) = 0;
+  };
+
+
+  class ORTHANC_PUBLIC HttpServer : public boost::noncopyable
+  {
+  private:
+    // http://stackoverflow.com/questions/311166/stdauto-ptr-or-boostshared-ptr-for-pimpl-idiom
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    IHttpHandler *handler_;
+
+    typedef std::set<std::string> RegisteredUsers;
+    RegisteredUsers registeredUsers_;
+
+    bool remoteAllowed_;
+    bool authentication_;
+    bool ssl_;
+    std::string certificate_;
+    uint16_t port_;
+    IIncomingHttpRequestFilter* filter_;
+    bool keepAlive_;
+    bool httpCompression_;
+    IHttpExceptionFormatter* exceptionFormatter_;
+    std::string realm_;
+    unsigned int threadsCount_;
+    bool tcpNoDelay_;
+    unsigned int requestTimeout_;  // In seconds
+  
+    bool IsRunning() const;
+
+  public:
+    HttpServer();
+
+    ~HttpServer();
+
+    void SetPortNumber(uint16_t port);
+
+    uint16_t GetPortNumber() const
+    {
+      return port_;
+    }
+
+    void Start();
+
+    void Stop();
+
+    void ClearUsers();
+
+    void RegisterUser(const char* username,
+                      const char* password);
+
+    bool IsAuthenticationEnabled() const
+    {
+      return authentication_;
+    }
+
+    void SetAuthenticationEnabled(bool enabled);
+
+    bool IsSslEnabled() const
+    {
+      return ssl_;
+    }
+
+    void SetSslEnabled(bool enabled);
+
+    bool IsKeepAliveEnabled() const
+    {
+      return keepAlive_;
+    }
+
+    void SetKeepAliveEnabled(bool enabled);
+
+    const std::string& GetSslCertificate() const
+    {
+      return certificate_;
+    }
+
+    void SetSslCertificate(const char* path);
+
+    bool IsRemoteAccessAllowed() const
+    {
+      return remoteAllowed_;
+    }
+
+    void SetRemoteAccessAllowed(bool allowed);
+
+    bool IsHttpCompressionEnabled() const
+    {
+      return httpCompression_;;
+    }
+
+    void SetHttpCompressionEnabled(bool enabled);
+
+    IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
+    {
+      return filter_;
+    }
+
+    void SetIncomingHttpRequestFilter(IIncomingHttpRequestFilter& filter);
+
+    ChunkStore& GetChunkStore();
+
+    bool IsValidBasicHttpAuthentication(const std::string& basic) const;
+
+    void Register(IHttpHandler& handler);
+
+    bool HasHandler() const
+    {
+      return handler_ != NULL;
+    }
+
+    IHttpHandler& GetHandler() const;
+
+    void SetHttpExceptionFormatter(IHttpExceptionFormatter& formatter);
+
+    IHttpExceptionFormatter* GetExceptionFormatter()
+    {
+      return exceptionFormatter_;
+    }
+
+    const std::string& GetRealm() const
+    {
+      return realm_;
+    }
+
+    void SetRealm(const std::string& realm)
+    {
+      realm_ = realm;
+    }
+
+    void SetThreadsCount(unsigned int threads);
+
+    unsigned int GetThreadsCount() const
+    {
+      return threadsCount_;
+    }
+
+    // New in Orthanc 1.5.2, not available for Mongoose
+    void SetTcpNoDelay(bool tcpNoDelay);
+
+    bool IsTcpNoDelay() const
+    {
+      return tcpNoDelay_;
+    }
+
+    void SetRequestTimeout(unsigned int seconds);
+
+    unsigned int GetRequestTimeout() const
+    {
+      return requestTimeout_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,251 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpStreamTranscoder.h"
+
+#include "../OrthancException.h"
+#include "../Compression/ZlibCompressor.h"
+
+#include <string.h>   // For memcpy()
+#include <cassert>
+
+#include <stdio.h>
+
+namespace Orthanc
+{
+  void HttpStreamTranscoder::ReadSource(std::string& buffer)
+  {
+    if (source_.SetupHttpCompression(false, false) != HttpCompression_None)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    uint64_t size = source_.GetContentLength();
+    if (static_cast<uint64_t>(static_cast<size_t>(size)) != size)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    buffer.resize(static_cast<size_t>(size));
+    size_t offset = 0;
+
+    while (source_.ReadNextChunk())
+    {
+      size_t chunkSize = static_cast<size_t>(source_.GetChunkSize());
+      memcpy(&buffer[offset], source_.GetChunkContent(), chunkSize);
+      offset += chunkSize;
+    }
+
+    if (offset != size)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  HttpCompression HttpStreamTranscoder::SetupZlibCompression(bool deflateAllowed)
+  {
+    uint64_t size = source_.GetContentLength();
+
+    if (size == 0)
+    {
+      return HttpCompression_None;
+    }
+
+    if (size < sizeof(uint64_t))
+    {
+      throw OrthancException(ErrorCode_CorruptedFile);
+    }
+
+    if (deflateAllowed)
+    {
+      bytesToSkip_ = sizeof(uint64_t);
+
+      return HttpCompression_Deflate;
+    }
+    else
+    {
+      // TODO Use stream-based zlib decoding to reduce memory usage
+      std::string compressed;
+      ReadSource(compressed);
+
+      uncompressed_.reset(new BufferHttpSender);
+
+      ZlibCompressor compressor;
+      IBufferCompressor::Uncompress(uncompressed_->GetBuffer(), compressor, compressed);
+
+      return HttpCompression_None;
+    }
+  }
+
+
+  HttpCompression HttpStreamTranscoder::SetupHttpCompression(bool gzipAllowed,
+                                                             bool deflateAllowed)
+  {
+    if (ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    ready_ = true;
+
+    switch (sourceCompression_)
+    {
+      case CompressionType_None:
+        return HttpCompression_None;
+
+      case CompressionType_ZlibWithSize:
+        return SetupZlibCompression(deflateAllowed);
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  uint64_t HttpStreamTranscoder::GetContentLength()
+  {
+    if (!ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (uncompressed_.get() != NULL)
+    {
+      return uncompressed_->GetContentLength();
+    }
+    else
+    {
+      uint64_t length = source_.GetContentLength();
+      if (length < bytesToSkip_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return length - bytesToSkip_;
+    }
+  }
+
+
+  bool HttpStreamTranscoder::ReadNextChunk()
+  {
+    if (!ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (uncompressed_.get() != NULL)
+    {
+      return uncompressed_->ReadNextChunk();
+    }
+
+    assert(skipped_ <= bytesToSkip_);
+    if (skipped_ == bytesToSkip_)
+    {
+      // We have already skipped the first bytes of the stream
+      currentChunkOffset_ = 0;
+      return source_.ReadNextChunk();
+    }
+
+    // This condition can only be true on the first call to "ReadNextChunk()"
+    for (;;)
+    {
+      assert(skipped_ < bytesToSkip_);
+
+      bool ok = source_.ReadNextChunk();
+      if (!ok)
+      {
+        throw OrthancException(ErrorCode_CorruptedFile);
+      }
+
+      size_t remaining = static_cast<size_t>(bytesToSkip_ - skipped_);
+      size_t s = source_.GetChunkSize();
+
+      if (s < remaining)
+      {
+        skipped_ += s;
+      }
+      else if (s == remaining)
+      {
+        // We have skipped enough bytes, but we must read a new chunk
+        currentChunkOffset_ = 0;            
+        skipped_ = bytesToSkip_;
+        return source_.ReadNextChunk();
+      }
+      else
+      {
+        // We have skipped enough bytes, and we have enough data in the current chunk
+        assert(s > remaining);
+        currentChunkOffset_ = remaining;
+        skipped_ = bytesToSkip_;
+        return true;
+      }
+    }
+  }
+
+
+  const char* HttpStreamTranscoder::GetChunkContent()
+  {
+    if (!ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (uncompressed_.get() != NULL)
+    {
+      return uncompressed_->GetChunkContent();
+    }
+    else
+    {
+      return source_.GetChunkContent() + currentChunkOffset_;
+    }
+  }
+
+  size_t HttpStreamTranscoder::GetChunkSize()
+  {
+    if (!ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    if (uncompressed_.get() != NULL)
+    {
+      return uncompressed_->GetChunkSize();
+    }
+    else
+    {
+      return static_cast<size_t>(source_.GetChunkSize() - currentChunkOffset_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpStreamTranscoder.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,94 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "BufferHttpSender.h"
+
+#include "../Compatibility.h"
+
+#include <memory>  // For std::unique_ptr
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HttpStreamTranscoder : public IHttpStreamAnswer
+  {
+  private:
+    IHttpStreamAnswer& source_;
+    CompressionType    sourceCompression_;
+    uint64_t           bytesToSkip_;
+    uint64_t           skipped_;
+    uint64_t           currentChunkOffset_;
+    bool               ready_;
+
+    std::unique_ptr<BufferHttpSender>  uncompressed_;
+
+    void ReadSource(std::string& buffer);
+
+    HttpCompression SetupZlibCompression(bool deflateAllowed);
+
+  public:
+    HttpStreamTranscoder(IHttpStreamAnswer& source,
+                         CompressionType compression) : 
+      source_(source),
+      sourceCompression_(compression),
+      bytesToSkip_(0),
+      skipped_(0),
+      currentChunkOffset_(0),
+      ready_(false)
+    {
+    }
+
+    // This is the first method to be called
+    virtual HttpCompression SetupHttpCompression(bool gzipAllowed,
+                                                 bool deflateAllowed);
+
+    virtual bool HasContentFilename(std::string& filename)
+    {
+      return source_.HasContentFilename(filename);
+    }
+
+    virtual std::string GetContentType()
+    {
+      return source_.GetContentType();
+    }
+
+    virtual uint64_t GetContentLength();
+
+    virtual bool ReadNextChunk();
+
+    virtual const char* GetChunkContent();
+
+    virtual size_t GetChunkSize();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,298 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "HttpToolbox.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <iostream>
+
+#include "HttpOutput.h"
+#include "StringHttpOutput.h"
+
+
+static const char* LOCALHOST = "127.0.0.1";
+
+
+
+namespace Orthanc
+{
+  static void SplitGETNameValue(IHttpHandler::GetArguments& result,
+                                const char* start,
+                                const char* end)
+  {
+    std::string name, value;
+    
+    const char* equal = strchr(start, '=');
+    if (equal == NULL || equal >= end)
+    {
+      name = std::string(start, end - start);
+      //value = "";
+    }
+    else
+    {
+      name = std::string(start, equal - start);
+      value = std::string(equal + 1, end);
+    }
+
+    Toolbox::UrlDecode(name);
+    Toolbox::UrlDecode(value);
+
+    result.push_back(std::make_pair(name, value));
+  }
+
+
+  void HttpToolbox::ParseGetArguments(IHttpHandler::GetArguments& result, 
+                                      const char* query)
+  {
+    const char* pos = query;
+
+    while (pos != NULL)
+    {
+      const char* ampersand = strchr(pos, '&');
+      if (ampersand)
+      {
+        SplitGETNameValue(result, pos, ampersand);
+        pos = ampersand + 1;
+      }
+      else
+      {
+        // No more ampersand, this is the last argument
+        SplitGETNameValue(result, pos, pos + strlen(pos));
+        pos = NULL;
+      }
+    }
+  }
+
+
+  void  HttpToolbox::ParseGetQuery(UriComponents& uri,
+                                   IHttpHandler::GetArguments& getArguments, 
+                                   const char* query)
+  {
+    const char *questionMark = ::strchr(query, '?');
+    if (questionMark == NULL)
+    {
+      // No question mark in the string
+      Toolbox::SplitUriComponents(uri, query);
+      getArguments.clear();
+    }
+    else
+    {
+      Toolbox::SplitUriComponents(uri, std::string(query, questionMark));
+      HttpToolbox::ParseGetArguments(getArguments, questionMark + 1);
+    }    
+  }
+
+ 
+  std::string HttpToolbox::GetArgument(const IHttpHandler::Arguments& getArguments,
+                                       const std::string& name,
+                                       const std::string& defaultValue)
+  {
+    IHttpHandler::Arguments::const_iterator it = getArguments.find(name);
+    if (it == getArguments.end())
+    {
+      return defaultValue;
+    }
+    else
+    {
+      return it->second;
+    }
+  }
+
+
+  std::string HttpToolbox::GetArgument(const IHttpHandler::GetArguments& getArguments,
+                                       const std::string& name,
+                                       const std::string& defaultValue)
+  {
+    for (size_t i = 0; i < getArguments.size(); i++)
+    {
+      if (getArguments[i].first == name)
+      {
+        return getArguments[i].second;
+      }
+    }
+
+    return defaultValue;
+  }
+
+
+
+  void HttpToolbox::ParseCookies(IHttpHandler::Arguments& result, 
+                                 const IHttpHandler::Arguments& httpHeaders)
+  {
+    result.clear();
+
+    IHttpHandler::Arguments::const_iterator it = httpHeaders.find("cookie");
+    if (it != httpHeaders.end())
+    {
+      const std::string& cookies = it->second;
+
+      size_t pos = 0;
+      while (pos != std::string::npos)
+      {
+        size_t nextSemicolon = cookies.find(";", pos);
+        std::string cookie;
+
+        if (nextSemicolon == std::string::npos)
+        {
+          cookie = cookies.substr(pos);
+          pos = std::string::npos;
+        }
+        else
+        {
+          cookie = cookies.substr(pos, nextSemicolon - pos);
+          pos = nextSemicolon + 1;
+        }
+
+        size_t equal = cookie.find("=");
+        if (equal != std::string::npos)
+        {
+          std::string name = Toolbox::StripSpaces(cookie.substr(0, equal));
+          std::string value = Toolbox::StripSpaces(cookie.substr(equal + 1));
+          result[name] = value;
+        }
+      }
+    }
+  }
+
+
+  void HttpToolbox::CompileGetArguments(IHttpHandler::Arguments& compiled,
+                                        const IHttpHandler::GetArguments& source)
+  {
+    compiled.clear();
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      compiled[source[i].first] = source[i].second;
+    }
+  }
+
+
+  bool HttpToolbox::SimpleGet(std::string& result,
+                              IHttpHandler& handler,
+                              RequestOrigin origin,
+                              const std::string& uri,
+                              const IHttpHandler::Arguments& httpHeaders)
+  {
+    UriComponents curi;
+    IHttpHandler::GetArguments getArguments;
+    ParseGetQuery(curi, getArguments, uri.c_str());
+
+    StringHttpOutput stream;
+    HttpOutput http(stream, false /* no keep alive */);
+
+    if (handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Get, curi, 
+                       httpHeaders, getArguments, NULL /* no body for GET */, 0))
+    {
+      stream.GetOutput(result);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  static bool SimplePostOrPut(std::string& result,
+                              IHttpHandler& handler,
+                              RequestOrigin origin,
+                              HttpMethod method,
+                              const std::string& uri,
+                              const void* bodyData,
+                              size_t bodySize,
+                              const IHttpHandler::Arguments& httpHeaders)
+  {
+    IHttpHandler::GetArguments getArguments;  // No GET argument for POST/PUT
+
+    UriComponents curi;
+    Toolbox::SplitUriComponents(curi, uri);
+
+    StringHttpOutput stream;
+    HttpOutput http(stream, false /* no keep alive */);
+
+    if (handler.Handle(http, origin, LOCALHOST, "", method, curi, 
+                       httpHeaders, getArguments, bodyData, bodySize))
+    {
+      stream.GetOutput(result);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool HttpToolbox::SimplePost(std::string& result,
+                               IHttpHandler& handler,
+                               RequestOrigin origin,
+                               const std::string& uri,
+                               const void* bodyData,
+                               size_t bodySize,
+                               const IHttpHandler::Arguments& httpHeaders)
+  {
+    return SimplePostOrPut(result, handler, origin, HttpMethod_Post, uri, bodyData, bodySize, httpHeaders);
+  }
+
+
+  bool HttpToolbox::SimplePut(std::string& result,
+                              IHttpHandler& handler,
+                              RequestOrigin origin,
+                              const std::string& uri,
+                              const void* bodyData,
+                              size_t bodySize,
+                              const IHttpHandler::Arguments& httpHeaders)
+  {
+    return SimplePostOrPut(result, handler, origin, HttpMethod_Put, uri, bodyData, bodySize, httpHeaders);
+  }
+
+
+  bool HttpToolbox::SimpleDelete(IHttpHandler& handler,
+                                 RequestOrigin origin,
+                                 const std::string& uri,
+                                 const IHttpHandler::Arguments& httpHeaders)
+  {
+    UriComponents curi;
+    Toolbox::SplitUriComponents(curi, uri);
+
+    IHttpHandler::GetArguments getArguments;  // No GET argument for DELETE
+
+    StringHttpOutput stream;
+    HttpOutput http(stream, false /* no keep alive */);
+
+    return handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, 
+                          httpHeaders, getArguments, NULL /* no body for DELETE */, 0);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/HttpToolbox.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+#include "IHttpHandler.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC HttpToolbox : public boost::noncopyable
+  {
+  public:
+    static void ParseGetArguments(IHttpHandler::GetArguments& result, 
+                                  const char* query);
+
+    static void ParseGetQuery(UriComponents& uri,
+                              IHttpHandler::GetArguments& getArguments, 
+                              const char* query);
+
+    static std::string GetArgument(const IHttpHandler::Arguments& getArguments,
+                                   const std::string& name,
+                                   const std::string& defaultValue);
+
+    static std::string GetArgument(const IHttpHandler::GetArguments& getArguments,
+                                   const std::string& name,
+                                   const std::string& defaultValue);
+
+    static void ParseCookies(IHttpHandler::Arguments& result, 
+                             const IHttpHandler::Arguments& httpHeaders);
+
+    static void CompileGetArguments(IHttpHandler::Arguments& compiled,
+                                    const IHttpHandler::GetArguments& source);
+
+    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,
+                           const std::string& uri,
+                           const void* bodyData,
+                           size_t bodySize,
+                          const IHttpHandler::Arguments& httpHeaders);
+
+    static bool SimplePut(std::string& result,
+                          IHttpHandler& handler,
+                          RequestOrigin origin,
+                          const std::string& uri,
+                          const void* bodyData,
+                          size_t bodySize,
+                          const IHttpHandler::Arguments& httpHeaders);
+
+    static bool SimpleDelete(IHttpHandler& handler,
+                             RequestOrigin origin,
+                             const std::string& uri,
+                             const IHttpHandler::Arguments& httpHeaders);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/IHttpHandler.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,96 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.h"
+#include "../Toolbox.h"
+#include "HttpOutput.h"
+
+#include <map>
+#include <set>
+#include <vector>
+#include <string>
+#include <memory>
+
+namespace Orthanc
+{
+  class IHttpHandler : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>                  Arguments;
+    typedef std::vector< std::pair<std::string, std::string> >  GetArguments;
+
+
+    class IChunkedRequestReader : public boost::noncopyable
+    {
+    public:
+      virtual ~IChunkedRequestReader()
+      {
+      }
+
+      virtual void AddBodyChunk(const void* data,
+                                size_t size) = 0;
+
+      virtual void Execute(HttpOutput& output) = 0;
+    };
+
+
+    virtual ~IHttpHandler()
+    {
+    }
+
+    /**
+     * This function allows one to deal with chunked transfers (new in
+     * Orthanc 1.5.7). It is only called if "method" is POST or PUT.
+     **/
+    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                            RequestOrigin origin,
+                                            const char* remoteIp,
+                                            const char* username,
+                                            HttpMethod method,
+                                            const UriComponents& uri,
+                                            const Arguments& headers) = 0;
+
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const void* bodyData,
+                        size_t bodySize) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/IHttpOutputStream.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IHttpOutputStream : public boost::noncopyable
+  {
+  public:
+    virtual ~IHttpOutputStream()
+    {
+    }
+
+    virtual void OnHttpStatusReceived(HttpStatus status) = 0;
+
+    virtual void Send(bool isHeader, const void* buffer, size_t length) = 0;
+
+    // Disable HTTP keep alive for this single HTTP connection. Must
+    // be called before sending the "HTTP/1.1 200 OK" header.
+    virtual void DisableKeepAlive() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/IHttpStreamAnswer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <boost/noncopyable.hpp>
+#include <string>
+
+namespace Orthanc
+{
+  class IHttpStreamAnswer : public boost::noncopyable
+  {
+  public:
+    virtual ~IHttpStreamAnswer()
+    {
+    }
+
+    // This is the first method to be called
+    virtual HttpCompression SetupHttpCompression(bool gzipAllowed,
+                                                 bool deflateAllowed) = 0;
+
+    virtual bool HasContentFilename(std::string& filename) = 0;
+
+    virtual std::string GetContentType() = 0;
+
+    virtual uint64_t GetContentLength() = 0;
+
+    virtual bool ReadNextChunk() = 0;
+
+    virtual const char* GetChunkContent() = 0;
+
+    virtual size_t GetChunkSize() = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/IIncomingHttpRequestFilter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,357 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MultipartStreamReader.h"
+
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <boost/algorithm/string/predicate.hpp>
+
+#if defined(_MSC_VER)
+#  include <BaseTsd.h>   // Definition of ssize_t
+#endif
+
+namespace Orthanc
+{
+  static void ParseHeaders(MultipartStreamReader::HttpHeaders& headers,
+                           StringMatcher::Iterator start,
+                           StringMatcher::Iterator end)
+  {
+    std::string tmp(start, end);
+
+    std::vector<std::string> lines;
+    Toolbox::TokenizeString(lines, tmp, '\n');
+
+    headers.clear();
+
+    for (size_t i = 0; i < lines.size(); i++)
+    {
+      size_t separator = lines[i].find(':');
+      if (separator != std::string::npos)
+      {
+        std::string key = Toolbox::StripSpaces(lines[i].substr(0, separator));
+        std::string value = Toolbox::StripSpaces(lines[i].substr(separator + 1));
+
+        Toolbox::ToLowerCase(key);
+        headers[key] = value;
+      }
+    }
+  }
+
+
+  static bool LookupHeaderSizeValue(size_t& target,
+                                    const MultipartStreamReader::HttpHeaders& headers,
+                                    const std::string& key)
+  {
+    MultipartStreamReader::HttpHeaders::const_iterator it = headers.find(key);
+    if (it == headers.end())
+    {
+      return false;
+    }
+    else
+    {
+      int64_t value;
+        
+      try
+      {
+        value = boost::lexical_cast<int64_t>(it->second);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      if (value < 0)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        target = static_cast<size_t>(value);
+        return true;
+      }
+    }
+  }
+
+
+  void MultipartStreamReader::ParseStream()
+  {
+    if (handler_ == NULL ||
+        state_ == State_Done)
+    {
+      return;
+    }
+      
+    std::string corpus;
+    buffer_.Flatten(corpus);
+
+    StringMatcher::Iterator current = corpus.begin();
+    StringMatcher::Iterator corpusEnd = corpus.end();
+
+    if (state_ == State_UnusedArea)
+    {
+      /**
+       * "Before the first boundary is an area that is ignored by
+       * MIME-compliant clients. This area is generally used to put
+       * a message to users of old non-MIME clients."
+       * https://en.wikipedia.org/wiki/MIME#Multipart_messages
+       **/
+
+      if (boundaryMatcher_.Apply(current, corpusEnd))
+      {
+        current = boundaryMatcher_.GetMatchBegin();
+        state_ = State_Content;
+      }
+      else
+      {
+        // We have not seen the end of the unused area yet
+        std::string reminder(current, corpusEnd);
+        buffer_.AddChunkDestructive(reminder);
+        return;
+      }          
+    } 
+      
+    for (;;)
+    {
+      size_t patternSize = boundaryMatcher_.GetPattern().size();
+      size_t remainingSize = std::distance(current, corpusEnd);
+      if (remainingSize < patternSize + 2)
+      {
+        break;  // Not enough data available
+      }
+        
+      std::string boundary(current, current + patternSize + 2);
+      if (boundary == boundaryMatcher_.GetPattern() + "--")
+      {
+        state_ = State_Done;
+        return;
+      }
+        
+      if (boundary != boundaryMatcher_.GetPattern() + "\r\n")
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "Garbage between two items in a multipart stream");
+      }
+
+      StringMatcher::Iterator start = current + patternSize + 2;
+        
+      if (!headersMatcher_.Apply(start, corpusEnd))
+      {
+        break;  // Not enough data available
+      }
+
+      HttpHeaders headers;
+      ParseHeaders(headers, start, headersMatcher_.GetMatchBegin());
+
+      size_t contentLength = 0;
+      if (!LookupHeaderSizeValue(contentLength, headers, "content-length"))
+      {
+        if (boundaryMatcher_.Apply(headersMatcher_.GetMatchEnd(), corpusEnd))
+        {
+          size_t d = std::distance(headersMatcher_.GetMatchEnd(), boundaryMatcher_.GetMatchBegin());
+          if (d <= 1)
+          {
+            throw OrthancException(ErrorCode_NetworkProtocol);
+          }
+          else
+          {
+            contentLength = d - 2;
+          }
+        }
+        else
+        {
+          break;  // Not enough data available to have a full part
+        }
+      }
+
+      // Explicit conversion to avoid warning about signed vs. unsigned comparison
+      std::iterator_traits<StringMatcher::Iterator>::difference_type d = contentLength + 2;
+      if (d > std::distance(headersMatcher_.GetMatchEnd(), corpusEnd))
+      {
+        break;  // Not enough data available to have a full part
+      }
+
+      const char* p = headersMatcher_.GetPointerEnd() + contentLength;
+      if (p[0] != '\r' ||
+          p[1] != '\n')
+      {
+        throw OrthancException(ErrorCode_NetworkProtocol,
+                               "No endline at the end of a part");
+      }
+          
+      handler_->HandlePart(headers, headersMatcher_.GetPointerEnd(), contentLength);
+      current = headersMatcher_.GetMatchEnd() + contentLength + 2;
+    }
+
+    if (current != corpusEnd)
+    {
+      std::string reminder(current, corpusEnd);
+      buffer_.AddChunkDestructive(reminder);
+    }
+  }
+
+
+  MultipartStreamReader::MultipartStreamReader(const std::string& boundary) :
+    state_(State_UnusedArea),
+    handler_(NULL),
+    headersMatcher_("\r\n\r\n"),
+    boundaryMatcher_("--" + boundary),
+    blockSize_(10 * 1024 * 1024)
+  {
+  }
+
+
+  void MultipartStreamReader::SetBlockSize(size_t size)
+  {
+    if (size == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      blockSize_ = size;
+    }        
+  }
+
+    
+  void MultipartStreamReader::AddChunk(const void* chunk,
+                                       size_t size)
+  {
+    if (state_ != State_Done &&
+        size != 0)
+    {
+      size_t oldSize = buffer_.GetNumBytes();
+      
+      buffer_.AddChunk(chunk, size);
+
+      if (oldSize / blockSize_ != buffer_.GetNumBytes() / blockSize_)
+      {
+        ParseStream();
+      }
+    }
+  }
+
+
+  void MultipartStreamReader::AddChunk(const std::string& chunk)
+  {
+    if (!chunk.empty())
+    {
+      AddChunk(chunk.c_str(), chunk.size());
+    }
+  }
+
+
+  void MultipartStreamReader::CloseStream()
+  {
+    if (buffer_.GetNumBytes() != 0)
+    {
+      ParseStream();
+    }
+  }
+
+
+  bool MultipartStreamReader::GetMainContentType(std::string& contentType,
+                                                 const HttpHeaders& headers)
+  {
+    HttpHeaders::const_iterator it = headers.find("content-type");
+
+    if (it == headers.end())
+    {
+      return false;
+    }
+    else
+    {
+      contentType = it->second;
+      return true;
+    }
+  }
+
+
+  bool MultipartStreamReader::ParseMultipartContentType(std::string& contentType,
+                                                        std::string& subType,
+                                                        std::string& boundary,
+                                                        const std::string& contentTypeHeader)
+  {
+    std::vector<std::string> tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, contentTypeHeader, ';');
+
+    if (tokens.empty())
+    {
+      return false;
+    }
+
+    contentType = Orthanc::Toolbox::StripSpaces(tokens[0]);
+    Orthanc::Toolbox::ToLowerCase(contentType);
+
+    if (contentType.empty())
+    {
+      return false;
+    }
+
+    bool valid = false;
+    subType.clear();
+
+    for (size_t i = 0; i < tokens.size(); i++)
+    {
+      std::vector<std::string> items;
+      Orthanc::Toolbox::TokenizeString(items, tokens[i], '=');
+
+      if (items.size() == 2)
+      {
+        if (boost::iequals("boundary", Orthanc::Toolbox::StripSpaces(items[0])))
+        {
+          boundary = Orthanc::Toolbox::StripSpaces(items[1]);
+          valid = !boundary.empty();
+        }
+        else if (boost::iequals("type", Orthanc::Toolbox::StripSpaces(items[0])))
+        {
+          subType = Orthanc::Toolbox::StripSpaces(items[1]);
+          Orthanc::Toolbox::ToLowerCase(subType);
+
+          // https://bitbucket.org/sjodogne/orthanc/issues/54/decide-what-to-do-wrt-quoting-of-multipart
+          // https://tools.ietf.org/html/rfc7231#section-3.1.1.1
+          if (subType.size() >= 2 &&
+              subType[0] == '"' &&
+              subType[subType.size() - 1] == '"')
+          {
+            subType = subType.substr(1, subType.size() - 2);
+          }
+        }
+      }
+    }
+
+    return valid;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/MultipartStreamReader.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "StringMatcher.h"
+#include "../ChunkedBuffer.h"
+
+#include <map>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC MultipartStreamReader : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+    
+    class IHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~IHandler()
+      {
+      }
+      
+      virtual void HandlePart(const HttpHeaders& headers,
+                              const void* part,
+                              size_t size) = 0;
+    };
+    
+  private:
+    enum State
+    {
+      State_UnusedArea,
+      State_Content,
+      State_Done
+    };
+    
+    State          state_;
+    IHandler*      handler_;
+    StringMatcher  headersMatcher_;
+    StringMatcher  boundaryMatcher_;
+    ChunkedBuffer  buffer_;
+    size_t         blockSize_;
+
+    void ParseStream();
+
+  public:
+    MultipartStreamReader(const std::string& boundary);
+
+    void SetBlockSize(size_t size);
+
+    size_t GetBlockSize() const
+    {
+      return blockSize_;
+    }
+
+    void SetHandler(IHandler& handler)
+    {
+      handler_ = &handler;
+    }
+    
+    void AddChunk(const void* chunk,
+                  size_t size);
+
+    void AddChunk(const std::string& chunk);
+
+    void CloseStream();
+
+    static bool GetMainContentType(std::string& contentType,
+                                   const HttpHeaders& headers);
+
+    static bool ParseMultipartContentType(std::string& contentType,
+                                          std::string& subType,  // Possibly empty
+                                          std::string& boundary,
+                                          const std::string& contentTypeHeader);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "StringHttpOutput.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  void StringHttpOutput::OnHttpStatusReceived(HttpStatus status)
+  {
+    switch (status)
+    {
+      case HttpStatus_200_Ok:
+        found_ = true;
+        break;
+
+      case HttpStatus_404_NotFound:
+        found_ = false;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadRequest);
+    }
+  }
+
+  void StringHttpOutput::Send(bool isHeader, const void* buffer, size_t length)
+  {
+    if (!isHeader)
+    {
+      buffer_.AddChunk(buffer, length);
+    }
+  }
+
+  void StringHttpOutput::GetOutput(std::string& output)
+  {
+    if (found_)
+    {
+      buffer_.Flatten(output);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/StringHttpOutput.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IHttpOutputStream.h"
+
+#include "../ChunkedBuffer.h"
+
+namespace Orthanc
+{
+  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);
+
+    virtual void DisableKeepAlive()
+    {
+    }
+
+    void GetOutput(std::string& output);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/StringMatcher.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,142 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "StringMatcher.h"
+
+#include "../OrthancException.h"
+
+#include <boost/algorithm/searching/boyer_moore.hpp>
+//#include <boost/algorithm/searching/boyer_moore_horspool.hpp>
+//#include <boost/algorithm/searching/knuth_morris_pratt.hpp>
+
+namespace Orthanc
+{
+  class StringMatcher::Search
+  {
+  private:
+    typedef boost::algorithm::boyer_moore<Iterator>  Algorithm;
+    //typedef boost::algorithm::boyer_moore_horspool<std::string::const_iterator>  Algorithm;
+
+    Algorithm algorithm_;
+
+  public:
+    // WARNING - The lifetime of "pattern_" must be larger than
+    // "search_", as the latter internally keeps a pointer to "pattern" (*)
+    Search(const std::string& pattern) :
+      algorithm_(pattern.begin(), pattern.end())
+    {
+    }
+
+    Iterator Apply(Iterator start,
+                   Iterator end) const
+    {
+#if BOOST_VERSION >= 106200
+      return algorithm_(start, end).first;
+#else
+      return algorithm_(start, end);
+#endif
+    }
+  };
+    
+
+  StringMatcher::StringMatcher(const std::string& pattern) :
+    pattern_(pattern),
+    valid_(false)
+  {
+    // WARNING - Don't use "pattern" (local variable, will be
+    // destroyed once exiting the constructor) but "pattern_"
+    // (variable member, will last as long as the algorithm),
+    // otherwise lifetime is bad! (*)
+    search_.reset(new Search(pattern_));
+  }
+  
+
+  bool StringMatcher::Apply(Iterator start,
+                            Iterator end)
+  {
+    assert(search_.get() != NULL);
+    matchBegin_ = search_->Apply(start, end);
+
+    if (matchBegin_ == end)
+    {
+      valid_ = false;
+    }
+    else
+    {
+      matchEnd_ = matchBegin_ + pattern_.size();
+      assert(matchEnd_ <= end);
+      valid_ = true;
+    }
+
+    return valid_;
+  }
+
+
+  StringMatcher::Iterator StringMatcher::GetMatchBegin() const
+  {
+    if (valid_)
+    {
+      return matchBegin_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  StringMatcher::Iterator StringMatcher::GetMatchEnd() const
+  {
+    if (valid_)
+    {
+      return matchEnd_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const char* StringMatcher::GetPointerBegin() const
+  {
+    return &GetMatchBegin()[0];
+  }
+
+
+  const char* StringMatcher::GetPointerEnd() const
+  {
+    return GetPointerBegin() + pattern_.size();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/HttpServer/StringMatcher.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+
+#include <boost/noncopyable.hpp>
+#include <boost/shared_ptr.hpp>
+#include <string>
+
+namespace Orthanc
+{
+  // Convenience class that wraps a Boost algorithm for string matching
+  class ORTHANC_PUBLIC StringMatcher : public boost::noncopyable
+  {
+  public:
+    typedef std::string::const_iterator Iterator;
+
+  private:
+    class Search;
+      
+    boost::shared_ptr<Search>  search_;  // PImpl pattern
+    std::string                pattern_;
+    bool                       valid_;
+    Iterator                   matchBegin_;
+    Iterator                   matchEnd_;
+    
+  public:
+    StringMatcher(const std::string& pattern);
+
+    const std::string& GetPattern() const
+    {
+      return pattern_;
+    }
+
+    bool IsValid() const
+    {
+      return valid_;
+    }
+
+    bool Apply(Iterator start,
+               Iterator end);
+
+    bool Apply(const std::string& corpus)
+    {
+      return Apply(corpus.begin(), corpus.end());
+    }
+
+    Iterator GetMatchBegin() const;
+
+    Iterator GetMatchEnd() const;
+
+    const char* GetPointerBegin() const;
+
+    const char* GetPointerEnd() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/IDynamicObject.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  /**
+   * This class should be the ancestor to any class whose type is
+   * determined at the runtime, and that can be dynamically allocated.
+   * Being a child of IDynamicObject only implies the existence of a
+   * virtual destructor.
+   **/
+  class ORTHANC_PUBLIC IDynamicObject : public boost::noncopyable
+  {
+  public:
+    virtual ~IDynamicObject()
+    {
+    }
+  };
+  
+
+  /**
+   * This class is a simple implementation of a IDynamicObject that
+   * stores a single typed value.
+   */
+  template <typename T>
+  class SingleValueObject : public IDynamicObject
+  {
+  private:
+    T  value_;
+    
+  public:
+    explicit SingleValueObject(const T& value) :
+      value_(value)
+    {
+    }
+
+    const T& GetValue() const
+    {
+      return value_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/Font.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,419 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "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 "../OrthancException.h"
+#include "../Toolbox.h"
+#include "Image.h"
+#include "ImageProcessing.h"
+
+#include <stdio.h>
+#include <memory>
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  Font::~Font()
+  {
+    for (Characters::iterator it = characters_.begin();
+         it != characters_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  void Font::LoadFromMemory(const std::string& font)
+  {
+    Json::Value v;
+    Json::Reader reader;
+    if (!reader.parse(font, v) ||
+        v.type() != Json::objectValue ||
+        !v.isMember("Name") ||
+        !v.isMember("Size") ||
+        !v.isMember("Characters") ||
+        v["Name"].type() != Json::stringValue ||
+        v["Size"].type() != Json::intValue ||
+        v["Characters"].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFont);
+    }
+
+    name_ = v["Name"].asString();
+    size_ = v["Size"].asUInt();
+    maxHeight_ = 0;
+
+    Json::Value::Members characters = v["Characters"].getMemberNames();
+
+    for (size_t i = 0; i < characters.size(); i++)
+    {
+      const Json::Value& info = v["Characters"][characters[i]];
+      if (info.type() != Json::objectValue ||
+          !info.isMember("Advance") ||
+          !info.isMember("Bitmap") ||
+          !info.isMember("Height") ||
+          !info.isMember("Top") ||
+          !info.isMember("Width") ||
+          info["Advance"].type() != Json::intValue ||
+          info["Bitmap"].type() != Json::arrayValue ||
+          info["Height"].type() != Json::intValue ||
+          info["Top"].type() != Json::intValue ||
+          info["Width"].type() != Json::intValue)
+      {
+        throw OrthancException(ErrorCode_BadFont);
+      }
+
+      std::unique_ptr<Character> c(new Character);
+      
+      c->advance_ = info["Advance"].asUInt();
+      c->height_ = info["Height"].asUInt();
+      c->top_ = info["Top"].asUInt();
+      c->width_ = info["Width"].asUInt();
+      c->bitmap_.resize(info["Bitmap"].size());
+
+      if (c->height_ > maxHeight_)
+      {
+        maxHeight_ = c->height_;
+      }
+      
+      for (Json::Value::ArrayIndex j = 0; j < info["Bitmap"].size(); j++)
+      {
+        if (info["Bitmap"][j].type() != Json::intValue)
+        {
+          throw OrthancException(ErrorCode_BadFont);
+        }
+
+        int value = info["Bitmap"][j].asInt();
+        if (value < 0 || value > 255)
+        {
+          throw OrthancException(ErrorCode_BadFont);
+        }
+
+        c->bitmap_[j] = static_cast<uint8_t>(value);
+      }
+
+      int index = boost::lexical_cast<int>(characters[i]);
+      if (index < 0 || index > 255)
+      {
+        throw OrthancException(ErrorCode_BadFont);
+      }
+
+      characters_[static_cast<char>(index)] = c.release();
+    }
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void Font::LoadFromFile(const std::string& path)
+  {
+    std::string font;
+    SystemToolbox::ReadFile(font, path);
+    LoadFromMemory(font);
+  }
+#endif
+
+
+  static unsigned int MyMin(unsigned int a, 
+                            unsigned int b)
+  {
+    return a < b ? a : b;
+  }
+
+
+  void Font::DrawCharacter(ImageAccessor& target,
+                           const Character& character,
+                           int x,
+                           int y,
+                           const uint8_t color[4]) const
+  {
+    // Compute the bounds of the character
+    if (x >= static_cast<int>(target.GetWidth()) ||
+        y >= static_cast<int>(target.GetHeight()))
+    {
+      // The character is out of the image
+      return;
+    }
+
+    unsigned int left = x < 0 ? -x : 0;
+    unsigned int top = y < 0 ? -y : 0;
+    unsigned int width = MyMin(character.width_, target.GetWidth() - x);
+    unsigned int height = MyMin(character.height_, target.GetHeight() - y);
+
+    unsigned int bpp = target.GetBytesPerPixel();
+
+    // Blit the font bitmap OVER the target image
+    // https://en.wikipedia.org/wiki/Alpha_compositing
+
+    for (unsigned int cy = top; cy < height; cy++)
+    {
+      uint8_t* p = reinterpret_cast<uint8_t*>(target.GetRow(y + cy)) + (x + left) * bpp;
+      unsigned int pos = cy * character.width_ + left;
+
+      switch (target.GetFormat())
+      {
+        case PixelFormat_Grayscale8:
+        {
+          assert(bpp == 1);
+          for (unsigned int cx = left; cx < width; cx++, pos++, p++)
+          {
+            uint16_t alpha = character.bitmap_[pos];
+            uint16_t value = alpha * static_cast<uint16_t>(color[0]) + (255 - alpha) * static_cast<uint16_t>(*p);
+            *p = static_cast<uint8_t>(value >> 8);
+          }
+
+          break;
+        }
+
+        case PixelFormat_RGB24:
+        {
+          assert(bpp == 3);
+          for (unsigned int cx = left; cx < width; cx++, pos++, p += 3)
+          {
+            uint16_t alpha = character.bitmap_[pos];
+            for (uint8_t i = 0; i < 3; i++)
+            {
+              uint16_t value = alpha * static_cast<uint16_t>(color[i]) + (255 - alpha) * static_cast<uint16_t>(p[i]);
+              p[i] = static_cast<uint8_t>(value >> 8);
+            }
+          }
+
+          break;
+        }
+
+        case PixelFormat_RGBA32:
+        case PixelFormat_BGRA32:
+        {
+          assert(bpp == 4);
+
+          for (unsigned int cx = left; cx < width; cx++, pos++, p += 4)
+          {
+            float alpha = static_cast<float>(character.bitmap_[pos]) / 255.0f;
+            float beta = (1.0f - alpha) * static_cast<float>(p[3]) / 255.0f;
+            float denom = 1.0f / (alpha + beta);
+
+            for (uint8_t i = 0; i < 3; i++)
+            {
+              p[i] = static_cast<uint8_t>((alpha * static_cast<float>(color[i]) +
+                                           beta * static_cast<float>(p[i])) * denom);
+            }
+
+            p[3] = static_cast<uint8_t>(255.0f * (alpha + beta));
+          }
+
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+  }
+
+
+  void Font::DrawInternal(ImageAccessor& target,
+                          const std::string& utf8,
+                          int x,
+                          int y,
+                          const uint8_t color[4]) const
+  {
+    if (target.GetFormat() != PixelFormat_Grayscale8 &&
+        target.GetFormat() != PixelFormat_RGB24 &&
+        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++)
+    {
+      if (s[i] == '\n')
+      {
+        // Go to the next line
+        a = x;
+        y += maxHeight_ + 1;
+      }
+      else
+      {
+        Characters::const_iterator c = characters_.find(s[i]);
+        if (c != characters_.end())
+        {
+          DrawCharacter(target, *c->second, a, y + static_cast<int>(c->second->top_), color);
+          a += c->second->advance_;
+        }
+      }
+    }
+  }
+
+
+  void Font::Draw(ImageAccessor& target,
+                  const std::string& utf8,
+                  int x,
+                  int y,
+                  uint8_t grayscale) const
+  {
+    uint8_t color[4] = { grayscale, grayscale, grayscale, 255 };
+    DrawInternal(target, utf8, x, y, color);
+  }
+
+
+  void Font::Draw(ImageAccessor& target,
+                  const std::string& utf8,
+                  int x,
+                  int y,
+                  uint8_t r,
+                  uint8_t g,
+                  uint8_t b) const
+  {
+    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);
+  }
+
+
+  void Font::ComputeTextExtent(unsigned int& width,
+                               unsigned int& height,
+                               const std::string& utf8) const
+  {
+    width = 0;
+    height = 0;
+    
+#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
+
+    // Compute the text extent
+    unsigned int x = 0;
+    unsigned int y = 0;
+    
+    for (size_t i = 0; i < s.size(); i++)
+    {
+      if (s[i] == '\n')
+      {
+        // Go to the next line
+        x = 0;
+        y += (maxHeight_ + 1);
+      }
+      else
+      {
+        Characters::const_iterator c = characters_.find(s[i]);
+        if (c != characters_.end())
+        {
+          x += c->second->advance_;
+
+          unsigned int bottom = y + c->second->top_ + c->second->height_;
+          if (bottom > height)
+          {
+            height = bottom;
+          }
+          
+          if (x > width)
+          {
+            width = x;
+          }
+        }
+      }
+    }
+  }
+
+
+  ImageAccessor* Font::Render(const std::string& utf8,
+                              PixelFormat format,
+                              uint8_t r,
+                              uint8_t g,
+                              uint8_t b) const
+  {
+    unsigned int width, height;
+    ComputeTextExtent(width, height, utf8);
+    
+    std::unique_ptr<ImageAccessor>  target(new Image(format, width, height, false));
+    ImageProcessing::Set(*target, 0, 0, 0, 255);
+    Draw(*target, utf8, 0, 0, r, g, b);
+
+    return target.release();
+  }
+
+
+  ImageAccessor* Font::RenderAlpha(const std::string& utf8) const
+  {
+    unsigned int width, height;
+    ComputeTextExtent(width, height, utf8);
+
+    std::unique_ptr<ImageAccessor>  target(new Image(PixelFormat_Grayscale8, width, height, false));
+    ImageProcessing::Set(*target, 0);
+    Draw(*target, utf8, 0, 0, 255);
+
+    return target.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/Font.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,129 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+
+#include "ImageAccessor.h"
+
+#include <stdint.h>
+#include <vector>
+#include <map>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC Font : public boost::noncopyable
+  {
+  private:
+    struct Character
+    {
+      unsigned int  width_;
+      unsigned int  height_;
+      unsigned int  top_;
+      unsigned int  advance_;
+      std::vector<uint8_t>  bitmap_;
+    };
+
+    typedef std::map<char, Character*>  Characters;
+
+    std::string   name_;
+    unsigned int  size_;
+    Characters    characters_;
+    unsigned int  maxHeight_;
+
+    void DrawCharacter(ImageAccessor& target,
+                       const Character& character,
+                       int x,
+                       int y,
+                       const uint8_t color[4]) const;
+
+    void DrawInternal(ImageAccessor& target,
+                      const std::string& utf8,
+                      int x,
+                      int y,
+                      const uint8_t color[4]) const;
+
+  public:
+    Font() : 
+      size_(0), 
+      maxHeight_(0)
+    {
+    }
+
+    ~Font();
+
+    void LoadFromMemory(const std::string& font);
+
+#if ORTHANC_SANDBOXED == 0
+    void LoadFromFile(const std::string& path);
+#endif
+
+    const std::string& GetName() const
+    {
+      return name_;
+    }
+
+    unsigned int GetSize() const
+    {
+      return size_;
+    }
+
+    void Draw(ImageAccessor& target,
+              const std::string& utf8,
+              int x,
+              int y,
+              uint8_t grayscale) const;
+
+    void Draw(ImageAccessor& target,
+              const std::string& utf8,
+              int x,
+              int y,
+              uint8_t r,
+              uint8_t g,
+              uint8_t b) const;
+
+    void ComputeTextExtent(unsigned int& width,
+                           unsigned int& height,
+                           const std::string& utf8) const;
+
+    ImageAccessor* Render(const std::string& utf8,
+                          PixelFormat format,
+                          uint8_t r,
+                          uint8_t g,
+                          uint8_t b) const;
+
+    ImageAccessor* RenderAlpha(const std::string& utf8) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/FontRegistry.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "FontRegistry.h"
+
+#include "../OrthancException.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  FontRegistry::~FontRegistry()
+  {
+    for (Fonts::iterator it = fonts_.begin(); it != fonts_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  void FontRegistry::AddFromMemory(const std::string& font)
+  {
+    std::unique_ptr<Font> f(new Font);
+    f->LoadFromMemory(font);
+    fonts_.push_back(f.release());
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void FontRegistry::AddFromFile(const std::string& path)
+  {
+    std::unique_ptr<Font> f(new Font);
+    f->LoadFromFile(path);
+    fonts_.push_back(f.release());
+  }
+#endif
+
+
+  const Font& FontRegistry::GetFont(size_t i) const
+  {
+    if (i >= fonts_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return *fonts_[i];
+    }
+  }
+
+  const Font* FontRegistry::FindFont(const std::string& fontName) const
+  {
+    for (Fonts::const_iterator it = fonts_.begin(); it != fonts_.end(); it++)
+    {
+      if ((*it)->GetName() == fontName)
+      {
+        return *it;
+      }
+    }
+
+    return NULL;
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/FontRegistry.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Font.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC FontRegistry : public boost::noncopyable
+  {
+  private:
+    typedef std::vector<Font*>  Fonts;
+
+    Fonts  fonts_;
+
+  public:
+    ~FontRegistry();
+
+    void AddFromMemory(const std::string& font);
+
+#if ORTHANC_SANDBOXED == 0
+    void AddFromFile(const std::string& path);
+#endif
+
+    size_t GetSize() const
+    {
+      return fonts_.size();
+    }
+
+    const Font& GetFont(size_t i) const;
+
+    const Font* FindFont(const std::string& fontName) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/IImageWriter.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/Images/IImageWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/Images/Image.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.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::unique_ptr<Image> target(new Image(source.GetFormat(), source.GetWidth(), source.GetHeight(), false));
+    ImageProcessing::Copy(*target, source);
+    return target.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/Image.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ImageBuffer.h"
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC Image : public ImageAccessor
+  {
+  private:
+    ImageBuffer  image_;
+
+  public:
+    Image(PixelFormat format,
+          unsigned int width,
+          unsigned int height,
+          bool forceMinimalPitch);
+
+    static Image* Clone(const ImageAccessor& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageAccessor.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,307 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ImageAccessor.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../ChunkedBuffer.h"
+
+#include <stdint.h>
+#include <cassert>
+#include <boost/lexical_cast.hpp>
+
+
+
+namespace Orthanc
+{
+  template <typename PixelType>
+  static void ToMatlabStringInternal(ChunkedBuffer& target,
+                                     const ImageAccessor& source)
+  {
+    target.AddChunk("double([ ");
+
+    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    {
+      const PixelType* p = reinterpret_cast<const PixelType*>(source.GetConstRow(y));
+
+      std::string s;
+      if (y > 0)
+      {
+        s = "; ";
+      }
+
+      s.reserve(source.GetWidth() * 8);
+
+      for (unsigned int x = 0; x < source.GetWidth(); x++, p++)
+      {
+        s += boost::lexical_cast<std::string>(static_cast<double>(*p)) + " ";
+      }
+
+      target.AddChunk(s);
+    }
+
+    target.AddChunk("])");
+  }
+
+
+  static void RGB24ToMatlabString(ChunkedBuffer& target,
+                                  const ImageAccessor& source)
+  {
+    assert(source.GetFormat() == PixelFormat_RGB24);
+
+    target.AddChunk("double(permute(reshape([ ");
+
+    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    {
+      const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+      
+      std::string s;
+      s.reserve(source.GetWidth() * 3 * 8);
+      
+      for (unsigned int x = 0; x < 3 * source.GetWidth(); x++, p++)
+      {
+        s += boost::lexical_cast<std::string>(static_cast<int>(*p)) + " ";
+      }
+      
+      target.AddChunk(s);
+    }
+
+    target.AddChunk("], [ 3 " + boost::lexical_cast<std::string>(source.GetHeight()) +
+                    " " + boost::lexical_cast<std::string>(source.GetWidth()) + " ]), [ 3 2 1 ]))");
+  }
+
+
+  void* ImageAccessor::GetBuffer() const
+  {
+    if (readOnly_)
+    {
+      throw OrthancException(ErrorCode_ReadOnly,
+                             "Trying to write to a read-only image");
+    }
+
+    return buffer_;
+  }
+
+
+  const void* ImageAccessor::GetConstRow(unsigned int y) const
+  {
+    if (buffer_ != NULL)
+    {
+      return buffer_ + y * pitch_;
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  void* ImageAccessor::GetRow(unsigned int y) const
+  {
+    if (readOnly_)
+    {
+      throw OrthancException(ErrorCode_ReadOnly,
+                             "Trying to write to a read-only image");
+    }
+
+    if (buffer_ != NULL)
+    {
+      return buffer_ + y * pitch_;
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  void ImageAccessor::AssignEmpty(PixelFormat format)
+  {
+    readOnly_ = false;
+    format_ = format;
+    width_ = 0;
+    height_ = 0;
+    pitch_ = 0;
+    buffer_ = NULL;
+  }
+
+
+  void ImageAccessor::AssignReadOnly(PixelFormat format,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     unsigned int pitch,
+                                     const void *buffer)
+  {
+    readOnly_ = true;
+    format_ = format;
+    width_ = width;
+    height_ = height;
+    pitch_ = pitch;
+    buffer_ = reinterpret_cast<uint8_t*>(const_cast<void*>(buffer));
+
+    if (GetBytesPerPixel() * width_ > pitch_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void ImageAccessor::AssignWritable(PixelFormat format,
+                                     unsigned int width,
+                                     unsigned int height,
+                                     unsigned int pitch,
+                                     void *buffer)
+  {
+    readOnly_ = false;
+    format_ = format;
+    width_ = width;
+    height_ = height;
+    pitch_ = pitch;
+    buffer_ = reinterpret_cast<uint8_t*>(buffer);
+
+    if (GetBytesPerPixel() * width_ > pitch_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void ImageAccessor::GetWriteableAccessor(ImageAccessor& target) const
+  {
+    if (readOnly_)
+    {
+      throw OrthancException(ErrorCode_ReadOnly);
+    }
+    else
+    {
+      target.AssignWritable(format_, width_, height_, pitch_, buffer_);
+    }
+  }
+
+
+  void ImageAccessor::ToMatlabString(std::string& target) const
+  {
+    ChunkedBuffer buffer;
+
+    switch (GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        ToMatlabStringInternal<uint8_t>(buffer, *this);
+        break;
+
+      case PixelFormat_Grayscale16:
+        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;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }   
+
+    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_)
+    {
+      throw OrthancException(ErrorCode_ReadOnly,
+                             "Trying to modify the format of a read-only image");
+    }
+
+    if (::Orthanc::GetBytesPerPixel(format) != ::Orthanc::GetBytesPerPixel(format_))
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+
+    format_ = format;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageAccessor.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,161 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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>
+#include <stdint.h>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC 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_;
+    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()
+    {
+      AssignEmpty(PixelFormat_Grayscale8);
+    }
+
+    virtual ~ImageAccessor()
+    {
+    }
+
+    bool IsReadOnly() const
+    {
+      return readOnly_;
+    }
+
+    PixelFormat GetFormat() const
+    {
+      return format_;
+    }
+
+    unsigned int GetBytesPerPixel() const
+    {
+      return ::Orthanc::GetBytesPerPixel(format_);
+    }
+
+    unsigned int GetWidth() const
+    {
+      return width_;
+    }
+
+    unsigned int GetHeight() const
+    {
+      return height_;
+    }
+
+    unsigned int GetPitch() const
+    {
+      return pitch_;
+    }
+
+    unsigned int GetSize() const
+    {
+      return GetHeight() * GetPitch();
+    }
+
+    const void* GetConstBuffer() const
+    {
+      return buffer_;
+    }
+
+    void* GetBuffer() const;
+
+    const void* GetConstRow(unsigned int y) const;
+
+    void* GetRow(unsigned int y) const;
+
+    void AssignEmpty(PixelFormat format);
+
+    void AssignReadOnly(PixelFormat format,
+                        unsigned int width,
+                        unsigned int height,
+                        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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageBuffer.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,180 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ImageBuffer.h"
+
+#include "../OrthancException.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+namespace Orthanc
+{
+  void ImageBuffer::Allocate()
+  {
+    if (changed_)
+    {
+      Deallocate();
+
+      /*
+        if (forceMinimalPitch_)
+        {
+        TODO: Align pitch and memory buffer to optimal size for SIMD.
+        }
+      */
+
+      pitch_ = GetBytesPerPixel() * width_;
+      size_t size = pitch_ * height_;
+
+      if (size == 0)
+      {
+        buffer_ = NULL;
+      }
+      else
+      {
+        buffer_ = malloc(size);
+        if (buffer_ == NULL)
+        {
+          throw OrthancException(ErrorCode_NotEnoughMemory,
+                                 "Failed to allocate an image buffer of size " + boost::lexical_cast<std::string>(width_) + "x" + boost::lexical_cast<std::string>(height_));
+        }
+      }
+
+      changed_ = false;
+    }
+  }
+
+
+  void ImageBuffer::Deallocate()
+  {
+    if (buffer_ != NULL)
+    {
+      free(buffer_);
+      buffer_ = NULL;
+      changed_ = true;
+    }
+  }
+
+
+  ImageBuffer::ImageBuffer(PixelFormat format,
+                           unsigned int width,
+                           unsigned int height,
+                           bool forceMinimalPitch) :
+    forceMinimalPitch_(forceMinimalPitch)
+  {
+    Initialize();
+    SetWidth(width);
+    SetHeight(height);
+    SetFormat(format);
+  }
+
+
+  void ImageBuffer::Initialize()
+  {
+    changed_ = false;
+    forceMinimalPitch_ = true;
+    format_ = PixelFormat_Grayscale8;
+    width_ = 0;
+    height_ = 0;
+    pitch_ = 0;
+    buffer_ = NULL;
+  }
+
+
+  void ImageBuffer::SetFormat(PixelFormat format)
+  {
+    if (format != format_)
+    {
+      changed_ = true;
+      format_ = format;
+    }
+  }
+
+
+  void ImageBuffer::SetWidth(unsigned int width)
+  {
+    if (width != width_)
+    {
+      changed_ = true;
+      width_ = width;     
+    }
+  }
+
+
+  void ImageBuffer::SetHeight(unsigned int height)
+  {
+    if (height != height_)
+    {
+      changed_ = true;
+      height_ = height;     
+    }
+  }
+
+  
+  void ImageBuffer::GetReadOnlyAccessor(ImageAccessor& accessor)
+  {
+    Allocate();
+    accessor.AssignReadOnly(format_, width_, height_, pitch_, buffer_);
+  }
+  
+
+  void ImageBuffer::GetWriteableAccessor(ImageAccessor& accessor)
+  {
+    Allocate();
+    accessor.AssignWritable(format_, width_, height_, pitch_, buffer_);
+  }
+
+
+  void ImageBuffer::AcquireOwnership(ImageBuffer& other)
+  {
+    // Remove the content of the current image
+    Deallocate();
+
+    // Force the allocation of the other image (if not already
+    // allocated)
+    other.Allocate();
+
+    // Transfer the content of the other image
+    changed_ = false;
+    forceMinimalPitch_ = other.forceMinimalPitch_;
+    format_ = other.format_;
+    width_ = other.width_;
+    height_ = other.height_;
+    pitch_ = other.pitch_;
+    buffer_ = other.buffer_;
+
+    // Force the reinitialization of the other image
+    other.Initialize();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageBuffer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <vector>
+#include <stdint.h>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ImageBuffer : public boost::noncopyable
+  {
+  private:
+    bool changed_;
+
+    bool forceMinimalPitch_;  // Currently unused
+    PixelFormat format_;
+    unsigned int width_;
+    unsigned int height_;
+    unsigned int pitch_;
+    void *buffer_;
+
+    void Initialize();
+    
+    void Allocate();
+
+    void Deallocate();
+
+  public:
+    ImageBuffer(PixelFormat format,
+                unsigned int width,
+                unsigned int height,
+                bool forceMinimalPitch);
+
+    ImageBuffer()
+    {
+      Initialize();
+    }
+
+    ~ImageBuffer()
+    {
+      Deallocate();
+    }
+
+    PixelFormat GetFormat() const
+    {
+      return format_;
+    }
+
+    void SetFormat(PixelFormat format);
+
+    unsigned int GetWidth() const
+    {
+      return width_;
+    }
+
+    void SetWidth(unsigned int width);
+
+    unsigned int GetHeight() const
+    {
+      return height_;
+    }
+
+    void SetHeight(unsigned int height);
+
+    unsigned int GetBytesPerPixel() const
+    {
+      return ::Orthanc::GetBytesPerPixel(format_);
+    }
+
+    void GetReadOnlyAccessor(ImageAccessor& accessor);
+
+    void GetWriteableAccessor(ImageAccessor& accessor);
+
+    bool IsMinimalPitchForced() const
+    {
+      return forceMinimalPitch_;
+    }
+
+    void AcquireOwnership(ImageBuffer& other);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageProcessing.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2466 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ImageProcessing.h"
+
+#include "Image.h"
+#include "ImageTraits.h"
+#include "PixelTraits.h"
+#include "../OrthancException.h"
+
+#ifdef __EMSCRIPTEN__
+/* 
+   Avoid this error:
+   -----------------
+   .../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion]
+   .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:333:28: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here
+   .../mnt/c/osi/dev/orthanc/Core/Images/ImageProcessing.cpp:1006:9: note: in instantiation of function template specialization 'Orthanc::MultiplyConstantInternal<unsigned char, true>' requested here
+*/
+#pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion"
+#endif 
+
+#include <boost/math/special_functions/round.hpp>
+
+#include <cassert>
+#include <string.h>
+#include <limits>
+#include <stdint.h>
+#include <algorithm>
+
+namespace Orthanc
+{
+  double ImageProcessing::ImagePoint::GetDistanceTo(const ImagePoint& other) const
+  {
+    double dx = (double)(other.GetX() - GetX());
+    double dy = (double)(other.GetY() - GetY());
+    return sqrt(dx * dx + dy * dy);
+  }
+
+  double ImageProcessing::ImagePoint::GetDistanceToLine(double a, double b, double c) const // where ax + by + c = 0 is the equation of the line
+  {
+    return std::abs(a * static_cast<double>(GetX()) + b * static_cast<double>(GetY()) + c) / pow(a * a + b * b, 0.5);
+  }
+
+  template <typename TargetType, typename SourceType>
+  static void ConvertInternal(ImageAccessor& target,
+                              const ImageAccessor& source)
+  {
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
+    const TargetType minValue = std::numeric_limits<TargetType>::min();
+    const TargetType maxValue = std::numeric_limits<TargetType>::max();
+
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
+      const SourceType* s = reinterpret_cast<const SourceType*>(source.GetConstRow(y));
+
+      for (unsigned int x = 0; x < width; x++, t++, s++)
+      {
+        if (static_cast<int32_t>(*s) < static_cast<int32_t>(minValue))
+        {
+          *t = minValue;
+        }
+        else if (static_cast<int32_t>(*s) > static_cast<int32_t>(maxValue))
+        {
+          *t = maxValue;
+        }
+        else
+        {
+          *t = static_cast<TargetType>(*s);
+        }
+      }
+    }
+  }
+
+
+  template <typename SourceType>
+  static void ConvertGrayscaleToFloat(ImageAccessor& target,
+                                      const ImageAccessor& source)
+  {
+    assert(sizeof(float) == 4);
+
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+    
+    for (unsigned int y = 0; y < height; 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 < width; x++, t++, s++)
+      {
+        *t = static_cast<float>(*s);
+      }
+    }
+  }
+
+
+  template <PixelFormat TargetFormat>
+  static void ConvertFloatToGrayscale(ImageAccessor& target,
+                                      const ImageAccessor& source)
+  {
+    typedef typename PixelTraits<TargetFormat>::PixelType  TargetType;
+    
+    assert(sizeof(float) == 4);
+
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      TargetType* q = reinterpret_cast<TargetType*>(target.GetRow(y));
+      const float* p = reinterpret_cast<const float*>(source.GetConstRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++, q++)
+      {
+        PixelTraits<TargetFormat>::FloatToPixel(*q, *p);
+      }
+    }
+  }
+
+
+  template <typename TargetType>
+  static void ConvertColorToGrayscale(ImageAccessor& target,
+                                      const ImageAccessor& source)
+  {
+    assert(source.GetFormat() == PixelFormat_RGB24);
+
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
+    const TargetType minValue = std::numeric_limits<TargetType>::min();
+    const TargetType maxValue = std::numeric_limits<TargetType>::max();
+
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      TargetType* t = reinterpret_cast<TargetType*>(target.GetRow(y));
+      const uint8_t* s = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+
+      for (unsigned int x = 0; x < width; x++, t++, s += 3)
+      {
+        // 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])) / 10000;
+        
+        if (static_cast<int32_t>(v) < static_cast<int32_t>(minValue))
+        {
+          *t = minValue;
+        }
+        else if (static_cast<int32_t>(v) > static_cast<int32_t>(maxValue))
+        {
+          *t = maxValue;
+        }
+        else
+        {
+          *t = static_cast<TargetType>(v);
+        }
+      }
+    }
+  }
+
+
+  static void MemsetZeroInternal(ImageAccessor& image)
+  {
+    const unsigned int height = image.GetHeight();
+    const size_t lineSize = image.GetBytesPerPixel() * image.GetWidth();
+    const size_t pitch = image.GetPitch();
+
+    uint8_t *p = reinterpret_cast<uint8_t*>(image.GetBuffer());
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      memset(p, 0, lineSize);
+      p += pitch;
+    }
+  }
+
+
+  template <typename PixelType>
+  static void SetInternal(ImageAccessor& image,
+                          int64_t constant)
+  {
+    if (constant == 0 &&
+        (image.GetFormat() == PixelFormat_Grayscale8 ||
+         image.GetFormat() == PixelFormat_Grayscale16 ||
+         image.GetFormat() == PixelFormat_Grayscale32 ||
+         image.GetFormat() == PixelFormat_Grayscale64 ||
+         image.GetFormat() == PixelFormat_SignedGrayscale16))
+    {
+      MemsetZeroInternal(image);
+    }
+    else
+    {
+      const unsigned int width = image.GetWidth();
+      const unsigned int height = image.GetHeight();
+
+      for (unsigned int y = 0; y < height; y++)
+      {
+        PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+
+        for (unsigned int x = 0; x < width; x++, p++)
+        {
+          *p = static_cast<PixelType>(constant);
+        }
+      }
+    }
+  }
+
+
+  template <typename PixelType>
+  static void GetMinMaxValueInternal(PixelType& minValue,
+                                     PixelType& maxValue,
+                                     const ImageAccessor& source,
+                                     const PixelType LowestValue = std::numeric_limits<PixelType>::min())
+  {
+    // Deal with the special case of empty image
+    if (source.GetWidth() == 0 ||
+        source.GetHeight() == 0)
+    {
+      minValue = 0;
+      maxValue = 0;
+      return;
+    }
+
+    minValue = std::numeric_limits<PixelType>::max();
+    maxValue = LowestValue;
+
+    const unsigned int height = source.GetHeight();
+    const unsigned int width = source.GetWidth();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const PixelType* p = reinterpret_cast<const PixelType*>(source.GetConstRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++)
+      {
+        if (*p < minValue)
+        {
+          minValue = *p;
+        }
+
+        if (*p > maxValue)
+        {
+          maxValue = *p;
+        }
+      }
+    }
+  }
+
+
+
+  template <typename PixelType>
+  static void AddConstantInternal(ImageAccessor& image,
+                                  int64_t constant)
+  {
+    if (constant == 0)
+    {
+      return;
+    }
+
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    assert(sizeof(PixelType) <= 2);  // Safeguard to remember about "float/double"
+    const int64_t minValue = std::numeric_limits<PixelType>::min();
+    const int64_t maxValue = std::numeric_limits<PixelType>::max();
+
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++)
+      {
+        int64_t v = static_cast<int64_t>(*p) + constant;
+
+        if (v > maxValue)
+        {
+          *p = std::numeric_limits<PixelType>::max();
+        }
+        else if (v < minValue)
+        {
+          *p = std::numeric_limits<PixelType>::min();
+        }
+        else
+        {
+          *p = static_cast<PixelType>(v);
+        }
+      }
+    }
+  }
+
+
+
+  template <typename PixelType,
+            bool UseRound>
+  static void MultiplyConstantInternal(ImageAccessor& image,
+                                       float factor)
+  {
+    if (std::abs(factor - 1.0f) <= std::numeric_limits<float>::epsilon())
+    {
+      return;
+    }
+
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    assert(sizeof(PixelType) <= 2);  // Safeguard to remember about "float/double"
+    const int64_t minValue = std::numeric_limits<PixelType>::min();
+    const int64_t maxValue = std::numeric_limits<PixelType>::max();
+
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      PixelType* p = reinterpret_cast<PixelType*>(image.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++)
+      {
+        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)
+        {
+          *p = std::numeric_limits<PixelType>::max();
+        }
+        else if (v < minValue)
+        {
+          *p = std::numeric_limits<PixelType>::min();
+        }
+        else
+        {
+          *p = static_cast<PixelType>(v);
+        }
+      }
+    }
+  }
+
+
+  // Computes "a * x + b" at each pixel => Note that this is not the
+  // same convention as in "ShiftScale()"
+  template <typename TargetType,
+            typename SourceType,
+            bool UseRound,
+            bool Invert>
+  static void ShiftScaleInternal(ImageAccessor& target,
+                                 const ImageAccessor& source,
+                                 float a,
+                                 float b,
+                                 const TargetType LowestValue)
+  // This function can be applied inplace (source == target)
+  {
+    if (source.GetWidth() != target.GetWidth() ||
+        source.GetHeight() != target.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+
+    if (&source == &target &&
+        source.GetFormat() != target.GetFormat())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+    
+    const TargetType minPixelValue = LowestValue;
+    const TargetType maxPixelValue = std::numeric_limits<TargetType>::max();
+    const float minFloatValue = static_cast<float>(LowestValue);
+    const float maxFloatValue = static_cast<float>(maxPixelValue);
+
+    const unsigned int height = target.GetHeight();
+    const unsigned int width = target.GetWidth();
+    
+    for (unsigned int y = 0; y < height; y++)
+    {
+      TargetType* p = reinterpret_cast<TargetType*>(target.GetRow(y));
+      const SourceType* q = reinterpret_cast<const SourceType*>(source.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++, p++, q++)
+      {
+        float v = a * static_cast<float>(*q) + b;
+
+        if (v >= maxFloatValue)
+        {
+          *p = maxPixelValue;
+        }
+        else if (v <= minFloatValue)
+        {
+          *p = minPixelValue;
+        }
+        else if (UseRound)
+        {
+          // The "round" operation is very costly
+          *p = static_cast<TargetType>(boost::math::iround(v));
+        }
+        else
+        {
+          *p = static_cast<TargetType>(std::floor(v));
+        }
+
+        if (Invert)
+        {
+          *p = maxPixelValue - *p;
+        }
+      }
+    }
+  }
+
+  template <typename PixelType>
+  static void ShiftRightInternal(ImageAccessor& image,
+                                 unsigned int shift)
+  {
+    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 < width; x++, p++)
+      {
+        *p = *p >> shift;
+      }
+    }
+  }
+
+  template <typename PixelType>
+  static void ShiftLeftInternal(ImageAccessor& image,
+                                unsigned int shift)
+  {
+    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 < width; x++, p++)
+      {
+        *p = *p << shift;
+      }
+    }
+  }
+
+  void ImageProcessing::Copy(ImageAccessor& target,
+                             const ImageAccessor& source)
+  {
+    if (target.GetWidth() != source.GetWidth() ||
+        target.GetHeight() != source.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+
+    if (target.GetFormat() != source.GetFormat())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+
+    unsigned int lineSize = source.GetBytesPerPixel() * source.GetWidth();
+
+    assert(source.GetPitch() >= lineSize && target.GetPitch() >= lineSize);
+
+    for (unsigned int y = 0; y < source.GetHeight(); y++)
+    {
+      memcpy(target.GetRow(y), source.GetConstRow(y), lineSize);
+    }
+  }
+
+  template <typename TargetType, typename SourceType>
+  static void ApplyWindowingInternal(ImageAccessor& target,
+                                     const ImageAccessor& source,
+                                     float windowCenter,
+                                     float windowWidth,
+                                     float rescaleSlope,
+                                     float rescaleIntercept,
+                                     bool invert)
+  {
+    assert(sizeof(SourceType) == source.GetBytesPerPixel() &&
+           sizeof(TargetType) == target.GetBytesPerPixel());
+    
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    assert(sizeof(TargetType) <= 2);  // Safeguard to remember about "float/double"
+    const TargetType minTargetValue = std::numeric_limits<TargetType>::min();
+    const TargetType maxTargetValue = std::numeric_limits<TargetType>::max();
+    const float maxFloatValue = static_cast<float>(maxTargetValue);
+    
+    const float windowIntercept = windowCenter - windowWidth / 2.0f;
+    const float windowSlope = (maxFloatValue + 1.0f) / windowWidth;
+
+    const float a = rescaleSlope * windowSlope;
+    const float b = (rescaleIntercept - windowIntercept) * windowSlope;
+
+    if (invert)
+    {
+      ShiftScaleInternal<TargetType, SourceType, false, true>(target, source, a, b, minTargetValue);
+    }
+    else
+    {
+      ShiftScaleInternal<TargetType, SourceType, false, false>(target, source, a, b, minTargetValue);
+    }
+  }
+
+  void ImageProcessing::ApplyWindowing_Deprecated(ImageAccessor& target,
+                                                  const ImageAccessor& source,
+                                                  float windowCenter,
+                                                  float windowWidth,
+                                                  float rescaleSlope,
+                                                  float rescaleIntercept,
+                                                  bool invert)
+  {
+    if (target.GetWidth() != source.GetWidth() ||
+        target.GetHeight() != source.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+
+    switch (source.GetFormat())
+    {
+      case Orthanc::PixelFormat_Float32:
+      {
+        switch (target.GetFormat())
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            ApplyWindowingInternal<uint8_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          case Orthanc::PixelFormat_Grayscale16:
+            ApplyWindowingInternal<uint16_t, float>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+      };break;
+      case Orthanc::PixelFormat_Grayscale8:
+      {
+        switch (target.GetFormat())
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            ApplyWindowingInternal<uint8_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          case Orthanc::PixelFormat_Grayscale16:
+            ApplyWindowingInternal<uint16_t, uint8_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+      };break;
+      case Orthanc::PixelFormat_Grayscale16:
+      {
+        switch (target.GetFormat())
+        {
+          case Orthanc::PixelFormat_Grayscale8:
+            ApplyWindowingInternal<uint8_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          case Orthanc::PixelFormat_Grayscale16:
+            ApplyWindowingInternal<uint16_t, uint16_t>(target, source, windowCenter, windowWidth, rescaleSlope, rescaleIntercept, invert);
+            break;
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+      };break;
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::Convert(ImageAccessor& target,
+                                const ImageAccessor& source)
+  {
+    if (target.GetWidth() != source.GetWidth() ||
+        target.GetHeight() != source.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+
+    const unsigned int width = source.GetWidth();
+    const unsigned int height = source.GetHeight();
+
+    if (source.GetFormat() == target.GetFormat())
+    {
+      Copy(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale16 &&
+        source.GetFormat() == PixelFormat_Grayscale8)
+    {
+      ConvertInternal<uint16_t, uint8_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
+        source.GetFormat() == PixelFormat_Grayscale8)
+    {
+      ConvertInternal<int16_t, uint8_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_Grayscale16)
+    {
+      ConvertInternal<uint8_t, uint16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
+        source.GetFormat() == PixelFormat_Grayscale16)
+    {
+      ConvertInternal<int16_t, uint16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      ConvertInternal<uint8_t, int16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale16 &&
+        source.GetFormat() == PixelFormat_SignedGrayscale16)
+    {
+      ConvertInternal<uint16_t, int16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      ConvertColorToGrayscale<uint8_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale16 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      ConvertColorToGrayscale<uint16_t>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_SignedGrayscale16 &&
+        source.GetFormat() == PixelFormat_RGB24)
+    {
+      ConvertColorToGrayscale<int16_t>(target, source);
+      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)
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++, q++)
+        {
+          *q = static_cast<uint8_t>((2126 * static_cast<uint32_t>(p[0]) +
+                                     7152 * static_cast<uint32_t>(p[1]) +
+                                     0722 * static_cast<uint32_t>(p[2])) / 10000);
+          p += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_BGRA32)
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++, 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)
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          q[0] = p[0];
+          q[1] = p[1];
+          q[2] = p[2];
+          p += 4;
+          q += 3;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_RGB24 &&
+        source.GetFormat() == PixelFormat_BGRA32)
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          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)
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          q[0] = p[0];
+          q[1] = p[1];
+          q[2] = p[2];
+          q[3] = 255;   // Set the alpha channel to full opacity
+          p += 3;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_RGB24 &&
+        source.GetFormat() == PixelFormat_Grayscale8)
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          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 < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          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 < height; 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 < width; 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 < height; 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 < width; 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 < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          q[0] = p[2];
+          q[1] = p[1];
+          q[2] = p[0];
+          q[3] = 255;
+          p += 3;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if ((target.GetFormat() == PixelFormat_BGRA32 &&
+         source.GetFormat() == PixelFormat_RGBA32)
+        || (target.GetFormat() == PixelFormat_RGBA32 &&
+            source.GetFormat() == PixelFormat_BGRA32))
+    {
+      for (unsigned int y = 0; y < height; y++)
+      {
+        const uint8_t* p = reinterpret_cast<const uint8_t*>(source.GetConstRow(y));
+        uint8_t* q = reinterpret_cast<uint8_t*>(target.GetRow(y));
+        for (unsigned int x = 0; x < width; x++)
+        {
+          q[0] = p[2];
+          q[1] = p[1];
+          q[2] = p[0];
+          q[3] = p[3];
+          p += 4;
+          q += 4;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_RGB24 &&
+        source.GetFormat() == PixelFormat_RGB48)
+    {
+      for (unsigned int y = 0; y < height; 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 < width; x++)
+        {
+          q[0] = p[0] >> 8;
+          q[1] = p[1] >> 8;
+          q[2] = p[2] >> 8;
+          p += 3;
+          q += 3;
+        }
+      }
+
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale16 &&
+        source.GetFormat() == PixelFormat_Float32)
+    {
+      ConvertFloatToGrayscale<PixelFormat_Grayscale16>(target, source);
+      return;
+    }
+
+    if (target.GetFormat() == PixelFormat_Grayscale8 &&
+        source.GetFormat() == PixelFormat_Float32)
+    {
+      ConvertFloatToGrayscale<PixelFormat_Grayscale8>(target, source);
+      return;
+    }
+
+    throw OrthancException(ErrorCode_NotImplemented);
+  }
+
+
+
+  void ImageProcessing::Set(ImageAccessor& image,
+                            int64_t value)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        SetInternal<uint8_t>(image, value);
+        return;
+
+      case PixelFormat_Grayscale16:
+        SetInternal<uint16_t>(image, value);
+        return;
+
+      case PixelFormat_Grayscale32:
+        SetInternal<uint32_t>(image, value);
+        return;
+
+      case PixelFormat_Grayscale64:
+        SetInternal<uint64_t>(image, value);
+        return;
+
+      case PixelFormat_SignedGrayscale16:
+        SetInternal<int16_t>(image, value);
+        return;
+
+      case PixelFormat_Float32:
+        assert(sizeof(float) == 4);
+        SetInternal<float>(image, value);
+        return;
+
+      case PixelFormat_RGBA32:
+      case PixelFormat_BGRA32:
+      case PixelFormat_RGB24:
+      {
+        uint8_t v = static_cast<uint8_t>(value);
+        Set(image, v, v, v, v);  // Use the color version
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  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);
+    }
+
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      uint8_t* q = reinterpret_cast<uint8_t*>(image.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++)
+      {
+        for (unsigned int i = 0; i < size; i++)
+        {
+          q[i] = p[i];
+        }
+
+        q += size;
+      }
+    }
+  }
+
+  void ImageProcessing::Set(ImageAccessor& image,
+                            uint8_t red,
+                            uint8_t green,
+                            uint8_t blue,
+                            ImageAccessor& alpha)
+  {
+    uint8_t p[4];
+
+    if (alpha.GetWidth() != image.GetWidth() || alpha.GetHeight() != image.GetHeight())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageSize);
+    }
+
+    if (alpha.GetFormat() != PixelFormat_Grayscale8)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    switch (image.GetFormat())
+    {
+      case PixelFormat_RGBA32:
+        p[0] = red;
+        p[1] = green;
+        p[2] = blue;
+        break;
+
+      case PixelFormat_BGRA32:
+        p[0] = blue;
+        p[1] = green;
+        p[2] = red;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      uint8_t* q = reinterpret_cast<uint8_t*>(image.GetRow(y));
+      uint8_t* a = reinterpret_cast<uint8_t*>(alpha.GetRow(y));
+
+      for (unsigned int x = 0; x < width; x++)
+      {
+        for (unsigned int i = 0; i < 3; i++)
+        {
+          q[i] = p[i];
+        }
+        q[3] = *a;
+        q += 4;
+        ++a;
+      }
+    }
+  }
+
+
+  void ImageProcessing::ShiftRight(ImageAccessor& image,
+                                   unsigned int shift)
+  {
+    if (image.GetWidth() == 0 ||
+        image.GetHeight() == 0 ||
+        shift == 0)
+    {
+      // Nothing to do
+      return;
+    }
+
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+      {
+        ShiftRightInternal<uint8_t>(image, shift);
+        break;
+      }
+
+      case PixelFormat_Grayscale16:
+      {
+        ShiftRightInternal<uint16_t>(image, shift);
+        break;
+      }
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  void ImageProcessing::ShiftLeft(ImageAccessor& image,
+                                  unsigned int shift)
+  {
+    if (image.GetWidth() == 0 ||
+        image.GetHeight() == 0 ||
+        shift == 0)
+    {
+      // Nothing to do
+      return;
+    }
+
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+      {
+        ShiftLeftInternal<uint8_t>(image, shift);
+        break;
+      }
+
+      case PixelFormat_Grayscale16:
+      {
+        ShiftLeftInternal<uint16_t>(image, shift);
+        break;
+      }
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  void ImageProcessing::GetMinMaxIntegerValue(int64_t& minValue,
+                                              int64_t& maxValue,
+                                              const ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+      {
+        uint8_t a, b;
+        GetMinMaxValueInternal<uint8_t>(a, b, image);
+        minValue = a;
+        maxValue = b;
+        break;
+      }
+
+      case PixelFormat_Grayscale16:
+      {
+        uint16_t a, b;
+        GetMinMaxValueInternal<uint16_t>(a, b, image);
+        minValue = a;
+        maxValue = b;
+        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;
+        GetMinMaxValueInternal<int16_t>(a, b, image);
+        minValue = a;
+        maxValue = b;
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::GetMinMaxFloatValue(float& minValue,
+                                            float& maxValue,
+                                            const ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Float32:
+      {
+        assert(sizeof(float) == 4);
+        float a, b;
+
+        /**
+         * WARNING - On floating-point types, the minimal value is
+         * "-FLT_MAX" (as implemented by "::lowest()"), not "FLT_MIN"
+         * (as implemented by "::min()")
+         * https://en.cppreference.com/w/cpp/types/numeric_limits
+         **/
+        GetMinMaxValueInternal<float>(a, b, image, -std::numeric_limits<float>::max());
+        minValue = a;
+        maxValue = b;
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+
+  void ImageProcessing::AddConstant(ImageAccessor& image,
+                                    int64_t value)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        AddConstantInternal<uint8_t>(image, value);
+        return;
+
+      case PixelFormat_Grayscale16:
+        AddConstantInternal<uint16_t>(image, value);
+        return;
+
+      case PixelFormat_SignedGrayscale16:
+        AddConstantInternal<int16_t>(image, value);
+        return;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::MultiplyConstant(ImageAccessor& image,
+                                         float factor,
+                                         bool useRound)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        if (useRound)
+        {
+          MultiplyConstantInternal<uint8_t, true>(image, factor);
+        }
+        else
+        {
+          MultiplyConstantInternal<uint8_t, false>(image, factor);
+        }
+        return;
+
+      case PixelFormat_Grayscale16:
+        if (useRound)
+        {
+          MultiplyConstantInternal<uint16_t, true>(image, factor);
+        }
+        else
+        {
+          MultiplyConstantInternal<uint16_t, false>(image, factor);
+        }
+        return;
+
+      case PixelFormat_SignedGrayscale16:
+        if (useRound)
+        {
+          MultiplyConstantInternal<int16_t, true>(image, factor);
+        }
+        else
+        {
+          MultiplyConstantInternal<int16_t, false>(image, factor);
+        }
+        return;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::ShiftScale(ImageAccessor& image,
+                                   float offset,
+                                   float scaling,
+                                   bool useRound)
+  {
+    // Rewrite "(x + offset) * scaling" as "a * x + b"
+
+    const float a = scaling;
+    const float b = offset * scaling;
+    
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        if (useRound)
+        {
+          ShiftScaleInternal<uint8_t, uint8_t, true, false>(image, image, a, b, std::numeric_limits<uint8_t>::min());
+        }
+        else
+        {
+          ShiftScaleInternal<uint8_t, uint8_t, false, false>(image, image, a, b, std::numeric_limits<uint8_t>::min());
+        }
+        return;
+
+      case PixelFormat_Grayscale16:
+        if (useRound)
+        {
+          ShiftScaleInternal<uint16_t, uint16_t, true, false>(image, image, a, b, std::numeric_limits<uint16_t>::min());
+        }
+        else
+        {
+          ShiftScaleInternal<uint16_t, uint16_t, false, false>(image, image, a, b, std::numeric_limits<uint16_t>::min());
+        }
+        return;
+
+      case PixelFormat_SignedGrayscale16:
+        if (useRound)
+        {
+          ShiftScaleInternal<int16_t, int16_t, true, false>(image, image, a, b, std::numeric_limits<int16_t>::min());
+        }
+        else
+        {
+          ShiftScaleInternal<int16_t, int16_t, false, false>(image, image, a, b, std::numeric_limits<int16_t>::min());
+        }
+        return;
+
+      case PixelFormat_Float32:
+        // "::min()" must be replaced by "::lowest()" or "-::max()" if dealing with float or double.
+        if (useRound)
+        {
+          ShiftScaleInternal<float, float, true, false>(image, image, a, b, -std::numeric_limits<float>::max());
+        }
+        else
+        {
+          ShiftScaleInternal<float, float, false, false>(image, image, a, b, -std::numeric_limits<float>::max());
+        }
+        return;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::ShiftScale(ImageAccessor& target,
+                                   const ImageAccessor& source,
+                                   float offset,
+                                   float scaling,
+                                   bool useRound)
+  {
+    // Rewrite "(x + offset) * scaling" as "a * x + b"
+
+    const float a = scaling;
+    const float b = offset * scaling;
+    
+    switch (target.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+
+        switch (source.GetFormat())
+        {
+          case PixelFormat_Float32:
+            if (useRound)
+            {
+              ShiftScaleInternal<uint8_t, float, true, false>(
+                target, source, a, b, std::numeric_limits<uint8_t>::min());
+            }
+            else
+            {
+              ShiftScaleInternal<uint8_t, float, false, false>(
+                target, source, a, b, std::numeric_limits<uint8_t>::min());
+            }
+            return;
+
+          default:
+            throw OrthancException(ErrorCode_NotImplemented);
+        }
+        
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::Invert(ImageAccessor& image, int64_t maxValue)
+  {
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale16:
+      {
+        uint16_t maxValueUint16 = (uint16_t)(std::min(maxValue, static_cast<int64_t>(std::numeric_limits<uint16_t>::max())));
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          uint16_t* p = reinterpret_cast<uint16_t*>(image.GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++, p++)
+          {
+            *p = maxValueUint16 - (*p);
+          }
+        }
+
+        return;
+      }
+      case PixelFormat_Grayscale8:
+      {
+        uint8_t maxValueUint8 = (uint8_t)(std::min(maxValue, static_cast<int64_t>(std::numeric_limits<uint8_t>::max())));
+
+        for (unsigned int y = 0; y < height; y++)
+        {
+          uint8_t* p = reinterpret_cast<uint8_t*>(image.GetRow(y));
+
+          for (unsigned int x = 0; x < width; x++, p++)
+          {
+            *p = maxValueUint8 - (*p);
+          }
+        }
+
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+  }
+
+  void ImageProcessing::Invert(ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        return Invert(image, 255);
+      default:
+        throw OrthancException(ErrorCode_NotImplemented); // you should use the Invert(image, maxValue) overload
+    }
+  }
+
+
+
+  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);
+    }
+  }
+
+  void ComputePolygonExtent(int32_t& left, int32_t& right, int32_t& top, int32_t& bottom, const std::vector<ImageProcessing::ImagePoint>& points)
+  {
+    left = std::numeric_limits<int32_t>::max();
+    right = std::numeric_limits<int32_t>::min();
+    top = std::numeric_limits<int32_t>::max();
+    bottom = std::numeric_limits<int32_t>::min();
+
+    for (size_t i = 0; i < points.size(); i++)
+    {
+      const ImageProcessing::ImagePoint& p = points[i];
+      left = std::min(p.GetX(), left);
+      right = std::max(p.GetX(), right);
+      bottom = std::max(p.GetY(), bottom);
+      top = std::min(p.GetY(), top);
+    }
+  }
+
+  template <PixelFormat TargetFormat>
+  void FillPolygon_(ImageAccessor& image,
+                    const std::vector<ImageProcessing::ImagePoint>& points,
+                    int64_t value_)
+  {
+    typedef typename PixelTraits<TargetFormat>::PixelType  TargetType;
+
+    TargetType value = PixelTraits<TargetFormat>::IntegerToPixel(value_);
+    int imageWidth = static_cast<int>(image.GetWidth());
+    int imageHeight = static_cast<int>(image.GetHeight());
+    int32_t left;
+    int32_t right;
+    int32_t top;
+    int32_t bottom;
+
+    // TODO: test clipping in UT (in Trello board)
+    ComputePolygonExtent(left, right, top, bottom, points);
+
+    // clip the computed extent with the target image
+    // L and R
+    left = std::max(0, left);
+    left = std::min(imageWidth, left);
+    right = std::max(0, right);
+    right = std::min(imageWidth, right);
+    if (left > right)
+      std::swap(left, right);
+
+    // T and B
+    top = std::max(0, top);
+    top = std::min(imageHeight, top);
+    bottom = std::max(0, bottom);
+    bottom = std::min(imageHeight, bottom);
+    if (top > bottom)
+      std::swap(top, bottom);
+
+    // from http://alienryderflex.com/polygon_fill/
+
+    // convert all "corner"  points to double only once
+    std::vector<double> cpx;
+    std::vector<double> cpy;
+    size_t cpSize = points.size();
+    for (size_t i = 0; i < points.size(); i++)
+    {
+      if (points[i].GetX() < 0 || points[i].GetX() >= imageWidth
+          || points[i].GetY() < 0 || points[i].GetY() >= imageHeight)
+      {
+        throw Orthanc::OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      cpx.push_back((double)points[i].GetX());
+      cpy.push_back((double)points[i].GetY());
+    }
+
+    // Draw the lines segments
+    for (size_t i = 0; i < (points.size() -1); i++)
+    {
+      ImageProcessing::DrawLineSegment(image, points[i].GetX(), points[i].GetY(), points[i+1].GetX(), points[i+1].GetY(), value_);
+    }
+    ImageProcessing::DrawLineSegment(image, points[points.size() -1].GetX(), points[points.size() -1].GetY(), points[0].GetX(), points[0].GetY(), value_);
+
+    std::vector<int32_t> nodeX;
+    nodeX.resize(cpSize);
+    int  nodes, pixelX, pixelY, i, j, swap ;
+
+    //  Loop through the rows of the image.
+    for (pixelY = top; pixelY < bottom; pixelY++)
+    {
+      double y = (double)pixelY;
+      //  Build a list of nodes.
+      nodes = 0;
+      j = static_cast<int>(cpSize) - 1;
+
+      for (i = 0; i < static_cast<int>(cpSize); i++)
+      {
+        if ((cpy[i] < y && cpy[j] >=  y) || (cpy[j] < y && cpy[i] >= y))
+        {
+          nodeX[nodes++] = (int32_t)(cpx[i] + (y - cpy[i])/(cpy[j] - cpy[i]) * (cpx[j] - cpx[i]));
+        }
+        j=i;
+      }
+
+      //  Sort the nodes, via a simple “Bubble” sort.
+      i=0;
+      while (i < nodes-1)
+      {
+        if (nodeX[i] > nodeX[i+1])
+        {
+          swap = nodeX[i];
+          nodeX[i] = nodeX[i+1];
+          nodeX[i+1] = swap;
+          if (i > 0)
+          {
+            i--;
+          }
+        }
+        else
+        {
+          i++;
+        }
+      }
+
+      TargetType* row = reinterpret_cast<TargetType*>(image.GetRow(pixelY));
+      //  Fill the pixels between node pairs.
+      for (i = 0; i < nodes; i += 2)
+      {
+        if (nodeX[i] >= right)
+          break;
+
+        if (nodeX[i + 1] >= left)
+        {
+          if (nodeX[i] < left)
+          {
+            nodeX[i] = left;
+          }
+
+          if (nodeX[i + 1] > right)
+          {
+            nodeX[i + 1] = right;
+          }
+
+          for (pixelX = nodeX[i]; pixelX <= nodeX[i + 1]; pixelX++)
+          {
+            *(row + pixelX) = value;
+          }
+        }
+      }
+    }
+  }
+
+  void ImageProcessing::FillPolygon(ImageAccessor& image,
+                                    const std::vector<ImagePoint>& points,
+                                    int64_t value)
+  {
+    switch (image.GetFormat())
+    {
+      case Orthanc::PixelFormat_Grayscale8:
+      {
+        FillPolygon_<Orthanc::PixelFormat_Grayscale8>(image, points, value);
+        break;
+      }
+      case Orthanc::PixelFormat_Grayscale16:
+      {
+        FillPolygon_<Orthanc::PixelFormat_Grayscale16>(image, points, value);
+        break;
+      }
+      case Orthanc::PixelFormat_SignedGrayscale16:
+      {
+        FillPolygon_<Orthanc::PixelFormat_SignedGrayscale16>(image, points, value);
+        break;
+      }
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+  }
+
+
+  template <PixelFormat Format>
+  static void ResizeInternal(ImageAccessor& target,
+                             const ImageAccessor& source)
+  {
+    assert(target.GetFormat() == source.GetFormat() &&
+           target.GetFormat() == Format);
+      
+    const unsigned int sourceWidth = source.GetWidth();
+    const unsigned int sourceHeight = source.GetHeight();
+    const unsigned int targetWidth = target.GetWidth();
+    const unsigned int targetHeight = target.GetHeight();
+
+    if (targetWidth == 0 || targetHeight == 0)
+    {
+      return;
+    }
+
+    if (sourceWidth == 0 || sourceHeight == 0)
+    {
+      // Avoids division by zero below
+      ImageProcessing::Set(target, 0);
+      return;
+    }
+      
+    const float scaleX = static_cast<float>(sourceWidth) / static_cast<float>(targetWidth);
+    const float scaleY = static_cast<float>(sourceHeight) / static_cast<float>(targetHeight);
+
+
+    /**
+     * Create two lookup tables to quickly know the (x,y) position
+     * in the source image, given the (x,y) position in the target
+     * image.
+     **/
+      
+    std::vector<unsigned int>  lookupX(targetWidth);
+      
+    for (unsigned int x = 0; x < targetWidth; x++)
+    {
+      int sourceX = static_cast<int>(std::floor((static_cast<float>(x) + 0.5f) * scaleX));
+      if (sourceX < 0)
+      {
+        sourceX = 0;  // Should never happen
+      }
+      else if (sourceX >= static_cast<int>(sourceWidth))
+      {
+        sourceX = sourceWidth - 1;
+      }
+
+      lookupX[x] = static_cast<unsigned int>(sourceX);
+    }
+      
+    std::vector<unsigned int>  lookupY(targetHeight);
+      
+    for (unsigned int y = 0; y < targetHeight; y++)
+    {
+      int sourceY = static_cast<int>(std::floor((static_cast<float>(y) + 0.5f) * scaleY));
+      if (sourceY < 0)
+      {
+        sourceY = 0;  // Should never happen
+      }
+      else if (sourceY >= static_cast<int>(sourceHeight))
+      {
+        sourceY = sourceHeight - 1;
+      }
+
+      lookupY[y] = static_cast<unsigned int>(sourceY);
+    }
+
+
+    /**
+     * Actual resizing
+     **/
+      
+    for (unsigned int targetY = 0; targetY < targetHeight; targetY++)
+    {
+      unsigned int sourceY = lookupY[targetY];
+
+      for (unsigned int targetX = 0; targetX < targetWidth; targetX++)
+      {
+        unsigned int sourceX = lookupX[targetX];
+
+        typename ImageTraits<Format>::PixelType pixel;
+        ImageTraits<Format>::GetPixel(pixel, source, sourceX, sourceY);
+        ImageTraits<Format>::SetPixel(target, pixel, targetX, targetY);
+      }
+    }            
+  }
+
+
+
+  void ImageProcessing::Resize(ImageAccessor& target,
+                               const ImageAccessor& source)
+  {
+    if (source.GetFormat() != source.GetFormat())
+    {
+      throw OrthancException(ErrorCode_IncompatibleImageFormat);
+    }
+
+    if (source.GetWidth() == target.GetWidth() &&
+        source.GetHeight() == target.GetHeight())
+    {
+      Copy(target, source);
+      return;
+    }
+
+    switch (source.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        ResizeInternal<PixelFormat_Grayscale8>(target, source);
+        break;
+
+      case PixelFormat_Float32:
+        ResizeInternal<PixelFormat_Float32>(target, source);
+        break;
+
+      case PixelFormat_RGB24:
+        ResizeInternal<PixelFormat_RGB24>(target, source);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  ImageAccessor* ImageProcessing::Halve(const ImageAccessor& source,
+                                        bool forceMinimalPitch)
+  {
+    std::unique_ptr<Image> target(new Image(source.GetFormat(), source.GetWidth() / 2,
+                                            source.GetHeight() / 2, forceMinimalPitch));
+    Resize(*target, source);
+    return target.release();
+  }
+
+    
+  template <PixelFormat Format>
+  static void FlipXInternal(ImageAccessor& image)
+  {     
+    const unsigned int height = image.GetHeight();
+    const unsigned int width = image.GetWidth();
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      for (unsigned int x1 = 0; x1 < width / 2; x1++)
+      {
+        unsigned int x2 = width - 1 - x1;
+          
+        typename ImageTraits<Format>::PixelType a, b;
+        ImageTraits<Format>::GetPixel(a, image, x1, y);
+        ImageTraits<Format>::GetPixel(b, image, x2, y);
+        ImageTraits<Format>::SetPixel(image, a, x2, y);
+        ImageTraits<Format>::SetPixel(image, b, x1, y);
+      }
+    }        
+  }
+
+    
+  void ImageProcessing::FlipX(ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        FlipXInternal<PixelFormat_Grayscale8>(image);
+        break;
+
+      case PixelFormat_RGB24:
+        FlipXInternal<PixelFormat_RGB24>(image);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+    
+  template <PixelFormat Format>
+  static void FlipYInternal(ImageAccessor& image)
+  {     
+    const unsigned int height = image.GetHeight();
+    const unsigned int width = image.GetWidth();
+
+    for (unsigned int y1 = 0; y1 < height / 2; y1++)
+    {
+      unsigned int y2 = height - 1 - y1;
+        
+      for (unsigned int x = 0; x < width; x++)
+      {
+        typename ImageTraits<Format>::PixelType a, b;
+        ImageTraits<Format>::GetPixel(a, image, x, y1);
+        ImageTraits<Format>::GetPixel(b, image, x, y2);
+        ImageTraits<Format>::SetPixel(image, a, x, y2);
+        ImageTraits<Format>::SetPixel(image, b, x, y1);
+      }
+    }        
+  }
+
+    
+  void ImageProcessing::FlipY(ImageAccessor& image)
+  {
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        FlipYInternal<PixelFormat_Grayscale8>(image);
+        break;
+
+      case PixelFormat_RGB24:
+        FlipYInternal<PixelFormat_RGB24>(image);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  // This is a slow implementation of horizontal convolution on one
+  // individual channel, that checks for out-of-image values
+  template <typename RawPixel, unsigned int ChannelsCount>
+  static float GetHorizontalConvolutionFloatSecure(const Orthanc::ImageAccessor& source,
+                                                   const std::vector<float>& horizontal,
+                                                   size_t horizontalAnchor,
+                                                   unsigned int x,
+                                                   unsigned int y,
+                                                   float leftBorder,
+                                                   float rightBorder,
+                                                   unsigned int channel)
+  {
+    const RawPixel* row = reinterpret_cast<const RawPixel*>(source.GetConstRow(y)) + channel;
+
+    float p = 0;
+
+    for (unsigned int k = 0; k < horizontal.size(); k++)
+    {
+      float value;
+
+      if (x + k < horizontalAnchor)   // Negation of "x - horizontalAnchor + k >= 0"
+      {
+        value = leftBorder;
+      }
+      else if (x + k >= source.GetWidth() + horizontalAnchor)   // Negation of "x - horizontalAnchor + k < width"
+      {
+        value = rightBorder;
+      }
+      else
+      {
+        // The value lies within the image
+        value = row[(x - horizontalAnchor + k) * ChannelsCount];
+      }
+
+      p += value * horizontal[k];
+    }
+
+    return p;
+  }
+  
+
+  
+  // This is an implementation of separable convolution that uses
+  // floating-point arithmetics, and an intermediate Float32
+  // image. The out-of-image values are taken as the border
+  // value. Further optimization is possible.
+  template <typename RawPixel, unsigned int ChannelsCount>
+  static void SeparableConvolutionFloat(ImageAccessor& image /* inplace */,
+                                        const std::vector<float>& horizontal,
+                                        size_t horizontalAnchor,
+                                        const std::vector<float>& vertical,
+                                        size_t verticalAnchor,
+                                        float normalization)
+  {
+    // WARNING - "::min()" should be replaced by "::lowest()" if
+    // dealing with float or double (which is not the case so far)
+    assert(sizeof(RawPixel) <= 2);  // Safeguard to remember about "float/double"
+
+    const unsigned int width = image.GetWidth();
+    const unsigned int height = image.GetHeight();
+    
+
+    /**
+     * Horizontal convolution
+     **/
+
+    Image tmp(PixelFormat_Float32, ChannelsCount * width, height, false);
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      const RawPixel* row = reinterpret_cast<const RawPixel*>(image.GetConstRow(y));
+
+      float leftBorder[ChannelsCount], rightBorder[ChannelsCount];
+      
+      for (unsigned int c = 0; c < ChannelsCount; c++)
+      {
+        leftBorder[c] = row[c];
+        rightBorder[c] = row[ChannelsCount * (width - 1) + c];
+      }
+
+      float* p = static_cast<float*>(tmp.GetRow(y));
+
+      if (width < horizontal.size())
+      {
+        // It is not possible to have the full kernel within the image, use the direct implementation
+        for (unsigned int x = 0; x < width; x++)
+        {
+          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
+          {
+            *p = GetHorizontalConvolutionFloatSecure<RawPixel, ChannelsCount>
+              (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c);
+          }
+        }
+      }
+      else
+      {
+        // Deal with the left border
+        for (unsigned int x = 0; x < horizontalAnchor; x++)
+        {
+          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
+          {
+            *p = GetHorizontalConvolutionFloatSecure<RawPixel, ChannelsCount>
+              (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c);
+          }
+        }
+
+        // Deal with the central portion of the image (all pixel values
+        // scanned by the kernel lie inside the image)
+
+        for (unsigned int x = 0; x < width - horizontal.size() + 1; x++)
+        {
+          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
+          {
+            *p = 0;
+            for (unsigned int k = 0; k < horizontal.size(); k++)
+            {
+              *p += static_cast<float>(row[(x + k) * ChannelsCount + c]) * horizontal[k];
+            }
+          }
+        }
+
+        // Deal with the right border
+        for (unsigned int x = static_cast<unsigned int>(
+               horizontalAnchor + width - horizontal.size() + 1); x < width; x++)
+        {
+          for (unsigned int c = 0; c < ChannelsCount; c++, p++)
+          {
+            *p = GetHorizontalConvolutionFloatSecure<RawPixel, ChannelsCount>
+              (image, horizontal, horizontalAnchor, x, y, leftBorder[c], rightBorder[c], c);
+          }
+        }
+      }
+    }
+
+
+    /**
+     * Vertical convolution
+     **/
+
+    std::vector<const float*> rows(vertical.size());
+
+    for (unsigned int y = 0; y < height; y++)
+    {
+      for (unsigned int k = 0; k < vertical.size(); k++)
+      {
+        if (y + k < verticalAnchor)
+        {
+          rows[k] = reinterpret_cast<const float*>(tmp.GetConstRow(0));   // Use top border
+        }
+        else if (y + k >= height + verticalAnchor)
+        {
+          rows[k] = reinterpret_cast<const float*>(tmp.GetConstRow(height - 1));  // Use bottom border
+        }
+        else
+        {
+          rows[k] = reinterpret_cast<const float*>(tmp.GetConstRow(static_cast<unsigned int>(y + k - verticalAnchor)));
+        }
+      }
+
+      RawPixel* p = reinterpret_cast<RawPixel*>(image.GetRow(y));
+        
+      for (unsigned int x = 0; x < width; x++)
+      {
+        for (unsigned int c = 0; c < ChannelsCount; c++, p++)
+        {
+          float accumulator = 0;
+        
+          for (unsigned int k = 0; k < vertical.size(); k++)
+          {
+            accumulator += rows[k][ChannelsCount * x + c] * vertical[k];
+          }
+
+          accumulator *= normalization;
+
+          if (accumulator <= static_cast<float>(std::numeric_limits<RawPixel>::min()))
+          {
+            *p = std::numeric_limits<RawPixel>::min();
+          }
+          else if (accumulator >= static_cast<float>(std::numeric_limits<RawPixel>::max()))
+          {
+            *p = std::numeric_limits<RawPixel>::max();
+          }
+          else
+          {
+            *p = static_cast<RawPixel>(accumulator);
+          }
+        }
+      }
+    }
+  }
+
+
+  void ImageProcessing::SeparableConvolution(ImageAccessor& image /* inplace */,
+                                             const std::vector<float>& horizontal,
+                                             size_t horizontalAnchor,
+                                             const std::vector<float>& vertical,
+                                             size_t verticalAnchor)
+  {
+    if (horizontal.size() == 0 ||
+        vertical.size() == 0 ||
+        horizontalAnchor >= horizontal.size() ||
+        verticalAnchor >= vertical.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    
+    if (image.GetWidth() == 0 ||
+        image.GetHeight() == 0)
+    {
+      return;
+    }
+    
+    /**
+     * Compute normalization
+     **/
+    
+    float sumHorizontal = 0;
+    for (size_t i = 0; i < horizontal.size(); i++)
+    {
+      sumHorizontal += horizontal[i];
+    }
+    
+    float sumVertical = 0;
+    for (size_t i = 0; i < vertical.size(); i++)
+    {
+      sumVertical += vertical[i];
+    }
+
+    if (fabsf(sumHorizontal) <= std::numeric_limits<float>::epsilon() ||
+        fabsf(sumVertical) <= std::numeric_limits<float>::epsilon())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange, "Singular convolution kernel");
+    }      
+
+    const float normalization = 1.0f / (sumHorizontal * sumVertical);
+
+    switch (image.GetFormat())
+    {
+      case PixelFormat_Grayscale8:
+        SeparableConvolutionFloat<uint8_t, 1u>
+          (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization);
+        break;
+
+      case PixelFormat_RGB24:
+        SeparableConvolutionFloat<uint8_t, 3u>
+          (image, horizontal, horizontalAnchor, vertical, verticalAnchor, normalization);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void ImageProcessing::SmoothGaussian5x5(ImageAccessor& image)
+  {
+    std::vector<float> kernel(5);
+    kernel[0] = 1;
+    kernel[1] = 4;
+    kernel[2] = 6;
+    kernel[3] = 4;
+    kernel[4] = 1;
+
+    SeparableConvolution(image, kernel, 2, kernel, 2);
+  }
+
+
+  void ImageProcessing::FitSize(ImageAccessor& target,
+                                const ImageAccessor& source)
+  {
+    if (target.GetWidth() == 0 ||
+        target.GetHeight() == 0)
+    {
+      return;
+    }
+
+    if (source.GetWidth() == target.GetWidth() &&
+        source.GetHeight() == target.GetHeight())
+    {
+      Copy(target, source);
+      return;
+    }
+
+    Set(target, 0);
+
+    // Preserve the aspect ratio
+    float cw = static_cast<float>(source.GetWidth());
+    float ch = static_cast<float>(source.GetHeight());
+    float r = std::min(
+      static_cast<float>(target.GetWidth()) / cw,
+      static_cast<float>(target.GetHeight()) / ch);
+
+    unsigned int sw = std::min(static_cast<unsigned int>(boost::math::iround(cw * r)), target.GetWidth());  
+    unsigned int sh = std::min(static_cast<unsigned int>(boost::math::iround(ch * r)), target.GetHeight());
+    Image resized(target.GetFormat(), sw, sh, false);
+  
+    //ImageProcessing::SmoothGaussian5x5(source);
+    ImageProcessing::Resize(resized, source);
+
+    assert(target.GetWidth() >= resized.GetWidth() &&
+           target.GetHeight() >= resized.GetHeight());
+    unsigned int offsetX = (target.GetWidth() - resized.GetWidth()) / 2;
+    unsigned int offsetY = (target.GetHeight() - resized.GetHeight()) / 2;
+
+    ImageAccessor region;
+    target.GetRegion(region, offsetX, offsetY, resized.GetWidth(), resized.GetHeight());
+    ImageProcessing::Copy(region, resized);
+  }
+
+
+  ImageAccessor* ImageProcessing::FitSize(const ImageAccessor& source,
+                                          unsigned int width,
+                                          unsigned int height)
+  {
+    std::unique_ptr<ImageAccessor> target(new Image(source.GetFormat(), width, height, false));
+    FitSize(*target, source);
+    return target.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageProcessing.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,197 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancFramework.h"
+
+#include "ImageAccessor.h"
+#include <vector>
+
+#include <stdint.h>
+#include <algorithm>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC ImageProcessing : public boost::noncopyable
+  {
+  public:
+    class ImagePoint
+    {
+      int32_t x_;
+      int32_t y_;
+      
+    public:
+      ImagePoint(int32_t x, int32_t y)
+        : x_(x),
+          y_(y)
+      {
+      }
+
+      int32_t GetX() const {return x_;}
+
+      int32_t GetY() const {return y_;}
+
+      void Set(int32_t x, int32_t y)
+      {
+        x_ = x;
+        y_ = y;
+      }
+
+      void ClipTo(int32_t minX, int32_t maxX, int32_t minY, int32_t maxY)
+      {
+        x_ = std::max(minX, std::min(maxX, x_));
+        y_ = std::max(minY, std::min(maxY, y_));
+      }
+
+      double GetDistanceTo(const ImagePoint& other) const;
+
+      double GetDistanceToLine(double a, double b, double c) const; // where ax + by + c = 0 is the equation of the line
+    };
+
+    static void Copy(ImageAccessor& target,
+                     const ImageAccessor& source);
+
+    static void Convert(ImageAccessor& target,
+                        const ImageAccessor& source);
+
+    static void ApplyWindowing_Deprecated(ImageAccessor& target,
+                                          const ImageAccessor& source,
+                                          float windowCenter,
+                                          float windowWidth,
+                                          float rescaleSlope,
+                                          float rescaleIntercept,
+                                          bool invert);
+
+    static void Set(ImageAccessor& image,
+                    int64_t value);
+
+    static void Set(ImageAccessor& image,
+                    uint8_t red,
+                    uint8_t green,
+                    uint8_t blue,
+                    uint8_t alpha);
+
+    static void Set(ImageAccessor& image,
+                    uint8_t red,
+                    uint8_t green,
+                    uint8_t blue,
+                    ImageAccessor& alpha);
+
+    static void ShiftRight(ImageAccessor& target,
+                           unsigned int shift);
+
+    static void ShiftLeft(ImageAccessor& target,
+                          unsigned int shift);
+
+    static void GetMinMaxIntegerValue(int64_t& minValue,
+                                      int64_t& maxValue,
+                                      const ImageAccessor& image);
+
+    static void GetMinMaxFloatValue(float& minValue,
+                                    float& maxValue,
+                                    const ImageAccessor& image);
+
+    static void AddConstant(ImageAccessor& image,
+                            int64_t value);
+
+    // "useRound" is expensive
+    static void MultiplyConstant(ImageAccessor& image,
+                                 float factor,
+                                 bool useRound);
+
+    // Computes "(x + offset) * scaling" inplace. "useRound" is expensive.
+    static void ShiftScale(ImageAccessor& image,
+                           float offset,
+                           float scaling,
+                           bool useRound);
+
+    static void ShiftScale(ImageAccessor& target,
+                           const ImageAccessor& source,
+                           float offset,
+                           float scaling,
+                           bool useRound);
+
+    static void Invert(ImageAccessor& image);
+
+    static void Invert(ImageAccessor& image, int64_t maxValue);
+
+    static void DrawLineSegment(ImageAccessor& image,
+                                int x0,
+                                int y0,
+                                int x1,
+                                int y1,
+                                int64_t value);
+
+    static void DrawLineSegment(ImageAccessor& image,
+                                int x0,
+                                int y0,
+                                int x1,
+                                int y1,
+                                uint8_t red,
+                                uint8_t green,
+                                uint8_t blue,
+                                uint8_t alpha);
+
+    static void FillPolygon(ImageAccessor& image,
+                            const std::vector<ImagePoint>& points,
+                            int64_t value);
+
+    static void Resize(ImageAccessor& target,
+                       const ImageAccessor& source);
+
+    static ImageAccessor* Halve(const ImageAccessor& source,
+                                bool forceMinimalPitch);
+
+    static void FlipX(ImageAccessor& image);
+
+    static void FlipY(ImageAccessor& image);
+
+    static void SeparableConvolution(ImageAccessor& image /* inplace */,
+                                     const std::vector<float>& horizontal,
+                                     size_t horizontalAnchor,
+                                     const std::vector<float>& vertical,
+                                     size_t verticalAnchor);
+
+    static void SmoothGaussian5x5(ImageAccessor& image);
+
+    static void FitSize(ImageAccessor& target,
+                        const ImageAccessor& source);
+    
+    static ImageAccessor* FitSize(const ImageAccessor& source,
+                                  unsigned int width,
+                                  unsigned int height);
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/ImageTraits.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/JpegErrorManager.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,70 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "JpegErrorManager.h"
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    void JpegErrorManager::OutputMessage(j_common_ptr cinfo)
+    {
+      char message[JMSG_LENGTH_MAX];
+      (*cinfo->err->format_message) (cinfo, message);
+
+      JpegErrorManager* that = reinterpret_cast<JpegErrorManager*>(cinfo->err);
+      that->message = std::string(message);
+    }
+
+
+    void JpegErrorManager::ErrorExit(j_common_ptr cinfo)
+    {
+      (*cinfo->err->output_message) (cinfo);
+
+      JpegErrorManager* that = reinterpret_cast<JpegErrorManager*>(cinfo->err);
+      longjmp(that->setjmp_buffer, 1);
+    }
+      
+
+    JpegErrorManager::JpegErrorManager()
+    {
+      memset(&pub, 0, sizeof(struct jpeg_error_mgr));
+      memset(&setjmp_buffer, 0, sizeof(jmp_buf));
+
+      jpeg_std_error(&pub);
+      pub.error_exit = ErrorExit;
+      pub.output_message = OutputMessage;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/JpegErrorManager.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 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>
+#include <setjmp.h>
+#include <string>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class JpegErrorManager 
+    {
+    private:
+      struct jpeg_error_mgr pub;  /* "public" fields */
+      jmp_buf setjmp_buffer;      /* for return to caller */
+      std::string message;
+
+      static void OutputMessage(j_common_ptr cinfo);
+
+      static void ErrorExit(j_common_ptr cinfo);
+
+    public:
+      JpegErrorManager();
+
+      struct jpeg_error_mgr* GetPublic()
+      {
+        return &pub;
+      }
+
+      jmp_buf& GetJumpBuffer()
+      {
+        return setjmp_buffer;
+      }
+
+      const std::string& GetMessage() const
+      {
+        return message;
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/JpegReader.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "JpegReader.h"
+
+#include "JpegErrorManager.h"
+#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)
+  {
+    // 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;
+    if (cinfo.output_components == 1 &&
+        cinfo.out_color_space == JCS_GRAYSCALE)
+    {
+      format = PixelFormat_Grayscale8;
+    }
+    else if (cinfo.output_components == 3 &&
+             cinfo.out_color_space == JCS_RGB)
+    {
+      format = PixelFormat_RGB24;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    unsigned int pitch = cinfo.output_width * cinfo.output_components;
+
+    /* Make a one-row-high sample array that will go away when done with image */
+    JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) ((j_common_ptr) &cinfo, JPOOL_IMAGE, pitch, 1);
+
+    try
+    {
+      content.resize(pitch * cinfo.output_height);
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    accessor.AssignWritable(format, cinfo.output_width, cinfo.output_height, pitch, 
+                            content.empty() ? NULL : &content[0]);
+
+    uint8_t* target = reinterpret_cast<uint8_t*>(&content[0]);
+    while (cinfo.output_scanline < cinfo.output_height) 
+    {
+      jpeg_read_scanlines(&cinfo, buffer, 1);
+      memcpy(target, buffer[0], pitch);
+      target += pitch;
+    }
+
+    // Everything went fine, "setjmp()" didn't get called
+
+    jpeg_finish_decompress(&cinfo);
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void JpegReader::ReadFromFile(const std::string& filename)
+  {
+    FILE* fp = SystemToolbox::OpenFile(filename, FileMode_ReadBinary);
+    if (!fp)
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    struct jpeg_decompress_struct cinfo;
+    memset(&cinfo, 0, sizeof(struct jpeg_decompress_struct));
+
+    Internals::JpegErrorManager jerr;
+    cinfo.err = jerr.GetPublic();
+    
+    if (setjmp(jerr.GetJumpBuffer())) 
+    {
+      jpeg_destroy_decompress(&cinfo);
+      fclose(fp);
+
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG decoding: " + jerr.GetMessage());
+    }
+
+    // Below this line, we are under the scope of a "setjmp"
+
+    jpeg_create_decompress(&cinfo);
+    jpeg_stdio_src(&cinfo, fp);
+
+    try
+    {
+      Uncompress(cinfo, content_, *this);
+    }
+    catch (OrthancException&)
+    {
+      jpeg_destroy_decompress(&cinfo);
+      fclose(fp);
+      throw;
+    }
+
+    jpeg_destroy_decompress(&cinfo);
+    fclose(fp);
+  }
+#endif
+
+
+  void JpegReader::ReadFromMemory(const void* buffer,
+                                  size_t size)
+  {
+    struct jpeg_decompress_struct cinfo;
+    memset(&cinfo, 0, sizeof(struct jpeg_decompress_struct));
+
+    Internals::JpegErrorManager jerr;
+    cinfo.err = jerr.GetPublic();
+    
+    if (setjmp(jerr.GetJumpBuffer())) 
+    {
+      jpeg_destroy_decompress(&cinfo);
+      throw OrthancException(ErrorCode_InternalError,
+        "Error during JPEG decoding: " + jerr.GetMessage());
+    }
+
+    // Below this line, we are under the scope of a "setjmp"
+    jpeg_create_decompress(&cinfo);
+    jpeg_mem_src(&cinfo, 
+      const_cast<unsigned char*>(
+        reinterpret_cast<const unsigned char*>(buffer)),
+      static_cast<unsigned long>(size));
+
+    try
+    {
+      Uncompress(cinfo, content_, *this);
+    }
+    catch (OrthancException&)
+    {
+      jpeg_destroy_decompress(&cinfo);
+      throw;
+    }
+
+    jpeg_destroy_decompress(&cinfo);
+  }
+
+
+  void JpegReader::ReadFromMemory(const std::string& buffer)
+  {
+    if (buffer.empty())
+    {
+      ReadFromMemory(NULL, 0);
+    }
+    else
+    {
+      ReadFromMemory(buffer.c_str(), buffer.size());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/JpegReader.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_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>
+
+namespace Orthanc
+{
+  class JpegReader : public ImageAccessor
+  {
+  private:
+    std::string  content_;
+
+  public:
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
+
+    void ReadFromMemory(const void* buffer,
+                        size_t size);
+
+    void ReadFromMemory(const std::string& buffer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/JpegWriter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,213 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "JpegWriter.h"
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+#include "JpegErrorManager.h"
+
+#if ORTHANC_SANDBOXED == 0
+#  include "../SystemToolbox.h"
+#endif
+
+#include <stdlib.h>
+#include <vector>
+
+namespace Orthanc
+{
+  static void GetLines(std::vector<uint8_t*>& lines,
+                       unsigned int height,
+                       unsigned int pitch,
+                       PixelFormat format,
+                       const void* buffer)
+  {
+    if (format != PixelFormat_Grayscale8 &&
+        format != PixelFormat_RGB24)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    lines.resize(height);
+
+    uint8_t* base = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(buffer));
+    for (unsigned int y = 0; y < height; y++)
+    {
+      lines[y] = base + static_cast<intptr_t>(y) * static_cast<intptr_t>(pitch);
+    }
+  }
+
+
+  static void Compress(struct jpeg_compress_struct& cinfo,
+                       std::vector<uint8_t*>& lines,
+                       unsigned int width,
+                       unsigned int height,
+                       PixelFormat format,
+                       uint8_t quality)
+  {
+    cinfo.image_width = width;
+    cinfo.image_height = height;
+
+    switch (format)
+    {
+      case PixelFormat_Grayscale8:
+        cinfo.input_components = 1;
+        cinfo.in_color_space = JCS_GRAYSCALE;
+        break;
+
+      case PixelFormat_RGB24:
+        cinfo.input_components = 3;
+        cinfo.in_color_space = JCS_RGB;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    jpeg_set_defaults(&cinfo);
+
+    // 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);
+  }
+                       
+
+  void JpegWriter::SetQuality(uint8_t quality)
+  {
+    if (quality == 0 || quality > 100)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    quality_ = quality;
+  }
+
+
+#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 = SystemToolbox::OpenFile(filename, FileMode_WriteBinary);
+    if (fp == NULL)
+    {
+      throw OrthancException(ErrorCode_CannotWriteFile);
+    }
+
+    std::vector<uint8_t*> lines;
+    GetLines(lines, height, pitch, format, buffer);
+
+    struct jpeg_compress_struct cinfo;
+    memset(&cinfo, 0, sizeof(struct jpeg_compress_struct));
+
+    Internals::JpegErrorManager jerr;
+    cinfo.err = jerr.GetPublic();
+
+    if (setjmp(jerr.GetJumpBuffer())) 
+    {
+      /* If we get here, the JPEG code has signaled an error.
+       * We need to clean up the JPEG object, close the input file, and return.
+       */
+      jpeg_destroy_compress(&cinfo);
+      fclose(fp);
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG encoding: " + jerr.GetMessage());
+    }
+
+    // Do not allocate data on the stack below this line!
+
+    jpeg_create_compress(&cinfo);
+    jpeg_stdio_dest(&cinfo, fp);
+    Compress(cinfo, lines, width, height, format, quality_);
+
+    // Everything went fine, "setjmp()" didn't get called
+
+    fclose(fp);
+  }
+#endif
+
+
+  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);
+
+    struct jpeg_compress_struct cinfo;
+    memset(&cinfo, 0, sizeof(struct jpeg_compress_struct));
+
+    Internals::JpegErrorManager jerr;
+
+    unsigned char* data = NULL;
+    unsigned long size;
+
+    if (setjmp(jerr.GetJumpBuffer())) 
+    {
+      jpeg_destroy_compress(&cinfo);
+
+      if (data != NULL)
+      {
+        free(data);
+      }
+
+      throw OrthancException(ErrorCode_InternalError,
+                             "Error during JPEG encoding: " + jerr.GetMessage());
+    }
+
+    // Do not allocate data on the stack below this line!
+
+    jpeg_create_compress(&cinfo);
+    cinfo.err = jerr.GetPublic();
+    jpeg_mem_dest(&cinfo, &data, &size);
+
+    Compress(cinfo, lines, width, height, format, quality_);
+
+    // Everything went fine, "setjmp()" didn't get called
+
+    jpeg.assign(reinterpret_cast<const char*>(data), size);
+    free(data);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/JpegWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 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 "IImageWriter.h"
+
+namespace Orthanc
+{
+  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_;
+
+  public:
+    JpegWriter() : quality_(90)
+    {
+    }
+
+    void SetQuality(uint8_t quality);
+
+    uint8_t GetQuality() const
+    {
+      return quality_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PamReader.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <stdlib.h>  // For malloc/free
+#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;
+
+    {
+      intptr_t bufferAddr = reinterpret_cast<intptr_t>(&content_[offset]);
+      if((bufferAddr % 8) == 0)
+        LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr;
+      else
+        LOG(TRACE) << "PamReader::ParseContent() image address = " << bufferAddr << " (not a multiple of 8!)";
+    }
+    
+    // if we want to enforce alignment, we need to use a freshly allocated
+    // buffer, since we have no alignment guarantees on the original one
+    if (enforceAligned_)
+    {
+      if (alignedImageBuffer_ != NULL)
+        free(alignedImageBuffer_);
+      alignedImageBuffer_ = malloc(pitch * height);
+      memcpy(alignedImageBuffer_, &content_[offset], pitch* height);
+      content_ = "";
+      AssignWritable(format, width, height, pitch, alignedImageBuffer_);
+    }
+    else
+    {
+      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)
+        {
+#if ORTHANC_ENABLE_WASM == 1
+          /* 
+          
+          crash (2019-08-05):
+
+          Uncaught abort(alignment fault) at Error
+            at jsStackTrace
+            at stackTrace
+            at abort
+            at alignfault
+            at SAFE_HEAP_LOAD_i32_2_2 (wasm-function[251132]:39)
+            at __ZN7Orthanc9PamReader12ParseContentEv (wasm-function[11457]:8088)
+
+          Web Assembly IS LITTLE ENDIAN!
+
+          Perhaps in htobe16 ?
+          */
+          uint8_t* srcdst = reinterpret_cast<uint8_t*>(pixel);
+          uint8_t tmp = srcdst[0];
+          srcdst[0] = srcdst[1];
+          srcdst[1] = tmp;
+#else
+          // 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));
+#endif
+        }
+      }
+    }
+  }
+
+  
+#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();
+  }
+
+  void PamReader::ReadFromMemory(const void* buffer,
+                                 size_t size)
+  {
+    content_.assign(reinterpret_cast<const char*>(buffer), size);
+    ParseContent();
+  }
+
+  PamReader::~PamReader()
+  {
+    if (alignedImageBuffer_ != NULL)
+    {
+      free(alignedImageBuffer_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PamReader.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,94 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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();
+    
+    /**
+    Whether we want to use the default malloc alignment in the image buffer,
+    at the expense of an extra copy
+    */
+    bool enforceAligned_;
+
+    /**
+    This is actually a copy of wrappedContent_, but properly aligned.
+
+    It is only used if the enforceAligned parameter is set to true in the
+    constructor.
+    */
+    void* alignedImageBuffer_;
+    
+    /**
+    Points somewhere in the content_ buffer.      
+    */
+    ImageAccessor wrappedContent_;
+
+    /**
+    Raw content (file bytes or answer from the server, for instance). 
+    */
+    std::string content_;
+
+  public:
+    /**
+    See doc for field enforceAligned_
+    */
+    PamReader(bool enforceAligned = false) :
+      enforceAligned_(enforceAligned),
+      alignedImageBuffer_(NULL)
+    {
+    }
+
+    virtual ~PamReader();
+
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
+
+    void ReadFromMemory(const std::string& buffer);
+
+    void ReadFromMemory(const void* buffer,
+                        size_t size);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PamWriter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,160 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_SignedGrayscale16:
+      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/OrthancFramework/Sources/Images/PamWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/Images/PixelTraits.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,412 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <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()))
+      {
+        return std::numeric_limits<PixelType>::min();
+      }
+      else if (value > static_cast<int64_t>(std::numeric_limits<PixelType>::max()))
+      {
+        return std::numeric_limits<PixelType>::max();        
+      }
+      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)
+    {
+      value += 0.5f;
+      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 SetMinValue(PixelType& target)
+    {
+      // std::numeric_limits<float>::lowest is not supported on
+      // all compilers (for instance, Visual Studio 9.0 2008)
+      target = -std::numeric_limits<float>::max();
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void SetMaxValue(PixelType& target)
+    {
+      target = std::numeric_limits<float>::max();
+    }
+
+    ORTHANC_FORCE_INLINE
+    static void FloatToPixel(PixelType& target,
+                             float value)
+    {
+      target = value;
+    }
+
+    ORTHANC_FORCE_INLINE
+    static float PixelToFloat(const PixelType& source)
+    {
+      return source;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PngReader.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,325 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PngReader.h"
+
+#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
+    {
+      FILE* fp_;
+
+      FileRabi(const char* filename)
+      {
+        fp_ = SystemToolbox::OpenFile(filename, FileMode_ReadBinary);
+        if (!fp_)
+        {
+          throw OrthancException(ErrorCode_InexistentFile);
+        }
+      }
+
+      ~FileRabi()
+      {
+        if (fp_)
+        {
+          fclose(fp_);
+        }
+      }
+    };
+  }
+#endif
+
+
+  struct PngReader::PngRabi
+  {
+    png_structp png_;
+    png_infop info_;
+    png_infop endInfo_;
+
+    void Destruct()
+    {
+      if (png_)
+      {
+        png_destroy_read_struct(&png_, &info_, &endInfo_);
+
+        png_ = NULL;
+        info_ = NULL;
+        endInfo_ = NULL;
+      }
+    }
+
+    PngRabi()
+    {
+      png_ = NULL;
+      info_ = NULL;
+      endInfo_ = NULL;
+
+      png_ = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
+      if (!png_)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      info_ = png_create_info_struct(png_);
+      if (!info_)
+      {
+        png_destroy_read_struct(&png_, NULL, NULL);
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+
+      endInfo_ = png_create_info_struct(png_);
+      if (!info_)
+      {
+        png_destroy_read_struct(&png_, &info_, NULL);
+        throw OrthancException(ErrorCode_NotEnoughMemory);
+      }
+    }
+
+    ~PngRabi()
+    {
+      Destruct();
+    }
+
+    static void MemoryCallback(png_structp png_ptr, 
+                               png_bytep data, 
+                               png_size_t size);
+  };
+
+
+  void PngReader::CheckHeader(const void* header)
+  {
+    int is_png = !png_sig_cmp((png_bytep) header, 0, 8);
+    if (!is_png)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+  PngReader::PngReader()
+  {
+  }
+
+  void PngReader::Read(PngRabi& rabi)
+  {
+    png_set_sig_bytes(rabi.png_, 8);
+
+    png_read_info(rabi.png_, rabi.info_);
+
+    png_uint_32 width, height;
+    int bit_depth, color_type, interlace_type;
+    int compression_type, filter_method;
+    // get size and bit-depth of the PNG-image
+    png_get_IHDR(rabi.png_, rabi.info_,
+                 &width, &height,
+                 &bit_depth, &color_type, &interlace_type,
+                 &compression_type, &filter_method);
+
+    PixelFormat format;
+    unsigned int pitch;
+
+    if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 8)
+    {
+      format = PixelFormat_Grayscale8;
+      pitch = width;
+    }
+    else if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 16)
+    {
+      format = PixelFormat_Grayscale16;
+      pitch = 2 * width;
+
+      if (Toolbox::DetectEndianness() == Endianness_Little)
+      {
+        png_set_swap(rabi.png_);
+      }
+    }
+    else if (color_type == PNG_COLOR_TYPE_RGB && bit_depth == 8)
+    {
+      format = PixelFormat_RGB24;
+      pitch = 3 * width;
+    }
+    else if (color_type == PNG_COLOR_TYPE_RGBA && bit_depth == 8)
+    {
+      format = PixelFormat_RGBA32;
+      pitch = 4 * width;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    data_.resize(height * pitch);
+
+    if (height == 0 || width == 0)
+    {
+      // Empty image, we are done
+      AssignEmpty(format);
+      return;
+    }
+    
+    png_read_update_info(rabi.png_, rabi.info_);
+
+    std::vector<png_bytep> rows(height);
+    for (size_t i = 0; i < height; i++)
+    {
+      rows[i] = &data_[0] + i * pitch;
+    }
+
+    png_read_image(rabi.png_, &rows[0]);
+
+    AssignWritable(format, width, height, pitch, &data_[0]);
+  }
+
+
+#if ORTHANC_SANDBOXED == 0
+  void PngReader::ReadFromFile(const std::string& filename)
+  {
+    FileRabi f(filename.c_str());
+
+    char header[8];
+    if (fread(header, 1, 8, f.fp_) != 8)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    CheckHeader(header);
+
+    PngRabi rabi;
+
+    if (setjmp(png_jmpbuf(rabi.png_)))
+    {
+      rabi.Destruct();
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    png_init_io(rabi.png_, f.fp_);
+
+    Read(rabi);
+  }
+#endif
+
+
+  namespace
+  {
+    struct MemoryBuffer
+    {
+      const uint8_t* buffer_;
+      size_t size_;
+      size_t pos_;
+      bool ok_;
+    };
+  }
+
+
+  void PngReader::PngRabi::MemoryCallback(png_structp png_ptr, 
+                                          png_bytep outBytes, 
+                                          png_size_t byteCountToRead)
+  {
+    MemoryBuffer* from = reinterpret_cast<MemoryBuffer*>(png_get_io_ptr(png_ptr));
+
+    if (!from->ok_)
+    {
+      return;
+    }
+
+    if (from->pos_ + byteCountToRead > from->size_)
+    {
+      from->ok_ = false;
+      return;
+    }
+
+    memcpy(outBytes, from->buffer_ + from->pos_, byteCountToRead);
+
+    from->pos_ += byteCountToRead;
+  }
+
+
+  void PngReader::ReadFromMemory(const void* buffer,
+                                 size_t size)
+  {
+    if (size < 8)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    CheckHeader(buffer);
+
+    PngRabi rabi;
+
+    if (setjmp(png_jmpbuf(rabi.png_)))
+    {
+      rabi.Destruct();
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    MemoryBuffer tmp;
+    tmp.buffer_ = reinterpret_cast<const uint8_t*>(buffer) + 8;  // We skip the header
+    tmp.size_ = size - 8;
+    tmp.pos_ = 0;
+    tmp.ok_ = true;
+
+    png_set_read_fn(rabi.png_, &tmp, PngRabi::MemoryCallback);
+
+    Read(rabi);
+
+    if (!tmp.ok_)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+  void PngReader::ReadFromMemory(const std::string& buffer)
+  {
+    if (buffer.size() != 0)
+    {
+      ReadFromMemory(&buffer[0], buffer.size());
+    }
+    else
+    {
+      ReadFromMemory(NULL, 0);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PngReader.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_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"
+
+#include <vector>
+#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
+  {
+  private:
+    struct PngRabi;
+
+    std::vector<uint8_t> data_;
+
+    void CheckHeader(const void* header);
+
+    void Read(PngRabi& rabi);
+
+  public:
+    PngReader();
+
+#if ORTHANC_SANDBOXED == 0
+    void ReadFromFile(const std::string& filename);
+#endif
+
+    void ReadFromMemory(const void* buffer,
+                        size_t size);
+
+    void ReadFromMemory(const std::string& buffer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PngWriter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,274 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PngWriter.h"
+
+#include <vector>
+#include <stdint.h>
+#include <png.h>
+#include "../OrthancException.h"
+#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
+/*
+  void write_row_callback(png_ptr, png_uint_32 row, int pass)
+  {
+  }*/
+
+
+
+
+/*  bool isError_;
+
+// http://www.libpng.org/pub/png/book/chapter14.html#png.ch14.div.2
+
+static void ErrorHandler(png_structp png, png_const_charp message)
+{
+printf("** [%s]\n", message);
+
+PngWriter* that = (PngWriter*) png_get_error_ptr(png);
+that->isError_ = true;
+printf("** %d\n", (int)that);
+
+//((PngWriter*) payload)->isError_ = true;
+}
+
+static void WarningHandler(png_structp png, png_const_charp message)
+{
+  printf("++ %d\n", (int)message);
+}*/
+
+
+namespace Orthanc
+{
+  struct PngWriter::PImpl
+  {
+    png_structp png_;
+    png_infop info_;
+
+    // Filled by Prepare()
+    std::vector<uint8_t*> rows_;
+    int bitDepth_;
+    int colorType_;
+  };
+
+
+
+  PngWriter::PngWriter() : pimpl_(new PImpl)
+  {
+    pimpl_->png_ = NULL;
+    pimpl_->info_ = NULL;
+
+    pimpl_->png_ = png_create_write_struct
+      (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); //this, ErrorHandler, WarningHandler);
+    if (!pimpl_->png_)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+
+    pimpl_->info_ = png_create_info_struct(pimpl_->png_);
+    if (!pimpl_->info_)
+    {
+      png_destroy_write_struct(&pimpl_->png_, NULL);
+      throw OrthancException(ErrorCode_NotEnoughMemory);
+    }
+  }
+
+  PngWriter::~PngWriter()
+  {
+    if (pimpl_->info_)
+    {
+      png_destroy_info_struct(pimpl_->png_, &pimpl_->info_);
+    }
+
+    if (pimpl_->png_)
+    {
+      png_destroy_write_struct(&pimpl_->png_, NULL);
+    }
+  }
+
+
+
+  void PngWriter::Prepare(unsigned int width,
+                          unsigned int height,
+                          unsigned int pitch,
+                          PixelFormat format,
+                          const void* buffer)
+  {
+    pimpl_->rows_.resize(height);
+    for (unsigned int y = 0; y < height; y++)
+    {
+      pimpl_->rows_[y] = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(buffer)) + y * pitch;
+    }
+
+    switch (format)
+    {
+    case PixelFormat_RGB24:
+      pimpl_->bitDepth_ = 8;
+      pimpl_->colorType_ = PNG_COLOR_TYPE_RGB;
+      break;
+
+    case PixelFormat_RGBA32:
+      pimpl_->bitDepth_ = 8;
+      pimpl_->colorType_ = PNG_COLOR_TYPE_RGBA;
+      break;
+
+    case PixelFormat_Grayscale8:
+      pimpl_->bitDepth_ = 8;
+      pimpl_->colorType_ = PNG_COLOR_TYPE_GRAY;
+      break;
+
+    case PixelFormat_Grayscale16:
+    case PixelFormat_SignedGrayscale16:
+      pimpl_->bitDepth_ = 16;
+      pimpl_->colorType_ = PNG_COLOR_TYPE_GRAY;
+      break;
+
+    default:
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void PngWriter::Compress(unsigned int width,
+                           unsigned int height,
+                           unsigned int pitch,
+                           PixelFormat format)
+  {
+    png_set_IHDR(pimpl_->png_, pimpl_->info_, width, height,
+                 pimpl_->bitDepth_, pimpl_->colorType_, PNG_INTERLACE_NONE,
+                 PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);
+
+    png_write_info(pimpl_->png_, pimpl_->info_);
+
+    if (height > 0)
+    {
+      switch (format)
+      {
+      case PixelFormat_Grayscale16:
+      case PixelFormat_SignedGrayscale16:
+      {
+        int transforms = 0;
+        if (Toolbox::DetectEndianness() == Endianness_Little)
+        {
+          transforms = PNG_TRANSFORM_SWAP_ENDIAN;
+        }
+
+        png_set_rows(pimpl_->png_, pimpl_->info_, &pimpl_->rows_[0]);
+        png_write_png(pimpl_->png_, pimpl_->info_, transforms, NULL);
+
+        break;
+      }
+
+      default:
+        png_write_image(pimpl_->png_, &pimpl_->rows_[0]);
+      }
+    }
+
+    png_write_end(pimpl_->png_, NULL);
+  }
+
+
+#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 = SystemToolbox::OpenFile(filename, FileMode_WriteBinary);
+    if (!fp)
+    {
+      throw OrthancException(ErrorCode_CannotWriteFile);
+    }    
+
+    png_init_io(pimpl_->png_, fp);
+
+    if (setjmp(png_jmpbuf(pimpl_->png_)))
+    {
+      // Error during writing PNG
+      throw OrthancException(ErrorCode_CannotWriteFile);      
+    }
+
+    Compress(width, height, pitch, format);
+
+    fclose(fp);
+  }
+#endif
+
+
+  static void MemoryCallback(png_structp png_ptr, 
+                             png_bytep data, 
+                             png_size_t size)
+  {
+    ChunkedBuffer* buffer = reinterpret_cast<ChunkedBuffer*>(png_get_io_ptr(png_ptr));
+    buffer->AddChunk(data, size);
+  }
+
+
+
+  void PngWriter::WriteToMemoryInternal(std::string& png,
+                                        unsigned int width,
+                                        unsigned int height,
+                                        unsigned int pitch,
+                                        PixelFormat format,
+                                        const void* buffer)
+  {
+    ChunkedBuffer chunks;
+
+    Prepare(width, height, pitch, format, buffer);
+
+    if (setjmp(png_jmpbuf(pimpl_->png_)))
+    {
+      // Error during writing PNG
+      throw OrthancException(ErrorCode_InternalError);      
+    }
+
+    png_set_write_fn(pimpl_->png_, &chunks, MemoryCallback, NULL);
+
+    Compress(width, height, pitch, format);
+
+    chunks.Flatten(png);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Images/PngWriter.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_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>
+
+namespace Orthanc
+{
+  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_;
+
+    void Compress(unsigned int width,
+                  unsigned int height,
+                  unsigned int pitch,
+                  PixelFormat format);
+
+    void Prepare(unsigned int width,
+                 unsigned int height,
+                 unsigned int pitch,
+                 PixelFormat format,
+                 const void* buffer);
+
+  public:
+    PngWriter();
+
+    ~PngWriter();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot unserialize job of type: " + type);
+    }
+  }
+
+
+  IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "Log")
+    {
+      return new LogJobOperation;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot unserialize operation of type: " + type);
+    }
+  }
+
+
+  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
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Cannot unserialize value of type: " + type);
+    }
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/GenericJobUnserializer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/IJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(const std::string& jobId) = 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;
+
+    // This function can only be called if the job has reached its
+    // "success" state
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/IJobUnserializer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,164 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#ifdef __EMSCRIPTEN__
+/* 
+Avoid this error:
+
+.../boost/math/special_functions/round.hpp:118:12: warning: implicit conversion from 'std::__2::numeric_limits<long long>::type' (aka 'long long') to 'float' changes value from 9223372036854775807 to 9223372036854775808 [-Wimplicit-int-float-conversion]
+.../boost/math/special_functions/round.hpp:125:11: note: in instantiation of function template specialization 'boost::math::llround<float, boost::math::policies::policy<boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy> >' requested here
+.../orthanc/Core/JobsEngine/JobInfo.cpp:69:44: note: in instantiation of function template specialization 'boost::math::llround<float>' requested here
+
+.../boost/math/special_functions/round.hpp:86:12: warning: implicit conversion from 'std::__2::numeric_limits<int>::type' (aka 'int') to 'float' changes value from 2147483647 to 2147483648 [-Wimplicit-int-float-conversion]
+.../boost/math/special_functions/round.hpp:93:11: note: in instantiation of function template specialization 'boost::math::iround<float, boost::math::policies::policy<boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy, boost::math::policies::default_policy> >' requested here
+.../orthanc/Core/JobsEngine/JobInfo.cpp:133:39: note: in instantiation of function template specialization 'boost::math::iround<float>' requested here
+*/
+#pragma GCC diagnostic ignored "-Wimplicit-int-float-conversion"
+#endif 
+
+#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/OrthancFramework/Sources/JobsEngine/JobInfo.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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,
+                       const std::string& details,
+                       IJob& job) :
+    errorCode_(code),
+    progress_(job.GetProgress()),
+    publicContent_(Json::objectValue),
+    details_(details)
+  {
+    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/OrthancFramework/Sources/JobsEngine/JobStatus.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_;
+    std::string    details_;
+
+  public:
+    JobStatus();
+
+    JobStatus(ErrorCode code,
+              const std::string& details,
+              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_;
+    }
+
+    const std::string& GetDetails() const
+    {
+      return details_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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,
+                                       const char* details)
+  {
+    JobStepResult result(JobStepCode_Failure);
+    result.error_ = error;
+
+    if (details != NULL)
+    {
+      result.failureDetails_ = details;
+    }
+    
+    return result;
+  }
+
+
+  JobStepResult JobStepResult::Failure(const OrthancException& exception)
+  {
+    return Failure(exception.GetErrorCode(),
+                   exception.HasDetails() ? exception.GetDetails() : NULL);
+  }
+  
+
+  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);
+    }
+  }
+
+
+  const std::string& JobStepResult::GetFailureDetails() const
+  {
+    if (code_ == JobStepCode_Failure)
+    {
+      return failureDetails_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/JobStepResult.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 OrthancException;
+  
+  class JobStepResult
+  {
+  private:
+    JobStepCode   code_;
+    unsigned int  timeout_;
+    ErrorCode     error_;
+    std::string   failureDetails_;
+    
+    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,
+                                 const char* details);
+
+    static JobStepResult Failure(const OrthancException& exception);
+
+    JobStepCode GetCode() const
+    {
+      return code_;
+    }
+
+    unsigned int GetRetryTimeout() const;
+
+    ErrorCode GetFailureCode() const;
+
+    const std::string& GetFailureDetails() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/JobsEngine.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,326 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(running.GetId());
+    }
+    catch (OrthancException& e)
+    {
+      result = JobStepResult::Failure(e);
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      result = JobStepResult::Failure(ErrorCode_BadFileFormat, NULL);
+    }
+    catch (...)
+    {
+      result = JobStepResult::Failure(ErrorCode_InternalError, NULL);
+    }
+
+    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(), result.GetFailureDetails());
+        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(size_t maxCompletedJobs) :
+    state_(State_Setup),
+    registry_(new JobsRegistry(maxCompletedJobs)),
+    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);
+    }
+
+    assert(registry_.get() != NULL);
+    const size_t maxCompletedJobs = registry_->GetMaxCompletedJobs();
+    registry_.reset(new JobsRegistry(unserializer, serialized, maxCompletedJobs));
+  }
+
+
+  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/OrthancFramework/Sources/JobsEngine/JobsEngine.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Compatibility.h"
+
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class JobsEngine : public boost::noncopyable
+  {
+  private:
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Stopping,
+      State_Done
+    };
+
+    boost::mutex                 stateMutex_;
+    State                        state_;
+    std::unique_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(size_t maxCompletedJobs);
+
+    ~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/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1474 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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* 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::unique_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
+      {
+        VLOG(1) << "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()
+  {
+    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();
+    }
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::SetCompletedJob(JobHandler& job,
+                                     bool success)
+  {
+    job.SetState(success ? JobState_Success : JobState_Failure);
+
+    completedJobs_.push_back(&job);
+    someJobComplete_.notify_all();
+  }
+
+
+  void JobsRegistry::MarkRunningAsCompleted(JobHandler& job,
+                                            CompletedReason reason)
+  {
+    const char* tmp;
+
+    switch (reason)
+    {
+      case CompletedReason_Success:
+        tmp = "success";
+        break;
+
+      case CompletedReason_Failure:
+        tmp = "failure";
+        break;
+
+      case CompletedReason_Canceled:
+        tmp = "cancel";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    LOG(INFO) << "Job has completed with " << tmp << ": " << job.GetId();
+
+    CheckInvariants();
+
+    assert(job.GetState() == JobState_Running);
+    SetCompletedJob(job, reason == CompletedReason_Success);
+
+    if (reason == CompletedReason_Canceled)
+    {
+      job.SetLastErrorCode(ErrorCode_CanceledJob);
+    }
+
+    if (observer_ != NULL)
+    {
+      if (reason == CompletedReason_Success)
+      {
+        observer_->SignalJobSuccess(job.GetId());
+      }
+      else
+      {
+        observer_->SignalJobFailure(job.GetId());
+      }
+    }
+
+    // WARNING: The following call might make "job" invalid if the job
+    // history size is empty
+    ForgetOldCompletedJobs();
+  }
+
+
+  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();
+  }
+
+
+  size_t JobsRegistry::GetMaxCompletedJobs()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+    return maxCompletedJobs_;
+  }
+
+
+  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;
+    }
+  }
+
+
+  bool JobsRegistry::GetJobOutput(std::string& output,
+                                  MimeType& mime,
+                                  const std::string& job,
+                                  const std::string& key)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::const_iterator found = jobsIndex_.find(job);
+
+    if (found == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      const JobHandler& handler = *found->second;
+
+      if (handler.GetState() == JobState_Success)
+      {
+        return handler.GetJob().GetOutput(output, mime, key);
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+
+
+  void JobsRegistry::SubmitInternal(std::string& id,
+                                    JobHandler* handler)
+  {
+    if (handler == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    std::unique_ptr<JobHandler>  protection(handler);
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      CheckInvariants();
+
+      id = handler->GetId();
+      int priority = handler->GetPriority();
+
+      jobsIndex_.insert(std::make_pair(id, protection.release()));
+
+      switch (handler->GetState())
+      {
+        case JobState_Pending:
+        case JobState_Retry:
+        case JobState_Running:
+          handler->SetState(JobState_Pending);
+          pendingJobs_.push(handler);
+          pendingJobAvailable_.notify_one();
+          break;
+
+        case JobState_Success:
+          SetCompletedJob(*handler, true);
+          break;
+
+        case JobState_Failure:
+          SetCompletedJob(*handler, false);
+          break;
+
+        case JobState_Paused:
+          break;
+
+        default:
+        {
+          std::string details = ("A job should not be loaded from state: " +
+                                 std::string(EnumerationToString(handler->GetState())));
+          throw OrthancException(ErrorCode_InternalError, details);
+        }
+      }
+
+      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+
+      if (observer_ != NULL)
+      {
+        observer_->SignalJobSubmitted(id);
+      }
+
+      // WARNING: The following call might make "handler" invalid if
+      // the job history size is empty
+      ForgetOldCompletedJobs();
+    }
+  }
+
+
+  void JobsRegistry::Submit(std::string& id,
+                            IJob* job,        // Takes ownership
+                            int priority)
+  {
+    SubmitInternal(id, new JobHandler(job, priority));
+  }
+
+
+  void JobsRegistry::Submit(IJob* job,        // Takes ownership
+                            int priority)
+  {
+    std::string id;
+    SubmitInternal(id, new JobHandler(job, priority));
+  }
+
+
+  void 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 (typically happens if
+          // "JobsHistorySize" is 0)
+          throw OrthancException(ErrorCode_InexistentItem,
+                                 "Cannot retrieve the status of the job, "
+                                 "make sure that \"JobsHistorySize\" is not 0");
+        }
+        else if (state == JobState_Failure)
+        {
+          // Failure
+          JobsIndex::const_iterator it = jobsIndex_.find(id);
+          if (it != jobsIndex_.end())  // Should always be true, already tested in GetStateInternal()
+          {
+            ErrorCode code = it->second->GetLastStatus().GetErrorCode();
+            const std::string& details = it->second->GetLastStatus().GetDetails();
+
+            if (details.empty())
+            {
+              throw OrthancException(code);
+            }
+            else
+            {
+              throw OrthancException(code, details);
+            }
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+        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();
+          }
+
+          return;
+        }
+        else
+        {
+          // This job has not finished yet, wait for new completion
+          someJobComplete_.wait(lock);
+        }
+      }
+    }
+  }
+
+
+  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);
+      }
+
+      // WARNING: The following call might make "handler" invalid if
+      // the job history size is empty
+      ForgetOldCompletedJobs();
+
+      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_, canceled_ ? CompletedReason_Canceled : CompletedReason_Failure);
+          break;
+
+        case JobState_Success:
+          registry_.MarkRunningAsCompleted(*handler_, CompletedReason_Success);
+          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,
+                                              const std::string& details)
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      JobStatus status(code, details, *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[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,
+                             size_t maxCompletedJobs) :
+    maxCompletedJobs_(maxCompletedJobs),
+    observer_(NULL)
+  {
+    if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY ||
+        !s.isMember(JOBS) ||
+        s[JOBS].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members members = s[JOBS].getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = members.begin();
+         it != members.end(); ++it)
+    {
+      std::unique_ptr<JobHandler> job;
+
+      try
+      {
+        job.reset(new JobHandler(unserializer, s[JOBS][*it], *it));
+      }
+      catch (OrthancException& e)
+      {
+        LOG(WARNING) << "Cannot unserialize one job from previous execution, "
+                     << "skipping it: " << e.What();
+        continue;
+      }
+
+      const boost::posix_time::ptime lastChangeTime = job->GetLastStateChangeTime();
+
+      std::string id;
+      SubmitInternal(id, job.release());
+
+      // Check whether the job has not been removed (which could be
+      // the case if the "maxCompletedJobs_" value gets smaller)
+      JobsIndex::iterator found = jobsIndex_.find(id);
+      if (found != jobsIndex_.end())
+      {
+        // The job still lies in the history: Update the time of its
+        // last change to the time that was serialized
+        assert(found->second != NULL);
+        found->second->SetLastStateChangeTime(lastChangeTime);
+      }
+    }
+  }
+
+
+  void JobsRegistry::GetStatistics(unsigned int& pending,
+                                   unsigned int& running,
+                                   unsigned int& success,
+                                   unsigned int& failed)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    pending = 0;
+    running = 0;
+    success = 0;
+    failed = 0;
+
+    for (JobsIndex::const_iterator it = jobsIndex_.begin();
+         it != jobsIndex_.end(); ++it)
+    {
+      JobHandler& job = *it->second;
+
+      switch (job.GetState())
+      {
+        case JobState_Retry:
+        case JobState_Pending:
+          pending ++;
+          break;
+
+        case JobState_Paused:
+        case JobState_Running:
+          running ++;
+          break;
+
+        case JobState_Success:
+          success ++;
+          break;
+
+        case JobState_Failure:
+          failed ++;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,255 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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:
+    enum CompletedReason
+    {
+      CompletedReason_Success,
+      CompletedReason_Failure,
+      CompletedReason_Canceled
+    };
+
+    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,
+                                CompletedReason reason);
+
+    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);
+
+  public:
+    JobsRegistry(size_t maxCompletedJobs) :
+      maxCompletedJobs_(maxCompletedJobs),
+      observer_(NULL)
+    {
+    }
+
+    JobsRegistry(IJobUnserializer& unserializer,
+                 const Json::Value& s,
+                 size_t maxCompletedJobs);
+
+    ~JobsRegistry();
+
+    void SetMaxCompletedJobs(size_t i);
+
+    size_t GetMaxCompletedJobs();
+
+    void ListJobs(std::set<std::string>& target);
+
+    bool GetJobInfo(JobInfo& target,
+                    const std::string& id);
+
+    bool GetJobOutput(std::string& output,
+                      MimeType& mime,
+                      const std::string& job,
+                      const std::string& key);
+
+    void Serialize(Json::Value& target);
+
+    void Submit(std::string& id,
+                IJob* job,        // Takes ownership
+                int priority);
+
+    void Submit(IJob* job,        // Takes ownership
+                int priority);
+
+    void 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();
+
+    void GetStatistics(unsigned int& pending,
+                       unsigned int& running,
+                       unsigned int& success,
+                       unsigned int& errors);
+
+    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,
+                        const std::string& details);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/Operations/IJobOperation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+namespace Orthanc
+{
+  class IJobOperation : public boost::noncopyable
+  {
+  public:
+    virtual ~IJobOperation()
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input) = 0;
+
+    virtual void Serialize(Json::Value& result) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValue.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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::unique_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/OrthancFramework/Sources/JobsEngine/Operations/JobOperationValues.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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)
+  {
+    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/OrthancFramework/Sources/JobsEngine/Operations/LogJobOperation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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);
+
+    virtual void Serialize(Json::Value& result) const
+    {
+      result = Json::objectValue;
+      result["Type"] = "Log";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/Operations/NullOperationValue.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,475 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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* 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::unique_ptr<IJobOperation>       operation_;
+    std::unique_ptr<JobOperationValues>  originalInputs_;
+    std::unique_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()
+    {
+      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);
+
+      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);
+  }
+
+  
+  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(const std::string& jobId)
+  {
+    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);
+        }
+
+        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();
+    }
+
+    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();
+    }
+  }
+
+
+  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[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));
+    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/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <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_;
+
+    void NotifyDone() const;
+
+  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);
+      
+      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() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual float GetProgress() ORTHANC_OVERRIDE;
+
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    {
+      target = "SequenceOfOperations";
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    void AwakeTrailingSleep()
+    {
+      operationAdded_.notify_one();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/Operations/StringOperationValue.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(const std::string& jobId)
+  {
+    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(jobId))
+      {
+        // Error
+        if (!permissive_)
+        {
+          return JobStepResult::Failure(ErrorCode_InternalError, NULL);
+        }
+      }
+    }
+    catch (OrthancException& e)
+    {
+      if (permissive_)
+      {
+        LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What();
+      }
+      else
+      {
+        return JobStepResult::Failure(e);
+      }
+    }
+
+    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::unique_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/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,142 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(const std::string& jobId) = 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() ORTHANC_OVERRIDE;
+    
+    virtual void Start() ORTHANC_OVERRIDE
+    {
+      started_ = true;
+    }
+    
+    virtual float GetProgress() ORTHANC_OVERRIDE;
+
+    bool IsStarted() const
+    {
+      return started_;
+    }
+
+    const ICommand& GetCommand(size_t index) const;
+      
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
+    
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+    
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,252 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      if (!that_.HandleInstance(instance_))
+      {
+        that_.failedInstances_.insert(instance_);
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = instance_;
+    }
+  };
+
+
+  class SetOfInstancesJob::TrailingStepCommand : public SetOfInstancesJob::ICommand
+  {
+  private:
+    SetOfInstancesJob& that_;
+
+  public:
+    TrailingStepCommand(SetOfInstancesJob& that) :
+      that_(that)
+    {
+    }       
+      
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      return that_.HandleTrailingStep();
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      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();
+  }
+
+
+  static const char* KEY_TRAILING_STEP = "TrailingStep";
+  static const char* KEY_FAILED_INSTANCES = "FailedInstances";
+  static const char* KEY_PARENT_RESOURCES = "ParentResources";
+
+  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
+  {
+    SetOfCommandsJob::GetPublicContent(target);
+    target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
+    target["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
+
+    if (!parentResources_.empty())
+    {
+      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
+    }
+  }
+
+
+  bool SetOfInstancesJob::Serialize(Json::Value& target) 
+  {
+    if (SetOfCommandsJob::Serialize(target))
+    {
+      target[KEY_TRAILING_STEP] = hasTrailingStep_;
+      SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
+      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
+      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_PARENT_RESOURCES))
+    {
+      // Backward compatibility with Orthanc <= 1.5.6
+      SerializationToolbox::ReadSetOfStrings(parentResources_, source, KEY_PARENT_RESOURCES);
+    }
+    
+    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/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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_;
+    std::set<std::string>  parentResources_;
+
+  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
+
+    // Only used for reporting in the public content
+    // https://groups.google.com/d/msg/orthanc-users/9GCV88GLEzw/6wAgP_PRAgAJ
+    void AddParentResource(const std::string& resource)
+    {
+      parentResources_.insert(resource);
+    }
+    
+    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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Logging.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,817 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Logging.h"
+
+#include "OrthancException.h"
+
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    const char* EnumerationToString(LogLevel level)
+    {
+      switch (level)
+      {
+        case LogLevel_ERROR:
+          return "ERROR";
+
+        case LogLevel_WARNING:
+          return "WARNING";
+
+        case LogLevel_INFO:
+          return "INFO";
+
+        case LogLevel_TRACE:
+          return "TRACE";
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    LogLevel StringToLogLevel(const char *level)
+    {
+      if (strcmp(level, "ERROR") == 0)
+      {
+        return LogLevel_ERROR;
+      }
+      else if (strcmp(level, "WARNING") == 0)
+      {
+        return LogLevel_WARNING;
+      }
+      else if (strcmp(level, "INFO") == 0)
+      {
+        return LogLevel_INFO;
+      }
+      else if (strcmp(level, "TRACE") == 0)
+      {
+        return LogLevel_TRACE;
+      }
+      else 
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+}
+
+
+#if ORTHANC_ENABLE_LOGGING != 1
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    void InitializePluginContext(void* pluginContext)
+    {
+    }
+
+    void Initialize()
+    {
+    }
+
+    void Finalize()
+    {
+    }
+
+    void Reset()
+    {
+    }
+
+    void Flush()
+    {
+    }
+
+    void EnableInfoLevel(bool enabled)
+    {
+    }
+
+    void EnableTraceLevel(bool enabled)
+    {
+    }
+    
+    bool IsTraceLevelEnabled()
+    {
+      return false;
+    }
+
+    bool IsInfoLevelEnabled()
+    {
+      return false;
+    }
+    
+    void SetTargetFile(const std::string& path)
+    {
+    }
+
+    void SetTargetFolder(const std::string& path)
+    {
+    }
+  }
+}
+
+
+#elif ORTHANC_ENABLE_LOGGING_STDIO == 1
+
+/*********************************************************
+ * Logger compatible with <stdio.h> OR logger that sends its
+ * output to the emscripten html5 api (depending on the 
+ * definition of __EMSCRIPTEN__)
+ *********************************************************/
+
+#include <stdio.h>
+
+#ifdef __EMSCRIPTEN__
+#  include <emscripten/html5.h>
+#endif
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    static bool infoEnabled_ = false;
+    static bool traceEnabled_ = false;
+    
+#ifdef __EMSCRIPTEN__
+    static void ErrorLogFunc(const char* msg)
+    {
+      emscripten_console_error(msg);
+    }
+
+    static void WarningLogFunc(const char* msg)
+    {
+      emscripten_console_warn(msg);
+    }
+
+    static void InfoLogFunc(const char* msg)
+    {
+      emscripten_console_log(msg);
+    }
+
+    static void TraceLogFunc(const char* msg)
+    {
+      emscripten_console_log(msg);
+    }
+#else  /* __EMSCRIPTEN__ not #defined */
+    static void ErrorLogFunc(const char* msg)
+    {
+      fprintf(stderr, "E: %s\n", msg);
+    }
+
+    static void WarningLogFunc(const char*)
+    {
+      fprintf(stdout, "W: %s\n", msg);
+    }
+
+    static void InfoLogFunc(const char*)
+    {
+      fprintf(stdout, "I: %s\n", msg);
+    }
+
+    static void TraceLogFunc(const char*)
+    {
+      fprintf(stdout, "T: %s\n", msg);
+    }
+#endif  /* __EMSCRIPTEN__ */
+
+
+    InternalLogger::~InternalLogger()
+    {
+      std::string message = messageStream_.str();
+
+      switch (level_)
+      {
+        case LogLevel_ERROR:
+          ErrorLogFunc(message.c_str());
+          break;
+
+        case LogLevel_WARNING:
+          WarningLogFunc(message.c_str());
+          break;
+
+        case LogLevel_INFO:
+          if (infoEnabled_)
+          {
+            InfoLogFunc(message.c_str());
+            // TODO: stone_console_info(message_.c_str());
+          }
+          break;
+
+        case LogLevel_TRACE:
+          if (traceEnabled_)
+          {
+            TraceLogFunc(message.c_str());
+          }
+          break;
+
+        default:
+        {
+          std::stringstream ss;
+          ss << "Unknown log level (" << level_ << ") for message: " << message;
+          std::string s = ss.str();
+          ErrorLogFunc(s.c_str());
+        }
+      }
+    }
+
+    void InitializePluginContext(void* pluginContext)
+    {
+    }
+
+    void Initialize()
+    {
+    }
+
+    void Finalize()
+    {
+    }
+
+    void Reset()
+    {
+    }
+
+    void Flush()
+    {
+    }
+
+    void EnableInfoLevel(bool enabled)
+    {
+      infoEnabled_ = enabled;
+
+      if (!enabled)
+      {
+        // Also disable the "TRACE" level when info-level debugging is disabled
+        traceEnabled_ = false;
+      }
+    }
+
+    bool IsInfoLevelEnabled()
+    {
+      return infoEnabled_;
+    }
+
+    void EnableTraceLevel(bool enabled)
+    {
+      traceEnabled_ = enabled;
+    }
+
+    bool IsTraceLevelEnabled()
+    {
+      return traceEnabled_;
+    }
+
+    void SetTargetFile(const std::string& path)
+    {
+    }
+
+    void SetTargetFolder(const std::string& path)
+    {
+    }
+  }
+}
+
+
+#else
+
+/*********************************************************
+ * Logger compatible with the Orthanc plugin SDK, or that
+ * mimics behavior from Google Log.
+ *********************************************************/
+
+#include <cassert>
+
+namespace
+{
+  /**
+   * This is minimal implementation of the context for an Orthanc
+   * plugin, limited to the logging facilities, and that is binary
+   * compatible with the definitions of "OrthancCPlugin.h"
+   **/
+  typedef enum 
+  {
+    _OrthancPluginService_LogInfo = 1,
+    _OrthancPluginService_LogWarning = 2,
+    _OrthancPluginService_LogError = 3,
+    _OrthancPluginService_INTERNAL = 0x7fffffff
+  } _OrthancPluginService;
+
+  typedef struct _OrthancPluginContext_t
+  {
+    void*          pluginsManager;
+    const char*    orthancVersion;
+    void         (*Free) (void* buffer);
+    int32_t      (*InvokeService) (struct _OrthancPluginContext_t* context,
+                                   _OrthancPluginService service,
+                                   const void* params);
+  } OrthancPluginContext;
+}
+  
+
+#include "Enumerations.h"
+#include "SystemToolbox.h"
+
+#include <fstream>
+#include <boost/filesystem.hpp>
+#include <boost/thread.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+
+namespace
+{
+  struct LoggingStreamsContext
+  {
+    std::string  targetFile_;
+    std::string  targetFolder_;
+
+    std::ostream* error_;
+    std::ostream* warning_;
+    std::ostream* info_;
+
+    std::unique_ptr<std::ofstream> file_;
+
+    LoggingStreamsContext() : 
+      error_(&std::cerr),
+      warning_(&std::cerr),
+      info_(&std::cerr)
+    {
+    }
+  };
+}
+
+
+
+static std::unique_ptr<LoggingStreamsContext> loggingStreamsContext_;
+static boost::mutex                           loggingStreamsMutex_;
+static Orthanc::Logging::NullStream           nullStream_;
+static OrthancPluginContext*                  pluginContext_ = NULL;
+static bool                                   infoEnabled_ = false;
+static bool                                   traceEnabled_ = false;
+
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    static void GetLogPath(boost::filesystem::path& log,
+                           boost::filesystem::path& link,
+                           const std::string& suffix,
+                           const std::string& directory)
+    {
+      /**
+         From Google Log documentation:
+
+         Unless otherwise specified, logs will be written to the filename
+         "<program name>.<hostname>.<user name>.log<suffix>.",
+         followed by the date, time, and pid (you can't prevent the date,
+         time, and pid from being in the filename).
+
+         In this implementation : "hostname" and "username" are not used
+      **/
+
+      boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
+      boost::filesystem::path root(directory);
+      boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
+      
+      if (!boost::filesystem::exists(root) ||
+          !boost::filesystem::is_directory(root))
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile);
+      }
+
+      char date[64];
+      sprintf(date, "%04d%02d%02d-%02d%02d%02d.%d",
+              static_cast<int>(now.date().year()),
+              now.date().month().as_number(),
+              now.date().day().as_number(),
+              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();
+
+      log = (root / (programName + ".log" + suffix + "." + std::string(date)));
+      link = (root / (programName + ".log" + suffix));
+    }
+
+
+    static void PrepareLogFolder(std::unique_ptr<std::ofstream>& file,
+                                 const std::string& suffix,
+                                 const std::string& directory)
+    {
+      boost::filesystem::path log, link;
+      GetLogPath(log, link, suffix, directory);
+
+#if !defined(_WIN32) && (defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__)))
+      boost::filesystem::remove(link);
+      boost::filesystem::create_symlink(log.filename(), link);
+#endif
+
+      file.reset(new std::ofstream(log.string().c_str()));
+    }
+
+
+    // "loggingStreamsMutex_" must be locked
+    static void CheckFile(std::unique_ptr<std::ofstream>& f)
+    {
+      if (loggingStreamsContext_->file_.get() == NULL ||
+          !loggingStreamsContext_->file_->is_open())
+      {
+        throw OrthancException(ErrorCode_CannotWriteFile);
+      }
+    }
+    
+
+    static void GetLinePrefix(std::string& prefix,
+                              LogLevel level,
+                              const char* file,
+                              int line)
+    {
+      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:
+
+         "Log lines have this form:
+
+         Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
+
+         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"
+
+         In this implementation, "threadid" is not printed.
+      **/
+
+      char c;
+      switch (level)
+      {
+        case LogLevel_ERROR:
+          c = 'E';
+          break;
+
+        case LogLevel_WARNING:
+          c = 'W';
+          break;
+
+        case LogLevel_INFO:
+          c = 'I';
+          break;
+
+        case LogLevel_TRACE:
+          c = 'T';
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);            
+      }
+
+      char date[64];
+      sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ",
+              c,
+              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()));
+
+      prefix = (std::string(date) + path.filename().string() + ":" +
+                boost::lexical_cast<std::string>(line) + "] ");
+    }
+    
+
+    void InitializePluginContext(void* pluginContext)
+    {
+      assert(sizeof(_OrthancPluginService) == sizeof(int32_t));
+
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+      loggingStreamsContext_.reset(NULL);
+      pluginContext_ = reinterpret_cast<OrthancPluginContext*>(pluginContext);
+    }
+
+
+    void Initialize()
+    {
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+
+      if (loggingStreamsContext_.get() == NULL)
+      {
+        loggingStreamsContext_.reset(new LoggingStreamsContext);
+      }
+    }
+
+    void Finalize()
+    {
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+      loggingStreamsContext_.reset(NULL);
+    }
+
+    void Reset()
+    {
+      // Recover the old logging context
+      std::unique_ptr<LoggingStreamsContext> old;
+
+      {
+        boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+        if (loggingStreamsContext_.get() == NULL)
+        {
+          return;
+        }
+        else
+        {
+#if __cplusplus < 201103L
+          old.reset(loggingStreamsContext_.release());
+#else
+          old = std::move(loggingStreamsContext_);
+#endif
+
+          // Create a new logging context, 
+          loggingStreamsContext_.reset(new LoggingStreamsContext);
+        }
+      }
+      
+      if (!old->targetFolder_.empty())
+      {
+        SetTargetFolder(old->targetFolder_);
+      }
+      else if (!old->targetFile_.empty())
+      {
+        SetTargetFile(old->targetFile_);
+      }
+    }
+
+
+    void EnableInfoLevel(bool enabled)
+    {
+      infoEnabled_ = enabled;
+      
+      if (!enabled)
+      {
+        // Also disable the "TRACE" level when info-level debugging is disabled
+        traceEnabled_ = false;
+      }
+    }
+
+    bool IsInfoLevelEnabled()
+    {
+      return infoEnabled_;
+    }
+
+    void EnableTraceLevel(bool enabled)
+    {
+      traceEnabled_ = enabled;
+      
+      if (enabled)
+      {
+        // Also enable the "INFO" level when trace-level debugging is enabled
+        infoEnabled_ = true;
+      }
+    }
+
+    bool IsTraceLevelEnabled()
+    {
+      return traceEnabled_;
+    }
+
+
+    void SetTargetFolder(const std::string& path)
+    {
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+      if (loggingStreamsContext_.get() != NULL)
+      {
+        PrepareLogFolder(loggingStreamsContext_->file_, "" /* no suffix */, path);
+        CheckFile(loggingStreamsContext_->file_);
+
+        loggingStreamsContext_->targetFile_.clear();
+        loggingStreamsContext_->targetFolder_ = path;
+        loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get();
+        loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get();
+        loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get();
+      }
+    }
+
+
+    void SetTargetFile(const std::string& path)
+    {
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+
+      if (loggingStreamsContext_.get() != NULL)
+      {
+        loggingStreamsContext_->file_.reset(new std::ofstream(path.c_str(), std::fstream::app));
+        CheckFile(loggingStreamsContext_->file_);
+
+        loggingStreamsContext_->targetFile_ = path;
+        loggingStreamsContext_->targetFolder_.clear();
+        loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get();
+        loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get();
+        loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get();
+      }
+    }
+
+
+    InternalLogger::InternalLogger(LogLevel level,
+                                   const char* file,
+                                   int line) : 
+      lock_(loggingStreamsMutex_, boost::defer_lock_t()),
+      level_(level),
+      stream_(&nullStream_)  // By default, logging to "/dev/null" is simulated
+    {
+      if (pluginContext_ != NULL)
+      {
+        // We are logging using the Orthanc plugin SDK
+
+        if (level == LogLevel_TRACE)
+        {
+          // No trace level in plugins, directly exit as the stream is
+          // set to "/dev/null"
+          return;
+        }
+        else
+        {
+          pluginStream_.reset(new std::stringstream);
+          stream_ = pluginStream_.get();
+        }
+      }
+      else
+      {
+        // We are logging in a standalone application, not inside an Orthanc plugin
+
+        if ((level == LogLevel_INFO  && !infoEnabled_) ||
+            (level == LogLevel_TRACE && !traceEnabled_))
+        {
+          // This logging level is disabled, directly exit as the
+          // stream is set to "/dev/null"
+          return;
+        }
+
+        std::string prefix;
+        GetLinePrefix(prefix, level, file, line);
+
+        {
+          // We lock the global mutex. The mutex is locked until the
+          // destructor is called: No change in the output can be done.
+          lock_.lock();
+      
+          if (loggingStreamsContext_.get() == NULL)
+          {
+            fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
+            lock_.unlock();
+            return;
+          }
+
+          switch (level)
+          {
+            case LogLevel_ERROR:
+              stream_ = loggingStreamsContext_->error_;
+              break;
+              
+            case LogLevel_WARNING:
+              stream_ = loggingStreamsContext_->warning_;
+              break;
+              
+            case LogLevel_INFO:
+            case LogLevel_TRACE:
+              stream_ = loggingStreamsContext_->info_;
+              break;
+              
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+
+          if (stream_ == &nullStream_)
+          {
+            // The logging is disabled for this level, we can release
+            // the global mutex.
+            lock_.unlock();
+          }
+          else
+          {
+            try
+            {
+              (*stream_) << prefix;
+            }
+            catch (...)
+            { 
+              // Something is going really wrong, probably running out of
+              // memory. Fallback to a degraded mode.
+              stream_ = loggingStreamsContext_->error_;
+              (*stream_) << "E???? ??:??:??.?????? ] ";
+            }
+          }
+        }
+      }
+    }
+
+
+    InternalLogger::~InternalLogger()
+    {
+      if (pluginStream_.get() != NULL)
+      {
+        // We are logging through the Orthanc SDK
+        
+        std::string message = pluginStream_->str();
+
+        if (pluginContext_ != NULL)
+        {
+          switch (level_)
+          {
+            case LogLevel_ERROR:
+              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str());
+              break;
+
+            case LogLevel_WARNING:
+              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str());
+              break;
+
+            case LogLevel_INFO:
+              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
+              break;
+
+            default:
+              break;
+          }
+        }
+      }
+      else if (stream_ != &nullStream_)
+      {
+        *stream_ << "\n";
+        stream_->flush();
+      }
+    }
+      
+
+    void Flush()
+    {
+      if (pluginContext_ != NULL)
+      {
+        boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+
+        if (loggingStreamsContext_.get() != NULL &&
+            loggingStreamsContext_->file_.get() != NULL)
+        {
+          loggingStreamsContext_->file_->flush();
+        }
+      }
+    }
+    
+
+    void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream,
+                                        std::ostream& warningStream,
+                                        std::ostream& infoStream)
+    {
+      boost::mutex::scoped_lock lock(loggingStreamsMutex_);
+
+      loggingStreamsContext_.reset(new LoggingStreamsContext);
+      loggingStreamsContext_->error_ = &errorStream;
+      loggingStreamsContext_->warning_ = &warningStream;
+      loggingStreamsContext_->info_ = &infoStream;
+    }
+  }
+}
+
+
+#endif   // ORTHANC_ENABLE_LOGGING
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Logging.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,221 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+// To have ORTHANC_ENABLE_LOGGING defined if using the shared library
+#include "OrthancFramework.h"
+
+#include <iostream>
+
+#if !defined(ORTHANC_ENABLE_LOGGING)
+#  error The macro ORTHANC_ENABLE_LOGGING must be defined
+#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
+
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    enum LogLevel
+    {
+      LogLevel_ERROR,
+      LogLevel_WARNING,
+      LogLevel_INFO,
+      LogLevel_TRACE
+    };
+    
+    ORTHANC_PUBLIC const char* EnumerationToString(LogLevel level);
+
+    ORTHANC_PUBLIC LogLevel StringToLogLevel(const char* level);
+
+    // "pluginContext" must be of type "OrthancPluginContext"
+    ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext);
+
+    ORTHANC_PUBLIC void Initialize();
+
+    ORTHANC_PUBLIC void Finalize();
+
+    ORTHANC_PUBLIC void Reset();
+
+    ORTHANC_PUBLIC void Flush();
+
+    ORTHANC_PUBLIC void EnableInfoLevel(bool enabled);
+
+    ORTHANC_PUBLIC void EnableTraceLevel(bool enabled);
+
+    ORTHANC_PUBLIC bool IsTraceLevelEnabled();
+
+    ORTHANC_PUBLIC bool IsInfoLevelEnabled();
+
+    ORTHANC_PUBLIC void SetTargetFile(const std::string& path);
+
+    ORTHANC_PUBLIC void SetTargetFolder(const std::string& path);
+
+    struct NullStream : public std::ostream 
+    {
+      NullStream() : 
+        std::ios(0), 
+        std::ostream(0)
+      {
+      }
+      
+      template <typename T>
+      std::ostream& operator<< (const T& message)
+      {
+        return *this;
+      }
+    };
+  }
+}
+
+
+#if ORTHANC_ENABLE_LOGGING != 1
+#  define LOG(level)   ::Orthanc::Logging::NullStream()
+#  define VLOG(level)  ::Orthanc::Logging::NullStream()
+#else /* ORTHANC_ENABLE_LOGGING == 1 */
+#  define LOG(level)  ::Orthanc::Logging::InternalLogger        \
+  (::Orthanc::Logging::LogLevel_ ## level, __FILE__, __LINE__)
+#  define VLOG(level) ::Orthanc::Logging::InternalLogger        \
+  (::Orthanc::Logging::LogLevel_TRACE, __FILE__, __LINE__)
+#endif
+
+
+
+#if (ORTHANC_ENABLE_LOGGING == 1 &&             \
+     ORTHANC_ENABLE_LOGGING_STDIO == 1)
+// This is notably for WebAssembly
+
+#include <boost/lexical_cast.hpp>
+#include <boost/noncopyable.hpp>
+#include <sstream>
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    class ORTHANC_PUBLIC InternalLogger : public boost::noncopyable
+    {
+    private:
+      LogLevel           level_;
+      std::stringstream  messageStream_;
+
+    public:
+      InternalLogger(LogLevel level,
+                     const char* file  /* ignored */,
+                     int line  /* ignored */) :
+        level_(level)
+      {
+      }
+
+      ~InternalLogger();
+      
+      template <typename T>
+        std::ostream& operator<< (const T& message)
+      {
+        return messageStream_ << boost::lexical_cast<std::string>(message);
+      }
+    };
+  }
+}
+
+#endif
+
+
+
+#if (ORTHANC_ENABLE_LOGGING == 1 &&             \
+     ORTHANC_ENABLE_LOGGING_STDIO == 0)
+
+#include "Compatibility.h"  // For std::unique_ptr<>
+
+#include <boost/lexical_cast.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/thread/mutex.hpp>
+#include <sstream>
+
+namespace Orthanc
+{
+  namespace Logging
+  {
+    class ORTHANC_PUBLIC InternalLogger : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock           lock_;
+      LogLevel                            level_;
+      std::unique_ptr<std::stringstream>  pluginStream_;
+      std::ostream*                       stream_;
+
+    public:
+      InternalLogger(LogLevel level,
+                     const char* file,
+                     int line);
+
+      ~InternalLogger();
+      
+      template <typename T>
+        std::ostream& operator<< (const T& message)
+      {
+        return (*stream_) << boost::lexical_cast<std::string>(message);
+      }
+    };
+
+
+    /**
+     * Set custom logging streams for the error, warning and info
+     * logs. This function may not be called if a log file or folder
+     * has been set beforehand. All three references must be valid.
+     *
+     * Please ensure the supplied streams remain alive and valid as
+     * long as logging calls are performed. In order to prevent
+     * dangling pointer usage, it is mandatory to call
+     * Orthanc::Logging::Reset() before the stream objects are
+     * destroyed and the references become invalid.
+     *
+     * This function must only be used by unit tests. It is ignored if
+     * InitializePluginContext() was called.
+     **/
+    ORTHANC_PUBLIC void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream,
+                                                       std::ostream& warningStream, 
+                                                       std::ostream& infoStream);
+  }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Lua/LuaContext.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,690 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "LuaContext.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <set>
+#include <cassert>
+#include <boost/lexical_cast.hpp>
+
+extern "C" 
+{
+#include <lualib.h>
+#include <lauxlib.h>
+}
+
+namespace Orthanc
+{
+  static bool OnlyContainsDigits(const std::string& s)
+  {
+    for (size_t i = 0; i < s.size(); i++)
+    {
+      if (!isdigit(s[i]))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+  
+  LuaContext& LuaContext::GetLuaContext(lua_State *state)
+  {
+    const void* value = GetGlobalVariable(state, "_LuaContext");
+    assert(value != NULL);
+
+    return *const_cast<LuaContext*>(reinterpret_cast<const LuaContext*>(value));
+  }
+
+  int LuaContext::PrintToLog(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // http://medek.wordpress.com/2009/02/03/wrapping-lua-errors-and-print-function/
+    int nArgs = lua_gettop(state);
+    lua_getglobal(state, "tostring");
+
+    // Make sure you start at 1 *NOT* 0 for arrays in Lua.
+    std::string result;
+
+    for (int i = 1; i <= nArgs; i++)
+    {
+      const char *s;
+      lua_pushvalue(state, -1);
+      lua_pushvalue(state, i);
+      lua_call(state, 1, 1);
+      s = lua_tostring(state, -1);
+
+      if (result.size() > 0)
+        result.append(", ");
+
+      if (s == NULL)
+        result.append("<No conversion to string>");
+      else
+        result.append(s);
+ 
+      lua_pop(state, 1);
+    }
+
+    LOG(WARNING) << "Lua says: " << result;         
+    that.log_.append(result);
+    that.log_.append("\n");
+
+    return 0;
+  }
+
+
+  int LuaContext::ParseJson(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    int nArgs = lua_gettop(state);
+    if (nArgs != 1 ||
+        !lua_isstring(state, 1))    // Password
+    {
+      lua_pushnil(state);
+      return 1;
+    }
+
+    const char* str = lua_tostring(state, 1);
+
+    Json::Value value;
+    Json::Reader reader;
+    if (reader.parse(str, str + strlen(str), value))
+    {
+      that.PushJson(value);
+    }
+    else
+    {
+      lua_pushnil(state);
+    }
+
+    return 1;
+  }
+
+
+  int LuaContext::DumpJson(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    int nArgs = lua_gettop(state);
+    if ((nArgs != 1 && nArgs != 2) ||
+        (nArgs == 2 && !lua_isboolean(state, 2)))
+    {
+      lua_pushnil(state);
+      return 1;
+    }
+
+    bool keepStrings = false;
+    if (nArgs == 2)
+    {
+      keepStrings = lua_toboolean(state, 2) ? true : false;
+    }
+
+    Json::Value json;
+    that.GetJson(json, state, 1, keepStrings);
+
+    Json::FastWriter writer;
+    std::string s = writer.write(json);
+    lua_pushlstring(state, s.c_str(), s.size());
+
+    return 1;
+  }
+
+
+#if ORTHANC_ENABLE_CURL == 1
+  int LuaContext::SetHttpCredentials(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs != 2 ||
+        !lua_isstring(state, 1) ||  // Username
+        !lua_isstring(state, 2))    // Password
+    {
+      LOG(ERROR) << "Lua: Bad parameters to SetHttpCredentials()";
+    }
+    else
+    {
+      // Configure the HTTP client
+      const char* username = lua_tostring(state, 1);
+      const char* password = lua_tostring(state, 2);
+      that.httpClient_.SetCredentials(username, password);
+    }
+
+    return 0;
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CURL == 1
+  bool LuaContext::AnswerHttpQuery(lua_State* state)
+  {
+    std::string str;
+
+    try
+    {
+      httpClient_.Apply(str);
+    }
+    catch (OrthancException&)
+    {
+      return false;
+    }
+
+    // Return the result of the HTTP request
+    lua_pushlstring(state, str.c_str(), str.size());
+
+    return true;
+  }
+#endif
+  
+
+#if ORTHANC_ENABLE_CURL == 1
+  void LuaContext::SetHttpHeaders(int top)
+  {
+    std::map<std::string, std::string> headers;
+    GetDictionaryArgument(headers, lua_, top, false /* keep key case as provided by Lua script */);
+      
+    httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request
+
+    for (std::map<std::string, std::string>::const_iterator
+           it = headers.begin(); it != headers.end(); ++it)
+    {
+      httpClient_.AddHeader(it->first, it->second);
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CURL == 1
+  int LuaContext::CallHttpGet(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    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);
+      return 1;
+    }
+
+    // Configure the HTTP client class
+    const char* url = lua_tostring(state, 1);
+    that.httpClient_.SetMethod(HttpMethod_Get);
+    that.httpClient_.SetUrl(url);
+    that.httpClient_.GetBody().clear();
+    that.SetHttpHeaders(2);
+
+    // Do the HTTP GET request
+    if (!that.AnswerHttpQuery(state))
+    {
+      LOG(ERROR) << "Lua: Error in HttpGet() for URL " << url;
+      lua_pushnil(state);
+    }
+
+    return 1;
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CURL == 1
+  int LuaContext::CallHttpPostOrPut(lua_State *state,
+                                    HttpMethod method)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    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);
+      return 1;
+    }
+
+    // Configure the HTTP client class
+    const char* url = lua_tostring(state, 1);
+    that.httpClient_.SetMethod(method);
+    that.httpClient_.SetUrl(url);
+    that.SetHttpHeaders(3);
+
+    if (nArgs >= 2 && !lua_isnil(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
+    {
+      that.httpClient_.GetBody().clear();
+    }
+
+    // Do the HTTP POST/PUT request
+    if (!that.AnswerHttpQuery(state))
+    {
+      LOG(ERROR) << "Lua: Error in HttpPost() or HttpPut() for URL " << url;
+      lua_pushnil(state);
+    }
+
+    return 1;
+  }
+#endif
+  
+
+#if ORTHANC_ENABLE_CURL == 1
+  int LuaContext::CallHttpPost(lua_State *state)
+  {
+    return CallHttpPostOrPut(state, HttpMethod_Post);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CURL == 1
+  int LuaContext::CallHttpPut(lua_State *state)
+  {
+    return CallHttpPostOrPut(state, HttpMethod_Put);
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_CURL == 1
+  int LuaContext::CallHttpDelete(lua_State *state)
+  {
+    LuaContext& that = GetLuaContext(state);
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs < 1 || nArgs > 2 || !lua_isstring(state, 1))  // URL
+    {
+      LOG(ERROR) << "Lua: Bad parameters to HttpDelete()";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    // Configure the HTTP client class
+    const char* url = lua_tostring(state, 1);
+    that.httpClient_.SetMethod(HttpMethod_Delete);
+    that.httpClient_.SetUrl(url);
+    that.httpClient_.GetBody().clear();
+    that.SetHttpHeaders(2);
+
+    // Do the HTTP DELETE request
+    std::string s;
+    if (!that.httpClient_.Apply(s))
+    {
+      LOG(ERROR) << "Lua: Error in HttpDelete() for URL " << url;
+      lua_pushnil(state);
+    }
+    else
+    {
+      lua_pushstring(state, "SUCCESS");
+    }
+
+    return 1;
+  }
+#endif
+
+
+  void LuaContext::PushJson(const Json::Value& value)
+  {
+    if (value.isString())
+    {
+      const std::string s = value.asString();
+      lua_pushlstring(lua_, s.c_str(), s.size());
+    }
+    else if (value.isDouble())
+    {
+      lua_pushnumber(lua_, value.asDouble());
+    }
+    else if (value.isInt())
+    {
+      lua_pushinteger(lua_, value.asInt());
+    }
+    else if (value.isUInt())
+    {
+      lua_pushinteger(lua_, value.asUInt());
+    }
+    else if (value.isBool())
+    {
+      lua_pushboolean(lua_, value.asBool());
+    }
+    else if (value.isNull())
+    {
+      lua_pushnil(lua_);
+    }
+    else if (value.isArray())
+    {
+      lua_newtable(lua_);
+
+      // http://lua-users.org/wiki/SimpleLuaApiExample
+      for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+      {
+        // Push the table index (note the "+1" because of Lua conventions)
+        lua_pushnumber(lua_, i + 1);
+
+        // Push the value of the cell
+        PushJson(value[i]);
+
+        // Stores the pair in the table
+        lua_rawset(lua_, -3);
+      }
+    }
+    else if (value.isObject())
+    {
+      lua_newtable(lua_);
+
+      Json::Value::Members members = value.getMemberNames();
+
+      for (Json::Value::Members::const_iterator 
+             it = members.begin(); it != members.end(); ++it)
+      {
+        // Push the index of the cell
+        lua_pushlstring(lua_, it->c_str(), it->size());
+
+        // Push the value of the cell
+        PushJson(value[*it]);
+
+        // Stores the pair in the table
+        lua_rawset(lua_, -3);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_JsonToLuaTable);
+    }
+  }
+
+
+  void LuaContext::GetJson(Json::Value& result,
+                           lua_State* state,
+                           int top,
+                           bool keepStrings)
+  {
+    if (lua_istable(state, top))
+    {
+      Json::Value tmp = Json::objectValue;
+      bool isArray = true;
+      size_t size = 0;
+
+      // Code adapted from: http://stackoverflow.com/a/6142700/881731
+      
+      // Push another reference to the table on top of the stack (so we know
+      // where it is, and this function can work for negative, positive and
+      // pseudo indices
+      lua_pushvalue(state, top);
+      // stack now contains: -1 => table
+      lua_pushnil(state);
+      // stack now contains: -1 => nil; -2 => table
+      while (lua_next(state, -2))
+      {
+        // stack now contains: -1 => value; -2 => key; -3 => table
+        // copy the key so that lua_tostring does not modify the original
+        lua_pushvalue(state, -2);
+        // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table
+        std::string key(lua_tostring(state, -1));
+        Json::Value v;
+        GetJson(v, state, -2, keepStrings);
+
+        tmp[key] = v;
+
+        size += 1;
+        try
+        {
+          if (!OnlyContainsDigits(key) ||
+              boost::lexical_cast<size_t>(key) != size)
+          {
+            isArray = false;
+          }
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+          isArray = false;
+        }
+        
+        // pop value + copy of key, leaving original key
+        lua_pop(state, 2);
+        // stack now contains: -1 => key; -2 => table
+      }
+      // stack now contains: -1 => table (when lua_next returns 0 it pops the key
+      // but does not push anything.)
+      // Pop table
+      lua_pop(state, 1);
+
+      // Stack is now the same as it was on entry to this function
+
+      if (isArray)
+      {
+        result = Json::arrayValue;
+        for (size_t i = 0; i < size; i++)
+        {
+          result.append(tmp[boost::lexical_cast<std::string>(i + 1)]);
+        }
+      }
+      else
+      {
+        result = tmp;
+      }
+    }
+    else if (lua_isnil(state, top))
+    {
+      result = Json::nullValue;
+    }
+    else if (!keepStrings &&
+             lua_isboolean(state, top))
+    {
+      result = lua_toboolean(state, top) ? true : false;
+    }
+    else if (!keepStrings &&
+             lua_isnumber(state, top))
+    {
+      // Convert to "int" if truncation does not loose precision
+      double value = static_cast<double>(lua_tonumber(state, top));
+      int truncated = static_cast<int>(value);
+
+      if (std::abs(value - static_cast<double>(truncated)) <= 
+          std::numeric_limits<double>::epsilon())
+      {
+        result = truncated;
+      }
+      else
+      {
+        result = value;
+      }
+    }
+    else if (lua_isstring(state, top))
+    {
+      // Caution: The "lua_isstring()" case must be the last, since
+      // Lua can convert most types to strings by default.
+      result = std::string(lua_tostring(state, top));
+    }
+    else if (lua_isboolean(state, top))
+    {
+      result = lua_toboolean(state, top) ? true : false;
+    }
+    else
+    {
+      LOG(WARNING) << "Unsupported Lua type when returning Json";
+      result = Json::nullValue;
+    }
+  }
+
+
+  LuaContext::LuaContext()
+  {
+    lua_ = luaL_newstate();
+    if (!lua_)
+    {
+      throw OrthancException(ErrorCode_CannotCreateLua);
+    }
+
+    luaL_openlibs(lua_);
+    lua_register(lua_, "print", PrintToLog);
+    lua_register(lua_, "ParseJson", ParseJson);
+    lua_register(lua_, "DumpJson", DumpJson);
+    
+#if ORTHANC_ENABLE_CURL == 1
+    lua_register(lua_, "HttpGet", CallHttpGet);
+    lua_register(lua_, "HttpPost", CallHttpPost);
+    lua_register(lua_, "HttpPut", CallHttpPut);
+    lua_register(lua_, "HttpDelete", CallHttpDelete);
+    lua_register(lua_, "SetHttpCredentials", SetHttpCredentials);
+#endif
+
+    SetGlobalVariable("_LuaContext", this);
+  }
+
+
+  LuaContext::~LuaContext()
+  {
+    lua_close(lua_);
+  }
+
+
+  void LuaContext::ExecuteInternal(std::string* output,
+                                   const std::string& command)
+  {
+    log_.clear();
+    int error = (luaL_loadbuffer(lua_, command.c_str(), command.size(), "line") ||
+                 lua_pcall(lua_, 0, 0, 0));
+
+    if (error) 
+    {
+      assert(lua_gettop(lua_) >= 1);
+
+      std::string description(lua_tostring(lua_, -1));
+      lua_pop(lua_, 1); /* pop error message from the stack */
+      throw OrthancException(ErrorCode_CannotExecuteLua, description);
+    }
+
+    if (output != NULL)
+    {
+      *output = log_;
+    }
+  }
+
+
+  bool LuaContext::IsExistingFunction(const char* name)
+  {
+    lua_settop(lua_, 0);
+    lua_getglobal(lua_, name);
+    return lua_type(lua_, -1) == LUA_TFUNCTION;
+  }
+
+
+  void LuaContext::Execute(Json::Value& output,
+                           const std::string& command)
+  {
+    std::string s;
+    ExecuteInternal(&s, command);
+
+    Json::Reader reader;
+    if (!reader.parse(s, output))
+    {
+      throw OrthancException(ErrorCode_BadJson);
+    }
+  }
+
+
+  void LuaContext::RegisterFunction(const char* name,
+                                    lua_CFunction func)
+  {
+    lua_register(lua_, name, func);
+  }
+
+
+  void LuaContext::SetGlobalVariable(const char* name,
+                                     void* value)
+  {
+    lua_pushlightuserdata(lua_, value);
+    lua_setglobal(lua_, name);
+  }
+
+  
+  const void* LuaContext::GetGlobalVariable(lua_State* state,
+                                            const char* name)
+  {
+    lua_getglobal(state, name);
+    assert(lua_type(state, -1) == LUA_TLIGHTUSERDATA);
+    const void* value = lua_topointer(state, -1);
+    lua_pop(state, 1);
+    return value;
+  }
+
+
+  void LuaContext::GetDictionaryArgument(std::map<std::string, std::string>& target,
+                                         lua_State* state,
+                                         int top,
+                                         bool keyToLowerCase)
+  {
+    target.clear();
+
+    if (lua_gettop(state) >= top)
+    {
+      Json::Value headers;
+      GetJson(headers, state, top, true);
+
+      Json::Value::Members members = headers.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        std::string key = members[i];
+
+        if (keyToLowerCase)
+        {
+          Toolbox::ToLowerCase(key);
+        }
+        
+        target[key] = headers[members[i]].asString();
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Lua/LuaContext.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+// To have ORTHANC_ENABLE_LUA defined if using the shared library
+#include "../OrthancFramework.h"
+
+#if !defined(ORTHANC_ENABLE_LUA)
+#  error The macro ORTHANC_ENABLE_LUA must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_CURL)
+#  error Macro ORTHANC_ENABLE_CURL must be defined
+#endif
+
+#if ORTHANC_ENABLE_LUA == 0
+#  error The Lua support is disabled, cannot include this file
+#endif
+
+#if ORTHANC_ENABLE_CURL == 1
+#  include "../HttpClient.h"
+#endif
+
+extern "C" 
+{
+#include <lua.h>
+}
+
+#include <boost/noncopyable.hpp>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC LuaContext : public boost::noncopyable
+  {
+  private:
+    friend class LuaFunctionCall;
+
+    lua_State *lua_;
+    std::string log_;
+
+#if ORTHANC_ENABLE_CURL == 1
+    HttpClient httpClient_;
+#endif
+
+    static int PrintToLog(lua_State *state);
+    static int ParseJson(lua_State *state);
+    static int DumpJson(lua_State *state);
+
+#if ORTHANC_ENABLE_CURL == 1
+    static int SetHttpCredentials(lua_State *state);
+    static int CallHttpPostOrPut(lua_State *state,
+                                 HttpMethod method);
+    static int CallHttpGet(lua_State *state);
+    static int CallHttpPost(lua_State *state);
+    static int CallHttpPut(lua_State *state);
+    static int CallHttpDelete(lua_State *state);
+#endif
+
+    bool AnswerHttpQuery(lua_State* state);
+
+    void ExecuteInternal(std::string* output,
+                         const std::string& command);
+
+    static void GetJson(Json::Value& result,
+                        lua_State* state,
+                        int top,
+                        bool keepStrings);
+
+    void SetHttpHeaders(int top);
+    
+  public:
+    LuaContext();
+
+    ~LuaContext();
+
+    void Execute(const std::string& command)
+    {
+      ExecuteInternal(NULL, command);
+    }
+
+    void Execute(std::string& output,
+                 const std::string& command)
+    {
+      ExecuteInternal(&output, command);
+    }
+
+    void Execute(Json::Value& output,
+                 const std::string& command);
+
+    bool IsExistingFunction(const char* name);
+
+#if ORTHANC_ENABLE_CURL == 1
+    void SetHttpCredentials(const char* username,
+                            const char* password)
+    {
+      httpClient_.SetCredentials(username, password);
+    }
+#endif
+
+    void RegisterFunction(const char* name,
+                          lua_CFunction func);
+
+    void SetGlobalVariable(const char* name,
+                           void* value);
+
+    static LuaContext& GetLuaContext(lua_State *state);
+
+    static const void* GetGlobalVariable(lua_State* state,
+                                         const char* name);
+
+    void PushJson(const Json::Value& value);
+
+    static void GetDictionaryArgument(std::map<std::string, std::string>& target,
+                                      lua_State* state,
+                                      int top,
+                                      bool keyToLowerCase);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,191 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "LuaFunctionCall.h"
+
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+#include <cassert>
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  void LuaFunctionCall::CheckAlreadyExecuted()
+  {
+    if (isExecuted_)
+    {
+      throw OrthancException(ErrorCode_LuaAlreadyExecuted);
+    }
+  }
+
+  LuaFunctionCall::LuaFunctionCall(LuaContext& context,
+                                   const char* functionName) : 
+    context_(context),
+    isExecuted_(false)
+  {
+    // Clear the stack to fulfill the invariant
+    lua_settop(context_.lua_, 0);
+    lua_getglobal(context_.lua_, functionName);
+  }
+
+  void LuaFunctionCall::PushString(const std::string& value)
+  {
+    CheckAlreadyExecuted();
+    lua_pushlstring(context_.lua_, value.c_str(), value.size());
+  }
+
+  void LuaFunctionCall::PushBoolean(bool value)
+  {
+    CheckAlreadyExecuted();
+    lua_pushboolean(context_.lua_, value);
+  }
+
+  void LuaFunctionCall::PushInteger(int value)
+  {
+    CheckAlreadyExecuted();
+    lua_pushinteger(context_.lua_, value);
+  }
+
+  void LuaFunctionCall::PushDouble(double value)
+  {
+    CheckAlreadyExecuted();
+    lua_pushnumber(context_.lua_, value);
+  }
+
+  void LuaFunctionCall::PushJson(const Json::Value& value)
+  {
+    CheckAlreadyExecuted();
+    context_.PushJson(value);
+  }
+
+  void LuaFunctionCall::ExecuteInternal(int numOutputs)
+  {
+    CheckAlreadyExecuted();
+
+    assert(lua_gettop(context_.lua_) >= 1);
+    int nargs = lua_gettop(context_.lua_) - 1;
+    int error = lua_pcall(context_.lua_, nargs, numOutputs, 0);
+
+    if (error) 
+    {
+      assert(lua_gettop(context_.lua_) >= 1);
+          
+      std::string description(lua_tostring(context_.lua_, -1));
+      lua_pop(context_.lua_, 1); /* pop error message from the stack */
+
+      throw OrthancException(ErrorCode_CannotExecuteLua, description);
+    }
+
+    if (lua_gettop(context_.lua_) < numOutputs)
+    {
+      throw OrthancException(ErrorCode_LuaBadOutput);
+    }
+
+    isExecuted_ = true;
+  }
+
+  bool LuaFunctionCall::ExecutePredicate()
+  {
+    ExecuteInternal(1);
+    
+    if (!lua_isboolean(context_.lua_, 1))
+    {
+      throw OrthancException(ErrorCode_NotLuaPredicate);
+    }
+
+    return lua_toboolean(context_.lua_, 1) != 0;
+  }
+
+
+  void LuaFunctionCall::ExecuteToJson(Json::Value& result,
+                                      bool keepStrings)
+  {
+    ExecuteInternal(1);
+    context_.GetJson(result, context_.lua_, lua_gettop(context_.lua_), keepStrings);
+  }
+
+
+  void LuaFunctionCall::ExecuteToString(std::string& result)
+  {
+    ExecuteInternal(1);
+    
+    int top = lua_gettop(context_.lua_);
+    if (lua_isstring(context_.lua_, top))
+    {
+      result = lua_tostring(context_.lua_, top);
+    }
+    else
+    {
+      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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Lua/LuaFunctionCall.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "LuaContext.h"
+
+#include "../DicomFormat/DicomArray.h"
+#include "../DicomFormat/DicomMap.h"
+
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC LuaFunctionCall : public boost::noncopyable
+  {
+  private:
+    LuaContext& context_;
+    bool isExecuted_;
+
+    void CheckAlreadyExecuted();
+
+  protected:
+    void ExecuteInternal(int numOutputs);
+
+    lua_State* GetState()
+    {
+      return context_.lua_;
+    }
+
+  public:
+    LuaFunctionCall(LuaContext& context,
+                    const char* functionName);
+
+    void PushString(const std::string& value);
+
+    void PushBoolean(bool value);
+
+    void PushInteger(int value);
+
+    void PushDouble(double value);
+
+    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);
+    }
+
+    bool ExecutePredicate();
+
+    void ExecuteToJson(Json::Value& result,
+                       bool keepStrings);
+
+    void ExecuteToString(std::string& result);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MetricsRegistry.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,330 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "MetricsRegistry.h"
+
+#include "ChunkedBuffer.h"
+#include "Compatibility.h"
+#include "OrthancException.h"
+
+namespace Orthanc
+{
+  static const boost::posix_time::ptime GetNow()
+  {
+    return boost::posix_time::microsec_clock::universal_time();
+  }
+
+  class MetricsRegistry::Item
+  {
+  private:
+    MetricsType               type_;
+    boost::posix_time::ptime  time_;
+    bool                      hasValue_;
+    float                     value_;
+    
+    void Touch(float value,
+               const boost::posix_time::ptime& now)
+    {
+      hasValue_ = true;
+      value_ = value;
+      time_ = now;
+    }
+
+    void Touch(float value)
+    {
+      Touch(value, GetNow());
+    }
+
+    void UpdateMax(float value,
+                   int duration)
+    {
+      if (hasValue_)
+      {
+        const boost::posix_time::ptime now = GetNow();
+
+        if (value > value_ ||
+            (now - time_).total_seconds() > duration)
+        {
+          Touch(value, now);
+        }
+      }
+      else
+      {
+        Touch(value);
+      }
+    }
+    
+    void UpdateMin(float value,
+                   int duration)
+    {
+      if (hasValue_)
+      {
+        const boost::posix_time::ptime now = GetNow();
+        
+        if (value < value_ ||
+            (now - time_).total_seconds() > duration)
+        {
+          Touch(value, now);
+        }
+      }
+      else
+      {
+        Touch(value);
+      }
+    }
+
+  public:
+    Item(MetricsType type) :
+    type_(type),
+    hasValue_(false)
+    {
+    }
+
+    MetricsType GetType() const
+    {
+      return type_;
+    }
+
+    void Update(float value)
+    {
+      switch (type_)
+      {
+        case MetricsType_Default:
+          Touch(value);
+          break;
+          
+        case MetricsType_MaxOver10Seconds:
+          UpdateMax(value, 10);
+          break;
+
+        case MetricsType_MaxOver1Minute:
+          UpdateMax(value, 60);
+          break;
+
+        case MetricsType_MinOver10Seconds:
+          UpdateMin(value, 10);
+          break;
+
+        case MetricsType_MinOver1Minute:
+          UpdateMin(value, 60);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_NotImplemented);
+      }
+    }
+
+    bool HasValue() const
+    {
+      return hasValue_;
+    }
+
+    const boost::posix_time::ptime& GetTime() const
+    {
+      if (hasValue_)
+      {
+        return time_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    float GetValue() const
+    {
+      if (hasValue_)
+      {
+        return value_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+  };
+
+
+  MetricsRegistry::~MetricsRegistry()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void MetricsRegistry::SetEnabled(bool enabled)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    enabled_ = enabled;
+  }
+
+
+  void MetricsRegistry::Register(const std::string& name,
+                                 MetricsType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      content_[name] = new Item(type);
+    }
+    else
+    {
+      assert(found->second != NULL);
+
+      // This metrics already exists: Only recreate it if there is a
+      // mismatch in the type of metrics
+      if (found->second->GetType() != type)
+      {
+        delete found->second;
+        found->second = new Item(type);
+      }
+    }    
+  }
+
+
+  void MetricsRegistry::SetValueInternal(const std::string& name,
+                                         float value,
+                                         MetricsType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      std::unique_ptr<Item> item(new Item(type));
+      item->Update(value);
+      content_[name] = item.release();
+    }
+    else
+    {
+      assert(found->second != NULL);
+      found->second->Update(value);
+    }
+  }
+
+
+  MetricsType MetricsRegistry::GetMetricsType(const std::string& name)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::const_iterator found = content_.find(name);
+
+    if (found == content_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      assert(found->second != NULL);
+      return found->second->GetType();
+    }
+  }
+
+
+  void MetricsRegistry::ExportPrometheusText(std::string& s)
+  {
+    // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch
+    static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1));
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    s.clear();
+
+    if (!enabled_)
+    {
+      return;
+    }
+
+    ChunkedBuffer buffer;
+
+    for (Content::const_iterator it = content_.begin();
+         it != content_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (it->second->HasValue())
+      {
+        boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH;
+
+        std::string line = (it->first + " " +
+                            boost::lexical_cast<std::string>(it->second->GetValue()) + " " + 
+                            boost::lexical_cast<std::string>(diff.total_milliseconds()) + "\n");
+
+        buffer.AddChunk(line);
+      }
+    }
+
+    buffer.Flatten(s);
+  }
+
+
+  void MetricsRegistry::SharedMetrics::Add(float delta)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    value_ += delta;
+    registry_.SetValue(name_, value_);
+  }
+
+
+  void  MetricsRegistry::Timer::Start()
+  {
+    if (registry_.IsEnabled())
+    {
+      active_ = true;
+      start_ = GetNow();
+    }
+    else
+    {
+      active_ = false;
+    }
+  }
+
+
+  MetricsRegistry::Timer::~Timer()
+  {
+    if (active_)
+    {   
+      boost::posix_time::time_duration diff = GetNow() - start_;
+      registry_.SetValue(
+        name_, static_cast<float>(diff.total_milliseconds()), type_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MetricsRegistry.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,191 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The class MetricsRegistry cannot be used in sandboxed environments
+#endif
+
+#include <boost/thread/mutex.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  enum MetricsType
+  {
+    MetricsType_Default,
+    MetricsType_MaxOver10Seconds,
+    MetricsType_MaxOver1Minute,
+    MetricsType_MinOver10Seconds,
+    MetricsType_MinOver1Minute
+  };
+  
+  class ORTHANC_PUBLIC MetricsRegistry : public boost::noncopyable
+  {
+  private:
+    class Item;
+
+    typedef std::map<std::string, Item*>   Content;
+
+    bool          enabled_;
+    boost::mutex  mutex_;
+    Content       content_;
+
+    void SetValueInternal(const std::string& name,
+                          float value,
+                          MetricsType type);
+
+  public:
+    MetricsRegistry() :
+      enabled_(true)
+    {
+    }
+
+    ~MetricsRegistry();
+
+    bool IsEnabled() const
+    {
+      return enabled_;
+    }
+
+    void SetEnabled(bool enabled);
+
+    void Register(const std::string& name,
+                  MetricsType type);
+
+    void SetValue(const std::string& name,
+                  float value,
+                  MetricsType type)
+    {
+      // Inlining to avoid loosing time if metrics are disabled
+      if (enabled_)
+      {
+        SetValueInternal(name, value, type);
+      }
+    }
+    
+    void SetValue(const std::string& name,
+                  float value)
+    {
+      SetValue(name, value, MetricsType_Default);
+    }
+
+    MetricsType GetMetricsType(const std::string& name);
+
+    // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format
+    void ExportPrometheusText(std::string& s);
+
+
+    class ORTHANC_PUBLIC SharedMetrics : public boost::noncopyable
+    {
+    private:
+      boost::mutex      mutex_;
+      MetricsRegistry&  registry_;
+      std::string       name_;
+      float             value_;
+
+    public:
+      SharedMetrics(MetricsRegistry& registry,
+                    const std::string& name,
+                    MetricsType type) :
+        registry_(registry),
+        name_(name),
+        value_(0)
+      {
+      }
+
+      void Add(float delta);
+    };
+
+
+    class ORTHANC_PUBLIC ActiveCounter : public boost::noncopyable
+    {
+    private:
+      SharedMetrics&   metrics_;
+
+    public:
+      ActiveCounter(SharedMetrics& metrics) :
+        metrics_(metrics)
+      {
+        metrics_.Add(1);
+      }
+
+      ~ActiveCounter()
+      {
+        metrics_.Add(-1);
+      }
+    };
+
+
+    class ORTHANC_PUBLIC Timer : public boost::noncopyable
+    {
+    private:
+      MetricsRegistry&          registry_;
+      std::string               name_;
+      MetricsType               type_;
+      bool                      active_;
+      boost::posix_time::ptime  start_;
+
+      void Start();
+
+    public:
+      Timer(MetricsRegistry& registry,
+            const std::string& name) :
+        registry_(registry),
+        name_(name),
+        type_(MetricsType_MaxOver10Seconds)
+      {
+        Start();
+      }
+
+      Timer(MetricsRegistry& registry,
+            const std::string& name,
+            MetricsType type) :
+        registry_(registry),
+        name_(name),
+        type_(type)
+      {
+        Start();
+      }
+
+      ~Timer();
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/IRunnableBySteps.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+{
+  class IRunnableBySteps : public IDynamicObject
+  {
+  public:
+    virtual ~IRunnableBySteps()
+    {
+    }
+
+    // Must return "true" if the runnable wishes to continue. Must
+    // return "false" if the runnable has not finished its job.
+    virtual bool Step() = 0;
+
+    static void RunUntilDone(IRunnableBySteps& runnable)
+    {
+      while (runnable.Step());
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,171 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RunnableWorkersPool.h"
+
+#include "SharedMessageQueue.h"
+#include "../Compatibility.h"
+#include "../OrthancException.h"
+#include "../Logging.h"
+
+namespace Orthanc
+{
+  struct RunnableWorkersPool::PImpl
+  {
+    class Worker
+    {
+    private:
+      const bool&           continue_;
+      SharedMessageQueue&   queue_;
+      boost::thread         thread_;
+ 
+      static void WorkerThread(Worker* that)
+      {
+        while (that->continue_)
+        {
+          try
+          {
+            std::unique_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 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";
+          }
+        }
+      }
+
+    public:
+      Worker(const bool& globalContinue,
+             SharedMessageQueue& queue) : 
+        continue_(globalContinue),
+        queue_(queue)
+      {
+        thread_ = boost::thread(WorkerThread, this);
+      }
+
+      void Join()
+      {
+        if (thread_.joinable())
+        {
+          thread_.join();
+        }
+      }
+    };
+
+
+    bool                  continue_;
+    std::vector<Worker*>  workers_;
+    SharedMessageQueue    queue_;
+  };
+
+
+
+  RunnableWorkersPool::RunnableWorkersPool(size_t countWorkers) : pimpl_(new PImpl)
+  {
+    pimpl_->continue_ = true;
+
+    if (countWorkers == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    pimpl_->workers_.resize(countWorkers);
+
+    for (size_t i = 0; i < countWorkers; i++)
+    {
+      pimpl_->workers_[i] = new PImpl::Worker(pimpl_->continue_, pimpl_->queue_);
+    }
+  }
+
+
+  void RunnableWorkersPool::Stop()
+  {
+    if (pimpl_->continue_)
+    {
+      pimpl_->continue_ = false;
+
+      for (size_t i = 0; i < pimpl_->workers_.size(); i++)
+      {
+        PImpl::Worker* worker = pimpl_->workers_[i];
+
+        if (worker != NULL)
+        {
+          worker->Join();
+          delete worker;
+        }
+      }
+    }
+  }
+
+
+  RunnableWorkersPool::~RunnableWorkersPool()
+  {
+    Stop();
+  }
+
+
+  void RunnableWorkersPool::Add(IRunnableBySteps* runnable)
+  {
+    if (!pimpl_->continue_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    pimpl_->queue_.Enqueue(runnable);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/RunnableWorkersPool.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IRunnableBySteps.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class RunnableWorkersPool : public boost::noncopyable
+  {
+  private:
+    struct PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    void Stop();
+
+  public:
+    explicit RunnableWorkersPool(size_t countWorkers);
+
+    ~RunnableWorkersPool();
+
+    void Add(IRunnableBySteps* runnable);  // Takes the ownership
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Semaphore.h"
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  Semaphore::Semaphore(unsigned int availableResources) :
+    availableResources_(availableResources)
+  {
+    if (availableResources_ == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  void Semaphore::Release()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    availableResources_++;
+    condition_.notify_one(); 
+  }
+
+  void Semaphore::Acquire()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    while (availableResources_ == 0)
+    {
+      condition_.wait(lock);
+    }
+
+    availableResources_--;
+  }
+
+  bool Semaphore::TryAcquire()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (availableResources_ == 0)
+    {
+      return false;
+    }
+
+    availableResources_--;
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/Semaphore.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class Semaphore : public boost::noncopyable
+  {
+  private:
+    unsigned int availableResources_;
+    boost::mutex mutex_;
+    boost::condition_variable condition_;
+    
+    void Release();
+
+    void Acquire();
+
+    bool TryAcquire();
+  public:
+    explicit Semaphore(unsigned int availableResources);
+
+    unsigned int GetAvailableResourcesCount() const
+    {
+      return availableResources_;
+    }
+
+
+    class Locker : public boost::noncopyable
+    {
+    private:
+      Semaphore&  that_;
+
+    public:
+      explicit Locker(Semaphore& that) :
+        that_(that)
+      {
+        that_.Acquire();
+      }
+
+      ~Locker()
+      {
+        that_.Release();
+      }
+    };
+
+    class TryLocker : public boost::noncopyable
+    {
+    private:
+      Semaphore&  that_;
+      bool        isAcquired_;
+
+    public:
+      explicit TryLocker(Semaphore& that) :
+        that_(that)
+      {
+        isAcquired_ = that_.TryAcquire();
+      }
+
+      ~TryLocker()
+      {
+        if (isAcquired_)
+        {
+          that_.Release();
+        }
+      }
+
+      bool IsAcquired() const
+      {
+        return isAcquired_;
+      }
+    };
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "SharedMessageQueue.h"
+
+
+#include "../Compatibility.h"
+
+
+/**
+ * FIFO (queue):
+ * 
+ *            back                         front
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ * Enqueue -> |  |  |  |  |  |  |  |  |  |  |  |
+ *            |  |  |  |  |  |  |  |  |  |  |  | -> Dequeue
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ *                                            ^
+ *                                            |
+ *                                      Make room here
+ *
+ *
+ * LIFO (stack):
+ * 
+ *            back                         front
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ *            |  |  |  |  |  |  |  |  |  |  |  | <- Enqueue
+ *            |  |  |  |  |  |  |  |  |  |  |  | -> Dequeue
+ *            +--+--+--+--+--+--+--+--+--+--+--+
+ *              ^
+ *              |
+ *        Make room here
+ **/
+
+
+namespace Orthanc
+{
+  SharedMessageQueue::SharedMessageQueue(unsigned int maxSize) :
+    isFifo_(true),
+    maxSize_(maxSize)
+  {
+  }
+
+
+  SharedMessageQueue::~SharedMessageQueue()
+  {
+    for (Queue::iterator it = queue_.begin(); it != queue_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  void SharedMessageQueue::Enqueue(IDynamicObject* message)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (maxSize_ != 0 && queue_.size() > maxSize_)
+    {
+      if (isFifo_)
+      {
+        // Too many elements in the queue: Make room
+        delete queue_.front();
+        queue_.pop_front();
+      }
+      else
+      {
+        // Too many elements in the stack: Make room
+        delete queue_.back();
+        queue_.pop_back();
+      }
+    }
+
+    if (isFifo_)
+    {
+      // Queue policy (FIFO)
+      queue_.push_back(message);
+    }
+    else
+    {
+      // Stack policy (LIFO)
+      queue_.push_front(message);
+    }
+
+    elementAvailable_.notify_one();
+  }
+
+
+  IDynamicObject* SharedMessageQueue::Dequeue(int32_t millisecondsTimeout)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Wait for a message to arrive in the FIFO queue
+    while (queue_.empty())
+    {
+      if (millisecondsTimeout == 0)
+      {
+        elementAvailable_.wait(lock);
+      }
+      else
+      {
+        bool success = elementAvailable_.timed_wait
+          (lock, boost::posix_time::milliseconds(millisecondsTimeout));
+        if (!success)
+        {
+          return NULL;
+        }
+      }
+    }
+
+    std::unique_ptr<IDynamicObject> message(queue_.front());
+    queue_.pop_front();
+
+    if (queue_.empty())
+    {
+      emptied_.notify_all();
+    }
+
+    return message.release();
+  }
+
+
+
+  bool SharedMessageQueue::WaitEmpty(int32_t millisecondsTimeout)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    
+    // Wait for the queue to become empty
+    while (!queue_.empty())
+    {
+      if (millisecondsTimeout == 0)
+      {
+        emptied_.wait(lock);
+      }
+      else
+      {
+        if (!emptied_.timed_wait
+            (lock, boost::posix_time::milliseconds(millisecondsTimeout)))
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  void SharedMessageQueue::SetFifoPolicy()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    isFifo_ = true;
+  }
+
+  void SharedMessageQueue::SetLifoPolicy()
+  {
+    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::unique_ptr<IDynamicObject> message(queue_.front());
+        queue_.pop_front();
+      }
+
+      emptied_.notify_all();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/MultiThreading/SharedMessageQueue.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#include <stdint.h>
+#include <list>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC SharedMessageQueue : public boost::noncopyable
+  {
+  private:
+    typedef std::list<IDynamicObject*>  Queue;
+
+    bool isFifo_;
+    unsigned int maxSize_;
+    Queue queue_;
+    boost::mutex mutex_;
+    boost::condition_variable elementAvailable_;
+    boost::condition_variable emptied_;
+
+  public:
+    explicit SharedMessageQueue(unsigned int maxSize = 0);
+    
+    ~SharedMessageQueue();
+
+    // This transfers the ownership of the message
+    void Enqueue(IDynamicObject* message);
+
+    // The caller is responsible to delete the dequeud message!
+    IDynamicObject* Dequeue(int32_t millisecondsTimeout);
+
+    bool WaitEmpty(int32_t millisecondsTimeout);
+
+    bool IsFifoPolicy() const
+    {
+      return isFifo_;
+    }
+
+    bool IsLifoPolicy() const
+    {
+      return !isFifo_;
+    }
+
+    void SetFifoPolicy();
+
+    void SetLifoPolicy();
+
+    void Clear();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/OrthancException.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Compatibility.h"
+#include "Enumerations.h"
+#include "Logging.h"
+#include "OrthancFramework.h"
+
+#include <stdint.h>
+#include <string>
+#include <memory>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC OrthancException
+  {
+  private:
+    OrthancException();  // Forbidden
+    
+    OrthancException& operator= (const OrthancException&);  // Forbidden
+
+    ErrorCode  errorCode_;
+    HttpStatus httpStatus_;
+
+    // New in Orthanc 1.5.0
+    std::unique_ptr<std::string>  details_;
+    
+  public:
+    OrthancException(const OrthancException& other) : 
+      errorCode_(other.errorCode_),
+      httpStatus_(other.httpStatus_)
+    {
+      if (other.details_.get() != NULL)
+      {
+        details_.reset(new std::string(*other.details_));
+      }
+    }
+
+    explicit OrthancException(ErrorCode errorCode) : 
+      errorCode_(errorCode),
+      httpStatus_(ConvertErrorCodeToHttpStatus(errorCode))
+    {
+    }
+
+    OrthancException(ErrorCode errorCode,
+                     const std::string& details,
+                     bool log = true) :
+      errorCode_(errorCode),
+      httpStatus_(ConvertErrorCodeToHttpStatus(errorCode)),
+      details_(new std::string(details))
+    {
+#if ORTHANC_ENABLE_LOGGING == 1
+      if (log)
+      {
+        LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details;
+      }
+#endif
+    }
+
+    OrthancException(ErrorCode errorCode,
+                     HttpStatus httpStatus) :
+      errorCode_(errorCode),
+      httpStatus_(httpStatus)
+    {
+    }
+
+    OrthancException(ErrorCode errorCode,
+                     HttpStatus httpStatus,
+                     const std::string& details,
+                     bool log = true) :
+      errorCode_(errorCode),
+      httpStatus_(httpStatus),
+      details_(new std::string(details))
+    {
+#if ORTHANC_ENABLE_LOGGING == 1
+      if (log)
+      {
+        LOG(ERROR) << EnumerationToString(errorCode_) << ": " << details;
+      }
+#endif
+    }
+
+    ErrorCode GetErrorCode() const
+    {
+      return errorCode_;
+    }
+
+    HttpStatus GetHttpStatus() const
+    {
+      return httpStatus_;
+    }
+
+    const char* What() const
+    {
+      return EnumerationToString(errorCode_);
+    }
+
+    bool HasDetails() const
+    {
+      return details_.get() != NULL;
+    }
+
+    const char* GetDetails() const
+    {
+      if (details_.get() == NULL)
+      {
+        return "";
+      }
+      else
+      {
+        return details_->c_str();
+      }
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/OrthancFramework.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#if !defined(ORTHANC_ENABLE_CURL)
+#  error Macro ORTHANC_ENABLE_CURL must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error Macro ORTHANC_ENABLE_SSL must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK)
+#  error Macro ORTHANC_ENABLE_DCMTK must be defined
+#endif
+
+#if !defined(ORTHANC_ENABLE_DCMTK_NETWORKING)
+#  error Macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
+#endif
+
+#if ORTHANC_ENABLE_CURL == 1
+#  include "HttpClient.h"
+#endif
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "DicomParsing/FromDcmtkBridge.h"
+#  if ORTHANC_ENABLE_DCMTK_NETWORKING == 1
+#    include <dcmtk/dcmnet/dul.h>
+#  endif
+#endif
+
+#include "Logging.h"
+#include "Toolbox.h"
+
+
+namespace Orthanc
+{
+  void InitializeFramework(const std::string& locale,
+                           bool loadPrivateDictionary)
+  {
+    Logging::Initialize();
+
+#if (ORTHANC_ENABLE_LOCALE == 1) && !defined(__EMSCRIPTEN__)  // No global locale in wasm/asm.js
+    if (locale.empty())
+    {
+      Toolbox::InitializeGlobalLocale(NULL);
+    }
+    else
+    {
+      Toolbox::InitializeGlobalLocale(locale.c_str());
+    }
+#endif
+
+    Toolbox::InitializeOpenSsl();
+
+#if ORTHANC_ENABLE_CURL == 1
+    HttpClient::GlobalInitialize();
+#endif
+
+#if ORTHANC_ENABLE_DCMTK == 1
+    FromDcmtkBridge::InitializeDictionary(true);
+    FromDcmtkBridge::InitializeCodecs();
+#endif
+
+#if (ORTHANC_ENABLE_DCMTK == 1 &&               \
+     ORTHANC_ENABLE_DCMTK_NETWORKING == 1)
+    /* Disable "gethostbyaddr" (which results in memory leaks) and use raw IP addresses */
+    dcmDisableGethostbyaddr.set(OFTrue);
+#endif
+  }
+  
+
+  void FinalizeFramework()
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    FromDcmtkBridge::FinalizeCodecs();
+#endif
+
+#if ORTHANC_ENABLE_CURL == 1
+    HttpClient::GlobalFinalize();
+#endif
+    
+    Toolbox::FinalizeOpenSsl();
+
+#if (ORTHANC_ENABLE_LOCALE == 1) && !defined(__EMSCRIPTEN__)
+    Toolbox::FinalizeGlobalLocale();
+#endif
+    
+    Logging::Finalize();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/OrthancFramework.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+/**
+ * Besides the "pragma once" above that only protects this file,
+ * define a macro to prevent including different versions of
+ * "OrthancFramework.h"
+ **/
+#ifndef __ORTHANC_FRAMEWORK_H
+#define __ORTHANC_FRAMEWORK_H
+
+#if !defined(ORTHANC_BUILDING_FRAMEWORK_LIBRARY)
+#  error The macro ORTHANC_BUILDING_FRAMEWORK_LIBRARY must be defined
+#endif
+
+/**
+ * It is implied that if this file is used, we're building the Orthanc
+ * framework (not using it as a shared library): We don't use the
+ * common "BUILDING_DLL"
+ * construction. https://gcc.gnu.org/wiki/Visibility
+ **/
+#if ORTHANC_BUILDING_FRAMEWORK_LIBRARY == 1
+#  if defined(_WIN32) || defined (__CYGWIN__)
+#    define ORTHANC_PUBLIC __declspec(dllexport)
+#    define ORTHANC_LOCAL
+#  else
+#    if __GNUC__ >= 4
+#      define ORTHANC_PUBLIC __attribute__((visibility ("default")))
+#      define ORTHANC_LOCAL  __attribute__((visibility ("hidden")))
+#    else
+#      define ORTHANC_PUBLIC
+#      define ORTHANC_LOCAL
+#      pragma warning Unknown dynamic link import/export semantics
+#    endif
+#  endif
+#else
+#  define ORTHANC_PUBLIC
+#  define ORTHANC_LOCAL
+#endif
+
+
+#include <string>
+
+namespace Orthanc
+{
+  ORTHANC_PUBLIC void InitializeFramework(const std::string& locale,
+                                          bool loadPrivateDictionary);
+  
+  ORTHANC_PUBLIC void FinalizeFramework();
+}
+
+
+#endif /* __ORTHANC_FRAMEWORK_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Pkcs11.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,311 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot create an OpenSSL engine for PKCS#11");
+      }
+
+      // Create a PKCS#11 context using libp11
+      context_ = pkcs11_new();
+      if (!context_)
+      {
+        ENGINE_free(engine);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot create a libp11 context for PKCS#11");
+      }
+
+      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()) ||
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L // OpenSSL 1.0.2
+          !ENGINE_set_ECDSA(engine, PKCS11_get_ecdsa_method()) ||
+          !ENGINE_set_ECDH(engine, PKCS11_get_ecdh_method()) ||
+#else
+          !ENGINE_set_EC(engine, PKCS11_get_ec_key_method()) ||
+#endif
+
+          // Make OpenSSL know about our PKCS#11 engine
+          !ENGINE_add(engine))
+      {
+        pkcs11_finish(context_);
+        ENGINE_free(engine);
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot initialize the OpenSSL engine for PKCS#11");
+      }
+
+      // 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_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "The PKCS#11 engine has already been initialized");
+      }
+
+      if (module.empty() ||
+          !SystemToolbox::IsRegularFile(module))
+      {
+        throw OrthancException(
+          ErrorCode_InexistentFile,
+          "The PKCS#11 module must be a path to one shared library (DLL or .so)");
+      }
+
+      ENGINE* engine = LoadEngine();
+      if (!engine)
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot create an OpenSSL engine for PKCS#11");
+      }
+
+      if (!ENGINE_ctrl_cmd_string(engine, "MODULE_PATH", module.c_str(), 0))
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot configure the OpenSSL dynamic engine for PKCS#11");
+      }
+
+      if (verbose)
+      {
+        ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0);
+      }
+
+      if (!pin.empty() &&
+          !ENGINE_ctrl_cmd_string(engine, "PIN", pin.c_str(), 0)) 
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot set the PIN code for PKCS#11");
+      }
+  
+      if (!ENGINE_init(engine))
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot initialize the OpenSSL dynamic engine for PKCS#11");
+      }
+
+      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/OrthancFramework/Sources/Pkcs11.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/PrecompiledHeaders.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,34 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/PrecompiledHeaders.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(_WIN32) && !defined(NOMINMAX)
+#define NOMINMAX
+#endif
+
+#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
+
+#include "OrthancFramework.h"  // Must be the first one
+
+//#include <boost/date_time/posix_time/posix_time.hpp>
+//#include <boost/filesystem.hpp>
+#include <boost/lexical_cast.hpp>
+//#include <boost/locale.hpp>
+//#include <boost/regex.hpp>
+#include <boost/thread.hpp>
+#include <boost/thread/shared_mutex.hpp>
+
+#include <json/value.h>
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
+
+#include "Compatibility.h"
+#include "Enumerations.h"
+#include "Logging.h"
+#include "OrthancException.h"
+#include "OrthancFramework.h"
+#include "Toolbox.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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApi.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,278 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApi.h"
+
+#include "../Logging.h"
+
+#include <stdlib.h>   // To define "_exit()" under Windows
+#include <stdio.h>
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    class HttpHandlerVisitor : public RestApiHierarchy::IVisitor
+    {
+    private:
+      RestApi& api_;
+      RestApiOutput& output_;
+      RequestOrigin origin_;
+      const char* remoteIp_;
+      const char* username_;
+      HttpMethod method_;
+      const IHttpHandler::Arguments& headers_;
+      const IHttpHandler::Arguments& getArguments_;
+      const void* bodyData_;
+      size_t bodySize_;
+
+    public:
+      HttpHandlerVisitor(RestApi& api,
+                         RestApiOutput& output,
+                         RequestOrigin origin,
+                         const char* remoteIp,
+                         const char* username,
+                         HttpMethod method,
+                         const IHttpHandler::Arguments& headers,
+                         const IHttpHandler::Arguments& getArguments,
+                         const void* bodyData,
+                         size_t bodySize) :
+        api_(api),
+        output_(output),
+        origin_(origin),
+        remoteIp_(remoteIp),
+        username_(username),
+        method_(method),
+        headers_(headers),
+        getArguments_(getArguments),
+        bodyData_(bodyData),
+        bodySize_(bodySize)
+      {
+      }
+
+      virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                         const UriComponents& uri,
+                         const IHttpHandler::Arguments& components,
+                         const UriComponents& trailing)
+      {
+        if (resource.HasHandler(method_))
+        {
+          switch (method_)
+          {
+            case HttpMethod_Get:
+            {
+              RestApiGetCall call(output_, api_, origin_, remoteIp_, username_, 
+                                  headers_, components, trailing, uri, getArguments_);
+              resource.Handle(call);
+              return true;
+            }
+
+            case HttpMethod_Post:
+            {
+              RestApiPostCall call(output_, api_, origin_, remoteIp_, username_, 
+                                   headers_, components, trailing, uri, bodyData_, bodySize_);
+              resource.Handle(call);
+              return true;
+            }
+
+            case HttpMethod_Delete:
+            {
+              RestApiDeleteCall call(output_, api_, origin_, remoteIp_, username_, 
+                                     headers_, components, trailing, uri);
+              resource.Handle(call);
+              return true;
+            }
+
+            case HttpMethod_Put:
+            {
+              RestApiPutCall call(output_, api_, origin_, remoteIp_, username_, 
+                                  headers_, components, trailing, uri, bodyData_, bodySize_);
+              resource.Handle(call);
+              return true;
+            }
+
+            default:
+              return false;
+          }
+        }
+
+        return false;
+      }
+    };
+  }
+
+
+
+  static void AddMethod(std::string& target,
+                        const std::string& method)
+  {
+    if (target.size() > 0)
+      target += "," + method;
+    else
+      target = method;
+  }
+
+  static std::string  MethodsToString(const std::set<HttpMethod>& methods)
+  {
+    std::string s;
+
+    if (methods.find(HttpMethod_Get) != methods.end())
+    {
+      AddMethod(s, "GET");
+    }
+
+    if (methods.find(HttpMethod_Post) != methods.end())
+    {
+      AddMethod(s, "POST");
+    }
+
+    if (methods.find(HttpMethod_Put) != methods.end())
+    {
+      AddMethod(s, "PUT");
+    }
+
+    if (methods.find(HttpMethod_Delete) != methods.end())
+    {
+      AddMethod(s, "DELETE");
+    }
+
+    return s;
+  }
+
+
+
+  bool RestApi::Handle(HttpOutput& output,
+                       RequestOrigin origin,
+                       const char* remoteIp,
+                       const char* username,
+                       HttpMethod method,
+                       const UriComponents& uri,
+                       const Arguments& headers,
+                       const GetArguments& getArguments,
+                       const void* bodyData,
+                       size_t bodySize)
+  {
+    RestApiOutput wrappedOutput(output, method);
+
+#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
+      Arguments::const_iterator it = headers.find("accept");
+      if (it != headers.end())
+      {
+        std::vector<std::string> accepted;
+        Toolbox::TokenizeString(accepted, it->second, ';');
+        for (size_t i = 0; i < accepted.size(); i++)
+        {
+          if (accepted[i] == MIME_XML)
+          {
+            wrappedOutput.SetConvertJsonToXml(true);
+          }
+
+          if (accepted[i] == MIME_JSON)
+          {
+            wrappedOutput.SetConvertJsonToXml(false);
+          }
+        }
+      }
+    }
+#endif
+
+    Arguments compiled;
+    HttpToolbox::CompileGetArguments(compiled, getArguments);
+
+    HttpHandlerVisitor visitor(*this, wrappedOutput, origin, remoteIp, username, 
+                               method, headers, compiled, bodyData, bodySize);
+
+    if (root_.LookupResource(uri, visitor))
+    {
+      wrappedOutput.Finalize();
+      return true;
+    }
+
+    std::set<HttpMethod> methods;
+    root_.GetAcceptedMethods(methods, uri);
+
+    if (methods.empty())
+    {
+      return false;  // This URI is not served by this REST API
+    }
+    else
+    {
+      LOG(INFO) << "REST method " << EnumerationToString(method) 
+                << " not allowed on: " << Toolbox::FlattenUri(uri);
+
+      output.SendMethodNotAllowed(MethodsToString(methods));
+
+      return true;
+    }
+  }
+
+  void RestApi::Register(const std::string& path,
+                         RestApiGetCall::Handler handler)
+  {
+    root_.Register(path, handler);
+  }
+
+  void RestApi::Register(const std::string& path,
+                         RestApiPutCall::Handler handler)
+  {
+    root_.Register(path, handler);
+  }
+
+  void RestApi::Register(const std::string& path,
+                         RestApiPostCall::Handler handler)
+  {
+    root_.Register(path, handler);
+  }
+
+  void RestApi::Register(const std::string& path,
+                         RestApiDeleteCall::Handler handler)
+  {
+    root_.Register(path, handler);
+  }
+  
+  void RestApi::AutoListChildren(RestApiGetCall& call)
+  {
+    RestApi& context = call.GetContext();
+
+    Json::Value directory;
+    if (context.root_.GetDirectory(directory, call.GetFullUri()))
+    {
+      call.GetOutput().AnswerJson(directory);
+    }    
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApi.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiHierarchy.h"
+#include "../Compatibility.h"
+
+#include <list>
+
+namespace Orthanc
+{
+  class RestApi : public IHttpHandler
+  {
+  private:
+    RestApiHierarchy root_;
+
+  public:
+    static void AutoListChildren(RestApiGetCall& call);
+
+    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                            RequestOrigin origin,
+                                            const char* remoteIp,
+                                            const char* username,
+                                            HttpMethod method,
+                                            const UriComponents& uri,
+                                            const Arguments& headers)
+    {
+      return false;
+    }
+
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const void* bodyData,
+                        size_t bodySize);
+
+    void Register(const std::string& path,
+                  RestApiGetCall::Handler handler);
+
+    void Register(const std::string& path,
+                  RestApiPutCall::Handler handler);
+
+    void Register(const std::string& path,
+                  RestApiPostCall::Handler handler);
+
+    void Register(const std::string& path,
+                  RestApiDeleteCall::Handler handler);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiCall.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  bool RestApiCall::ParseJsonRequestInternal(Json::Value& result,
+                                             const void* body,
+                                             size_t size)
+  {
+    result.clear();
+    Json::Reader reader;
+    return reader.parse(reinterpret_cast<const char*>(body),
+                        reinterpret_cast<const char*>(body) + size, result);
+  }
+
+
+  std::string RestApiCall::FlattenUri() const
+  {
+    std::string s = "/";
+
+    for (size_t i = 0; i < fullUri_.size(); i++)
+    {
+      s += fullUri_[i] + "/";
+    }
+
+    return s;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiCall.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../HttpServer/IHttpHandler.h"
+#include "../HttpServer/HttpToolbox.h"
+#include "RestApiPath.h"
+#include "RestApiOutput.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class RestApi;
+
+  class RestApiCall : public boost::noncopyable
+  {
+  private:
+    RestApiOutput& output_;
+    RestApi& context_;
+    RequestOrigin origin_;
+    const char* remoteIp_;
+    const char* username_;
+    const IHttpHandler::Arguments& httpHeaders_;
+    const IHttpHandler::Arguments& uriComponents_;
+    const UriComponents& trailing_;
+    const UriComponents& fullUri_;
+
+  protected:
+    static bool ParseJsonRequestInternal(Json::Value& result,
+                                         const void* body,
+                                         size_t size);
+
+  public:
+    RestApiCall(RestApiOutput& output,
+                RestApi& context,
+                RequestOrigin origin,
+                const char* remoteIp,
+                const char* username,
+                const IHttpHandler::Arguments& httpHeaders,
+                const IHttpHandler::Arguments& uriComponents,
+                const UriComponents& trailing,
+                const UriComponents& fullUri) :
+      output_(output),
+      context_(context),
+      origin_(origin),
+      remoteIp_(remoteIp),
+      username_(username),
+      httpHeaders_(httpHeaders),
+      uriComponents_(uriComponents),
+      trailing_(trailing),
+      fullUri_(fullUri)
+    {
+    }
+
+    RestApiOutput& GetOutput()
+    {
+      return output_;
+    }
+
+    RestApi& GetContext()
+    {
+      return context_;
+    }
+    
+    const UriComponents& GetFullUri() const
+    {
+      return fullUri_;
+    }
+    
+    const UriComponents& GetTrailingUri() const
+    {
+      return trailing_;
+    }
+
+    std::string GetUriComponent(const std::string& name,
+                                const std::string& defaultValue) const
+    {
+      return HttpToolbox::GetArgument(uriComponents_, name, defaultValue);
+    }
+
+    std::string GetHttpHeader(const std::string& name,
+                              const std::string& defaultValue) const
+    {
+      return HttpToolbox::GetArgument(httpHeaders_, name, defaultValue);
+    }
+
+    const IHttpHandler::Arguments& GetHttpHeaders() const
+    {
+      return httpHeaders_;
+    }
+
+    void ParseCookies(IHttpHandler::Arguments& result) const
+    {
+      HttpToolbox::ParseCookies(result, httpHeaders_);
+    }
+
+    std::string FlattenUri() const;
+
+    RequestOrigin GetRequestOrigin() const
+    {
+      return origin_;
+    }
+
+    const char* GetRemoteIp() const
+    {
+      return remoteIp_;
+    }
+
+    const char* GetUsername() const
+    {
+      return username_;
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiDeleteCall.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiDeleteCall : public RestApiCall
+  {
+  public:
+    typedef void (*Handler) (RestApiDeleteCall& call);
+    
+    RestApiDeleteCall(RestApiOutput& output,
+                      RestApi& context,
+                      RequestOrigin origin,
+                      const char* remoteIp,
+                      const char* username,
+                      const IHttpHandler::Arguments& httpHeaders,
+                      const IHttpHandler::Arguments& uriComponents,
+                      const UriComponents& trailing,
+                      const UriComponents& fullUri) :
+      RestApiCall(output, context, origin, remoteIp, username,
+                  httpHeaders, uriComponents, trailing, fullUri)
+    {
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const
+    {
+      result.clear();
+      return true;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiGetCall.h"
+
+namespace Orthanc
+{
+  bool RestApiGetCall::ParseJsonRequest(Json::Value& result) const
+  {
+    result.clear();
+
+    for (IHttpHandler::Arguments::const_iterator 
+           it = getArguments_.begin(); it != getArguments_.end(); ++it)
+    {
+      result[it->first] = it->second;
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiGetCall.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiGetCall : public RestApiCall
+  {
+  private:
+    const IHttpHandler::Arguments& getArguments_;
+
+  public:
+    typedef void (*Handler) (RestApiGetCall& call);   
+
+    RestApiGetCall(RestApiOutput& output,
+                   RestApi& context,
+                   RequestOrigin origin,
+                   const char* remoteIp,
+                   const char* username,
+                   const IHttpHandler::Arguments& httpHeaders,
+                   const IHttpHandler::Arguments& uriComponents,
+                   const UriComponents& trailing,
+                   const UriComponents& fullUri,
+                   const IHttpHandler::Arguments& getArguments) :
+      RestApiCall(output, context, origin, remoteIp, username, 
+                  httpHeaders, uriComponents, trailing, fullUri),
+      getArguments_(getArguments)
+    {
+    }
+
+    std::string GetArgument(const std::string& name,
+                            const std::string& defaultValue) const
+    {
+      return HttpToolbox::GetArgument(getArguments_, name, defaultValue);
+    }
+
+    bool HasArgument(const std::string& name) const
+    {
+      return getArguments_.find(name) != getArguments_.end();
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,476 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiHierarchy.h"
+
+#include "../OrthancException.h"
+
+#include <cassert>
+#include <stdio.h>
+
+namespace Orthanc
+{
+  RestApiHierarchy::Resource::Resource() : 
+    getHandler_(NULL), 
+    postHandler_(NULL),
+    putHandler_(NULL), 
+    deleteHandler_(NULL)
+  {
+  }
+
+
+  bool RestApiHierarchy::Resource::HasHandler(HttpMethod method) const
+  {
+    switch (method)
+    {
+      case HttpMethod_Get:
+        return getHandler_ != NULL;
+
+      case HttpMethod_Post:
+        return postHandler_ != NULL;
+
+      case HttpMethod_Put:
+        return putHandler_ != NULL;
+
+      case HttpMethod_Delete:
+        return deleteHandler_ != NULL;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::IsEmpty() const
+  {
+    return (getHandler_ == NULL &&
+            postHandler_ == NULL &&
+            putHandler_ == NULL &&
+            deleteHandler_ == NULL);
+  }
+
+
+  RestApiHierarchy& RestApiHierarchy::AddChild(Children& children,
+                                               const std::string& name)
+  {
+    Children::iterator it = children.find(name);
+
+    if (it == children.end())
+    {
+      // Create new child
+      RestApiHierarchy *child = new RestApiHierarchy;
+      children[name] = child;
+      return *child;
+    }
+    else
+    {
+      return *it->second;
+    }
+  }
+
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiGetCall& call) const
+  {
+    if (getHandler_ != NULL)
+    {
+      getHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiPutCall& call) const
+  {
+    if (putHandler_ != NULL)
+    {
+      putHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiPostCall& call) const
+  {
+    if (postHandler_ != NULL)
+    {
+      postHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool RestApiHierarchy::Resource::Handle(RestApiDeleteCall& call) const
+  {
+    if (deleteHandler_ != NULL)
+    {
+      deleteHandler_(call);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+  void RestApiHierarchy::DeleteChildren(Children& children)
+  {
+    for (Children::iterator it = children.begin();
+         it != children.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  template <typename Handler>
+  void RestApiHierarchy::RegisterInternal(const RestApiPath& path,
+                                          Handler handler,
+                                          size_t level)
+  {
+    if (path.GetLevelCount() == level)
+    {
+      if (path.IsUniversalTrailing())
+      {
+        universalHandlers_.Register(handler);
+      }
+      else
+      {
+        handlers_.Register(handler);
+      }
+    }
+    else
+    {
+      RestApiHierarchy* child;
+      if (path.IsWildcardLevel(level))
+      {
+        child = &AddChild(wildcardChildren_, path.GetWildcardName(level));
+      }
+      else
+      {
+        child = &AddChild(children_, path.GetLevelName(level));
+      }
+
+      child->RegisterInternal(path, handler, level + 1);
+    }
+  }
+
+
+  bool RestApiHierarchy::LookupResource(IHttpHandler::Arguments& components,
+                                       const UriComponents& uri,
+                                       IVisitor& visitor,
+                                       size_t level)
+  {
+    if (uri.size() != 0 &&
+        level > uri.size())
+    {
+      return false;
+    }
+
+    UriComponents trailing;
+
+    // Look for an exact match on the resource of interest
+      if (uri.size() == 0 ||
+          level == uri.size())
+    {
+      if (!handlers_.IsEmpty() &&
+          visitor.Visit(handlers_, uri, components, trailing))
+      {
+        return true;
+      }
+    }
+
+
+    if (level < uri.size())  // A recursive call is possible
+    {
+      // Try and go down in the hierarchy, using an exact match for the child
+      Children::const_iterator child = children_.find(uri[level]);
+      if (child != children_.end())
+      {
+        if (child->second->LookupResource(components, uri, visitor, level + 1))
+        {
+          return true;
+        }
+      }
+
+      // Try and go down in the hierarchy, using wildcard rules for children
+      for (child = wildcardChildren_.begin();
+           child != wildcardChildren_.end(); ++child)
+      {
+        IHttpHandler::Arguments subComponents = components;
+        subComponents[child->first] = uri[level];
+
+        if (child->second->LookupResource(subComponents, uri, visitor, level + 1))
+        {
+          return true;
+        }        
+      }
+    }
+
+
+    // As a last resort, call the universal handlers, if any
+    if (!universalHandlers_.IsEmpty())
+    {
+      trailing.resize(uri.size() - level);
+      size_t pos = 0;
+      for (size_t i = level; i < uri.size(); i++, pos++)
+      {
+        trailing[pos] = uri[i];
+      }
+
+      assert(pos == trailing.size());
+
+      if (visitor.Visit(universalHandlers_, uri, components, trailing))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  bool RestApiHierarchy::CanGenerateDirectory() const
+  {
+    return (universalHandlers_.IsEmpty() &&
+            wildcardChildren_.empty());
+  }
+
+
+  bool RestApiHierarchy::GetDirectory(Json::Value& result,
+                                      const UriComponents& uri,
+                                      size_t level)
+  {
+    if (uri.size() == level)
+    {
+      if (CanGenerateDirectory())
+      {
+        result = Json::arrayValue;
+
+        for (Children::const_iterator it = children_.begin();
+             it != children_.end(); ++it)
+        {
+          result.append(it->first);
+        }
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    Children::const_iterator child = children_.find(uri[level]);
+    if (child != children_.end())
+    {
+      if (child->second->GetDirectory(result, uri, level + 1))
+      {
+        return true;
+      }
+    }
+
+    for (child = wildcardChildren_.begin(); 
+         child != wildcardChildren_.end(); ++child)
+    {
+      if (child->second->GetDirectory(result, uri, level + 1))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+                       
+
+  RestApiHierarchy::~RestApiHierarchy()
+  {
+    DeleteChildren(children_);
+    DeleteChildren(wildcardChildren_);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiGetCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiPutCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiPostCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::Register(const std::string& uri,
+                                  RestApiDeleteCall::Handler handler)
+  {
+    RestApiPath path(uri);
+    RegisterInternal(path, handler, 0);
+  }
+
+  void RestApiHierarchy::CreateSiteMap(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    /*std::string s = " ";
+      if (handlers_.HasHandler(HttpMethod_Get))
+      {
+      s += "GET ";
+      }
+
+      if (handlers_.HasHandler(HttpMethod_Post))
+      {
+      s += "POST ";
+      }
+
+      if (handlers_.HasHandler(HttpMethod_Put))
+      {
+      s += "PUT ";
+      }
+
+      if (handlers_.HasHandler(HttpMethod_Delete))
+      {
+      s += "DELETE ";
+      }
+
+      target = s;*/
+      
+    for (Children::const_iterator it = children_.begin();
+         it != children_.end(); ++it)
+    {
+      it->second->CreateSiteMap(target[it->first]);
+    }
+      
+    for (Children::const_iterator it = wildcardChildren_.begin();
+         it != wildcardChildren_.end(); ++it)
+    {
+      it->second->CreateSiteMap(target["<" + it->first + ">"]);
+    }
+  }
+
+
+  bool RestApiHierarchy::LookupResource(const UriComponents& uri,
+                                        IVisitor& visitor)
+  {
+    IHttpHandler::Arguments components;
+    return LookupResource(components, uri, visitor, 0);
+  }    
+
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class AcceptedMethodsVisitor : public RestApiHierarchy::IVisitor
+    {
+    private:
+      std::set<HttpMethod>& methods_;
+
+    public:
+      AcceptedMethodsVisitor(std::set<HttpMethod>& methods) : methods_(methods)
+      {
+      }
+
+      virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                         const UriComponents& uri,
+                         const IHttpHandler::Arguments& components,
+                         const UriComponents& trailing)
+      {
+        if (trailing.size() == 0)  // Ignore universal handlers
+        {
+          if (resource.HasHandler(HttpMethod_Get))
+          {
+            methods_.insert(HttpMethod_Get);
+          }
+
+          if (resource.HasHandler(HttpMethod_Post))
+          {
+            methods_.insert(HttpMethod_Post);
+          }
+
+          if (resource.HasHandler(HttpMethod_Put))
+          {
+            methods_.insert(HttpMethod_Put);
+          }
+
+          if (resource.HasHandler(HttpMethod_Delete))
+          {
+            methods_.insert(HttpMethod_Delete);
+          }
+        }
+
+        return false;  // Continue to check all the possible ways to access this URI
+      }
+    };
+  }
+
+  void RestApiHierarchy::GetAcceptedMethods(std::set<HttpMethod>& methods,
+                                            const UriComponents& uri)
+  {
+    IHttpHandler::Arguments components;
+    AcceptedMethodsVisitor visitor(methods);
+    if (LookupResource(components, uri, visitor, 0))
+    {
+      Json::Value d;
+      if (GetDirectory(d, uri))
+      {
+        methods.insert(HttpMethod_Get);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiHierarchy.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiGetCall.h"
+#include "RestApiPostCall.h"
+#include "RestApiPutCall.h"
+#include "RestApiDeleteCall.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC RestApiHierarchy : public boost::noncopyable
+  {
+  public:
+    class ORTHANC_PUBLIC Resource : public boost::noncopyable
+    {
+    private:
+      RestApiGetCall::Handler     getHandler_;
+      RestApiPostCall::Handler    postHandler_;
+      RestApiPutCall::Handler     putHandler_;
+      RestApiDeleteCall::Handler  deleteHandler_;
+
+    public:
+      Resource();
+
+      bool HasHandler(HttpMethod method) const;
+
+      void Register(RestApiGetCall::Handler handler)
+      {
+        getHandler_ = handler;
+      }
+
+      void Register(RestApiPutCall::Handler handler)
+      {
+        putHandler_ = handler;
+      }
+
+      void Register(RestApiPostCall::Handler handler)
+      {
+        postHandler_ = handler;
+      }
+
+      void Register(RestApiDeleteCall::Handler handler)
+      {
+        deleteHandler_ = handler;
+      }
+
+      bool IsEmpty() const;
+
+      bool Handle(RestApiGetCall& call) const;
+
+      bool Handle(RestApiPutCall& call) const;
+
+      bool Handle(RestApiPostCall& call) const;
+
+      bool Handle(RestApiDeleteCall& call) const;
+    };
+
+
+    class IVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~IVisitor()
+      {
+      }
+
+      virtual bool Visit(const Resource& resource,
+                         const UriComponents& uri,
+                         const IHttpHandler::Arguments& components,
+                         const UriComponents& trailing) = 0;
+    };
+
+
+  private:
+    typedef std::map<std::string, RestApiHierarchy*>  Children;
+
+    Resource  handlers_;
+    Children  children_;
+    Children  wildcardChildren_;
+    Resource  universalHandlers_;
+
+    static RestApiHierarchy& AddChild(Children& children,
+                                      const std::string& name);
+
+    static void DeleteChildren(Children& children);
+
+    template <typename Handler>
+    void RegisterInternal(const RestApiPath& path,
+                          Handler handler,
+                          size_t level);
+
+    bool CanGenerateDirectory() const;
+
+    bool LookupResource(IHttpHandler::Arguments& components,
+                        const UriComponents& uri,
+                        IVisitor& visitor,
+                        size_t level);
+
+    bool GetDirectory(Json::Value& result,
+                      const UriComponents& uri,
+                      size_t level);
+
+  public:
+    ~RestApiHierarchy();
+
+    void Register(const std::string& uri,
+                  RestApiGetCall::Handler handler);
+
+    void Register(const std::string& uri,
+                  RestApiPutCall::Handler handler);
+
+    void Register(const std::string& uri,
+                  RestApiPostCall::Handler handler);
+
+    void Register(const std::string& uri,
+                  RestApiDeleteCall::Handler handler);
+
+    void CreateSiteMap(Json::Value& target) const;
+
+    bool GetDirectory(Json::Value& result,
+                      const UriComponents& uri)
+    {
+      return GetDirectory(result, uri, 0);
+    }
+
+    bool LookupResource(const UriComponents& uri,
+                        IVisitor& visitor);
+
+    void GetAcceptedMethods(std::set<HttpMethod>& methods,
+                            const UriComponents& uri);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,222 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiOutput.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+
+
+namespace Orthanc
+{
+  RestApiOutput::RestApiOutput(HttpOutput& output,
+                               HttpMethod method) : 
+    output_(output),
+    method_(method),
+    convertJsonToXml_(false)
+  {
+    alreadySent_ = false;
+  }
+
+  RestApiOutput::~RestApiOutput()
+  {
+  }
+
+  void RestApiOutput::Finalize()
+  {
+    if (!alreadySent_)
+    {
+      if (method_ == HttpMethod_Post)
+      {
+        output_.SendStatus(HttpStatus_400_BadRequest);
+      }
+      else
+      {
+        output_.SendStatus(HttpStatus_404_NotFound);
+      }
+    }
+  }
+  
+  void RestApiOutput::CheckStatus()
+  {
+    if (alreadySent_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void RestApiOutput::AnswerStream(IHttpStreamAnswer& stream)
+  {
+    CheckStatus();
+    output_.Answer(stream);
+    alreadySent_ = true;
+  }
+
+  void RestApiOutput::AnswerJson(const Json::Value& value)
+  {
+    CheckStatus();
+
+    if (convertJsonToXml_)
+    {
+#if ORTHANC_ENABLE_PUGIXML == 1
+      std::string s;
+      Toolbox::JsonToXml(s, value);
+
+      output_.SetContentType(MIME_XML_UTF8);
+      output_.Answer(s);
+#else
+      throw OrthancException(ErrorCode_InternalError,
+                             "Orthanc was compiled without XML support");
+#endif
+    }
+    else
+    {
+      Json::StyledWriter writer;
+      std::string s = writer.write(value);
+      
+      output_.SetContentType(MIME_JSON_UTF8);      
+      output_.Answer(s);
+    }
+
+    alreadySent_ = true;
+  }
+
+  void RestApiOutput::AnswerBuffer(const std::string& buffer,
+                                   MimeType contentType)
+  {
+    AnswerBuffer(buffer.size() == 0 ? NULL : buffer.c_str(),
+                 buffer.size(), contentType);
+  }
+
+  void RestApiOutput::AnswerBuffer(const void* buffer,
+                                   size_t length,
+                                   MimeType contentType)
+  {
+    CheckStatus();
+
+    if (convertJsonToXml_ &&
+        contentType == MimeType_Json)
+    {
+      Json::Value json;
+      Json::Reader reader;
+      if (reader.parse(reinterpret_cast<const char*>(buffer),
+                       reinterpret_cast<const char*>(buffer) + length, json))
+      {
+        AnswerJson(json);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The REST API tries and answers with an invalid JSON file");
+      } 
+    }
+    else
+    {
+      output_.SetContentType(contentType);
+      output_.Answer(buffer, length);
+      alreadySent_ = true;
+    }
+  }
+
+  void RestApiOutput::Redirect(const std::string& path)
+  {
+    CheckStatus();
+    output_.Redirect(path);
+    alreadySent_ = true;
+  }
+
+  void RestApiOutput::SignalErrorInternal(HttpStatus status,
+					  const char* message,
+					  size_t messageSize)
+  {
+    if (status != HttpStatus_400_BadRequest &&
+        status != HttpStatus_403_Forbidden &&
+        status != HttpStatus_500_InternalServerError &&
+        status != HttpStatus_415_UnsupportedMediaType)
+    {
+      throw OrthancException(ErrorCode_BadHttpStatusInRest);
+    }
+
+    CheckStatus();
+    output_.SendStatus(status, message, messageSize);
+    alreadySent_ = true;    
+  }
+
+  void RestApiOutput::SignalError(HttpStatus status)
+  {
+    SignalErrorInternal(status, NULL, 0);
+  }
+
+  void RestApiOutput::SignalError(HttpStatus status,
+				  const std::string& message)
+  {
+    SignalErrorInternal(status, message.c_str(), message.size());
+  }
+
+  void RestApiOutput::SetCookie(const std::string& name,
+                                const std::string& value,
+                                unsigned int maxAge)
+  {
+    if (name.find(";") != std::string::npos ||
+        name.find(" ") != std::string::npos ||
+        value.find(";") != std::string::npos ||
+        value.find(" ") != std::string::npos)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    CheckStatus();
+
+    std::string v = value + ";path=/";
+
+    if (maxAge != 0)
+    {
+      v += ";max-age=" + boost::lexical_cast<std::string>(maxAge);
+    }
+
+    output_.SetCookie(name, v);
+  }
+
+  void RestApiOutput::ResetCookie(const std::string& name)
+  {
+    // This marks the cookie to be deleted by the browser in 1 second,
+    // and before it actually gets deleted, its value is set to the
+    // empty string
+    SetCookie(name, "", 1);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiOutput.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../HttpServer/HttpOutput.h"
+#include "../HttpServer/HttpFileSender.h"
+
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class RestApiOutput
+  {
+  private:
+    HttpOutput&  output_;
+    HttpMethod   method_;
+    bool         alreadySent_;
+    bool         convertJsonToXml_;
+
+    void CheckStatus();
+
+    void SignalErrorInternal(HttpStatus status,
+			     const char* message,
+			     size_t messageSize);
+
+  public:
+    RestApiOutput(HttpOutput& output,
+                  HttpMethod method);
+
+    ~RestApiOutput();
+
+    void SetConvertJsonToXml(bool convert)
+    {
+      convertJsonToXml_ = convert;
+    }
+
+    bool IsConvertJsonToXml() const
+    {
+      return convertJsonToXml_;
+    }
+
+    void AnswerStream(IHttpStreamAnswer& stream);
+
+    void AnswerJson(const Json::Value& value);
+
+    void AnswerBuffer(const std::string& buffer,
+                      MimeType contentType);
+
+    void AnswerBuffer(const void* buffer,
+                      size_t length,
+                      MimeType contentType);
+
+    void SignalError(HttpStatus status);
+
+    void SignalError(HttpStatus status,
+		     const std::string& message);
+
+    void Redirect(const std::string& path);
+
+    void SetCookie(const std::string& name,
+                   const std::string& value,
+                   unsigned int maxAge = 0);
+
+    void ResetCookie(const std::string& name);
+
+    void Finalize();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiPath.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,181 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiPath.h"
+
+#include "../OrthancException.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  RestApiPath::RestApiPath(const std::string& uri)
+  {
+    Toolbox::SplitUriComponents(uri_, uri);
+
+    if (uri_.size() == 0)
+    {
+      hasTrailing_ = false;
+      return;
+    }
+
+    if (uri_.back() == "*")
+    {
+      hasTrailing_ = true;
+      uri_.pop_back();
+    }
+    else
+    {
+      hasTrailing_ = false;
+    }
+
+    components_.resize(uri_.size());
+    for (size_t i = 0; i < uri_.size(); i++)
+    {
+      size_t s = uri_[i].size();
+      assert(s > 0);
+
+      if (uri_[i][0] == '{' && 
+          uri_[i][s - 1] == '}')
+      {
+        components_[i] = uri_[i].substr(1, s - 2);
+        uri_[i] = "";
+      }
+      else
+      {
+        components_[i] = "";
+      }
+    }
+  }
+
+  bool RestApiPath::Match(IHttpHandler::Arguments& components,
+                          UriComponents& trailing,
+                          const std::string& uriRaw) const
+  {
+    UriComponents uri;
+    Toolbox::SplitUriComponents(uri, uriRaw);
+    return Match(components, trailing, uri);
+  }
+
+  bool RestApiPath::Match(IHttpHandler::Arguments& components,
+                          UriComponents& trailing,
+                          const UriComponents& uri) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (uri.size() < uri_.size())
+    {
+      return false;
+    }
+
+    if (!hasTrailing_ && uri.size() > uri_.size())
+    {
+      return false;
+    }
+
+    components.clear();
+    trailing.clear();
+
+    assert(uri_.size() <= uri.size());
+    for (size_t i = 0; i < uri_.size(); i++)
+    {
+      if (components_[i].size() == 0)
+      {
+        // This URI component is not a free parameter
+        if (uri_[i] != uri[i])
+        {
+          return false;
+        }
+      }
+      else
+      {
+        // This URI component is a free parameter
+        components[components_[i]] = uri[i];
+      }
+    }
+
+    if (hasTrailing_)
+    {
+      trailing.assign(uri.begin() + uri_.size(), uri.end());
+    }
+
+    return true;
+  }
+
+
+  bool RestApiPath::Match(const UriComponents& uri) const
+  {
+    IHttpHandler::Arguments components;
+    UriComponents trailing;
+    return Match(components, trailing, uri);
+  }
+
+
+  bool RestApiPath::IsWildcardLevel(size_t level) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (level >= uri_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return uri_[level].length() == 0;
+  }
+
+  const std::string& RestApiPath::GetWildcardName(size_t level) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (!IsWildcardLevel(level))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    return components_[level];
+  }
+
+  const std::string& RestApiPath::GetLevelName(size_t level) const
+  {
+    assert(uri_.size() == components_.size());
+
+    if (IsWildcardLevel(level))
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    return uri_[level];
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiPath.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Toolbox.h"
+#include "../HttpServer/IHttpHandler.h"
+
+#include <map>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC RestApiPath : public boost::noncopyable
+  {
+  private:
+    UriComponents uri_;
+    bool hasTrailing_;
+    std::vector<std::string> components_;
+
+  public:
+    RestApiPath(const std::string& uri);
+
+    // This version is slower
+    bool Match(IHttpHandler::Arguments& components,
+               UriComponents& trailing,
+               const std::string& uriRaw) const;
+
+    bool Match(IHttpHandler::Arguments& components,
+               UriComponents& trailing,
+               const UriComponents& uri) const;
+
+    bool Match(const UriComponents& uri) const;
+
+    size_t GetLevelCount() const
+    {
+      return uri_.size();
+    }
+
+    bool IsWildcardLevel(size_t level) const;
+
+    bool IsUniversalTrailing() const
+    {
+      return hasTrailing_;
+    }
+
+    const std::string& GetWildcardName(size_t level) const;
+
+    const std::string& GetLevelName(size_t level) const;
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiPostCall.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiPostCall : public RestApiCall
+  {
+  private:
+    const void* bodyData_;
+    size_t bodySize_;
+
+  public:
+    typedef void (*Handler) (RestApiPostCall& call);
+    
+    RestApiPostCall(RestApiOutput& output,
+                    RestApi& context,
+                    RequestOrigin origin,
+                    const char* remoteIp,
+                    const char* username,
+                    const IHttpHandler::Arguments& httpHeaders,
+                    const IHttpHandler::Arguments& uriComponents,
+                    const UriComponents& trailing,
+                    const UriComponents& fullUri,
+                    const void* bodyData,
+                    size_t bodySize) :
+      RestApiCall(output, context, origin, remoteIp, username, 
+                  httpHeaders, uriComponents, trailing, fullUri),
+      bodyData_(bodyData),
+      bodySize_(bodySize)
+    {
+    }
+
+    const void* GetBodyData() const
+    {
+      return bodyData_;
+    }
+
+    size_t GetBodySize() const
+    {
+      return bodySize_;
+    }
+
+    void BodyToString(std::string& result) const
+    {
+      result.assign(reinterpret_cast<const char*>(bodyData_), bodySize_);
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const
+    {
+      return ParseJsonRequestInternal(result, reinterpret_cast<const char*>(bodyData_), bodySize_);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/RestApi/RestApiPutCall.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "RestApiCall.h"
+
+namespace Orthanc
+{
+  class RestApiPutCall : public RestApiCall
+  {
+  private:
+    const void* bodyData_;
+    size_t bodySize_;
+
+  public:
+    typedef void (*Handler) (RestApiPutCall& call);
+    
+    RestApiPutCall(RestApiOutput& output,
+                   RestApi& context,
+                   RequestOrigin origin,
+                   const char* remoteIp,
+                   const char* username,
+                   const IHttpHandler::Arguments& httpHeaders,
+                   const IHttpHandler::Arguments& uriComponents,
+                   const UriComponents& trailing,
+                   const UriComponents& fullUri,
+                   const void* bodyData,
+                   size_t bodySize) :
+      RestApiCall(output, context, origin, remoteIp, username,
+                  httpHeaders, uriComponents, trailing, fullUri),
+      bodyData_(bodyData),
+      bodySize_(bodySize)
+    {
+    }
+
+    const void* GetBodyData() const
+    {
+      return bodyData_;
+    }
+
+    size_t GetBodySize() const
+    {
+      return bodySize_;
+    }
+
+    void BodyToString(std::string& result) const
+    {
+      result.assign(reinterpret_cast<const char*>(bodyData_), bodySize_);
+    }
+
+    virtual bool ParseJsonRequest(Json::Value& result) const
+    {
+      return ParseJsonRequestInternal(result, reinterpret_cast<const char*>(bodyData_), bodySize_);
+    }      
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/Connection.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,399 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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.
+ **/
+
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../PrecompiledHeaders.h"
+#endif
+
+#include "Connection.h"
+#include "OrthancSQLiteException.h"
+
+#include <memory>
+#include <cassert>
+#include <string.h>
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../Logging.h"
+#endif
+
+#include "sqlite3.h"
+
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    Connection::Connection() :
+      db_(NULL),
+      transactionNesting_(0),
+      needsRollback_(false)
+    {
+    }
+
+
+    Connection::~Connection()
+    {
+      Close();
+    }
+
+
+    void Connection::CheckIsOpen() const
+    {
+      if (!db_)
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteNotOpened);
+      }
+    }
+
+    void Connection::Open(const std::string& path)
+    {
+      if (db_) 
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteAlreadyOpened);
+      }
+
+      int err = sqlite3_open(path.c_str(), &db_);
+      if (err != SQLITE_OK) 
+      {
+        Close();
+        db_ = NULL;
+        throw OrthancSQLiteException(ErrorCode_SQLiteCannotOpen);
+      }
+
+      // Execute PRAGMAs at this point
+      // http://www.sqlite.org/pragma.html
+      Execute("PRAGMA FOREIGN_KEYS=ON;");
+      Execute("PRAGMA RECURSIVE_TRIGGERS=ON;");
+    }
+
+    void Connection::OpenInMemory()
+    {
+      Open(":memory:");
+    }
+
+    void Connection::Close() 
+    {
+      ClearCache();
+
+      if (db_)
+      {
+        sqlite3_close(db_);
+        db_ = NULL;
+      }
+    }
+
+    void Connection::ClearCache()
+    {
+      for (CachedStatements::iterator 
+             it = cachedStatements_.begin(); 
+           it != cachedStatements_.end(); ++it)
+      {
+        delete it->second;
+      }
+
+      cachedStatements_.clear();
+    }
+
+
+    StatementReference& Connection::GetCachedStatement(const StatementId& id,
+                                                       const char* sql)
+    {
+      CachedStatements::iterator i = cachedStatements_.find(id);
+      if (i != cachedStatements_.end())
+      {
+        if (i->second->GetReferenceCount() >= 1)
+        {
+          throw OrthancSQLiteException(ErrorCode_SQLiteStatementAlreadyUsed);
+        }
+
+        return *i->second;
+      }
+      else
+      {
+        StatementReference* statement = new StatementReference(db_, sql);
+        cachedStatements_[id] = statement;
+        return *statement;
+      }
+    }
+
+
+    bool Connection::Execute(const char* sql) 
+    {
+#if ORTHANC_SQLITE_STANDALONE != 1
+      VLOG(1) << "SQLite::Connection::Execute " << sql;
+#endif
+
+      CheckIsOpen();
+
+      int error = sqlite3_exec(db_, sql, NULL, NULL, NULL);
+      if (error == SQLITE_ERROR)
+      {
+#if ORTHANC_SQLITE_STANDALONE != 1
+        LOG(ERROR) << "SQLite execute error: " << sqlite3_errmsg(db_)
+                   << " (" << sqlite3_extended_errcode(db_) << ")";
+#endif
+
+        throw OrthancSQLiteException(ErrorCode_SQLiteExecute);
+      }
+      else
+      {
+        return error == SQLITE_OK;
+      }
+    }
+
+    int  Connection::ExecuteAndReturnErrorCode(const char* sql)
+    {
+      CheckIsOpen();
+      return sqlite3_exec(db_, sql, NULL, NULL, NULL);
+    }
+
+    // Info querying -------------------------------------------------------------
+
+    bool Connection::IsSQLValid(const char* sql) 
+    {
+      sqlite3_stmt* stmt = NULL;
+      if (sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL) != SQLITE_OK)
+        return false;
+
+      sqlite3_finalize(stmt);
+      return true;
+    }
+
+    bool Connection::DoesTableOrIndexExist(const char* name, 
+                                           const char* type) const
+    {
+      // Our SQL is non-mutating, so this cast is OK.
+      Statement statement(const_cast<Connection&>(*this), 
+                          "SELECT name FROM sqlite_master WHERE type=? AND name=?");
+      statement.BindString(0, type);
+      statement.BindString(1, name);
+      return statement.Step();  // Table exists if any row was returned.
+    }
+
+    bool Connection::DoesTableExist(const char* table_name) const
+    {
+      return DoesTableOrIndexExist(table_name, "table");
+    }
+
+    bool Connection::DoesIndexExist(const char* index_name) const
+    {
+      return DoesTableOrIndexExist(index_name, "index");
+    }
+
+    bool Connection::DoesColumnExist(const char* table_name, const char* column_name) const
+    {
+      std::string sql("PRAGMA TABLE_INFO(");
+      sql.append(table_name);
+      sql.append(")");
+
+      // Our SQL is non-mutating, so this cast is OK.
+      Statement statement(const_cast<Connection&>(*this), sql.c_str());
+
+      while (statement.Step()) {
+        if (!statement.ColumnString(1).compare(column_name))
+          return true;
+      }
+      return false;
+    }
+
+    int64_t Connection::GetLastInsertRowId() const
+    {
+      return sqlite3_last_insert_rowid(db_);
+    }
+
+    int Connection::GetLastChangeCount() const
+    {
+      return sqlite3_changes(db_);
+    }
+
+    int Connection::GetErrorCode() const 
+    {
+      return sqlite3_errcode(db_);
+    }
+
+    int Connection::GetLastErrno() const 
+    {
+      int err = 0;
+      if (SQLITE_OK != sqlite3_file_control(db_, NULL, SQLITE_LAST_ERRNO, &err))
+        return -2;
+
+      return err;
+    }
+
+    const char* Connection::GetErrorMessage() const 
+    {
+      return sqlite3_errmsg(db_);
+    }
+
+
+    bool Connection::BeginTransaction()
+    {
+      if (needsRollback_)
+      {
+        assert(transactionNesting_ > 0);
+
+        // When we're going to rollback, fail on this begin and don't actually
+        // mark us as entering the nested transaction.
+        return false;
+      }
+
+      bool success = true;
+      if (!transactionNesting_) 
+      {
+        needsRollback_ = false;
+
+        Statement begin(*this, SQLITE_FROM_HERE, "BEGIN TRANSACTION");
+        if (!begin.Run())
+          return false;
+      }
+      transactionNesting_++;
+      return success;
+    }
+
+    void Connection::RollbackTransaction()
+    {
+      if (!transactionNesting_)
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction);
+      }
+
+      transactionNesting_--;
+
+      if (transactionNesting_ > 0)
+      {
+        // Mark the outermost transaction as needing rollback.
+        needsRollback_ = true;
+        return;
+      }
+
+      DoRollback();
+    }
+
+    bool Connection::CommitTransaction() 
+    {
+      if (!transactionNesting_) 
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteCommitWithoutTransaction);
+      }
+      transactionNesting_--;
+
+      if (transactionNesting_ > 0) 
+      {
+        // Mark any nested transactions as failing after we've already got one.
+        return !needsRollback_;
+      }
+
+      if (needsRollback_) 
+      {
+        DoRollback();
+        return false;
+      }
+
+      Statement commit(*this, SQLITE_FROM_HERE, "COMMIT");
+      return commit.Run();
+    }
+
+    void Connection::DoRollback() 
+    {
+      Statement rollback(*this, SQLITE_FROM_HERE, "ROLLBACK");
+      rollback.Run();
+      needsRollback_ = false;
+    }
+
+
+
+
+
+
+    static void ScalarFunctionCaller(sqlite3_context* rawContext,
+                                     int argc,
+                                     sqlite3_value** argv)
+    {
+      FunctionContext context(rawContext, argc, argv);
+
+      void* payload = sqlite3_user_data(rawContext);
+      assert(payload != NULL);
+
+      IScalarFunction& func = *reinterpret_cast<IScalarFunction*>(payload);
+      func.Compute(context);
+    }
+
+
+    static void ScalarFunctionDestroyer(void* payload)
+    {
+      assert(payload != NULL);
+      delete reinterpret_cast<IScalarFunction*>(payload);
+    }
+
+
+    IScalarFunction* Connection::Register(IScalarFunction* func)
+    {
+      int err = sqlite3_create_function_v2(db_, 
+                                           func->GetName(), 
+                                           func->GetCardinality(),
+                                           SQLITE_UTF8, 
+                                           func,
+                                           ScalarFunctionCaller,
+                                           NULL,
+                                           NULL,
+                                           ScalarFunctionDestroyer);
+
+      if (err != SQLITE_OK)
+      {
+        delete func;
+        throw OrthancSQLiteException(ErrorCode_SQLiteRegisterFunction);
+      }
+
+      return func;
+    }
+
+
+    void Connection::FlushToDisk()
+    {
+#if ORTHANC_SQLITE_STANDALONE != 1
+      VLOG(1) << "SQLite::Connection::FlushToDisk";
+#endif
+
+      int err = sqlite3_wal_checkpoint(db_, NULL);
+
+      if (err != SQLITE_OK)
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteFlush);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/Connection.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,175 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+#include "Statement.h"
+#include "IScalarFunction.h"
+#include "SQLiteTypes.h"
+
+#include <string>
+#include <map>
+
+#define SQLITE_FROM_HERE ::Orthanc::SQLite::StatementId(__FILE__, __LINE__)
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ORTHANC_PUBLIC Connection : NonCopyable
+    {
+      friend class Statement;
+      friend class Transaction;
+
+    private:
+      // All cached statements. Keeping a reference to these statements means that
+      // they'll remain active.
+      typedef std::map<StatementId, StatementReference*>  CachedStatements;
+      CachedStatements cachedStatements_;
+
+      // The actual sqlite database. Will be NULL before Init has been called or if
+      // Init resulted in an error.
+      sqlite3* db_;
+
+      // Number of currently-nested transactions.
+      int transactionNesting_;
+
+      // True if any of the currently nested transactions have been rolled back.
+      // When we get to the outermost transaction, this will determine if we do
+      // a rollback instead of a commit.
+      bool needsRollback_;
+
+      void ClearCache();
+
+      void CheckIsOpen() const;
+
+      sqlite3* GetWrappedObject()
+      {
+        return db_;
+      }
+
+      StatementReference& GetCachedStatement(const StatementId& id,
+                                             const char* sql);
+
+      bool DoesTableOrIndexExist(const char* name, 
+                                 const char* type) const;
+
+      void DoRollback();
+
+    public:
+      // The database is opened by calling Open[InMemory](). Any uncommitted
+      // transactions will be rolled back when this object is deleted.
+      Connection();
+      ~Connection();
+
+      void Open(const std::string& path);
+
+      void OpenInMemory();
+
+      void Close();
+
+      bool Execute(const char* sql);
+
+      bool Execute(const std::string& sql)
+      {
+        return Execute(sql.c_str());
+      }
+
+      void FlushToDisk();
+
+      IScalarFunction* Register(IScalarFunction* func);  // Takes the ownership of the function
+
+      // Info querying -------------------------------------------------------------
+
+      // Used to check a |sql| statement for syntactic validity. If the
+      // statement is valid SQL, returns true.
+      bool IsSQLValid(const char* sql);
+
+      // Returns true if the given table exists.
+      bool DoesTableExist(const char* table_name) const;
+
+      // Returns true if the given index exists.
+      bool DoesIndexExist(const char* index_name) const;
+    
+      // Returns true if a column with the given name exists in the given table.
+      bool DoesColumnExist(const char* table_name, const char* column_name) const;
+
+      // Returns sqlite's internal ID for the last inserted row. Valid only
+      // immediately after an insert.
+      int64_t GetLastInsertRowId() const;
+
+      // Returns sqlite's count of the number of rows modified by the last
+      // statement executed. Will be 0 if no statement has executed or the database
+      // is closed.
+      int GetLastChangeCount() const;
+
+      // Errors --------------------------------------------------------------------
+
+      // Returns the error code associated with the last sqlite operation.
+      int GetErrorCode() const;
+
+      // Returns the errno associated with GetErrorCode().  See
+      // SQLITE_LAST_ERRNO in SQLite documentation.
+      int GetLastErrno() const;
+
+      // Returns a pointer to a statically allocated string associated with the
+      // last sqlite operation.
+      const char* GetErrorMessage() const;
+
+
+      // Diagnostics (for unit tests) ----------------------------------------------
+
+      int ExecuteAndReturnErrorCode(const char* sql);
+    
+      bool HasCachedStatement(const StatementId& id) const
+      {
+        return cachedStatements_.find(id) != cachedStatements_.end();
+      }
+
+      int GetTransactionNesting() const
+      {
+        return transactionNesting_;
+      }
+
+      // Transactions --------------------------------------------------------------
+
+      bool BeginTransaction();
+      void RollbackTransaction();
+      bool CommitTransaction();      
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/FunctionContext.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,127 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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.
+ **/
+
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../PrecompiledHeaders.h"
+#endif
+
+#include "FunctionContext.h"
+#include "OrthancSQLiteException.h"
+
+#include <string>
+
+#include "sqlite3.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    FunctionContext::FunctionContext(struct sqlite3_context* context,
+                                     int argc,
+                                     Internals::SQLiteValue** argv)
+    {
+      assert(context != NULL);
+      assert(argc >= 0);
+      assert(argv != NULL);
+
+      context_ = context;
+      argc_ = static_cast<unsigned int>(argc);
+      argv_ = argv;
+    }
+
+    void FunctionContext::CheckIndex(unsigned int index) const
+    {
+      if (index >= argc_)
+      {
+        throw OrthancSQLiteException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    ColumnType FunctionContext::GetColumnType(unsigned int index) const
+    {
+      CheckIndex(index);
+      return static_cast<SQLite::ColumnType>(sqlite3_value_type(argv_[index]));
+    }
+
+    int FunctionContext::GetIntValue(unsigned int index) const
+    {
+      CheckIndex(index);
+      return sqlite3_value_int(argv_[index]);
+    }
+
+    int64_t FunctionContext::GetInt64Value(unsigned int index) const
+    {
+      CheckIndex(index);
+      return sqlite3_value_int64(argv_[index]);
+    }
+
+    double FunctionContext::GetDoubleValue(unsigned int index) const
+    {
+      CheckIndex(index);
+      return sqlite3_value_double(argv_[index]);
+    }
+
+    std::string FunctionContext::GetStringValue(unsigned int index) const
+    {
+      CheckIndex(index);
+      return std::string(reinterpret_cast<const char*>(sqlite3_value_text(argv_[index])));
+    }
+
+    bool FunctionContext::IsNullValue(unsigned int index) const
+    {
+      CheckIndex(index);
+      return sqlite3_value_type(argv_[index]) == SQLITE_NULL;
+    }
+  
+    void FunctionContext::SetNullResult()
+    {
+      sqlite3_result_null(context_);
+    }
+
+    void FunctionContext::SetIntResult(int value)
+    {
+      sqlite3_result_int(context_, value);
+    }
+
+    void FunctionContext::SetDoubleResult(double value)
+    {
+      sqlite3_result_double(context_, value);
+    }
+
+    void FunctionContext::SetStringResult(const std::string& str)
+    {
+      sqlite3_result_text(context_, str.data(), static_cast<int>(str.size()), SQLITE_TRANSIENT);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/FunctionContext.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,85 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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
+
+#include "Statement.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ORTHANC_PUBLIC FunctionContext : public NonCopyable
+    {
+      friend class Connection;
+
+    private:
+      struct sqlite3_context* context_;
+      unsigned int argc_;
+      Internals::SQLiteValue** argv_;
+
+      void CheckIndex(unsigned int index) const;
+
+    public:
+      FunctionContext(struct sqlite3_context* context,
+                      int argc,
+                      Internals::SQLiteValue** argv);
+
+      ColumnType GetColumnType(unsigned int index) const;
+ 
+      unsigned int GetParameterCount() const
+      {
+        return argc_;
+      }
+
+      int GetIntValue(unsigned int index) const;
+
+      int64_t GetInt64Value(unsigned int index) const;
+
+      double GetDoubleValue(unsigned int index) const;
+
+      std::string GetStringValue(unsigned int index) const;
+
+      bool IsNullValue(unsigned int index) const;
+  
+      void SetNullResult();
+
+      void SetIntResult(int value);
+
+      void SetDoubleResult(double value);
+
+      void SetStringResult(const std::string& str);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/IScalarFunction.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,58 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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
+
+#include "NonCopyable.h"
+#include "FunctionContext.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class IScalarFunction : public NonCopyable
+    {
+    public:
+      virtual ~IScalarFunction()
+      {
+      }
+
+      virtual const char* GetName() const = 0;
+
+      virtual unsigned int GetCardinality() const = 0;
+
+      virtual void Compute(FunctionContext& context) = 0;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/ITransaction.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,66 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+#include "NonCopyable.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ITransaction : public NonCopyable
+    {
+    public:
+      virtual ~ITransaction()
+      {
+      }
+
+      // Begins the transaction. This uses the default sqlite "deferred" transaction
+      // type, which means that the DB lock is lazily acquired the next time the
+      // database is accessed, not in the begin transaction command.
+      virtual void Begin() = 0;
+
+      // Rolls back the transaction. This will happen automatically if you do
+      // nothing when the transaction goes out of scope.
+      virtual void Rollback() = 0;
+
+      // Commits the transaction, returning true on success.
+      virtual void Commit() = 0;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/NonCopyable.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,60 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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 Google Inc., 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
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    // This class mimics "boost::noncopyable"
+    class NonCopyable
+    {
+    private:
+      NonCopyable(const NonCopyable&);
+
+      NonCopyable& operator= (const NonCopyable&);
+
+    protected:
+      NonCopyable()
+      {
+      }
+
+      ~NonCopyable()
+      {
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/OrthancSQLiteException.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,154 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+
+#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>
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    // Auto-generated by "Resources/GenerateErrorCodes.py"
+    enum ErrorCode
+    {
+      ErrorCode_ParameterOutOfRange,
+      ErrorCode_BadParameterType,
+      ErrorCode_SQLiteNotOpened,
+      ErrorCode_SQLiteAlreadyOpened,
+      ErrorCode_SQLiteCannotOpen,
+      ErrorCode_SQLiteStatementAlreadyUsed,
+      ErrorCode_SQLiteExecute,
+      ErrorCode_SQLiteRollbackWithoutTransaction,
+      ErrorCode_SQLiteCommitWithoutTransaction,
+      ErrorCode_SQLiteRegisterFunction,
+      ErrorCode_SQLiteFlush,
+      ErrorCode_SQLiteCannotRun,
+      ErrorCode_SQLiteCannotStep,
+      ErrorCode_SQLiteBindOutOfRange,
+      ErrorCode_SQLitePrepareStatement,
+      ErrorCode_SQLiteTransactionAlreadyStarted,
+      ErrorCode_SQLiteTransactionCommit,
+      ErrorCode_SQLiteTransactionBegin
+    };
+
+    class OrthancSQLiteException : public ::std::runtime_error
+    {
+    public:
+      OrthancSQLiteException(ErrorCode error) :
+        ::std::runtime_error(EnumerationToString(error))
+      {
+      }
+
+      // Auto-generated by "Resources/GenerateErrorCodes.py"
+      static const char* EnumerationToString(ErrorCode code)
+      {
+        switch (code)
+        {
+          case ErrorCode_ParameterOutOfRange:
+            return "Parameter out of range";
+
+          case ErrorCode_BadParameterType:
+            return "Bad type for a parameter";
+
+          case ErrorCode_SQLiteNotOpened:
+            return "SQLite: The database is not opened";
+
+          case ErrorCode_SQLiteAlreadyOpened:
+            return "SQLite: Connection is already open";
+
+          case ErrorCode_SQLiteCannotOpen:
+            return "SQLite: Unable to open the database";
+
+          case ErrorCode_SQLiteStatementAlreadyUsed:
+            return "SQLite: This cached statement is already being referred to";
+
+          case ErrorCode_SQLiteExecute:
+            return "SQLite: Cannot execute a command";
+
+          case ErrorCode_SQLiteRollbackWithoutTransaction:
+            return "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)";
+
+          case ErrorCode_SQLiteCommitWithoutTransaction:
+            return "SQLite: Committing a nonexistent transaction";
+
+          case ErrorCode_SQLiteRegisterFunction:
+            return "SQLite: Unable to register a function";
+
+          case ErrorCode_SQLiteFlush:
+            return "SQLite: Unable to flush the database";
+
+          case ErrorCode_SQLiteCannotRun:
+            return "SQLite: Cannot run a cached statement";
+
+          case ErrorCode_SQLiteCannotStep:
+            return "SQLite: Cannot step over a cached statement";
+
+          case ErrorCode_SQLiteBindOutOfRange:
+            return "SQLite: Bing a value while out of range (serious error)";
+
+          case ErrorCode_SQLitePrepareStatement:
+            return "SQLite: Cannot prepare a cached statement";
+
+          case ErrorCode_SQLiteTransactionAlreadyStarted:
+            return "SQLite: Beginning the same transaction twice";
+
+          case ErrorCode_SQLiteTransactionCommit:
+            return "SQLite: Failure when committing the transaction";
+
+          case ErrorCode_SQLiteTransactionBegin:
+            return "SQLite: Cannot start a transaction";
+
+          default:
+            return "Unknown error code";
+        }
+      }
+    };
+  }
+}
+
+#else
+#  include "../OrthancException.h"
+#  define OrthancSQLiteException ::Orthanc::OrthancException
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,40 @@
+Introduction
+============
+
+The code in this folder is a standalone object-oriented wrapper around
+SQLite3. It is derived from the code of Chromium:
+
+http://src.chromium.org/viewvc/chrome/trunk/src/sql/
+http://maxradi.us/documents/sqlite/
+
+
+Main differences with Chromium
+==============================
+
+* The reference counting mechanism has been reimplemented to make it 
+  simpler.
+* The OrthancException class is used for the exception mechanisms.
+* A statement is always valid (is_valid() always return true).
+* The classes and the methods have been renamed to meet Orthanc's
+  coding conventions.
+
+
+Reuse in another software
+=========================
+
+To use the Orthanc SQLite wrapper in another project than Orthanc, you
+just have to define the "ORTHANC_SQLITE_STANDALONE" macro.
+
+All the C++ exceptions generated by the wrapper will be objects of the
+class "::Orthanc::SQLite::OrthancSQLiteException", that derives from
+the standard exception class "::std::runtime_error".
+
+
+Licensing
+=========
+
+The code in this folder is licensed under the 3-clause BSD license, in
+order to respect the original license of the code.
+
+It is pretty straightforward to extract the code from this folder and
+to include it in another project. 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/SQLiteTypes.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,73 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.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
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/Statement.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,370 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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.
+ **/
+
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../PrecompiledHeaders.h"
+#endif
+
+#include "Statement.h"
+#include "Connection.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <algorithm>
+
+#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"
+
+#if defined(_MSC_VER)
+#define snprintf _snprintf
+#endif
+
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    int Statement::CheckError(int err, ErrorCode code) const
+    {
+      bool succeeded = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE);
+      if (!succeeded)
+      {
+#if ORTHANC_SQLITE_STANDALONE != 1
+        char buffer[128];
+        snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err);
+        LOG(ERROR) << buffer;
+#endif
+
+        throw OrthancSQLiteException(code);
+      }
+
+      return err;
+    }
+
+    void Statement::CheckOk(int err, ErrorCode code) const 
+    {
+      if (err == SQLITE_RANGE)
+      {
+        // Binding to a non-existent variable is evidence of a serious error.
+        throw OrthancSQLiteException(ErrorCode_SQLiteBindOutOfRange);
+      }
+      else if (err != SQLITE_OK)
+      {
+#if ORTHANC_SQLITE_STANDALONE != 1
+        char buffer[128];
+        snprintf(buffer, sizeof(buffer) - 1, "SQLite error code %d", err);
+        LOG(ERROR) << buffer;
+#endif
+
+        throw OrthancSQLiteException(code);
+      }
+    }
+
+
+    Statement::Statement(Connection& database,
+                         const StatementId& id,
+                         const std::string& sql) : 
+      reference_(database.GetCachedStatement(id, sql.c_str()))
+    {
+      Reset(true);
+      LOG_CREATE(sql);
+    }
+
+
+    Statement::Statement(Connection& database,
+                         const StatementId& id,
+                         const char* sql) : 
+      reference_(database.GetCachedStatement(id, sql))
+    {
+      Reset(true);
+      LOG_CREATE(sql);
+    }
+
+
+    Statement::Statement(Connection& database,
+                         const std::string& sql) :
+      reference_(database.GetWrappedObject(), sql.c_str())
+    {
+      LOG_CREATE(sql);
+    }
+
+
+    Statement::Statement(Connection& database,
+                         const char* sql) :
+      reference_(database.GetWrappedObject(), sql)
+    {
+      LOG_CREATE(sql);
+    }
+
+
+    bool Statement::Run()
+    {
+      LOG_APPLY(sqlite3_sql(GetStatement()));
+
+      return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotRun) == SQLITE_DONE;
+    }
+
+    bool Statement::Step()
+    {
+      LOG_APPLY(sqlite3_sql(GetStatement()));
+
+      return CheckError(sqlite3_step(GetStatement()), ErrorCode_SQLiteCannotStep) == SQLITE_ROW;
+    }
+
+    void Statement::Reset(bool clear_bound_vars) 
+    {
+      // We don't call CheckError() here because sqlite3_reset() returns
+      // the last error that Step() caused thereby generating a second
+      // spurious error callback.
+      if (clear_bound_vars)
+        sqlite3_clear_bindings(GetStatement());
+      //VLOG(1) << "SQLite::Statement::Reset";
+      sqlite3_reset(GetStatement());
+    }
+
+    std::string Statement::GetOriginalSQLStatement()
+    {
+      return std::string(sqlite3_sql(GetStatement()));
+    }
+
+
+    void Statement::BindNull(int col)
+    {
+      CheckOk(sqlite3_bind_null(GetStatement(), col + 1),
+              ErrorCode_BadParameterType);
+    }
+
+    void Statement::BindBool(int col, bool val) 
+    {
+      BindInt(col, val ? 1 : 0);
+    }
+
+    void Statement::BindInt(int col, int val) 
+    {
+      CheckOk(sqlite3_bind_int(GetStatement(), col + 1, val),
+              ErrorCode_BadParameterType);
+    }
+
+    void Statement::BindInt64(int col, int64_t val) 
+    {
+      CheckOk(sqlite3_bind_int64(GetStatement(), col + 1, val),
+              ErrorCode_BadParameterType);
+    }
+
+    void Statement::BindDouble(int col, double val) 
+    {
+      CheckOk(sqlite3_bind_double(GetStatement(), col + 1, val),
+              ErrorCode_BadParameterType);
+    }
+
+    void Statement::BindCString(int col, const char* val) 
+    {
+      CheckOk(sqlite3_bind_text(GetStatement(), col + 1, val, -1, SQLITE_TRANSIENT),
+              ErrorCode_BadParameterType);
+    }
+
+    void Statement::BindString(int col, const std::string& val) 
+    {
+      CheckOk(sqlite3_bind_text(GetStatement(),
+                                col + 1,
+                                val.data(),
+                                static_cast<int>(val.size()),
+                                SQLITE_TRANSIENT),
+              ErrorCode_BadParameterType);
+    }
+
+    /*void Statement::BindString16(int col, const string16& value) 
+      {
+      BindString(col, UTF16ToUTF8(value));
+      }*/
+
+    void Statement::BindBlob(int col, const void* val, int val_len) 
+    {
+      CheckOk(sqlite3_bind_blob(GetStatement(), col + 1, val, val_len, SQLITE_TRANSIENT),
+              ErrorCode_BadParameterType);
+    }
+
+
+    int Statement::ColumnCount() const 
+    {
+      return sqlite3_column_count(GetStatement());
+    }
+
+
+    ColumnType Statement::GetColumnType(int col) const 
+    {
+      // Verify that our enum matches sqlite's values.
+      assert(COLUMN_TYPE_INTEGER == SQLITE_INTEGER);
+      assert(COLUMN_TYPE_FLOAT == SQLITE_FLOAT);
+      assert(COLUMN_TYPE_TEXT == SQLITE_TEXT);
+      assert(COLUMN_TYPE_BLOB == SQLITE_BLOB);
+      assert(COLUMN_TYPE_NULL == SQLITE_NULL);
+
+      return static_cast<ColumnType>(sqlite3_column_type(GetStatement(), col));
+    }
+
+    ColumnType Statement::GetDeclaredColumnType(int col) const 
+    {
+      std::string column_type(sqlite3_column_decltype(GetStatement(), col));
+      std::transform(column_type.begin(), column_type.end(), column_type.begin(), tolower);
+
+      if (column_type == "integer")
+        return COLUMN_TYPE_INTEGER;
+      else if (column_type == "float")
+        return COLUMN_TYPE_FLOAT;
+      else if (column_type == "text")
+        return COLUMN_TYPE_TEXT;
+      else if (column_type == "blob")
+        return COLUMN_TYPE_BLOB;
+
+      return COLUMN_TYPE_NULL;
+    }
+
+    bool Statement::ColumnIsNull(int col) const 
+    {
+      return sqlite3_column_type(GetStatement(), col) == SQLITE_NULL;
+    }
+
+    bool Statement::ColumnBool(int col) const 
+    {
+      return !!ColumnInt(col);
+    }
+
+    int Statement::ColumnInt(int col) const 
+    {
+      return sqlite3_column_int(GetStatement(), col);
+    }
+
+    int64_t Statement::ColumnInt64(int col) const 
+    {
+      return sqlite3_column_int64(GetStatement(), col);
+    }
+
+    double Statement::ColumnDouble(int col) const 
+    {
+      return sqlite3_column_double(GetStatement(), col);
+    }
+
+    std::string Statement::ColumnString(int col) const 
+    {
+      const char* str = reinterpret_cast<const char*>(
+        sqlite3_column_text(GetStatement(), col));
+      int len = sqlite3_column_bytes(GetStatement(), col);
+
+      std::string result;
+      if (str && len > 0)
+        result.assign(str, len);
+      return result;
+    }
+
+    /*string16 Statement::ColumnString16(int col) const 
+      {
+      std::string s = ColumnString(col);
+      return !s.empty() ? UTF8ToUTF16(s) : string16();
+      }*/
+
+    int Statement::ColumnByteLength(int col) const 
+    {
+      return sqlite3_column_bytes(GetStatement(), col);
+    }
+
+    const void* Statement::ColumnBlob(int col) const 
+    {
+      return sqlite3_column_blob(GetStatement(), col);
+    }
+
+    bool Statement::ColumnBlobAsString(int col, std::string* blob) 
+    {
+      const void* p = ColumnBlob(col);
+      size_t len = ColumnByteLength(col);
+      blob->resize(len);
+      if (blob->size() != len) {
+        return false;
+      }
+      blob->assign(reinterpret_cast<const char*>(p), len);
+      return true;
+    }
+
+    /*bool Statement::ColumnBlobAsString16(int col, string16* val) const 
+      {
+      const void* data = ColumnBlob(col);
+      size_t len = ColumnByteLength(col) / sizeof(char16);
+      val->resize(len);
+      if (val->size() != len)
+      return false;
+      val->assign(reinterpret_cast<const char16*>(data), len);
+      return true;
+      }*/
+
+    /*bool Statement::ColumnBlobAsVector(int col, std::vector<char>* val) const 
+    {
+      val->clear();
+
+      const void* data = sqlite3_column_blob(GetStatement(), col);
+      int len = sqlite3_column_bytes(GetStatement(), col);
+      if (data && len > 0) {
+        val->resize(len);
+        memcpy(&(*val)[0], data, len);
+      }
+      return true;
+      }*/
+
+    /*bool Statement::ColumnBlobAsVector(
+      int col,
+      std::vector<unsigned char>* val) const 
+    {
+      return ColumnBlobAsVector(col, reinterpret_cast< std::vector<char>* >(val));
+      }*/
+
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/Statement.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,174 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+#include "NonCopyable.h"
+#include "OrthancSQLiteException.h"
+#include "StatementId.h"
+#include "StatementReference.h"
+
+#include <vector>
+#include <stdint.h>
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+#  include <gtest/gtest_prod.h>
+#endif
+
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class Connection;
+
+    // Possible return values from ColumnType in a statement. These
+    // should match the values in sqlite3.h.
+    enum ColumnType 
+    {
+      COLUMN_TYPE_INTEGER = 1,
+      COLUMN_TYPE_FLOAT = 2,
+      COLUMN_TYPE_TEXT = 3,
+      COLUMN_TYPE_BLOB = 4,
+      COLUMN_TYPE_NULL = 5
+    };
+
+    class ORTHANC_PUBLIC Statement : public NonCopyable
+    {
+      friend class Connection;
+
+#if ORTHANC_BUILD_UNIT_TESTS == 1
+      FRIEND_TEST(SQLStatementTest, Run);
+      FRIEND_TEST(SQLStatementTest, Reset);
+#endif
+
+    private:
+      StatementReference  reference_;
+
+      int CheckError(int err, 
+                     ErrorCode code) const;
+
+      void CheckOk(int err, 
+                   ErrorCode code) const;
+
+      struct sqlite3_stmt* GetStatement() const
+      {
+        return reference_.GetWrappedObject();
+      }
+
+    public:
+      Statement(Connection& database,
+                const std::string& sql);
+
+      Statement(Connection& database,
+                const StatementId& id,
+                const std::string& sql);
+
+      Statement(Connection& database,
+                const char* sql);
+
+      Statement(Connection& database,
+                const StatementId& id,
+                const char* sql);
+
+      ~Statement()
+      {
+        Reset();
+      }
+
+      bool Run();
+
+      bool Step();
+
+      // Diagnostics --------------------------------------------------------------
+
+      std::string GetOriginalSQLStatement();
+
+
+      // Binding -------------------------------------------------------------------
+
+      // These all take a 0-based argument index
+      void BindNull(int col);
+      void BindBool(int col, bool val);
+      void BindInt(int col, int val);
+      void BindInt64(int col, int64_t val);
+      void BindDouble(int col, double val);
+      void BindCString(int col, const char* val);
+      void BindString(int col, const std::string& val);
+      //void BindString16(int col, const string16& value);
+      void BindBlob(int col, const void* value, int value_len);
+
+
+      // Retrieving ----------------------------------------------------------------
+
+      // Returns the number of output columns in the result.
+      int ColumnCount() const;
+
+      // Returns the type associated with the given column.
+      //
+      // Watch out: the type may be undefined if you've done something to cause a
+      // "type conversion." This means requesting the value of a column of a type
+      // where that type is not the native type. For safety, call ColumnType only
+      // on a column before getting the value out in any way.
+      ColumnType GetColumnType(int col) const;
+      ColumnType GetDeclaredColumnType(int col) const;
+
+      // These all take a 0-based argument index.
+      bool ColumnIsNull(int col) const ;
+      bool ColumnBool(int col) const;
+      int ColumnInt(int col) const;
+      int64_t ColumnInt64(int col) const;
+      double ColumnDouble(int col) const;
+      std::string ColumnString(int col) const;
+      //string16 ColumnString16(int col) const;
+
+      // When reading a blob, you can get a raw pointer to the underlying data,
+      // along with the length, or you can just ask us to copy the blob into a
+      // vector. Danger! ColumnBlob may return NULL if there is no data!
+      int ColumnByteLength(int col) const;
+      const void* ColumnBlob(int col) const;
+      bool ColumnBlobAsString(int col, std::string* blob);
+      //bool ColumnBlobAsString16(int col, string16* val) const;
+      //bool ColumnBlobAsVector(int col, std::vector<char>* val) const;
+      //bool ColumnBlobAsVector(int col, std::vector<unsigned char>* val) const;
+
+      // Resets the statement to its initial condition. This includes any current
+      // result row, and also the bound variables if the |clear_bound_vars| is true.
+      void Reset(bool clear_bound_vars = true);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/StatementId.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,58 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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.
+ **/
+
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../PrecompiledHeaders.h"
+#endif
+
+#include "StatementId.h"
+
+#include <string.h>
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    bool StatementId::operator< (const StatementId& other) const
+    {
+      if (line_ != other.line_)
+        return line_ < other.line_;
+
+      return strcmp(file_, other.file_) < 0;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/StatementId.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,66 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+#if ORTHANC_SQLITE_STANDALONE == 1
+#  define ORTHANC_PUBLIC
+#else
+#  include "../OrthancFramework.h"
+#endif
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ORTHANC_PUBLIC StatementId
+    {
+    private:
+      const char* file_;
+      int line_;
+
+      StatementId(); // Forbidden
+
+    public:
+      StatementId(const char* file, int line) : file_(file), line_(line)
+      {
+      }
+
+      bool operator< (const StatementId& other) const;
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/StatementReference.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,161 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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.
+ **/
+
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../PrecompiledHeaders.h"
+#endif
+
+#include "StatementReference.h"
+#include "OrthancSQLiteException.h"
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../Logging.h"
+#endif
+
+#include <string>
+#include <cassert>
+#include "sqlite3.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    bool StatementReference::IsRoot() const
+    {
+      return root_ == NULL;
+    }
+
+    StatementReference::StatementReference()
+    {
+      root_ = NULL;
+      refCount_ = 0;
+      statement_ = NULL;
+      assert(IsRoot());
+    }
+
+    StatementReference::StatementReference(sqlite3* database,
+                                           const char* sql)
+    {
+      if (database == NULL || sql == NULL)
+      {
+        throw OrthancSQLiteException(ErrorCode_ParameterOutOfRange);
+      }
+
+      root_ = NULL;
+      refCount_ = 0;
+
+      int error = sqlite3_prepare_v2(database, sql, -1, &statement_, NULL);
+      if (error != SQLITE_OK)
+      {
+#if ORTHANC_SQLITE_STANDALONE != 1
+        int extended = sqlite3_extended_errcode(database);
+        LOG(ERROR) << "SQLite: " << sqlite3_errmsg(database) << " (" << extended << ")";
+        if (extended == SQLITE_IOERR_SHMSIZE  /* 4874 */)
+        {
+          LOG(ERROR) << "  This probably indicates that your filesystem is full";
+        }        
+#endif
+
+        throw OrthancSQLiteException(ErrorCode_SQLitePrepareStatement);
+      }
+
+      assert(IsRoot());
+    }
+
+    StatementReference::StatementReference(StatementReference& other)
+    {
+      refCount_ = 0;
+
+      if (other.IsRoot())
+      {
+        root_ = &other;
+      }
+      else
+      {
+        root_ = other.root_;
+      }
+
+      root_->refCount_++;
+      statement_ = root_->statement_;
+
+      assert(!IsRoot());
+    }
+
+    StatementReference::~StatementReference()
+    {
+      if (IsRoot())
+      {
+        if (refCount_ != 0)
+        {
+          // There remain references to this object. We cannot throw
+          // an exception because:
+          // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+          LOG(ERROR) << "Bad value of the reference counter";
+#endif
+        }
+        else if (statement_ != NULL)
+        {
+          sqlite3_finalize(statement_);
+        }
+      }
+      else
+      {
+        if (root_->refCount_ == 0)
+        {
+          // There remain references to this object. We cannot throw
+          // an exception because:
+          // http://www.parashift.com/c++-faq/dtors-shouldnt-throw.html
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+          LOG(ERROR) << "Bad value of the reference counter";
+#endif
+        }
+        else
+        {
+          root_->refCount_--;
+        }
+      }
+    }
+
+    uint32_t StatementReference::GetReferenceCount() const
+    {
+      return refCount_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/StatementReference.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,86 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+#if ORTHANC_SQLITE_STANDALONE == 1
+#  define ORTHANC_PUBLIC
+#else
+#  include "../OrthancFramework.h"
+#endif
+
+#include "NonCopyable.h"
+#include "SQLiteTypes.h"
+
+#include <stdint.h>
+#include <cassert>
+#include <stdlib.h>
+
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ORTHANC_PUBLIC StatementReference : NonCopyable
+    {
+    private:
+      StatementReference* root_;   // Only used for non-root nodes
+      uint32_t refCount_;         // Only used for root node
+      struct sqlite3_stmt* statement_;
+
+      bool IsRoot() const;
+
+    public:
+      StatementReference();
+
+      StatementReference(sqlite3* database,
+                         const char* sql);
+
+      StatementReference(StatementReference& other);
+
+      ~StatementReference();
+
+      uint32_t GetReferenceCount() const;
+
+      struct sqlite3_stmt* GetWrappedObject() const
+      {
+        assert(statement_ != NULL);
+        return statement_;
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/Transaction.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,104 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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.
+ **/
+
+
+#if ORTHANC_SQLITE_STANDALONE != 1
+#include "../PrecompiledHeaders.h"
+#endif
+
+#include "Transaction.h"
+#include "OrthancSQLiteException.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    Transaction::Transaction(Connection& connection) :
+      connection_(connection),
+      isOpen_(false)
+    {
+    }
+
+    Transaction::~Transaction()
+    {
+      if (isOpen_)
+      {
+        connection_.RollbackTransaction();
+      }
+    }
+
+    void Transaction::Begin()
+    {
+      if (isOpen_) 
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteTransactionAlreadyStarted);
+      }
+
+      isOpen_ = connection_.BeginTransaction();
+      if (!isOpen_)
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteTransactionBegin);
+      }
+    }
+
+    void Transaction::Rollback() 
+    {
+      if (!isOpen_) 
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction);
+      }
+
+      isOpen_ = false;
+
+      connection_.RollbackTransaction();
+    }
+
+    void Transaction::Commit() 
+    {
+      if (!isOpen_) 
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteRollbackWithoutTransaction);
+      }
+
+      isOpen_ = false;
+
+      if (!connection_.CommitTransaction())
+      {
+        throw OrthancSQLiteException(ErrorCode_SQLiteTransactionCommit);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SQLite/Transaction.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,71 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ *
+ * Copyright (C) 2012-2016 Sebastien Jodogne <s.jodogne@orthanc-labs.com>,
+ * Medical Physics Department, CHU of Liege, Belgium
+ *
+ * Copyright (c) 2012 The Chromium Authors. 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., 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
+
+#include "Connection.h"
+#include "ITransaction.h"
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class ORTHANC_PUBLIC Transaction : public ITransaction
+    {
+    private:
+      Connection& connection_;
+
+      // True when the transaction is open, false when it's already been committed
+      // or rolled back.
+      bool isOpen_;
+
+    public:
+      explicit Transaction(Connection& connection);
+
+      virtual ~Transaction();
+
+      // Returns true when there is a transaction that has been successfully begun.
+      bool IsOpen() const { return isOpen_; }
+
+      virtual void Begin();
+
+      virtual void Rollback();
+
+      virtual void Commit();
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SerializationToolbox.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,444 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#if ORTHANC_ENABLE_DCMTK == 1
+#  include "DicomParsing/FromDcmtkBridge.h"
+#endif
+
+namespace Orthanc
+{
+  static bool ParseTagInternal(DicomTag& tag,
+                               const char* name)
+  {
+#if ORTHANC_ENABLE_DCMTK == 1
+    try
+    {
+      tag = FromDcmtkBridge::ParseTag(name);
+      return true;
+    }
+    catch (OrthancException&)
+    {
+      return false;
+    }
+#else
+    return DicomTag::ParseHexadecimal(tag, name);
+#endif   
+  }
+
+    
+  std::string SerializationToolbox::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,
+                             "String value expected in field: " + field);
+    }
+    else
+    {
+      return value[field.c_str()].asString();
+    }
+  }
+
+
+  int SerializationToolbox::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,
+                             "Integer value expected in field: " + field);
+    }
+    else
+    {
+      return value[field.c_str()].asInt();
+    }    
+  }
+
+
+  int SerializationToolbox::ReadInteger(const Json::Value& value,
+                                        const std::string& field,
+                                        int defaultValue)
+  {
+    if (value.isMember(field.c_str()))
+    {
+      return ReadInteger(value, field);
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  unsigned int SerializationToolbox::ReadUnsignedInteger(const Json::Value& value,
+                                                         const std::string& field)
+  {
+    int tmp = ReadInteger(value, field);
+
+    if (tmp < 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Unsigned integer value expected in field: " + field);
+    }
+    else
+    {
+      return static_cast<unsigned int>(tmp);
+    }
+  }
+
+
+  bool SerializationToolbox::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,
+                             "Boolean value expected in field: " + field);
+    }
+    else
+    {
+      return value[field.c_str()].asBool();
+    }   
+  }
+
+  
+  void SerializationToolbox::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,
+                             "List of strings expected in field: " + field);
+    }
+
+    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,
+                               "List of strings expected in field: " + field);
+      }
+      else
+      {
+        target[i] = arr[i].asString();
+      }
+    }
+  }
+
+
+  void SerializationToolbox::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 SerializationToolbox::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 SerializationToolbox::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,
+                             "Set of DICOM tags expected in field: " + field);
+    }
+
+    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 ||
+          !ParseTagInternal(tag, arr[i].asCString()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Set of DICOM tags expected in field: " + field);
+      }
+      else
+      {
+        target.insert(tag);
+      }
+    }
+  }
+
+
+  void SerializationToolbox::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,
+                             "Associative array of strings to strings expected in field: " + field);
+    }
+
+    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,
+                               "Associative array of string to strings expected in field: " + field);
+      }
+      else
+      {
+        target[members[i]] = tmp.asString();
+      }
+    }
+  }
+
+
+  void SerializationToolbox::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,
+                             "Associative array of DICOM tags to strings expected in field: " + field);
+    }
+
+    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 (!ParseTagInternal(tag, members[i].c_str()) ||
+          tmp.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Associative array of DICOM tags to strings expected in field: " + field);
+      }
+      else
+      {
+        target[tag] = tmp.asString();
+      }
+    }
+  }
+
+
+  void SerializationToolbox::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 SerializationToolbox::WriteListOfStrings(Json::Value& target,
+                                                const std::list<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::list<std::string>::const_iterator it = values.begin();
+         it != values.end(); ++it)
+    {
+      value.append(*it);
+    }
+  }
+
+
+  void SerializationToolbox::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 SerializationToolbox::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 SerializationToolbox::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 SerializationToolbox::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/OrthancFramework/Sources/SerializationToolbox.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#include <json/value.h>
+#include <list>
+#include <map>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC SerializationToolbox
+  {
+  public:
+    static std::string ReadString(const Json::Value& value,
+                                  const std::string& field);
+
+    static int ReadInteger(const Json::Value& value,
+                           const std::string& field);
+
+    static int ReadInteger(const Json::Value& value,
+                           const std::string& field,
+                           int defaultValue);
+
+    static unsigned int ReadUnsignedInteger(const Json::Value& value,
+                                            const std::string& field);
+
+    static bool ReadBoolean(const Json::Value& value,
+                            const std::string& field);
+
+    static void ReadArrayOfStrings(std::vector<std::string>& target,
+                                   const Json::Value& value,
+                                   const std::string& field);
+
+    static void ReadListOfStrings(std::list<std::string>& target,
+                                  const Json::Value& value,
+                                  const std::string& field);
+
+    static void ReadSetOfStrings(std::set<std::string>& target,
+                                 const Json::Value& value,
+                                 const std::string& field);
+
+    static void ReadSetOfTags(std::set<DicomTag>& target,
+                              const Json::Value& value,
+                              const std::string& field);
+
+    static void ReadMapOfStrings(std::map<std::string, std::string>& values,
+                                 const Json::Value& target,
+                                 const std::string& field);
+
+    static void ReadMapOfTags(std::map<DicomTag, std::string>& values,
+                              const Json::Value& target,
+                              const std::string& field);
+
+    static void WriteArrayOfStrings(Json::Value& target,
+                                    const std::vector<std::string>& values,
+                                    const std::string& field);
+
+    static void WriteListOfStrings(Json::Value& target,
+                                   const std::list<std::string>& values,
+                                   const std::string& field);
+
+    static void WriteSetOfStrings(Json::Value& target,
+                                  const std::set<std::string>& values,
+                                  const std::string& field);
+
+    static void WriteSetOfTags(Json::Value& target,
+                               const std::set<DicomTag>& tags,
+                               const std::string& field);
+
+    static void WriteMapOfStrings(Json::Value& target,
+                                  const std::map<std::string, std::string>& values,
+                                  const std::string& field);
+
+    static 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/OrthancFramework/Sources/SharedLibrary.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,154 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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__)
+   
+    /**
+     * "RTLD_LOCAL" is the default, and is only present to be
+     * explicit. "RTLD_DEEPBIND" was added in Orthanc 1.6.0, in order
+     * to avoid crashes while loading plugins from the LSB binaries of
+     * the Orthanc core.
+     *
+     * BUT this had no effect, and this results in a crash if loading
+     * the Python 2.7 plugin => We disabled it again in Orthanc 1.6.1.
+     **/
+    
+#if 0 // && defined(RTLD_DEEPBIND)  // This is a GNU extension
+    // Disabled in Orthanc 1.6.1
+    handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL | RTLD_DEEPBIND);
+#else
+    handle_ = ::dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL);
+#endif
+
+    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)
+    {
+      throw OrthancException(
+        ErrorCode_SharedLibrary,
+        "Shared library does not expose function \"" + name + "\"");
+    }
+    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/OrthancFramework/Sources/SharedLibrary.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#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 ORTHANC_PUBLIC 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/OrthancFramework/Sources/SystemToolbox.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,764 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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()"
+#  include <stdlib.h>    // For "environ"
+#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>
+
+
+/*=========================================================================
+  The section below comes from the Boost 1.68.0 project:
+  https://github.com/boostorg/program_options/blob/boost-1.68.0/src/parsers.cpp
+  
+  Copyright Vladimir Prus 2002-2004.
+  Distributed under the Boost Software License, Version 1.0.
+  (See accompanying file LICENSE_1_0.txt
+  or copy at http://www.boost.org/LICENSE_1_0.txt)
+  =========================================================================*/
+
+// The 'environ' should be declared in some cases. E.g. Linux man page says:
+// (This variable must be declared in the user program, but is declared in 
+// the header file unistd.h in case the header files came from libc4 or libc5, 
+// and in case they came from glibc and _GNU_SOURCE was defined.) 
+// To be safe, declare it here.
+
+// It appears that on Mac OS X the 'environ' variable is not
+// available to dynamically linked libraries.
+// See: http://article.gmane.org/gmane.comp.lib.boost.devel/103843
+// See: http://lists.gnu.org/archive/html/bug-guile/2004-01/msg00013.html
+#if defined(__APPLE__) && defined(__DYNAMIC__)
+// The proper include for this is crt_externs.h, however it's not
+// available on iOS. The right replacement is not known. See
+// https://svn.boost.org/trac/boost/ticket/5053
+extern "C"
+{
+  extern char ***_NSGetEnviron(void);
+}
+#  define environ (*_NSGetEnviron()) 
+#else
+#  if defined(__MWERKS__)
+#    include <crtl.h>
+#  else
+#    if !defined(_WIN32) || defined(__COMO_VERSION__)
+extern char** environ;
+#    endif
+#  endif
+#endif
+
+
+/*=========================================================================
+  End of section from the Boost 1.68.0 project
+  =========================================================================*/
+
+
+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,
+                               bool log)
+  {
+    if (!IsRegularFile(path))
+    {
+      throw OrthancException(ErrorCode_RegularFileExpected,
+                             "The path does not point to a regular file: " + path,
+                             log);
+    }
+
+    boost::filesystem::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      throw OrthancException(ErrorCode_InexistentFile,
+                             "File not found: " + path,
+                             log);
+    }
+
+    std::streamsize size = GetStreamSize(f);
+    content.resize(static_cast<size_t>(size));
+    if (size != 0)
+    {
+      f.read(&content[0], size);
+    }
+
+    f.close();
+  }
+
+
+  bool SystemToolbox::ReadHeader(std::string& header,
+                                 const std::string& path,
+                                 size_t headerSize)
+  {
+    if (!IsRegularFile(path))
+    {
+      throw OrthancException(ErrorCode_RegularFileExpected,
+                             "The path does not point to a regular file: " + path);
+    }
+
+    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(&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()
+      throw OrthancException(ErrorCode_SystemCommand, "Cannot fork a child process");
+    }
+    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)
+    {
+      throw OrthancException(ErrorCode_SystemCommand,
+                             "System command failed with status code " +
+                             boost::lexical_cast<std::string>(status));
+    }
+  }
+
+
+  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;
+    }
+  }
+
+
+  MimeType SystemToolbox::AutodetectMimeType(const std::string& path)
+  {
+    std::string extension = boost::filesystem::extension(path);
+    Toolbox::ToLowerCase(extension);
+
+    // http://en.wikipedia.org/wiki/Mime_types
+    // Text types
+    if (extension == ".txt")
+    {
+      return MimeType_PlainText;
+    }
+    else if (extension == ".html")
+    {
+      return MimeType_Html;
+    }
+    else if (extension == ".xml")
+    {
+      return MimeType_Xml;
+    }
+    else if (extension == ".css")
+    {
+      return MimeType_Css;
+    }
+
+    // Application types
+    else if (extension == ".js")
+    {
+      return MimeType_JavaScript;
+    }
+    else if (extension == ".json" ||
+             extension == ".nmf"  /* manifest */)
+    {
+      return MimeType_Json;
+    }
+    else if (extension == ".pdf")
+    {
+      return MimeType_Pdf;
+    }
+    else if (extension == ".wasm")
+    {
+      return MimeType_WebAssembly;
+    }
+    else if (extension == ".nexe")
+    {
+      return MimeType_NaCl;
+    }
+    else if (extension == ".pexe")
+    {
+      return MimeType_PNaCl;
+    }
+
+    // Images types
+    else if (extension == ".jpg" ||
+             extension == ".jpeg")
+    {
+      return MimeType_Jpeg;
+    }
+    else if (extension == ".gif")
+    {
+      return MimeType_Gif;
+    }
+    else if (extension == ".png")
+    {
+      return MimeType_Png;
+    }
+    else if (extension == ".pam")
+    {
+      return MimeType_Pam;
+    }
+    else if (extension == ".svg")
+    {
+      return MimeType_Svg;
+    }
+
+    // Various types
+    else if (extension == ".woff")
+    {
+      return MimeType_Woff;
+    }
+    else if (extension == ".woff2")
+    {
+      return MimeType_Woff2;
+    }
+
+    // Default type
+    else
+    {
+      LOG(INFO) << "Unknown MIME type for extension \"" << extension << "\"";
+      return MimeType_Binary;
+    }
+  }
+
+
+  void SystemToolbox::GetEnvironmentVariables(std::map<std::string, std::string>& env)
+  {
+    env.clear();
+    
+    for (char **p = environ; *p != NULL; p++)
+    {
+      std::string v(*p);
+      size_t pos = v.find('=');
+
+      if (pos != std::string::npos)
+      {
+        std::string key = v.substr(0, pos);
+        std::string value = v.substr(pos + 1);
+        env[key] = value;
+      } 
+    }
+  }
+
+
+  std::string SystemToolbox::InterpretRelativePath(const std::string& baseDirectory,
+                                                   const std::string& relativePath)
+  {
+    boost::filesystem::path base(baseDirectory);
+    boost::filesystem::path relative(relativePath);
+
+    /**
+       The following lines should be equivalent to this one: 
+
+       return (base / relative).string();
+
+       However, for some unknown reason, some versions of Boost do not
+       make the proper path resolution when "baseDirectory" is an
+       absolute path. So, a hack is used below.
+    **/
+
+    if (relative.is_absolute())
+    {
+      return relative.string();
+    }
+    else
+    {
+      return (base / relative).string();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/SystemToolbox.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#include <map>
+#include <vector>
+#include <string>
+#include <stdint.h>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC SystemToolbox
+  {
+  public:
+    static void USleep(uint64_t microSeconds);
+
+    static ServerBarrierEvent ServerBarrier(const bool& stopFlag);
+
+    static ServerBarrierEvent ServerBarrier();
+
+    static void ReadFile(std::string& content,
+                         const std::string& path,
+                         bool log = true);
+
+    static bool ReadHeader(std::string& header,
+                           const std::string& path,
+                           size_t headerSize);
+
+    static void WriteFile(const void* content,
+                          size_t size,
+                          const std::string& path);
+
+    static void WriteFile(const std::string& content,
+                          const std::string& path);
+
+    static void RemoveFile(const std::string& path);
+
+    static uint64_t GetFileSize(const std::string& path);
+
+    static void MakeDirectory(const std::string& path);
+
+    static bool IsExistingFile(const std::string& path);
+
+    static std::string GetPathToExecutable();
+
+    static std::string GetDirectoryOfExecutable();
+
+    static void ExecuteSystemCommand(const std::string& command,
+                                     const std::vector<std::string>& arguments);
+
+    static int GetProcessId();
+
+    static bool IsRegularFile(const std::string& path);
+
+    static FILE* OpenFile(const std::string& path,
+                          FileMode mode);
+
+    static std::string GetNowIsoString(bool utc);
+
+    static void GetNowDicom(std::string& date,
+                            std::string& time,
+                            bool utc);
+
+    static unsigned int GetHardwareConcurrency();
+
+    static MimeType AutodetectMimeType(const std::string& path);
+
+    static void GetEnvironmentVariables(std::map<std::string, std::string>& env);
+
+    static std::string InterpretRelativePath(const std::string& baseDirectory,
+                                             const std::string& relativePath);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/TemporaryFile.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,140 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancException.h"
+#include "SystemToolbox.h"
+#include "Toolbox.h"
+
+#include <boost/filesystem.hpp>
+
+namespace Orthanc
+{
+  static std::string CreateTemporaryPath(const char* temporaryDirectory,
+                                         const char* extension)
+  {
+    boost::filesystem::path dir;
+
+    if (temporaryDirectory == NULL)
+    {
+#if BOOST_HAS_FILESYSTEM_V3 == 1
+      dir = boost::filesystem::temp_directory_path();
+#elif defined(__linux__)
+      dir = "/tmp";
+#else
+#  error Support your platform here
+#endif
+    }
+    else
+    {
+      dir = temporaryDirectory;
+    }
+
+    // We use UUID to create unique path to temporary files
+    const std::string uuid = Orthanc::Toolbox::GenerateUuid();
+
+    // New in Orthanc 1.5.8: Prefix the process ID to the name of the
+    // temporary files, in order to locate orphan temporary files that
+    // were left by instances of Orthanc that exited in non-clean way
+    // https://groups.google.com/d/msg/orthanc-users/MSJX53bw6Lw/d3S3lRRLAwAJ
+    std::string filename = "Orthanc-" + boost::lexical_cast<std::string>(SystemToolbox::GetProcessId()) + "-" + uuid;
+
+    if (extension != NULL)
+    {
+      filename.append(extension);
+    }
+
+    dir /= filename;
+    return dir.string();
+  }
+
+
+  TemporaryFile::TemporaryFile() : 
+    path_(CreateTemporaryPath(NULL, NULL))
+  {
+  }
+
+
+  TemporaryFile::TemporaryFile(const std::string& temporaryDirectory,
+                               const std::string& extension) :
+    path_(CreateTemporaryPath(temporaryDirectory.c_str(), extension.c_str()))
+  {
+  }
+
+
+  TemporaryFile::~TemporaryFile()
+  {
+    boost::filesystem::remove(path_);
+  }
+
+
+  void TemporaryFile::Write(const std::string& content)
+  {
+    try
+    {
+      SystemToolbox::WriteFile(content, path_);
+    }
+    catch (OrthancException& e)
+    {
+      throw OrthancException(e.GetErrorCode(),
+                             "Can't create temporary file \"" + path_ +
+                             "\" with " + boost::lexical_cast<std::string>(content.size()) +
+                             " bytes: Check you have write access to the "
+                             "temporary directory and that it is not full");
+    }
+  }
+
+
+  void TemporaryFile::Read(std::string& content) const
+  {
+    try
+    {
+      SystemToolbox::ReadFile(content, path_);
+    }
+    catch (OrthancException& e)
+    {
+      throw OrthancException(e.GetErrorCode(),
+                             "Can't read temporary file \"" + path_ +
+                             "\": Another process has corrupted the temporary directory");
+    }
+  }
+
+
+  void TemporaryFile::Touch()
+  {
+    std::string empty;
+    Write(empty);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/TemporaryFile.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#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 <boost/noncopyable.hpp>
+#include <string>
+
+namespace Orthanc
+{
+  class ORTHANC_PUBLIC TemporaryFile : public boost::noncopyable
+  {
+  private:
+    std::string path_;
+
+  public:
+    TemporaryFile();
+
+    TemporaryFile(const std::string& temporaryFolder,
+                  const std::string& extension);
+
+    ~TemporaryFile();
+
+    const std::string& GetPath() const
+    {
+      return path_;
+    }
+
+    void Write(const std::string& content);
+
+    void Read(std::string& content) const;
+
+    void Touch();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Toolbox.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2257 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "Toolbox.h"
+
+#include "Compatibility.h"
+#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>
+
+#if BOOST_VERSION >= 106600
+#  include <boost/uuid/detail/sha1.hpp>
+#else
+#  include <boost/uuid/sha1.hpp>
+#endif
+ 
+#include <string>
+#include <stdint.h>
+#include <string.h>
+#include <algorithm>
+#include <ctype.h>
+
+
+#if ORTHANC_ENABLE_MD5 == 1
+// TODO - Could be replaced by <boost/uuid/detail/md5.hpp> starting
+// with Boost >= 1.66.0
+#  include "../Resources/ThirdParty/md5/md5.h"
+#endif
+
+#if ORTHANC_ENABLE_BASE64 == 1
+#  include "../Resources/ThirdParty/base64/base64.h"
+#endif
+
+#if ORTHANC_ENABLE_LOCALE == 1
+#  include <boost/locale.hpp>
+#endif
+
+#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
+
+
+#if defined(_MSC_VER) && (_MSC_VER < 1800)
+// Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013
+extern "C"
+{
+  int64_t _strtoi64(const char *nptr, char **endptr, int base);
+  int64_t strtoll(const char *nptr, char **endptr, int base)
+  {
+    return _strtoi64(nptr, endptr, base);
+  } 
+}
+#endif
+
+
+#if defined(_WIN32)
+#  include <windows.h>   // For ::Sleep
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include "ChunkedBuffer.h"
+#endif
+
+
+// Inclusions for UUID
+// http://stackoverflow.com/a/1626302
+
+extern "C"
+{
+#if defined(_WIN32)
+#  include <rpc.h>
+#else
+#  include <uuid/uuid.h>
+#endif
+}
+
+
+#if defined(ORTHANC_STATIC_ICU)
+#  if (ORTHANC_STATIC_ICU == 1 && ORTHANC_ENABLE_LOCALE == 1)
+#    include <OrthancFrameworkResources.h>
+#    include <unicode/udata.h>
+#    include <unicode/uloc.h>
+#    include "Compression/GzipCompressor.h"
+
+static std::string  globalIcuData_;
+
+extern "C"
+{
+  // This is dummy content for the "icudt58_dat" (resp. "icudt63_dat")
+  // global variable from the autogenerated "icudt58l_dat.c"
+  // (resp. "icudt63l_dat.c") file that contains a huge C array. In
+  // Orthanc, this array is compressed using gzip and attached as a
+  // resource, then uncompressed during the launch of Orthanc by
+  // static function "InitializeIcu()".
+  struct
+  {
+    double bogus;
+    uint8_t *bytes;
+  } U_ICUDATA_ENTRY_POINT = { 0.0, NULL };
+}
+
+#    if defined(__LSB_VERSION__)
+extern "C"
+{
+  /**
+   * The "tzname" global variable is declared as "extern" but is not
+   * defined in any compilation module, if using Linux Standard Base,
+   * as soon as OpenSSL or cURL is in use on Ubuntu >= 18.04 (glibc >=
+   * 2.27). The variable "__tzname" is always properly declared *and*
+   * defined. The reason is unclear, and is maybe a bug in the gcc 4.8
+   * linker that is used by LSB if facing a weak symbol (as "tzname").
+   * This makes Orthanc crash if the timezone is set to UTC.
+   * https://groups.google.com/d/msg/orthanc-users/0m8sxxwSm1E/2p8du_89CAAJ
+   **/
+  char *tzname[2] = { (char *) "GMT", (char *) "GMT" };
+}
+#    endif
+
+#  endif
+#endif
+ 
+
+
+#if defined(__unix__) && ORTHANC_SANDBOXED != 1
+#  include "SystemToolbox.h"  // Check out "InitializeGlobalLocale()"
+#endif
+
+
+
+namespace Orthanc
+{
+  void Toolbox::LinesIterator::FindEndOfLine()
+  {
+    lineEnd_ = lineStart_;
+
+    while (lineEnd_ < content_.size() &&
+           content_[lineEnd_] != '\n' &&
+           content_[lineEnd_] != '\r')
+    {
+      lineEnd_ += 1;
+    }
+  }
+  
+
+  Toolbox::LinesIterator::LinesIterator(const std::string& content) :
+    content_(content),
+    lineStart_(0)
+  {
+    FindEndOfLine();
+  }
+
+    
+  bool Toolbox::LinesIterator::GetLine(std::string& target) const
+  {
+    assert(lineStart_ <= content_.size() &&
+           lineEnd_ <= content_.size() &&
+           lineStart_ <= lineEnd_);
+
+    if (lineStart_ == content_.size())
+    {
+      return false;
+    }
+    else
+    {
+      target = content_.substr(lineStart_, lineEnd_ - lineStart_);
+      return true;
+    }
+  }
+
+    
+  void Toolbox::LinesIterator::Next()
+  {
+    lineStart_ = lineEnd_;
+
+    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::ToUpperCase(std::string& s)
+  {
+    std::transform(s.begin(), s.end(), s.begin(), toupper);
+  }
+
+
+  void Toolbox::ToLowerCase(std::string& s)
+  {
+    std::transform(s.begin(), s.end(), s.begin(), tolower);
+  }
+
+
+  void Toolbox::ToUpperCase(std::string& result,
+                            const std::string& source)
+  {
+    result = source;
+    ToUpperCase(result);
+  }
+
+  void Toolbox::ToLowerCase(std::string& result,
+                            const std::string& source)
+  {
+    result = source;
+    ToLowerCase(result);
+  }
+
+
+  void Toolbox::SplitUriComponents(UriComponents& components,
+                                   const std::string& uri)
+  {
+    static const char URI_SEPARATOR = '/';
+
+    components.clear();
+
+    if (uri.size() == 0 ||
+        uri[0] != URI_SEPARATOR)
+    {
+      throw OrthancException(ErrorCode_UriSyntax);
+    }
+
+    // Count the number of slashes in the URI to make an assumption
+    // about the number of components in the URI
+    unsigned int estimatedSize = 0;
+    for (unsigned int i = 0; i < uri.size(); i++)
+    {
+      if (uri[i] == URI_SEPARATOR)
+        estimatedSize++;
+    }
+
+    components.reserve(estimatedSize - 1);
+
+    unsigned int start = 1;
+    unsigned int end = 1;
+    while (end < uri.size())
+    {
+      // This is the loop invariant
+      assert(uri[start - 1] == '/' && (end >= start));
+
+      if (uri[end] == '/')
+      {
+        components.push_back(std::string(&uri[start], end - start));
+        end++;
+        start = end;
+      }
+      else
+      {
+        end++;
+      }
+    }
+
+    if (start < uri.size())
+    {
+      components.push_back(std::string(&uri[start], end - start));
+    }
+
+    for (size_t i = 0; i < components.size(); i++)
+    {
+      if (components[i].size() == 0)
+      {
+        // Empty component, as in: "/coucou//e"
+        throw OrthancException(ErrorCode_UriSyntax);
+      }
+    }
+  }
+
+
+  void Toolbox::TruncateUri(UriComponents& target,
+                            const UriComponents& source,
+                            size_t fromLevel)
+  {
+    target.clear();
+
+    if (source.size() > fromLevel)
+    {
+      target.resize(source.size() - fromLevel);
+
+      size_t j = 0;
+      for (size_t i = fromLevel; i < source.size(); i++, j++)
+      {
+        target[j] = source[i];
+      }
+
+      assert(j == target.size());
+    }
+  }
+  
+
+
+  bool Toolbox::IsChildUri(const UriComponents& baseUri,
+                           const UriComponents& testedUri)
+  {
+    if (testedUri.size() < baseUri.size())
+    {
+      return false;
+    }
+
+    for (size_t i = 0; i < baseUri.size(); i++)
+    {
+      if (baseUri[i] != testedUri[i])
+        return false;
+    }
+
+    return true;
+  }
+
+
+  std::string Toolbox::FlattenUri(const UriComponents& components,
+                                  size_t fromLevel)
+  {
+    if (components.size() <= fromLevel)
+    {
+      return "/";
+    }
+    else
+    {
+      std::string r;
+
+      for (size_t i = fromLevel; i < components.size(); i++)
+      {
+        r += "/" + components[i];
+      }
+
+      return r;
+    }
+  }
+
+
+#if ORTHANC_ENABLE_MD5 == 1
+  static char GetHexadecimalCharacter(uint8_t value)
+  {
+    assert(value < 16);
+
+    if (value < 10)
+    {
+      return value + '0';
+    }
+    else
+    {
+      return (value - 10) + 'a';
+    }
+  }
+
+
+  void Toolbox::ComputeMD5(std::string& result,
+                           const std::string& data)
+  {
+    if (data.size() > 0)
+    {
+      ComputeMD5(result, &data[0], data.size());
+    }
+    else
+    {
+      ComputeMD5(result, NULL, 0);
+    }
+  }
+
+
+  void Toolbox::ComputeMD5(std::string& result,
+                           const void* data,
+                           size_t size)
+  {
+    md5_state_s state;
+    md5_init(&state);
+
+    if (size > 0)
+    {
+      md5_append(&state, 
+                 reinterpret_cast<const md5_byte_t*>(data), 
+                 static_cast<int>(size));
+    }
+
+    md5_byte_t actualHash[16];
+    md5_finish(&state, actualHash);
+
+    result.resize(32);
+    for (unsigned int i = 0; i < 16; i++)
+    {
+      result[2 * i] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] / 16));
+      result[2 * i + 1] = GetHexadecimalCharacter(static_cast<uint8_t>(actualHash[i] % 16));
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_BASE64 == 1
+  void Toolbox::EncodeBase64(std::string& result, 
+                             const std::string& data)
+  {
+    result.clear();
+    base64_encode(result, data);
+  }
+
+  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.clear();
+    base64_decode(result, data);
+  }
+
+
+  bool Toolbox::DecodeDataUriScheme(std::string& mime,
+                                    std::string& content,
+                                    const std::string& source)
+  {
+    boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)",
+                         boost::regex::icase /* case insensitive search */);
+
+    boost::cmatch what;
+    if (regex_match(source.c_str(), what, pattern))
+    {
+      mime = what[1];
+      DecodeBase64(content, what[2]);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  void Toolbox::EncodeDataUriScheme(std::string& result,
+                                    const std::string& mime,
+                                    const std::string& content)
+  {
+    result = "data:" + mime + ";base64,";
+    base64_encode(result, content);
+  }
+
+#endif
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  static const char* GetBoostLocaleEncoding(const Encoding sourceEncoding)
+  {
+    switch (sourceEncoding)
+    {
+      case Encoding_Utf8:
+        return "UTF-8";
+
+      case Encoding_Ascii:
+        return "ASCII";
+
+      case Encoding_Latin1:
+        return "ISO-8859-1";
+
+      case Encoding_Latin2:
+        return "ISO-8859-2";
+
+      case Encoding_Latin3:
+        return "ISO-8859-3";
+
+      case Encoding_Latin4:
+        return "ISO-8859-4";
+
+      case Encoding_Latin5:
+        return "ISO-8859-9";
+
+      case Encoding_Cyrillic:
+        return "ISO-8859-5";
+
+      case Encoding_Windows1251:
+        return "WINDOWS-1251";
+
+      case Encoding_Arabic:
+        return "ISO-8859-6";
+
+      case Encoding_Greek:
+        return "ISO-8859-7";
+
+      case Encoding_Hebrew:
+        return "ISO-8859-8";
+        
+      case Encoding_Japanese:
+        return "SHIFT-JIS";
+
+      case Encoding_Chinese:
+        return "GB18030";
+
+      case Encoding_Thai:
+#if BOOST_LOCALE_WITH_ICU == 1
+        return "tis620.2533";
+#else
+        return "TIS620.2533-0";
+#endif
+
+      case Encoding_Korean:
+        return "ISO-IR-149";
+
+      case Encoding_JapaneseKanji:
+        return "JIS";
+
+      case Encoding_SimplifiedChinese:
+        return "GB2312";
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+#endif
+
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.12.html#sect_C.12.1.1.2
+  std::string Toolbox::ConvertToUtf8(const std::string& source,
+                                     Encoding sourceEncoding,
+                                     bool hasCodeExtensions)
+  {
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Call Toolbox::InitializeGlobalLocale()");
+    }
+#endif
+
+    // The "::skip" flag makes boost skip invalid UTF-8
+    // characters. This can occur in badly-encoded DICOM files.
+    
+    try
+    {
+      if (sourceEncoding == Encoding_Ascii)
+      {
+        return ConvertToAscii(source);
+      }
+      else 
+      {
+        std::string s;
+        
+        if (sourceEncoding == Encoding_Utf8)
+        {
+          // Already in UTF-8: No conversion is required, but we ensure
+          // the output is correctly encoded
+          s = boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+        }
+        else
+        {
+          const char* encoding = GetBoostLocaleEncoding(sourceEncoding);
+          s = boost::locale::conv::to_utf<char>(source, encoding, boost::locale::conv::skip);
+        }
+
+        if (hasCodeExtensions)
+        {
+          std::string t;
+          RemoveIso2022EscapeSequences(t, s);
+          return t;
+        }
+        else
+        {
+          return s;
+        }        
+      }
+    }
+    catch (std::runtime_error& e)
+    {
+      // Bad input string or bad encoding
+      LOG(INFO) << e.what();
+      return ConvertToAscii(source);
+    }
+  }
+#endif
+  
+
+#if ORTHANC_ENABLE_LOCALE == 1
+  std::string Toolbox::ConvertFromUtf8(const std::string& source,
+                                       Encoding targetEncoding)
+  {
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "Call Toolbox::InitializeGlobalLocale()");
+    }
+#endif
+
+    // The "::skip" flag makes boost skip invalid UTF-8
+    // characters. This can occur in badly-encoded DICOM files.
+    
+    try
+    {
+      if (targetEncoding == Encoding_Utf8)
+      {
+        // Already in UTF-8: No conversion is required.
+        return boost::locale::conv::utf_to_utf<char>(source, boost::locale::conv::skip);
+      }
+      else if (targetEncoding == Encoding_Ascii)
+      {
+        return ConvertToAscii(source);
+      }
+      else
+      {
+        const char* encoding = GetBoostLocaleEncoding(targetEncoding);
+        return boost::locale::conv::from_utf<char>(source, encoding, boost::locale::conv::skip);
+      }
+    }
+    catch (std::runtime_error&)
+    {
+      // Bad input string or bad encoding
+      return ConvertToAscii(source);
+    }
+  }
+#endif
+
+
+  static bool IsAsciiCharacter(uint8_t c)
+  {
+    return (c != 0 &&
+            c <= 127 &&
+            (c == '\n' || !iscntrl(c)));
+  }
+
+
+  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 (!IsAsciiCharacter(*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;
+
+    result.reserve(source.size() + 1);
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsAsciiCharacter(source[i]))
+      {
+        result.push_back(source[i]);
+      }
+    }
+
+    return result;
+  }
+
+
+  void Toolbox::ComputeSHA1(std::string& result,
+                            const void* data,
+                            size_t size)
+  {
+    boost::uuids::detail::sha1 sha1;
+
+    if (size > 0)
+    {
+      sha1.process_bytes(data, size);
+    }
+
+    unsigned int digest[5];
+
+    // Sanity check for the memory layout: A SHA-1 digest is 160 bits wide
+    assert(sizeof(unsigned int) == 4 && sizeof(digest) == (160 / 8)); 
+    
+    sha1.get_digest(digest);
+
+    result.resize(8 * 5 + 4);
+    sprintf(&result[0], "%08x-%08x-%08x-%08x-%08x",
+            digest[0],
+            digest[1],
+            digest[2],
+            digest[3],
+            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 void* str,
+                       size_t size)
+  {
+    if (size == 0)
+    {
+      return false;
+    }
+
+    const char* start = reinterpret_cast<const char*>(str);
+    const char* end = start + size;
+
+    // Trim the beginning of the string
+    while (start < end)
+    {
+      if (*start == '\0' ||
+          isspace(*start))
+      {
+        start++;
+      }
+      else
+      {
+        break;
+      }
+    }
+
+    // Trim the trailing of the string
+    while (start < end)
+    {
+      if (*(end - 1) == '\0' ||
+          isspace(*(end - 1)))
+      {
+        end--;
+      }
+      else
+      {
+        break;
+      }
+    }
+
+    if (end - start != 44)
+    {
+      return false;
+    }
+
+    for (unsigned int i = 0; i < 44; i++)
+    {
+      if (i == 8 ||
+          i == 17 ||
+          i == 26 ||
+          i == 35)
+      {
+        if (start[i] != '-')
+          return false;
+      }
+      else
+      {
+        if (!isalnum(start[i]))
+          return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool Toolbox::IsSHA1(const std::string& s)
+  {
+    if (s.size() == 0)
+    {
+      return false;
+    }
+    else
+    {
+      return IsSHA1(s.c_str(), s.size());
+    }
+  }
+
+
+  std::string Toolbox::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);
+  }
+
+
+  static char Hex2Dec(char c)
+  {
+    return ((c >= '0' && c <= '9') ? c - '0' :
+            ((c >= 'a' && c <= 'f') ? c - 'a' + 10 : c - 'A' + 10));
+  }
+
+  void Toolbox::UrlDecode(std::string& s)
+  {
+    // http://en.wikipedia.org/wiki/Percent-encoding
+    // http://www.w3schools.com/tags/ref_urlencode.asp
+    // http://stackoverflow.com/questions/154536/encode-decode-urls-in-c
+
+    if (s.size() == 0)
+    {
+      return;
+    }
+
+    size_t source = 0;
+    size_t target = 0;
+
+    while (source < s.size())
+    {
+      if (s[source] == '%' &&
+          source + 2 < s.size() &&
+          isalnum(s[source + 1]) &&
+          isalnum(s[source + 2]))
+      {
+        s[target] = (Hex2Dec(s[source + 1]) << 4) | Hex2Dec(s[source + 2]);
+        source += 3;
+        target += 1;
+      }
+      else
+      {
+        if (s[source] == '+')
+          s[target] = ' ';
+        else
+          s[target] = s[source];
+
+        source++;
+        target++;
+      }
+    }
+
+    s.resize(target);
+  }
+
+
+  Endianness Toolbox::DetectEndianness()
+  {
+    // http://sourceforge.net/p/predef/wiki/Endianness/
+
+    uint32_t bufferView;
+
+    uint8_t* buffer = reinterpret_cast<uint8_t*>(&bufferView);
+
+    buffer[0] = 0x00;
+    buffer[1] = 0x01;
+    buffer[2] = 0x02;
+    buffer[3] = 0x03;
+
+    switch (bufferView) 
+    {
+      case 0x00010203: 
+        return Endianness_Big;
+
+      case 0x03020100: 
+        return Endianness_Little;
+        
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  std::string Toolbox::WildcardToRegularExpression(const std::string& source)
+  {
+    // TODO - Speed up this with a regular expression
+
+    std::string result = source;
+
+    // Escape all special characters
+    boost::replace_all(result, "\\", "\\\\");
+    boost::replace_all(result, "^", "\\^");
+    boost::replace_all(result, ".", "\\.");
+    boost::replace_all(result, "$", "\\$");
+    boost::replace_all(result, "|", "\\|");
+    boost::replace_all(result, "(", "\\(");
+    boost::replace_all(result, ")", "\\)");
+    boost::replace_all(result, "[", "\\[");
+    boost::replace_all(result, "]", "\\]");
+    boost::replace_all(result, "+", "\\+");
+    boost::replace_all(result, "/", "\\/");
+    boost::replace_all(result, "{", "\\{");
+    boost::replace_all(result, "}", "\\}");
+
+    // Convert wildcards '*' and '?' to their regex equivalents
+    boost::replace_all(result, "?", ".");
+    boost::replace_all(result, "*", ".*");
+
+    return result;
+  }
+
+
+  void Toolbox::TokenizeString(std::vector<std::string>& result,
+                               const std::string& value,
+                               char separator)
+  {
+    size_t countSeparators = 0;
+    
+    for (size_t i = 0; i < value.size(); i++)
+    {
+      if (value[i] == separator)
+      {
+        countSeparators++;
+      }
+    }
+    
+    result.clear();
+    result.reserve(countSeparators + 1);
+
+    std::string currentItem;
+
+    for (size_t i = 0; i < value.size(); i++)
+    {
+      if (value[i] == separator)
+      {
+        result.push_back(currentItem);
+        currentItem.clear();
+      }
+      else
+      {
+        currentItem.push_back(value[i]);
+      }
+    }
+
+    result.push_back(currentItem);
+  }
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  class ChunkedBufferWriter : public pugi::xml_writer
+  {
+  private:
+    ChunkedBuffer buffer_;
+
+  public:
+    virtual void write(const void *data, size_t size)
+    {
+      if (size > 0)
+      {
+        buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
+      }
+    }
+
+    void Flatten(std::string& s)
+    {
+      buffer_.Flatten(s);
+    }
+  };
+
+
+  static void JsonToXmlInternal(pugi::xml_node& target,
+                                const Json::Value& source,
+                                const std::string& arrayElement)
+  {
+    // http://jsoncpp.sourceforge.net/value_8h_source.html#l00030
+
+    switch (source.type())
+    {
+      case Json::nullValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value("null");
+        break;
+      }
+
+      case Json::intValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asInt());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::uintValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asUInt());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::realValue:
+      {
+        std::string s = boost::lexical_cast<std::string>(source.asFloat());
+        target.append_child(pugi::node_pcdata).set_value(s.c_str());
+        break;
+      }
+
+      case Json::stringValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value(source.asString().c_str());
+        break;
+      }
+
+      case Json::booleanValue:
+      {
+        target.append_child(pugi::node_pcdata).set_value(source.asBool() ? "true" : "false");
+        break;
+      }
+
+      case Json::arrayValue:
+      {
+        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+        {
+          pugi::xml_node node = target.append_child();
+          node.set_name(arrayElement.c_str());
+          JsonToXmlInternal(node, source[i], arrayElement);
+        }
+        break;
+      }
+        
+      case Json::objectValue:
+      {
+        Json::Value::Members members = source.getMemberNames();
+
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          pugi::xml_node node = target.append_child();
+          node.set_name(members[i].c_str());
+          JsonToXmlInternal(node, source[members[i]], arrayElement);          
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  void Toolbox::JsonToXml(std::string& target,
+                          const Json::Value& source,
+                          const std::string& rootElement,
+                          const std::string& arrayElement)
+  {
+    pugi::xml_document doc;
+
+    pugi::xml_node n = doc.append_child(rootElement.c_str());
+    JsonToXmlInternal(n, source, arrayElement);
+
+    pugi::xml_node decl = doc.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+
+    XmlToString(target, doc);
+  }
+
+  void Toolbox::XmlToString(std::string& target,
+                            const pugi::xml_document& source)
+  {
+    ChunkedBufferWriter writer;
+    source.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
+    writer.Flatten(target);
+  }
+#endif
+
+
+  
+  bool Toolbox::IsInteger(const std::string& str)
+  {
+    std::string s = StripSpaces(str);
+
+    if (s.size() == 0)
+    {
+      return false;
+    }
+
+    size_t pos = 0;
+    if (s[0] == '-')
+    {
+      if (s.size() == 1)
+      {
+        return false;
+      }
+
+      pos = 1;
+    }
+
+    while (pos < s.size())
+    {
+      if (!isdigit(s[pos]))
+      {
+        return false;
+      }
+
+      pos++;
+    }
+
+    return true;
+  }
+
+
+  void Toolbox::CopyJsonWithoutComments(Json::Value& target,
+                                        const Json::Value& source)
+  {
+    switch (source.type())
+    {
+      case Json::nullValue:
+        target = Json::nullValue;
+        break;
+
+      case Json::intValue:
+        target = source.asInt64();
+        break;
+
+      case Json::uintValue:
+        target = source.asUInt64();
+        break;
+
+      case Json::realValue:
+        target = source.asDouble();
+        break;
+
+      case Json::stringValue:
+        target = source.asString();
+        break;
+
+      case Json::booleanValue:
+        target = source.asBool();
+        break;
+
+      case Json::arrayValue:
+      {
+        target = Json::arrayValue;
+        for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+        {
+          Json::Value& item = target.append(Json::nullValue);
+          CopyJsonWithoutComments(item, source[i]);
+        }
+
+        break;
+      }
+
+      case Json::objectValue:
+      {
+        target = Json::objectValue;
+        Json::Value::Members members = source.getMemberNames();
+        for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
+        {
+          const std::string item = members[i];
+          CopyJsonWithoutComments(target[item], source[item]);
+        }
+
+        break;
+      }
+
+      default:
+        break;
+    }
+  }
+
+
+  bool Toolbox::StartsWith(const std::string& str,
+                           const std::string& prefix)
+  {
+    if (str.size() < prefix.size())
+    {
+      return false;
+    }
+    else
+    {
+      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;
+  }
+
+
+  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::unique_ptr<std::locale>  globalLocale_;
+
+  static bool SetGlobalLocale(const char* locale)
+  {
+    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& e)
+    {
+      LOG(ERROR) << "Cannot set globale locale to "
+                 << (locale ? std::string(locale) : "(null)")
+                 << ": " << e.what();
+      globalLocale_.reset(NULL);
+    }
+
+    return (globalLocale_.get() != NULL);
+  }
+
+  
+  static void InitializeIcu()
+  {
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      LOG(INFO) << "Setting up the ICU common data";
+
+      GzipCompressor compressor;
+      compressor.Uncompress(globalIcuData_,
+                            FrameworkResources::GetFileResourceBuffer(FrameworkResources::LIBICU_DATA),
+                            FrameworkResources::GetFileResourceSize(FrameworkResources::LIBICU_DATA));
+
+      std::string md5;
+      Toolbox::ComputeMD5(md5, globalIcuData_);
+
+      if (md5 != ORTHANC_ICU_DATA_MD5 ||
+          globalIcuData_.empty())
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot decode the ICU common data");
+      }
+
+      // "ICU data is designed to be 16-aligned"
+      // http://userguide.icu-project.org/icudata#TOC-Alignment
+
+      {
+        static const size_t ALIGN = 16;
+
+        UErrorCode status = U_ZERO_ERROR;
+
+        if (reinterpret_cast<intptr_t>(globalIcuData_.c_str()) % ALIGN == 0)
+        {
+          // Data is already properly aligned
+          udata_setCommonData(globalIcuData_.c_str(), &status);  
+        }
+        else
+        {
+          std::string aligned;
+          aligned.resize(globalIcuData_.size() + ALIGN - 1);
+
+          intptr_t offset = reinterpret_cast<intptr_t>(aligned.c_str()) % ALIGN;
+          if (offset != 0)
+          {
+            offset = ALIGN - offset;
+          }
+
+          if (offset + globalIcuData_.size() > aligned.size())
+          {
+            throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary");
+          }
+
+          // We don't use "memcpy()", as it expects its data to be aligned
+          const uint8_t* p = reinterpret_cast<uint8_t*>(&globalIcuData_[0]);
+          uint8_t* q = reinterpret_cast<uint8_t*>(&aligned[0]) + offset;
+          for (size_t i = 0; i < globalIcuData_.size(); i++, p++, q++)
+          {
+            *q = *p;
+          }
+        
+          globalIcuData_.swap(aligned);
+
+          const uint8_t* data = reinterpret_cast<const uint8_t*>(globalIcuData_.c_str()) + offset;
+        
+          if (reinterpret_cast<intptr_t>(data) % ALIGN != 0)
+          {
+            throw OrthancException(ErrorCode_InternalError, "Cannot align on 16-bytes boundary");
+          }
+          else
+          {
+            udata_setCommonData(data, &status);  
+          }
+        }
+
+        if (status != U_ZERO_ERROR)
+        {
+          throw OrthancException(ErrorCode_InternalError, "Cannot initialize ICU");
+        }
+      }
+
+      if (Toolbox::DetectEndianness() != Endianness_Little)
+      {
+        // TODO - The data table must be swapped (uint16_t)
+        throw OrthancException(ErrorCode_NotImplemented);
+      }
+
+      // "First-use of ICU from a single thread before the
+      // multi-threaded use of ICU begins", to make sure everything is
+      // properly initialized (should not be mandatory in our
+      // case). We let boost handle calls to "u_init()" and "u_cleanup()".
+      // http://userguide.icu-project.org/design#TOC-ICU-Initialization-and-Termination
+      uloc_getDefault();
+    }
+#endif
+  }
+  
+  void Toolbox::InitializeGlobalLocale(const char* locale)
+  {
+    InitializeIcu();
+
+#if defined(__unix__) && ORTHANC_SANDBOXED != 1
+    static const char* LOCALTIME = "/etc/localtime";
+    
+    if (!SystemToolbox::IsExistingFile(LOCALTIME))
+    {
+      // Check out file
+      // "boost_1_69_0/libs/locale/src/icu/time_zone.cpp": Direct
+      // access is made to this file if ICU is not used. Crash arises
+      // in Boost if the file is a symbolic link to a non-existing
+      // file (such as in Ubuntu 16.04 base Docker image).
+      throw OrthancException(
+        ErrorCode_InternalError,
+        "On UNIX-like systems, the file " + std::string(LOCALTIME) +
+        " must be present on the filesystem (install \"tzdata\" package on Debian)");
+    }
+#endif
+
+    // 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))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Cannot initialize global locale");
+    }
+
+  }
+
+
+  void Toolbox::FinalizeGlobalLocale()
+  {
+    globalLocale_.reset();
+  }
+
+
+  std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
+  {
+    bool error = (globalLocale_.get() == NULL);
+
+#if ORTHANC_STATIC_ICU == 1
+    if (globalIcuData_.empty())
+    {
+      error = true;
+    }
+#endif
+    
+    if (error)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No global locale was set, call Toolbox::InitializeGlobalLocale()");
+    }
+
+    /**
+     * 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, boost::locale::conv::skip);
+    w = boost::algorithm::to_upper_copy<std::wstring>(w, *globalLocale_);
+    return boost::locale::conv::utf_to_utf<char>(w, boost::locale::conv::skip);
+  }
+#endif
+
+
+
+#if ORTHANC_ENABLE_SSL == 0
+  /**
+   * OpenSSL is disabled
+   **/
+  void Toolbox::InitializeOpenSsl()
+  {
+  }
+  
+  void Toolbox::FinalizeOpenSsl()
+  {
+  }  
+
+
+#elif (ORTHANC_ENABLE_SSL == 1 &&               \
+       OPENSSL_VERSION_NUMBER < 0x10100000L) 
+  /**
+   * OpenSSL < 1.1.0
+   **/
+  void Toolbox::InitializeOpenSsl()
+  {
+    // https://wiki.openssl.org/index.php/Library_Initialization
+    SSL_library_init();
+    SSL_load_error_strings();
+    OpenSSL_add_all_algorithms();
+    ERR_load_crypto_strings();
+  }
+
+  void Toolbox::FinalizeOpenSsl()
+  {
+    // Finalize OpenSSL
+    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
+#ifdef FIPS_mode_set
+    FIPS_mode_set(0);
+#endif
+
+#if !defined(OPENSSL_NO_ENGINE)
+    ENGINE_cleanup();
+#endif
+    
+    CONF_modules_unload(1);
+    EVP_cleanup();
+    CRYPTO_cleanup_all_ex_data();
+    ERR_remove_state(0);
+    ERR_free_strings();
+  }
+
+  
+#elif (ORTHANC_ENABLE_SSL == 1 &&               \
+       OPENSSL_VERSION_NUMBER >= 0x10100000L) 
+  /**
+   * OpenSSL >= 1.1.0. In this case, the initialization is
+   * automatically done by the functions of OpenSSL.
+   * https://wiki.openssl.org/index.php/Library_Initialization
+   **/
+  void Toolbox::InitializeOpenSsl()
+  {
+  }
+
+  void Toolbox::FinalizeOpenSsl()
+  {
+  }
+
+#else
+#  error "Support your platform here"
+#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
+    uuid_t uuid;
+    uuid_generate_random ( uuid );
+    char s[37];
+    uuid_unparse ( uuid, s );
+#endif
+    return s;
+  }
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class VariableFormatter
+    {
+    public:
+      typedef std::map<std::string, std::string>   Dictionary;
+
+    private:
+      const Dictionary& dictionary_;
+
+    public:
+      VariableFormatter(const Dictionary& dictionary) :
+        dictionary_(dictionary)
+      {
+      }
+  
+      template<typename Out>
+      Out operator()(const boost::smatch& what,
+                     Out out) const
+      {
+        if (!what[1].str().empty())
+        {
+          // Variable without a default value
+          Dictionary::const_iterator found = dictionary_.find(what[1]);
+    
+          if (found != dictionary_.end())
+          {
+            const std::string& value = found->second;
+            out = std::copy(value.begin(), value.end(), out);
+          }
+        }
+        else
+        {
+          // Variable with a default value
+          std::string key;
+          std::string defaultValue;
+          
+          if (!what[2].str().empty())
+          {
+            key = what[2].str();
+            defaultValue = what[3].str();
+          }
+          else if (!what[4].str().empty())
+          {
+            key = what[4].str();
+            defaultValue = what[5].str();
+          }
+          else if (!what[6].str().empty())
+          {
+            key = what[6].str();
+            defaultValue = what[7].str();
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+
+          Dictionary::const_iterator found = dictionary_.find(key);
+    
+          if (found == dictionary_.end())
+          {
+            out = std::copy(defaultValue.begin(), defaultValue.end(), out);
+          }
+          else
+          {
+            const std::string& value = found->second;
+            out = std::copy(value.begin(), value.end(), out);
+          }
+        }
+    
+        return out;
+      }
+    };
+  }
+
+  
+  std::string Toolbox::SubstituteVariables(const std::string& source,
+                                           const std::map<std::string, std::string>& dictionary)
+  {
+    const boost::regex pattern("\\$\\{([^:]*?)\\}|"                 // ${what[1]}
+                               "\\$\\{([^:]*?):-([^'\"]*?)\\}|"     // ${what[2]:-what[3]}
+                               "\\$\\{([^:]*?):-\"([^\"]*?)\"\\}|"  // ${what[4]:-"what[5]"}
+                               "\\$\\{([^:]*?):-'([^']*?)'\\}");    // ${what[6]:-'what[7]'}
+
+    VariableFormatter formatter(dictionary);
+
+    return boost::regex_replace(source, pattern, formatter);
+  }
+
+
+  namespace Iso2022
+  {
+    /**
+       Returns whether the string s contains a single-byte control message
+       at index i
+    **/
+    static inline bool IsControlMessage1(const std::string& s, size_t i)
+    {
+      if (i < s.size())
+      {
+        char c = s[i];
+        return
+          (c == '\x0f') || // Locking shift zero
+          (c == '\x0e');   // Locking shift one
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       Returns whether the string s contains a double-byte control message
+       at index i
+    **/
+    static inline size_t IsControlMessage2(const std::string& s, size_t i)
+    {
+      if (i + 1 < s.size())
+      {
+        char c1 = s[i];
+        char c2 = s[i + 1];
+        return (c1 == 0x1b) && (
+          (c2 == '\x6e') || // Locking shift two
+          (c2 == '\x6f') || // Locking shift three
+          (c2 == '\x4e') || // Single shift two (alt)
+          (c2 == '\x4f') || // Single shift three (alt)
+          (c2 == '\x7c') || // Locking shift three right
+          (c2 == '\x7d') || // Locking shift two right
+          (c2 == '\x7e')    // Locking shift one right
+          );
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       Returns whether the string s contains a triple-byte control message
+       at index i
+    **/
+    static inline size_t IsControlMessage3(const std::string& s, size_t i)
+    {
+      if (i + 2 < s.size())
+      {
+        char c1 = s[i];
+        char c2 = s[i + 1];
+        char c3 = s[i + 2];
+        return ((c1 == '\x8e' && c2 == 0x1b && c3 == '\x4e') ||
+                (c1 == '\x8f' && c2 == 0x1b && c3 == '\x4f'));
+      }
+      else
+      {
+        return false;
+      }
+    }
+
+    /**
+       This function returns true if the index i in the supplied string s:
+       - is valid
+       - contains the c character
+       This function returns false otherwise.
+    **/
+    static inline bool TestCharValue(
+      const std::string& s, size_t i, char c)
+    {
+      if (i < s.size())
+        return s[i] == c;
+      else
+        return false;
+    }
+
+    /**
+       This function returns true if the index i in the supplied string s:
+       - is valid
+       - has a c character that is >= cMin and <= cMax (included)
+       This function returns false otherwise.
+    **/
+    static inline bool TestCharRange(
+      const std::string& s, size_t i, char cMin, char cMax)
+    {
+      if (i < s.size())
+        return (s[i] >= cMin) && (s[i] <= cMax);
+      else
+        return false;
+    }
+
+    /**
+       This function returns the total length in bytes of the escape sequence
+       located in string s at index i, if there is one, or 0 otherwise.
+    **/
+    static inline size_t GetEscapeSequenceLength(const std::string& s, size_t i)
+    {
+      if (TestCharValue(s, i, 0x1b))
+      {
+        size_t j = i+1;
+
+        // advance reading cursor while we are in a sequence 
+        while (TestCharRange(s, j, '\x20', '\x2f'))
+          ++j;
+
+        // check there is a valid termination byte AND we're long enough (there
+        // must be at least one byte between 0x20 and 0x2f
+        if (TestCharRange(s, j, '\x30', '\x7f') && (j - i) >= 2)
+          return j - i + 1;
+        else
+          return 0;
+      }
+      else
+        return 0;
+    }
+  }
+
+  
+
+  /**
+     This function will strip all ISO/IEC 2022 control codes and escape
+     sequences.
+     Please see https://en.wikipedia.org/wiki/ISO/IEC_2022 (as of 2019-02)
+     for a list of those.
+
+     Please note that this operation is potentially destructive, because
+     it removes the character set information from the byte stream.
+
+     However, in the case where the encoding is unique, then suppressing
+     the escape sequences allows one to provide us with a clean string after
+     conversion to utf-8 with boost.
+  **/
+  void Toolbox::RemoveIso2022EscapeSequences(std::string& dest, const std::string& src)
+  {
+    // we need AT MOST the same size as the source string in the output
+    dest.clear();
+    if (dest.capacity() < src.size())
+      dest.reserve(src.size());
+
+    size_t i = 0;
+
+    // uint8_t view to the string
+    while (i < src.size())
+    {
+      size_t j = i;
+
+      // The i index will only be incremented if a message is detected
+      // in that case, the message is skipped and the index is set to the
+      // next position to read
+      if (Iso2022::IsControlMessage1(src, i))
+        i += 1;
+      else if (Iso2022::IsControlMessage2(src, i))
+        i += 2;
+      else if (Iso2022::IsControlMessage3(src, i))
+        i += 3;
+      else
+        i += Iso2022::GetEscapeSequenceLength(src, i);
+
+      // if the index was NOT incremented, this means there was no message at
+      // this location: we then may copy the character at this index and 
+      // increment the index to point to the next read position
+      if (j == i)
+      {
+        dest.push_back(src[i]);
+        i++;
+      }
+    }
+  }
+
+
+  void Toolbox::Utf8ToUnicodeCharacter(uint32_t& unicode,
+                                       size_t& length,
+                                       const std::string& utf8,
+                                       size_t position)
+  {
+    // https://en.wikipedia.org/wiki/UTF-8
+
+    static const uint8_t MASK_IS_1_BYTE = 0x80;     // printf '0x%x\n' "$((2#10000000))"
+    static const uint8_t TEST_IS_1_BYTE = 0x00;
+ 
+    static const uint8_t MASK_IS_2_BYTES = 0xe0;    // printf '0x%x\n' "$((2#11100000))"
+    static const uint8_t TEST_IS_2_BYTES = 0xc0;    // printf '0x%x\n' "$((2#11000000))"
+
+    static const uint8_t MASK_IS_3_BYTES = 0xf0;    // printf '0x%x\n' "$((2#11110000))"
+    static const uint8_t TEST_IS_3_BYTES = 0xe0;    // printf '0x%x\n' "$((2#11100000))"
+
+    static const uint8_t MASK_IS_4_BYTES = 0xf8;    // printf '0x%x\n' "$((2#11111000))"
+    static const uint8_t TEST_IS_4_BYTES = 0xf0;    // printf '0x%x\n' "$((2#11110000))"
+
+    static const uint8_t MASK_CONTINUATION = 0xc0;  // printf '0x%x\n' "$((2#11000000))"
+    static const uint8_t TEST_CONTINUATION = 0x80;  // printf '0x%x\n' "$((2#10000000))"
+
+    if (position >= utf8.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    assert(sizeof(uint8_t) == sizeof(char));
+    const uint8_t* buffer = reinterpret_cast<const uint8_t*>(utf8.c_str()) + position;
+
+    if ((buffer[0] & MASK_IS_1_BYTE) == TEST_IS_1_BYTE)
+    {
+      length = 1;
+      unicode = buffer[0] & ~MASK_IS_1_BYTE;
+    }
+    else if ((buffer[0] & MASK_IS_2_BYTES) == TEST_IS_2_BYTES &&
+             position + 1 < utf8.size() &&
+             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION)
+    {
+      length = 2;
+      uint32_t a = buffer[0] & ~MASK_IS_2_BYTES;
+      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
+      unicode = (a << 6) | b;
+    }
+    else if ((buffer[0] & MASK_IS_3_BYTES) == TEST_IS_3_BYTES &&
+             position + 2 < utf8.size() &&
+             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION &&
+             (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION)
+    {
+      length = 3;
+      uint32_t a = buffer[0] & ~MASK_IS_3_BYTES;
+      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
+      uint32_t c = buffer[2] & ~MASK_CONTINUATION;
+      unicode = (a << 12) | (b << 6) | c;
+    }
+    else if ((buffer[0] & MASK_IS_4_BYTES) == TEST_IS_4_BYTES &&
+             position + 3 < utf8.size() &&
+             (buffer[1] & MASK_CONTINUATION) == TEST_CONTINUATION &&
+             (buffer[2] & MASK_CONTINUATION) == TEST_CONTINUATION &&
+             (buffer[3] & MASK_CONTINUATION) == TEST_CONTINUATION)
+    {
+      length = 4;
+      uint32_t a = buffer[0] & ~MASK_IS_4_BYTES;
+      uint32_t b = buffer[1] & ~MASK_CONTINUATION;
+      uint32_t c = buffer[2] & ~MASK_CONTINUATION;
+      uint32_t d = buffer[3] & ~MASK_CONTINUATION;
+      unicode = (a << 18) | (b << 12) | (c << 6) | d;
+    }
+    else
+    {
+      // This is not a valid UTF-8 encoding
+      throw OrthancException(ErrorCode_BadFileFormat, "Invalid UTF-8 string");
+    }
+  }
+
+
+  std::string Toolbox::LargeHexadecimalToDecimal(const std::string& hex)
+  {
+    /**
+     * NB: Focus of the code below is *not* efficiency, but
+     * readability!
+     **/
+    
+    for (size_t i = 0; i < hex.size(); i++)
+    {
+      const char c = hex[i];
+      if (!((c >= 'A' && c <= 'F') ||
+            (c >= 'a' && c <= 'f') ||
+            (c >= '0' && c <= '9')))
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange,
+                                        "Not an hexadecimal number");
+      }
+    }
+    
+    std::vector<uint8_t> decimal;
+    decimal.push_back(0);
+
+    for (size_t i = 0; i < hex.size(); i++)
+    {
+      uint8_t hexDigit = static_cast<uint8_t>(Hex2Dec(hex[i]));
+      assert(hexDigit <= 15);
+
+      for (size_t j = 0; j < decimal.size(); j++)
+      {
+        uint8_t val = static_cast<uint8_t>(decimal[j]) * 16 + hexDigit;  // Maximum: 9 * 16 + 15
+        assert(val <= 159 /* == 9 * 16 + 15 */);
+      
+        decimal[j] = val % 10;
+        hexDigit = val / 10;
+        assert(hexDigit <= 15 /* == 159 / 10 */);
+      }
+
+      while (hexDigit > 0)
+      {
+        decimal.push_back(hexDigit % 10);
+        hexDigit /= 10;
+      }
+    }
+
+    size_t start = 0;
+    while (start < decimal.size() &&
+           decimal[start] == '0')
+    {
+      start++;
+    }
+
+    std::string s;
+    s.reserve(decimal.size() - start);
+
+    for (size_t i = decimal.size(); i > start; i--)
+    {
+      s.push_back(decimal[i - 1] + '0');
+    }
+
+    return s;
+  }
+
+
+  std::string Toolbox::GenerateDicomPrivateUniqueIdentifier()
+  {
+    /**
+     * REFERENCE: "Creating a Privately Defined Unique Identifier
+     * (Informative)" / "UUID Derived UID"
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
+     * https://stackoverflow.com/a/46316162/881731
+     **/
+
+    std::string uuid = GenerateUuid();
+    assert(IsUuid(uuid) && uuid.size() == 36);
+
+    /**
+     * After removing the four dashes ("-") out of the 36-character
+     * UUID, we get a large hexadecimal number with 32 characters,
+     * each of those characters lying in the range [0,16[. The large
+     * number is thus in the [0,16^32[ = [0,256^16[ range. This number
+     * has a maximum of 39 decimal digits, as can be seen in Python:
+     * 
+     * # python -c 'import math; print(math.log(16**32))/math.log(10))'
+     * 38.531839445
+     *
+     * We now to convert the large hexadecimal number to a decimal
+     * number with up to 39 digits, remove the leading zeros, then
+     * prefix it with "2.25."
+     **/
+
+    // Remove the dashes
+    std::string hex = (uuid.substr(0, 8) +
+                       uuid.substr(9, 4) +
+                       uuid.substr(14, 4) +
+                       uuid.substr(19, 4) +
+                       uuid.substr(24, 12));
+    assert(hex.size() == 32);
+
+    return "2.25." + LargeHexadecimalToDecimal(hex);
+  }
+}
+
+
+
+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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/Toolbox.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,292 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#include <stdint.h>
+#include <vector>
+#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
+
+#if !defined(ORTHANC_ENABLE_SSL)
+#  error The macro ORTHANC_ENABLE_SSL 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
+ **/
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
+
+
+namespace Orthanc
+{
+  typedef std::vector<std::string> UriComponents;
+
+  class NullType
+  {
+  };
+
+  class ORTHANC_PUBLIC Toolbox
+  {
+  public:
+    class ORTHANC_PUBLIC LinesIterator
+    {
+    private:
+      const std::string& content_;
+      size_t             lineStart_;
+      size_t             lineEnd_;
+
+      void FindEndOfLine();
+  
+    public:
+      LinesIterator(const std::string& content);
+  
+      bool GetLine(std::string& target) const;
+
+      void Next();
+    };
+    
+    static void ToUpperCase(std::string& s);  // Inplace version
+
+    static void ToLowerCase(std::string& s);  // Inplace version
+
+    static void ToUpperCase(std::string& result,
+                            const std::string& source);
+
+    static void ToLowerCase(std::string& result,
+                            const std::string& source);
+
+    static void SplitUriComponents(UriComponents& components,
+                                   const std::string& uri);
+  
+    static void TruncateUri(UriComponents& target,
+                            const UriComponents& source,
+                            size_t fromLevel);
+  
+    static bool IsChildUri(const UriComponents& baseUri,
+                           const UriComponents& testedUri);
+
+    static std::string FlattenUri(const UriComponents& components,
+                                  size_t fromLevel = 0);
+
+#if ORTHANC_ENABLE_MD5 == 1
+    static void ComputeMD5(std::string& result,
+                           const std::string& data);
+
+    static void ComputeMD5(std::string& result,
+                           const void* data,
+                           size_t size);
+#endif
+
+    static void ComputeSHA1(std::string& result,
+                            const std::string& data);
+
+    static void ComputeSHA1(std::string& result,
+                            const void* data,
+                            size_t size);
+
+    static bool IsSHA1(const void* str,
+                       size_t size);
+
+    static bool IsSHA1(const std::string& s);
+
+#if ORTHANC_ENABLE_BASE64 == 1
+    static void DecodeBase64(std::string& result, 
+                             const std::string& data);
+
+    static void EncodeBase64(std::string& result, 
+                             const std::string& data);
+
+    static bool DecodeDataUriScheme(std::string& mime,
+                                    std::string& content,
+                                    const std::string& source);
+
+    static void EncodeDataUriScheme(std::string& result,
+                                    const std::string& mime,
+                                    const std::string& content);
+#endif
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    static std::string ConvertToUtf8(const std::string& source,
+                                     Encoding sourceEncoding,
+                                     bool hasCodeExtensions);
+
+    static std::string ConvertFromUtf8(const std::string& source,
+                                       Encoding targetEncoding);
+#endif
+
+    static bool IsAsciiString(const void* data,
+                              size_t size);
+
+    static bool IsAsciiString(const std::string& s);
+
+    static std::string ConvertToAscii(const std::string& source);
+
+    static std::string StripSpaces(const std::string& source);
+
+    // In-place percent-decoding for URL
+    static void UrlDecode(std::string& s);
+
+    static Endianness DetectEndianness();
+
+    static std::string WildcardToRegularExpression(const std::string& s);
+
+    static void TokenizeString(std::vector<std::string>& result,
+                               const std::string& source,
+                               char separator);
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+    static void JsonToXml(std::string& target,
+                          const Json::Value& source,
+                          const std::string& rootElement = "root",
+                          const std::string& arrayElement = "item");
+#endif
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+    static void XmlToString(std::string& target,
+                            const pugi::xml_document& source);
+#endif
+
+    static bool IsInteger(const std::string& str);
+
+    static void CopyJsonWithoutComments(Json::Value& target,
+                                        const Json::Value& source);
+
+    static bool StartsWith(const std::string& str,
+                           const std::string& prefix);
+
+    static void UriEncode(std::string& target,
+                          const std::string& source);
+
+    static std::string GetJsonStringField(const ::Json::Value& json,
+                                          const std::string& key,
+                                          const std::string& defaultValue);
+
+    static bool GetJsonBooleanField(const ::Json::Value& json,
+                                    const std::string& key,
+                                    bool defaultValue);
+
+    static int GetJsonIntegerField(const ::Json::Value& json,
+                                   const std::string& key,
+                                   int defaultValue);
+
+    static unsigned int GetJsonUnsignedIntegerField(const ::Json::Value& json,
+                                                    const std::string& key,
+                                                    unsigned int defaultValue);
+
+    static bool IsUuid(const std::string& str);
+
+    static bool StartsWithUuid(const std::string& str);
+
+#if ORTHANC_ENABLE_LOCALE == 1
+    static void InitializeGlobalLocale(const char* locale);
+
+    static void FinalizeGlobalLocale();
+
+    static std::string ToUpperCaseWithAccents(const std::string& source);
+#endif
+
+    static void InitializeOpenSsl();
+
+    static void FinalizeOpenSsl();
+
+    static std::string GenerateUuid();
+
+    static std::string SubstituteVariables(const std::string& source,
+                                           const std::map<std::string, std::string>& dictionary);
+
+    static void RemoveIso2022EscapeSequences(std::string& dest,
+                                             const std::string& src);
+
+    static void Utf8ToUnicodeCharacter(uint32_t& unicode,
+                                       size_t& utf8Length,
+                                       const std::string& utf8,
+                                       size_t position);
+
+    static std::string LargeHexadecimalToDecimal(const std::string& hex);
+
+    // http://dicom.nema.org/medical/dicom/2019a/output/chtml/part05/sect_B.2.html
+    static std::string GenerateDicomPrivateUniqueIdentifier();
+  };
+}
+
+
+
+
+/**
+ * The plain C, opaque data structure "OrthancLinesIterator" is a thin
+ * wrapper around Orthanc::Toolbox::LinesIterator, and is only used by
+ * "../Resources/Patches/dcmtk-dcdict_orthanc.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);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/WebServiceParameters.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,585 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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://"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Bad URL: " + url);
+    }
+
+    // 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())
+    {
+      throw OrthancException(
+        ErrorCode_BadFileFormat,
+        "The password for the HTTPS certificate is not provided: " + certificateFile);
+    }
+
+    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)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "The HTTP password is not provided");
+    }
+    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))
+      {
+        switch (peer[*it].type())
+        {
+          case Json::stringValue:
+            userProperties_[*it] = peer[*it].asString();
+            break;
+
+          case Json::booleanValue:
+            userProperties_[*it] = peer[*it].asBool() ? "1" : "0";
+            break;
+
+          case Json::intValue:
+            userProperties_[*it] = boost::lexical_cast<std::string>(peer[*it].asInt());
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_BadFileFormat,
+                                   "User-defined properties associated with a Web service must be strings: " + *it);
+        }
+      }
+    }
+  }
+
+
+  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))
+    {
+      throw OrthancException(
+        ErrorCode_ParameterOutOfRange,
+        "Cannot use this reserved key to name an user property: " + key);
+    }
+    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::GetBooleanUserProperty(const std::string& key,
+                                                    bool defaultValue) const
+  {
+    Dictionary::const_iterator found = userProperties_.find(key);
+
+    if (found == userProperties_.end())
+    {
+      return defaultValue;
+    }
+    else if (found->second == "0" ||
+             found->second == "false")
+    {
+      return false;
+    }
+    else if (found->second == "1" ||
+             found->second == "true")
+    {
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters "
+                             "of a Web service: Property \"" + key + "\" equals: " + found->second);
+    }    
+  }
+
+
+  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_))
+      {
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "Cannot open certificate file: " + certificateFile_);
+      }
+
+      if (!certificateKeyFile_.empty() && 
+          !SystemToolbox::IsRegularFile(certificateKeyFile_))
+      {
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "Cannot open key file: " + certificateKeyFile_);
+      }
+    }
+  }
+#endif
+
+
+  void WebServiceParameters::FormatPublic(Json::Value& target) const
+  {
+    target = Json::objectValue;
+
+    // Only return the public information identifying the destination.
+    // "Security"-related information such as passwords and HTTP
+    // headers are shown as "null" values.
+    target[KEY_URL] = url_;
+
+    if (!username_.empty())
+    {
+      target[KEY_USERNAME] = username_;
+      target[KEY_PASSWORD] = Json::nullValue;
+    }
+
+    if (!certificateFile_.empty())
+    {
+      target[KEY_CERTIFICATE_FILE] = certificateFile_;
+      target[KEY_CERTIFICATE_KEY_FILE] = Json::nullValue;
+      target[KEY_CERTIFICATE_KEY_PASSWORD] = Json::nullValue;      
+    }
+
+    target[KEY_PKCS11] = pkcs11Enabled_;
+
+    Json::Value headers = Json::arrayValue;
+      
+    for (Dictionary::const_iterator it = headers_.begin();
+         it != headers_.end(); ++it)
+    {
+      // Only list the HTTP headers, not their value
+      headers.append(it->first);
+    }
+
+    target[KEY_HTTP_HEADERS] = headers;
+
+    for (Dictionary::const_iterator it = userProperties_.begin();
+         it != userProperties_.end(); ++it)
+    {
+      target[it->first] = it->second;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/Sources/WebServiceParameters.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,186 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFramework.h"
+
+#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 ORTHANC_PUBLIC 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 GetBooleanUserProperty(const std::string& key,
+                                bool defaultValue) 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
+
+    void FormatPublic(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/FrameworkTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1342 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "../Core/EnumerationDictionary.h"
+
+#include "gtest/gtest.h"
+
+#include <ctype.h>
+
+#include "../Core/DicomFormat/DicomTag.h"
+#include "../Core/FileBuffer.h"
+#include "../Core/HttpServer/HttpToolbox.h"
+#include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
+#include "../Core/OrthancException.h"
+#include "../Core/SystemToolbox.h"
+#include "../Core/TemporaryFile.h"
+#include "../Core/Toolbox.h"
+
+
+using namespace Orthanc;
+
+
+TEST(Uuid, Generation)
+{
+  for (int i = 0; i < 10; i++)
+  {
+    std::string s = Toolbox::GenerateUuid();
+    ASSERT_TRUE(Toolbox::IsUuid(s));
+  }
+}
+
+TEST(Uuid, Test)
+{
+  ASSERT_FALSE(Toolbox::IsUuid(""));
+  ASSERT_FALSE(Toolbox::IsUuid("012345678901234567890123456789012345"));
+  ASSERT_TRUE(Toolbox::IsUuid("550e8400-e29b-41d4-a716-446655440000"));
+  ASSERT_FALSE(Toolbox::IsUuid("550e8400-e29b-41d4-a716-44665544000_"));
+  ASSERT_FALSE(Toolbox::IsUuid("01234567890123456789012345678901234_"));
+  ASSERT_FALSE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-44665544000"));
+  ASSERT_TRUE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000"));
+  ASSERT_TRUE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000 ok"));
+  ASSERT_FALSE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000ok"));
+}
+
+TEST(Toolbox, IsSHA1)
+{
+  ASSERT_FALSE(Toolbox::IsSHA1(""));
+  ASSERT_FALSE(Toolbox::IsSHA1("01234567890123456789012345678901234567890123"));
+  ASSERT_FALSE(Toolbox::IsSHA1("012345678901234567890123456789012345678901234"));
+  ASSERT_TRUE(Toolbox::IsSHA1("b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b9"));
+
+  std::string sha = "         b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b9          ";
+  ASSERT_TRUE(Toolbox::IsSHA1(sha));
+  sha[3] = '\0';
+  sha[53] = '\0';
+  ASSERT_TRUE(Toolbox::IsSHA1(sha));
+  sha[40] = '\0';
+  ASSERT_FALSE(Toolbox::IsSHA1(sha));
+  ASSERT_FALSE(Toolbox::IsSHA1("       "));
+
+  ASSERT_TRUE(Toolbox::IsSHA1("16738bc3-e47ed42a-43ce044c-a3414a45-cb069bd0"));
+
+  std::string s;
+  Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog");
+  ASSERT_TRUE(Toolbox::IsSHA1(s));
+  ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s);
+
+  ASSERT_FALSE(Toolbox::IsSHA1("b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b_"));
+}
+
+
+TEST(ParseGetArguments, Basic)
+{
+  IHttpHandler::GetArguments b;
+  HttpToolbox::ParseGetArguments(b, "aaa=baaa&bb=a&aa=c");
+
+  IHttpHandler::Arguments a;
+  HttpToolbox::CompileGetArguments(a, b);
+
+  ASSERT_EQ(3u, a.size());
+  ASSERT_EQ(a["aaa"], "baaa");
+  ASSERT_EQ(a["bb"], "a");
+  ASSERT_EQ(a["aa"], "c");
+}
+
+TEST(ParseGetArguments, BasicEmpty)
+{
+  IHttpHandler::GetArguments b;
+  HttpToolbox::ParseGetArguments(b, "aaa&bb=aa&aa");
+
+  IHttpHandler::Arguments a;
+  HttpToolbox::CompileGetArguments(a, b);
+
+  ASSERT_EQ(3u, a.size());
+  ASSERT_EQ(a["aaa"], "");
+  ASSERT_EQ(a["bb"], "aa");
+  ASSERT_EQ(a["aa"], "");
+}
+
+TEST(ParseGetArguments, Single)
+{
+  IHttpHandler::GetArguments b;
+  HttpToolbox::ParseGetArguments(b, "aaa=baaa");
+
+  IHttpHandler::Arguments a;
+  HttpToolbox::CompileGetArguments(a, b);
+
+  ASSERT_EQ(1u, a.size());
+  ASSERT_EQ(a["aaa"], "baaa");
+}
+
+TEST(ParseGetArguments, SingleEmpty)
+{
+  IHttpHandler::GetArguments b;
+  HttpToolbox::ParseGetArguments(b, "aaa");
+
+  IHttpHandler::Arguments a;
+  HttpToolbox::CompileGetArguments(a, b);
+
+  ASSERT_EQ(1u, a.size());
+  ASSERT_EQ(a["aaa"], "");
+}
+
+TEST(ParseGetQuery, Test1)
+{
+  UriComponents uri;
+  IHttpHandler::GetArguments b;
+  HttpToolbox::ParseGetQuery(uri, b, "/instances/test/world?aaa=baaa&bb=a&aa=c");
+
+  IHttpHandler::Arguments a;
+  HttpToolbox::CompileGetArguments(a, b);
+
+  ASSERT_EQ(3u, uri.size());
+  ASSERT_EQ("instances", uri[0]);
+  ASSERT_EQ("test", uri[1]);
+  ASSERT_EQ("world", uri[2]);
+  ASSERT_EQ(3u, a.size());
+  ASSERT_EQ(a["aaa"], "baaa");
+  ASSERT_EQ(a["bb"], "a");
+  ASSERT_EQ(a["aa"], "c");
+}
+
+TEST(ParseGetQuery, Test2)
+{
+  UriComponents uri;
+  IHttpHandler::GetArguments b;
+  HttpToolbox::ParseGetQuery(uri, b, "/instances/test/world");
+
+  IHttpHandler::Arguments a;
+  HttpToolbox::CompileGetArguments(a, b);
+
+  ASSERT_EQ(3u, uri.size());
+  ASSERT_EQ("instances", uri[0]);
+  ASSERT_EQ("test", uri[1]);
+  ASSERT_EQ("world", uri[2]);
+  ASSERT_EQ(0u, a.size());
+}
+
+TEST(Uri, SplitUriComponents)
+{
+  UriComponents c, d;
+  Toolbox::SplitUriComponents(c, "/cou/hello/world");
+  ASSERT_EQ(3u, c.size());
+  ASSERT_EQ("cou", c[0]);
+  ASSERT_EQ("hello", c[1]);
+  ASSERT_EQ("world", c[2]);
+
+  Toolbox::SplitUriComponents(c, "/cou/hello/world/");
+  ASSERT_EQ(3u, c.size());
+  ASSERT_EQ("cou", c[0]);
+  ASSERT_EQ("hello", c[1]);
+  ASSERT_EQ("world", c[2]);
+
+  Toolbox::SplitUriComponents(c, "/cou/hello/world/a");
+  ASSERT_EQ(4u, c.size());
+  ASSERT_EQ("cou", c[0]);
+  ASSERT_EQ("hello", c[1]);
+  ASSERT_EQ("world", c[2]);
+  ASSERT_EQ("a", c[3]);
+
+  Toolbox::SplitUriComponents(c, "/");
+  ASSERT_EQ(0u, c.size());
+
+  Toolbox::SplitUriComponents(c, "/hello");
+  ASSERT_EQ(1u, c.size());
+  ASSERT_EQ("hello", c[0]);
+
+  Toolbox::SplitUriComponents(c, "/hello/");
+  ASSERT_EQ(1u, c.size());
+  ASSERT_EQ("hello", c[0]);
+
+  ASSERT_THROW(Toolbox::SplitUriComponents(c, ""), OrthancException);
+  ASSERT_THROW(Toolbox::SplitUriComponents(c, "a"), OrthancException);
+  ASSERT_THROW(Toolbox::SplitUriComponents(c, "/coucou//coucou"), OrthancException);
+
+  c.clear();
+  c.push_back("test");
+  ASSERT_EQ("/", Toolbox::FlattenUri(c, 10));
+}
+
+
+TEST(Uri, Truncate)
+{
+  UriComponents c, d;
+  Toolbox::SplitUriComponents(c, "/cou/hello/world");
+
+  Toolbox::TruncateUri(d, c, 0);
+  ASSERT_EQ(3u, d.size());
+  ASSERT_EQ("cou", d[0]);
+  ASSERT_EQ("hello", d[1]);
+  ASSERT_EQ("world", d[2]);
+
+  Toolbox::TruncateUri(d, c, 1);
+  ASSERT_EQ(2u, d.size());
+  ASSERT_EQ("hello", d[0]);
+  ASSERT_EQ("world", d[1]);
+
+  Toolbox::TruncateUri(d, c, 2);
+  ASSERT_EQ(1u, d.size());
+  ASSERT_EQ("world", d[0]);
+
+  Toolbox::TruncateUri(d, c, 3);
+  ASSERT_EQ(0u, d.size());
+
+  Toolbox::TruncateUri(d, c, 4);
+  ASSERT_EQ(0u, d.size());
+
+  Toolbox::TruncateUri(d, c, 5);
+  ASSERT_EQ(0u, d.size());
+}
+
+
+TEST(Uri, Child)
+{
+  UriComponents c1;  Toolbox::SplitUriComponents(c1, "/hello/world");  
+  UriComponents c2;  Toolbox::SplitUriComponents(c2, "/hello/hello");  
+  UriComponents c3;  Toolbox::SplitUriComponents(c3, "/hello");  
+  UriComponents c4;  Toolbox::SplitUriComponents(c4, "/world");  
+  UriComponents c5;  Toolbox::SplitUriComponents(c5, "/");  
+
+  ASSERT_TRUE(Toolbox::IsChildUri(c1, c1));
+  ASSERT_FALSE(Toolbox::IsChildUri(c1, c2));
+  ASSERT_FALSE(Toolbox::IsChildUri(c1, c3));
+  ASSERT_FALSE(Toolbox::IsChildUri(c1, c4));
+  ASSERT_FALSE(Toolbox::IsChildUri(c1, c5));
+
+  ASSERT_FALSE(Toolbox::IsChildUri(c2, c1));
+  ASSERT_TRUE(Toolbox::IsChildUri(c2, c2));
+  ASSERT_FALSE(Toolbox::IsChildUri(c2, c3));
+  ASSERT_FALSE(Toolbox::IsChildUri(c2, c4));
+  ASSERT_FALSE(Toolbox::IsChildUri(c2, c5));
+
+  ASSERT_TRUE(Toolbox::IsChildUri(c3, c1));
+  ASSERT_TRUE(Toolbox::IsChildUri(c3, c2));
+  ASSERT_TRUE(Toolbox::IsChildUri(c3, c3));
+  ASSERT_FALSE(Toolbox::IsChildUri(c3, c4));
+  ASSERT_FALSE(Toolbox::IsChildUri(c3, c5));
+
+  ASSERT_FALSE(Toolbox::IsChildUri(c4, c1));
+  ASSERT_FALSE(Toolbox::IsChildUri(c4, c2));
+  ASSERT_FALSE(Toolbox::IsChildUri(c4, c3));
+  ASSERT_TRUE(Toolbox::IsChildUri(c4, c4));
+  ASSERT_FALSE(Toolbox::IsChildUri(c4, c5));
+
+  ASSERT_TRUE(Toolbox::IsChildUri(c5, c1));
+  ASSERT_TRUE(Toolbox::IsChildUri(c5, c2));
+  ASSERT_TRUE(Toolbox::IsChildUri(c5, c3));
+  ASSERT_TRUE(Toolbox::IsChildUri(c5, c4));
+  ASSERT_TRUE(Toolbox::IsChildUri(c5, c5));
+}
+
+TEST(Uri, AutodetectMimeType)
+{
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("../NOTES"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType(""));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("/"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("a/a"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\"));
+  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\a"));
+
+  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../NOTES.txt"));
+  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../coucou.xml/NOTES.txt"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("..\\coucou.\\NOTES.xml"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.xml"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.XmL"));
+
+  ASSERT_EQ(MimeType_JavaScript, SystemToolbox::AutodetectMimeType("NOTES.js"));
+  ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.json"));
+  ASSERT_EQ(MimeType_Pdf, SystemToolbox::AutodetectMimeType("NOTES.pdf"));
+  ASSERT_EQ(MimeType_Css, SystemToolbox::AutodetectMimeType("NOTES.css"));
+  ASSERT_EQ(MimeType_Html, SystemToolbox::AutodetectMimeType("NOTES.html"));
+  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("NOTES.txt"));
+  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("NOTES.xml"));
+  ASSERT_EQ(MimeType_Gif, SystemToolbox::AutodetectMimeType("NOTES.gif"));
+  ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpg"));
+  ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpeg"));
+  ASSERT_EQ(MimeType_Png, SystemToolbox::AutodetectMimeType("NOTES.png"));
+  ASSERT_EQ(MimeType_NaCl, SystemToolbox::AutodetectMimeType("NOTES.nexe"));
+  ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.nmf"));
+  ASSERT_EQ(MimeType_PNaCl, SystemToolbox::AutodetectMimeType("NOTES.pexe"));
+  ASSERT_EQ(MimeType_Svg, SystemToolbox::AutodetectMimeType("NOTES.svg"));
+  ASSERT_EQ(MimeType_Woff, SystemToolbox::AutodetectMimeType("NOTES.woff"));
+  ASSERT_EQ(MimeType_Woff2, SystemToolbox::AutodetectMimeType("NOTES.woff2"));
+
+  // Test primitives from the "RegisterDefaultExtensions()" that was
+  // present in the sample "Serve Folders plugin" of Orthanc 1.4.2
+  ASSERT_STREQ("application/javascript", EnumerationToString(SystemToolbox::AutodetectMimeType(".js")));
+  ASSERT_STREQ("application/json", EnumerationToString(SystemToolbox::AutodetectMimeType(".json")));
+  ASSERT_STREQ("application/json", EnumerationToString(SystemToolbox::AutodetectMimeType(".nmf")));
+  ASSERT_STREQ("application/octet-stream", EnumerationToString(SystemToolbox::AutodetectMimeType("")));
+  ASSERT_STREQ("application/wasm", EnumerationToString(SystemToolbox::AutodetectMimeType(".wasm")));
+  ASSERT_STREQ("application/x-font-woff", EnumerationToString(SystemToolbox::AutodetectMimeType(".woff")));
+  ASSERT_STREQ("application/x-nacl", EnumerationToString(SystemToolbox::AutodetectMimeType(".nexe")));
+  ASSERT_STREQ("application/x-pnacl", EnumerationToString(SystemToolbox::AutodetectMimeType(".pexe")));
+  ASSERT_STREQ("application/xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".xml")));
+  ASSERT_STREQ("font/woff2", EnumerationToString(SystemToolbox::AutodetectMimeType(".woff2")));
+  ASSERT_STREQ("image/gif", EnumerationToString(SystemToolbox::AutodetectMimeType(".gif")));
+  ASSERT_STREQ("image/jpeg", EnumerationToString(SystemToolbox::AutodetectMimeType(".jpeg")));
+  ASSERT_STREQ("image/jpeg", EnumerationToString(SystemToolbox::AutodetectMimeType(".jpg")));
+  ASSERT_STREQ("image/png", EnumerationToString(SystemToolbox::AutodetectMimeType(".png")));
+  ASSERT_STREQ("image/svg+xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".svg")));
+  ASSERT_STREQ("text/css", EnumerationToString(SystemToolbox::AutodetectMimeType(".css")));
+  ASSERT_STREQ("text/html", EnumerationToString(SystemToolbox::AutodetectMimeType(".html")));
+}
+
+TEST(Toolbox, ComputeMD5)
+{
+  std::string s;
+
+  // # echo -n "Hello" | md5sum
+
+  Toolbox::ComputeMD5(s, "Hello");
+  ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s);
+  Toolbox::ComputeMD5(s, "");
+  ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s);
+}
+
+TEST(Toolbox, ComputeSHA1)
+{
+  std::string s;
+  
+  Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog");
+  ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s);
+  Toolbox::ComputeSHA1(s, "");
+  ASSERT_EQ("da39a3ee-5e6b4b0d-3255bfef-95601890-afd80709", s);
+}
+
+TEST(Toolbox, PathToExecutable)
+{
+  printf("[%s]\n", SystemToolbox::GetPathToExecutable().c_str());
+  printf("[%s]\n", SystemToolbox::GetDirectoryOfExecutable().c_str());
+}
+
+TEST(Toolbox, StripSpaces)
+{
+  ASSERT_EQ("", Toolbox::StripSpaces("       \t  \r   \n  "));
+  ASSERT_EQ("coucou", Toolbox::StripSpaces("    coucou   \t  \r   \n  "));
+  ASSERT_EQ("cou   cou", Toolbox::StripSpaces("    cou   cou    \n  "));
+  ASSERT_EQ("c", Toolbox::StripSpaces("    \n\t c\r    \n  "));
+}
+
+TEST(Toolbox, Case)
+{
+  std::string s = "CoU";
+  std::string ss;
+
+  Toolbox::ToUpperCase(ss, s);
+  ASSERT_EQ("COU", ss);
+  Toolbox::ToLowerCase(ss, s);
+  ASSERT_EQ("cou", ss); 
+
+  s = "CoU";
+  Toolbox::ToUpperCase(s);
+  ASSERT_EQ("COU", s);
+
+  s = "CoU";
+  Toolbox::ToLowerCase(s);
+  ASSERT_EQ("cou", s);
+}
+
+
+TEST(Logger, Basic)
+{
+  LOG(INFO) << "I say hello";
+}
+
+TEST(Toolbox, ConvertFromLatin1)
+{
+  // This is a Latin-1 test string
+  const unsigned char data[10] = { 0xe0, 0xe9, 0xea, 0xe7, 0x26, 0xc6, 0x61, 0x62, 0x63, 0x00 };
+  
+  std::string s((char*) &data[0], 10);
+  ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s));
+
+  // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C"
+  std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false);
+  ASSERT_EQ(15u, utf8.size());
+  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[0]));
+  ASSERT_EQ(0xa0, static_cast<unsigned char>(utf8[1]));
+  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[2]));
+  ASSERT_EQ(0xa9, static_cast<unsigned char>(utf8[3]));
+  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[4]));
+  ASSERT_EQ(0xaa, static_cast<unsigned char>(utf8[5]));
+  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[6]));
+  ASSERT_EQ(0xa7, static_cast<unsigned char>(utf8[7]));
+  ASSERT_EQ(0x26, static_cast<unsigned char>(utf8[8]));
+  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[9]));
+  ASSERT_EQ(0x86, static_cast<unsigned char>(utf8[10]));
+  ASSERT_EQ(0x61, static_cast<unsigned char>(utf8[11]));
+  ASSERT_EQ(0x62, static_cast<unsigned char>(utf8[12]));
+  ASSERT_EQ(0x63, static_cast<unsigned char>(utf8[13]));
+  ASSERT_EQ(0x00, static_cast<unsigned char>(utf8[14]));  // Null-terminated string
+}
+
+
+TEST(Toolbox, FixUtf8)
+{
+  // This is a Latin-1 test string: "crane" with a circumflex accent
+  const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
+
+  std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char));
+
+  ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false), Encoding_Latin1));
+  ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false));
+}
+
+
+static int32_t GetUnicode(const uint8_t* data,
+                          size_t size,
+                          size_t expectedLength)
+{
+  std::string s((char*) &data[0], size);
+  uint32_t unicode;
+  size_t length;
+  Toolbox::Utf8ToUnicodeCharacter(unicode, length, s, 0);
+  if (length != expectedLength)
+  {
+    return -1;  // Error case
+  }
+  else
+  {
+    return unicode;
+  }
+}
+
+
+TEST(Toolbox, Utf8ToUnicode)
+{
+  // https://en.wikipedia.org/wiki/UTF-8
+  
+  ASSERT_EQ(1u, sizeof(char));
+  ASSERT_EQ(1u, sizeof(uint8_t));
+  
+  {
+    const uint8_t data[] = { 0x24 };
+    ASSERT_EQ(0x24, GetUnicode(data, 1, 1));
+    ASSERT_THROW(GetUnicode(data, 0, 1), OrthancException);
+  }
+  
+  {
+    const uint8_t data[] = { 0xc2, 0xa2 };
+    ASSERT_EQ(0xa2, GetUnicode(data, 2, 2));
+    ASSERT_THROW(GetUnicode(data, 1, 2), OrthancException);
+  }
+  
+  {
+    const uint8_t data[] = { 0xe0, 0xa4, 0xb9 };
+    ASSERT_EQ(0x0939, GetUnicode(data, 3, 3));
+    ASSERT_THROW(GetUnicode(data, 2, 3), OrthancException);
+  }
+  
+  {
+    const uint8_t data[] = { 0xe2, 0x82, 0xac };
+    ASSERT_EQ(0x20ac, GetUnicode(data, 3, 3));
+    ASSERT_THROW(GetUnicode(data, 2, 3), OrthancException);
+  }
+  
+  {
+    const uint8_t data[] = { 0xf0, 0x90, 0x8d, 0x88 };
+    ASSERT_EQ(0x010348, GetUnicode(data, 4, 4));
+    ASSERT_THROW(GetUnicode(data, 3, 4), OrthancException);
+  }
+  
+  {
+    const uint8_t data[] = { 0xe0 };
+    ASSERT_THROW(GetUnicode(data, 1, 1), OrthancException);
+  }
+}
+
+
+TEST(Toolbox, UrlDecode)
+{
+  std::string s;
+
+  s = "Hello%20World";
+  Toolbox::UrlDecode(s);
+  ASSERT_EQ("Hello World", s);
+
+  s = "%21%23%24%26%27%28%29%2A%2B%2c%2f%3A%3b%3d%3f%40%5B%5D%90%ff";
+  Toolbox::UrlDecode(s);
+  std::string ss = "!#$&'()*+,/:;=?@[]"; 
+  ss.push_back((char) 144); 
+  ss.push_back((char) 255);
+  ASSERT_EQ(ss, s);
+
+  s = "(2000%2C00A4)+Other";
+  Toolbox::UrlDecode(s);
+  ASSERT_EQ("(2000,00A4) Other", s);
+}
+
+
+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));
+
+  ASSERT_TRUE(Toolbox::IsAsciiString("Hello\nworld"));
+  ASSERT_FALSE(Toolbox::IsAsciiString("Hello\rworld"));
+
+  ASSERT_EQ("Hello\nworld", Toolbox::ConvertToAscii("Hello\nworld"));
+  ASSERT_EQ("Helloworld", Toolbox::ConvertToAscii("Hello\r\tworld"));
+}
+
+
+#if defined(__linux__)
+TEST(Toolbox, AbsoluteDirectory)
+{
+  ASSERT_EQ("/tmp/hello", SystemToolbox::InterpretRelativePath("/tmp", "hello"));
+  ASSERT_EQ("/tmp", SystemToolbox::InterpretRelativePath("/tmp", "/tmp"));
+}
+#endif
+
+
+TEST(Toolbox, WriteFile)
+{
+  std::string path;
+
+  {
+    TemporaryFile tmp;
+    path = tmp.GetPath();
+
+    std::string s;
+    s.append("Hello");
+    s.push_back('\0');
+    s.append("World");
+    ASSERT_EQ(11u, s.size());
+
+    SystemToolbox::WriteFile(s, path.c_str());
+
+    std::string t;
+    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(SystemToolbox::ReadFile(u, path.c_str()), OrthancException);
+}
+
+
+TEST(Toolbox, FileBuffer)
+{
+  FileBuffer f;
+  f.Append("a", 1);
+  f.Append("", 0);
+  f.Append("bc", 2);
+
+  std::string s;
+  f.Read(s);
+  ASSERT_EQ("abc", s);
+
+  ASSERT_THROW(f.Append("d", 1), OrthancException);  // File is closed
+}
+
+
+TEST(Toolbox, Wildcard)
+{
+  ASSERT_EQ("abcd", Toolbox::WildcardToRegularExpression("abcd"));
+  ASSERT_EQ("ab.*cd", Toolbox::WildcardToRegularExpression("ab*cd"));
+  ASSERT_EQ("ab..cd", Toolbox::WildcardToRegularExpression("ab??cd"));
+  ASSERT_EQ("a.*b.c.*d", Toolbox::WildcardToRegularExpression("a*b?c*d"));
+  ASSERT_EQ("a\\{b\\]", Toolbox::WildcardToRegularExpression("a{b]"));
+}
+
+
+TEST(Toolbox, Tokenize)
+{
+  std::vector<std::string> t;
+  
+  Toolbox::TokenizeString(t, "", ','); 
+  ASSERT_EQ(1u, t.size());
+  ASSERT_EQ("", t[0]);
+  
+  Toolbox::TokenizeString(t, "abc", ','); 
+  ASSERT_EQ(1u, t.size());
+  ASSERT_EQ("abc", t[0]);
+  
+  Toolbox::TokenizeString(t, "ab,cd,ef,", ','); 
+  ASSERT_EQ(4u, t.size());
+  ASSERT_EQ("ab", t[0]);
+  ASSERT_EQ("cd", t[1]);
+  ASSERT_EQ("ef", t[2]);
+  ASSERT_EQ("", t[3]);
+}
+
+TEST(Toolbox, Enumerations)
+{
+  ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8)));
+  ASSERT_EQ(Encoding_Ascii, StringToEncoding(EnumerationToString(Encoding_Ascii)));
+  ASSERT_EQ(Encoding_Latin1, StringToEncoding(EnumerationToString(Encoding_Latin1)));
+  ASSERT_EQ(Encoding_Latin2, StringToEncoding(EnumerationToString(Encoding_Latin2)));
+  ASSERT_EQ(Encoding_Latin3, StringToEncoding(EnumerationToString(Encoding_Latin3)));
+  ASSERT_EQ(Encoding_Latin4, StringToEncoding(EnumerationToString(Encoding_Latin4)));
+  ASSERT_EQ(Encoding_Latin5, StringToEncoding(EnumerationToString(Encoding_Latin5)));
+  ASSERT_EQ(Encoding_Cyrillic, StringToEncoding(EnumerationToString(Encoding_Cyrillic)));
+  ASSERT_EQ(Encoding_Arabic, StringToEncoding(EnumerationToString(Encoding_Arabic)));
+  ASSERT_EQ(Encoding_Greek, StringToEncoding(EnumerationToString(Encoding_Greek)));
+  ASSERT_EQ(Encoding_Hebrew, StringToEncoding(EnumerationToString(Encoding_Hebrew)));
+  ASSERT_EQ(Encoding_Japanese, StringToEncoding(EnumerationToString(Encoding_Japanese)));
+  ASSERT_EQ(Encoding_Chinese, StringToEncoding(EnumerationToString(Encoding_Chinese)));
+  ASSERT_EQ(Encoding_Thai, StringToEncoding(EnumerationToString(Encoding_Thai)));
+  ASSERT_EQ(Encoding_Korean, StringToEncoding(EnumerationToString(Encoding_Korean)));
+  ASSERT_EQ(Encoding_JapaneseKanji, StringToEncoding(EnumerationToString(Encoding_JapaneseKanji)));
+  ASSERT_EQ(Encoding_SimplifiedChinese, StringToEncoding(EnumerationToString(Encoding_SimplifiedChinese)));
+
+  ASSERT_EQ(ResourceType_Patient, StringToResourceType(EnumerationToString(ResourceType_Patient)));
+  ASSERT_EQ(ResourceType_Study, StringToResourceType(EnumerationToString(ResourceType_Study)));
+  ASSERT_EQ(ResourceType_Series, StringToResourceType(EnumerationToString(ResourceType_Series)));
+  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);
+
+  ASSERT_EQ(MimeType_Binary, StringToMimeType(EnumerationToString(MimeType_Binary)));
+  ASSERT_EQ(MimeType_Css, StringToMimeType(EnumerationToString(MimeType_Css)));
+  ASSERT_EQ(MimeType_Dicom, StringToMimeType(EnumerationToString(MimeType_Dicom)));
+  ASSERT_EQ(MimeType_Gif, StringToMimeType(EnumerationToString(MimeType_Gif)));
+  ASSERT_EQ(MimeType_Gzip, StringToMimeType(EnumerationToString(MimeType_Gzip)));
+  ASSERT_EQ(MimeType_Html, StringToMimeType(EnumerationToString(MimeType_Html)));
+  ASSERT_EQ(MimeType_JavaScript, StringToMimeType(EnumerationToString(MimeType_JavaScript)));
+  ASSERT_EQ(MimeType_Jpeg, StringToMimeType(EnumerationToString(MimeType_Jpeg)));
+  ASSERT_EQ(MimeType_Jpeg2000, StringToMimeType(EnumerationToString(MimeType_Jpeg2000)));
+  ASSERT_EQ(MimeType_Json, StringToMimeType(EnumerationToString(MimeType_Json)));
+  ASSERT_EQ(MimeType_NaCl, StringToMimeType(EnumerationToString(MimeType_NaCl)));
+  ASSERT_EQ(MimeType_PNaCl, StringToMimeType(EnumerationToString(MimeType_PNaCl)));
+  ASSERT_EQ(MimeType_Pam, StringToMimeType(EnumerationToString(MimeType_Pam)));
+  ASSERT_EQ(MimeType_Pdf, StringToMimeType(EnumerationToString(MimeType_Pdf)));
+  ASSERT_EQ(MimeType_PlainText, StringToMimeType(EnumerationToString(MimeType_PlainText)));
+  ASSERT_EQ(MimeType_Png, StringToMimeType(EnumerationToString(MimeType_Png)));
+  ASSERT_EQ(MimeType_Svg, StringToMimeType(EnumerationToString(MimeType_Svg)));
+  ASSERT_EQ(MimeType_WebAssembly, StringToMimeType(EnumerationToString(MimeType_WebAssembly)));
+  ASSERT_EQ(MimeType_Xml, StringToMimeType("application/xml"));
+  ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml"));
+  ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
+  ASSERT_EQ(MimeType_DicomWebJson, StringToMimeType(EnumerationToString(MimeType_DicomWebJson)));
+  ASSERT_EQ(MimeType_DicomWebXml, StringToMimeType(EnumerationToString(MimeType_DicomWebXml)));
+  ASSERT_THROW(StringToMimeType("nope"), OrthancException);
+
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance));
+
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study));
+  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series));
+  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance));
+}
+
+
+#if defined(__linux__) || defined(__OpenBSD__)
+#include <endian.h>
+#elif defined(__FreeBSD__)
+#include <machine/endian.h>
+#endif
+
+
+TEST(Toolbox, Endianness)
+{
+  // Parts of this test come from Adam Conrad
+  // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728822#5
+
+
+  /**
+   * Windows and OS X are assumed to always little-endian.
+   **/
+  
+#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__)
+
+#if !defined(__BYTE_ORDER)
+#  error Support your platform here
+#endif
+
+#  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
+}
+
+
+#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;
+  a["hello"] = "world";
+  a["42"] = 43;
+  a["b"] = Json::arrayValue;
+  a["b"].append("test");
+  a["b"].append("test2");
+
+  std::string s;
+  Toolbox::JsonToXml(s, a);
+
+  std::cout << s;
+}
+#endif
+
+
+#if !defined(_WIN32)
+TEST(Toolbox, ExecuteSystemCommand)
+{
+  std::vector<std::string> args(2);
+  args[0] = "Hello";
+  args[1] = "World";
+
+  SystemToolbox::ExecuteSystemCommand("echo", args);
+}
+#endif
+
+
+TEST(Toolbox, IsInteger)
+{
+  ASSERT_TRUE(Toolbox::IsInteger("00236"));
+  ASSERT_TRUE(Toolbox::IsInteger("-0042"));
+  ASSERT_TRUE(Toolbox::IsInteger("0"));
+  ASSERT_TRUE(Toolbox::IsInteger("-0"));
+
+  ASSERT_FALSE(Toolbox::IsInteger(""));
+  ASSERT_FALSE(Toolbox::IsInteger("42a"));
+  ASSERT_FALSE(Toolbox::IsInteger("42-"));
+}
+
+
+TEST(Toolbox, StartsWith)
+{
+  ASSERT_TRUE(Toolbox::StartsWith("hello world", ""));
+  ASSERT_TRUE(Toolbox::StartsWith("hello world", "hello"));
+  ASSERT_TRUE(Toolbox::StartsWith("hello world", "h"));
+  ASSERT_FALSE(Toolbox::StartsWith("hello world", "H"));
+  ASSERT_FALSE(Toolbox::StartsWith("h", "hello"));
+  ASSERT_TRUE(Toolbox::StartsWith("h", "h"));
+  ASSERT_FALSE(Toolbox::StartsWith("", "h"));
+}
+
+
+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));
+  }
+}
+
+
+TEST(Toolbox, SubstituteVariables)
+{
+  std::map<std::string, std::string> env;
+  env["NOPE"] = "nope";
+  env["WORLD"] = "world";
+
+  ASSERT_EQ("Hello world\r\nWorld \r\nDone world\r\n",
+            Toolbox::SubstituteVariables(
+              "Hello ${WORLD}\r\nWorld ${HELLO}\r\nDone ${WORLD}\r\n",
+              env));
+
+  ASSERT_EQ("world A a B world C 'c' D {\"a\":\"b\"} E ",
+            Toolbox::SubstituteVariables(
+              "${WORLD} A ${WORLD2:-a} B ${WORLD:-b} C ${WORLD2:-\"'c'\"} D ${WORLD2:-'{\"a\":\"b\"}'} E ${WORLD2:-}",
+              env));
+  
+  SystemToolbox::GetEnvironmentVariables(env);
+  ASSERT_TRUE(env.find("NOPE") == env.end());
+
+  // The "PATH" environment variable should always be available on
+  // machines running the unit tests
+  ASSERT_TRUE(env.find("PATH") != env.end() /* Case used by UNIX */ ||
+              env.find("Path") != env.end() /* Case used by Windows */);
+
+  env["PATH"] = "hello";
+  ASSERT_EQ("AhelloB",
+            Toolbox::SubstituteVariables("A${PATH}B", env));
+}
+
+
+TEST(MetricsRegistry, Basic)
+{
+  {
+    MetricsRegistry m;
+    m.SetEnabled(false);
+    m.SetValue("hello.world", 42.5f);
+    
+    std::string s;
+    m.ExportPrometheusText(s);
+    ASSERT_TRUE(s.empty());
+  }
+
+  {
+    MetricsRegistry m;
+    m.Register("hello.world", MetricsType_Default);
+    
+    std::string s;
+    m.ExportPrometheusText(s);
+    ASSERT_TRUE(s.empty());
+  }
+
+  {
+    MetricsRegistry m;
+    m.SetValue("hello.world", 42.5f);
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.world"));
+    ASSERT_THROW(m.GetMetricsType("nope"), OrthancException);
+    
+    std::string s;
+    m.ExportPrometheusText(s);
+
+    std::vector<std::string> t;
+    Toolbox::TokenizeString(t, s, '\n');
+    ASSERT_EQ(2u, t.size());
+    ASSERT_EQ("hello.world 42.5 ", t[0].substr(0, 17));
+    ASSERT_TRUE(t[1].empty());
+  }
+
+  {
+    MetricsRegistry m;
+    m.Register("hello.max", MetricsType_MaxOver10Seconds);
+    m.SetValue("hello.max", 10);
+    m.SetValue("hello.max", 20);
+    m.SetValue("hello.max", -10);
+    m.SetValue("hello.max", 5);
+
+    m.Register("hello.min", MetricsType_MinOver10Seconds);
+    m.SetValue("hello.min", 10);
+    m.SetValue("hello.min", 20);
+    m.SetValue("hello.min", -10);
+    m.SetValue("hello.min", 5);
+    
+    m.Register("hello.default", MetricsType_Default);
+    m.SetValue("hello.default", 10);
+    m.SetValue("hello.default", 20);
+    m.SetValue("hello.default", -10);
+    m.SetValue("hello.default", 5);
+    
+    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("hello.max"));
+    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("hello.min"));
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.default"));
+
+    std::string s;
+    m.ExportPrometheusText(s);
+
+    std::vector<std::string> t;
+    Toolbox::TokenizeString(t, s, '\n');
+    ASSERT_EQ(4u, t.size());
+    ASSERT_TRUE(t[3].empty());
+
+    std::map<std::string, std::string> u;
+    for (size_t i = 0; i < t.size() - 1; i++)
+    {
+      std::vector<std::string> v;
+      Toolbox::TokenizeString(v, t[i], ' ');
+      u[v[0]] = v[1];
+    }
+
+    ASSERT_EQ("20", u["hello.max"]);
+    ASSERT_EQ("-10", u["hello.min"]);
+    ASSERT_EQ("5", u["hello.default"]);
+  }
+
+  {
+    MetricsRegistry m;
+
+    m.SetValue("a", 10);
+    m.SetValue("b", 10, MetricsType_MinOver10Seconds);
+
+    m.Register("c", MetricsType_MaxOver10Seconds);
+    m.SetValue("c", 10, MetricsType_MinOver10Seconds);
+
+    m.Register("d", MetricsType_MaxOver10Seconds);
+    m.Register("d", MetricsType_Default);
+
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("a"));
+    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
+    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("c"));
+    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("d"));
+  }
+
+  {
+    MetricsRegistry m;
+
+    {
+      MetricsRegistry::Timer t1(m, "a");
+      MetricsRegistry::Timer t2(m, "b", MetricsType_MinOver10Seconds);
+    }
+
+    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("a"));
+    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/ImageProcessingTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1018 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Compatibility.h"
+#include "../Core/DicomFormat/DicomImageInformation.h"
+#include "../Core/Images/Image.h"
+#include "../Core/Images/ImageProcessing.h"
+#include "../Core/Images/ImageTraits.h"
+#include "../Core/OrthancException.h"
+
+#include <memory>
+
+using namespace Orthanc;
+
+
+TEST(DicomImageInformation, ExtractPixelFormat1)
+{
+  // Cardiac/MR*
+  DicomMap m;
+  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, false));
+  ASSERT_EQ(PixelFormat_Grayscale16, format);
+}
+
+
+TEST(DicomImageInformation, ExtractPixelFormat2)
+{
+  // Delphine CT
+  DicomMap m;
+  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, false));
+  ASSERT_EQ(PixelFormat_SignedGrayscale16, format);
+}
+
+
+
+namespace
+{
+  template <typename T>
+  class TestImageTraits : public ::testing::Test
+  {
+  private:
+    std::unique_ptr<Image>  image_;
+
+  protected:
+    virtual void SetUp() ORTHANC_OVERRIDE
+    {
+      image_.reset(new Image(ImageTraits::PixelTraits::GetPixelFormat(), 7, 9, false));
+    }
+
+    virtual void TearDown() ORTHANC_OVERRIDE
+    {
+      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());
+
+  float c = 0.0f;
+  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.0f;
+  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));
+    }
+  }
+}
+
+TYPED_TEST(TestIntegerImageTraits, FillPolygon)
+{
+  ImageAccessor& image = this->GetImage();
+
+  ImageProcessing::Set(image, 128);
+
+  // draw a triangle
+  std::vector<ImageProcessing::ImagePoint> points;
+  points.push_back(ImageProcessing::ImagePoint(1,1));
+  points.push_back(ImageProcessing::ImagePoint(1,5));
+  points.push_back(ImageProcessing::ImagePoint(5,5));
+
+  ImageProcessing::FillPolygon(image, points, 255);
+
+  // outside polygon
+  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 0, 0));
+  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 0, 6));
+  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 6, 6));
+  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 6, 0));
+
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 1));
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 2));
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 5));
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 2, 4));
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 5, 5));
+}
+
+TYPED_TEST(TestIntegerImageTraits, FillPolygonLargerThanImage)
+{
+  ImageAccessor& image = this->GetImage();
+
+  ImageProcessing::Set(image, 0);
+
+  std::vector<ImageProcessing::ImagePoint> points;
+  points.push_back(ImageProcessing::ImagePoint(0, 0));
+  points.push_back(ImageProcessing::ImagePoint(image.GetWidth(),0));
+  points.push_back(ImageProcessing::ImagePoint(image.GetWidth(),image.GetHeight()));
+  points.push_back(ImageProcessing::ImagePoint(0,image.GetHeight()));
+
+  ASSERT_THROW(ImageProcessing::FillPolygon(image, points, 255), OrthancException);
+}
+
+TYPED_TEST(TestIntegerImageTraits, FillPolygonFullImage)
+{
+  ImageAccessor& image = this->GetImage();
+
+  ImageProcessing::Set(image, 0);
+
+  std::vector<ImageProcessing::ImagePoint> points;
+  points.push_back(ImageProcessing::ImagePoint(0, 0));
+  points.push_back(ImageProcessing::ImagePoint(image.GetWidth() - 1,0));
+  points.push_back(ImageProcessing::ImagePoint(image.GetWidth() - 1,image.GetHeight() - 1));
+  points.push_back(ImageProcessing::ImagePoint(0,image.GetHeight() - 1));
+
+  ImageProcessing::FillPolygon(image, points, 255);
+
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 0, 0));
+  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, image.GetWidth() - 1, image.GetHeight() - 1));
+}
+
+
+
+
+static void SetGrayscale8Pixel(ImageAccessor& image,
+                               unsigned int x,
+                               unsigned int y,
+                               uint8_t value)
+{
+  ImageTraits<PixelFormat_Grayscale8>::SetPixel(image, value, x, y);
+}
+
+static bool TestGrayscale8Pixel(const ImageAccessor& image,
+                                unsigned int x,
+                                unsigned int y,
+                                uint8_t value)
+{
+  PixelTraits<PixelFormat_Grayscale8>::PixelType p;
+  ImageTraits<PixelFormat_Grayscale8>::GetPixel(p, image, x, y);
+  if (p != value) printf("%d %d\n", p, value);
+  return p == value;
+}
+
+static void SetGrayscale16Pixel(ImageAccessor& image,
+                                unsigned int x,
+                                unsigned int y,
+                                uint16_t value)
+{
+  ImageTraits<PixelFormat_Grayscale16>::SetPixel(image, value, x, y);
+}
+
+static bool TestGrayscale16Pixel(const ImageAccessor& image,
+                                 unsigned int x,
+                                 unsigned int y,
+                                 uint16_t value)
+{
+  PixelTraits<PixelFormat_Grayscale16>::PixelType p;
+  ImageTraits<PixelFormat_Grayscale16>::GetPixel(p, image, x, y);
+  if (p != value) printf("%d %d\n", p, value);
+  return p == value;
+}
+
+static void SetSignedGrayscale16Pixel(ImageAccessor& image,
+                                unsigned int x,
+                                unsigned int y,
+                                int16_t value)
+{
+  ImageTraits<PixelFormat_SignedGrayscale16>::SetPixel(image, value, x, y);
+}
+
+static bool TestSignedGrayscale16Pixel(const ImageAccessor& image,
+                                       unsigned int x,
+                                       unsigned int y,
+                                       int16_t value)
+{
+  PixelTraits<PixelFormat_SignedGrayscale16>::PixelType p;
+  ImageTraits<PixelFormat_SignedGrayscale16>::GetPixel(p, image, x, y);
+  if (p != value) printf("%d %d\n", p, value);
+  return p == value;
+}
+
+static void SetRGB24Pixel(ImageAccessor& image,
+                          unsigned int x,
+                          unsigned int y,
+                          uint8_t red,
+                          uint8_t green,
+                          uint8_t blue)
+{
+  PixelTraits<PixelFormat_RGB24>::PixelType p;
+  p.red_ = red;
+  p.green_ = green;
+  p.blue_ = blue;
+  ImageTraits<PixelFormat_RGB24>::SetPixel(image, p, x, y);
+}
+
+static bool TestRGB24Pixel(const ImageAccessor& image,
+                           unsigned int x,
+                           unsigned int y,
+                           uint8_t red,
+                           uint8_t green,
+                           uint8_t blue)
+{
+  PixelTraits<PixelFormat_RGB24>::PixelType p;
+  ImageTraits<PixelFormat_RGB24>::GetPixel(p, image, x, y);
+  bool ok = (p.red_ == red &&
+             p.green_ == green &&
+             p.blue_ == blue);
+  if (!ok) printf("%d,%d,%d  %d,%d,%d\n", p.red_, p.green_, p.blue_, red, green, blue);
+  return ok;
+}
+
+
+TEST(ImageProcessing, FlipGrayscale8)
+{
+  {
+    Image image(PixelFormat_Grayscale8, 0, 0, false);
+    ImageProcessing::FlipX(image);
+    ImageProcessing::FlipY(image);
+  }
+
+  {
+    Image image(PixelFormat_Grayscale8, 1, 1, false);
+    SetGrayscale8Pixel(image, 0, 0, 128);
+    ImageProcessing::FlipX(image);
+    ImageProcessing::FlipY(image);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 128));
+  }
+
+  {
+    Image image(PixelFormat_Grayscale8, 3, 2, false);
+    SetGrayscale8Pixel(image, 0, 0, 10);
+    SetGrayscale8Pixel(image, 1, 0, 20);
+    SetGrayscale8Pixel(image, 2, 0, 30);
+    SetGrayscale8Pixel(image, 0, 1, 40);
+    SetGrayscale8Pixel(image, 1, 1, 50);
+    SetGrayscale8Pixel(image, 2, 1, 60);
+
+    ImageProcessing::FlipX(image);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 30));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 20));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 10));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 60));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 50));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 40));
+
+    ImageProcessing::FlipY(image);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 60));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 50));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 40));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 30));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 20));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 10));
+  }
+}
+
+
+
+TEST(ImageProcessing, FlipRGB24)
+{
+  Image image(PixelFormat_RGB24, 2, 2, false);
+  SetRGB24Pixel(image, 0, 0, 10, 100, 110);
+  SetRGB24Pixel(image, 1, 0, 20, 100, 110);
+  SetRGB24Pixel(image, 0, 1, 30, 100, 110);
+  SetRGB24Pixel(image, 1, 1, 40, 100, 110);
+
+  ImageProcessing::FlipX(image);
+  ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 20, 100, 110));
+  ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 10, 100, 110));
+  ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 40, 100, 110));
+  ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 30, 100, 110));
+
+  ImageProcessing::FlipY(image);
+  ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 40, 100, 110));
+  ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 30, 100, 110));
+  ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 20, 100, 110));
+  ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 10, 100, 110));
+}
+
+
+TEST(ImageProcessing, ResizeBasicGrayscale8)
+{
+  Image source(PixelFormat_Grayscale8, 2, 2, false);
+  SetGrayscale8Pixel(source, 0, 0, 10);
+  SetGrayscale8Pixel(source, 1, 0, 20);
+  SetGrayscale8Pixel(source, 0, 1, 30);
+  SetGrayscale8Pixel(source, 1, 1, 40);
+
+  {
+    Image target(PixelFormat_Grayscale8, 2, 4, false);
+    ImageProcessing::Resize(target, source);
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 10));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 20));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 10));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 20));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 2, 30));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 2, 40));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 3, 30));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 3, 40));
+  }
+
+  {
+    Image target(PixelFormat_Grayscale8, 4, 2, false);
+    ImageProcessing::Resize(target, source);
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 10));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 10));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 20));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 20));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 30));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 30));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 1, 40));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 1, 40));
+  }
+}
+
+
+TEST(ImageProcessing, ResizeBasicRGB24)
+{
+  Image source(PixelFormat_RGB24, 2, 2, false);
+  SetRGB24Pixel(source, 0, 0, 10, 100, 110);
+  SetRGB24Pixel(source, 1, 0, 20, 100, 110);
+  SetRGB24Pixel(source, 0, 1, 30, 100, 110);
+  SetRGB24Pixel(source, 1, 1, 40, 100, 110);
+
+  {
+    Image target(PixelFormat_RGB24, 2, 4, false);
+    ImageProcessing::Resize(target, source);
+    ASSERT_TRUE(TestRGB24Pixel(target, 0, 0, 10, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 1, 0, 20, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 0, 1, 10, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 1, 1, 20, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 0, 2, 30, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 1, 2, 40, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 0, 3, 30, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 1, 3, 40, 100, 110));
+  }
+
+  {
+    Image target(PixelFormat_RGB24, 4, 2, false);
+    ImageProcessing::Resize(target, source);
+    ASSERT_TRUE(TestRGB24Pixel(target, 0, 0, 10, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 1, 0, 10, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 2, 0, 20, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 3, 0, 20, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 0, 1, 30, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 1, 1, 30, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 2, 1, 40, 100, 110));
+    ASSERT_TRUE(TestRGB24Pixel(target, 3, 1, 40, 100, 110));
+  }
+}
+
+
+TEST(ImageProcessing, ResizeEmptyGrayscale8)
+{
+  {
+    Image source(PixelFormat_Grayscale8, 0, 0, false);
+    Image target(PixelFormat_Grayscale8, 2, 2, false);
+    ImageProcessing::Resize(target, source);
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 0));
+    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 0));
+  }
+
+  {
+    Image source(PixelFormat_Grayscale8, 2, 2, false);
+    Image target(PixelFormat_Grayscale8, 0, 0, false);
+    ImageProcessing::Resize(target, source);
+  }
+}
+
+
+TEST(ImageProcessing, Convolution)
+{
+  std::vector<float> k1(5, 1);
+  std::vector<float> k2(1, 1);
+
+  {
+    Image image(PixelFormat_Grayscale8, 1, 1, false);
+    SetGrayscale8Pixel(image, 0, 0, 100);    
+    ImageProcessing::SeparableConvolution(image, k1, 2, k2, 0);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
+    ImageProcessing::SeparableConvolution(image, k1, 2, k1, 2);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
+    ImageProcessing::SeparableConvolution(image, k2, 0, k1, 2);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
+    ImageProcessing::SeparableConvolution(image, k2, 0, k2, 0);
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
+  }
+  
+  {
+    Image image(PixelFormat_RGB24, 1, 1, false);
+    SetRGB24Pixel(image, 0, 0, 10, 20, 30);    
+    ImageProcessing::SeparableConvolution(image, k1, 2, k2, 0);
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
+    ImageProcessing::SeparableConvolution(image, k1, 2, k1, 2);
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
+    ImageProcessing::SeparableConvolution(image, k2, 0, k1, 2);
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
+    ImageProcessing::SeparableConvolution(image, k2, 0, k2, 0);
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
+  }
+
+  {  
+    Image dirac(PixelFormat_Grayscale8, 9, 1, false);
+    ImageProcessing::Set(dirac, 0);
+    SetGrayscale8Pixel(dirac, 4, 0, 100);
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 100));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 100));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0));    
+    }
+  }
+
+  {  
+    Image dirac(PixelFormat_Grayscale8, 1, 9, false);
+    ImageProcessing::Set(dirac, 0);
+    SetGrayscale8Pixel(dirac, 0, 4, 100);
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 20));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 100));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 100));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0));    
+    }
+  }
+
+  {
+    Image dirac(PixelFormat_RGB24, 9, 1, false);
+    ImageProcessing::Set(dirac, 0);
+    SetRGB24Pixel(dirac, 4, 0, 100, 120, 140);
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 100, 120, 140));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 100, 120, 140));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0));    
+    }
+  }
+
+  {
+    Image dirac(PixelFormat_RGB24, 1, 9, false);
+    ImageProcessing::Set(dirac, 0);
+    SetRGB24Pixel(dirac, 0, 4, 100, 120, 140);
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 20, 24, 28));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 100, 120, 140));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0));    
+    }
+
+    {
+      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
+      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 100, 120, 140));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0));
+      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0));    
+    }
+  }
+}
+
+
+TEST(ImageProcessing, SmoothGaussian5x5)
+{
+  /**
+     Test the point spread function, as can be seen in Octave:
+     g1 = [ 1 4 6 4 1 ];
+     g1 /= sum(g1);
+     g2 = conv2(g1, g1');
+     floor(conv2(diag([ 0 0 100 0 0 ]), g2, 'same'))  % red/green channels
+     floor(conv2(diag([ 0 0 200 0 0 ]), g2, 'same'))  % blue channel
+  **/
+
+  {
+    Image image(PixelFormat_Grayscale8, 5, 5, false);
+    ImageProcessing::Set(image, 0);
+    SetGrayscale8Pixel(image, 2, 2, 100);
+    ImageProcessing::SmoothGaussian5x5(image);
+
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 2));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 0));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 6));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 9));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 1, 6));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 1, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 2, 2));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 2, 9));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 2, 14));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 2, 9));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 2, 2));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 3, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 3, 6));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 3, 9));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 3, 6));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 3, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 4, 0));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 4, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 4, 2));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 4, 1));
+    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 4, 0));
+  }
+
+  {
+    Image image(PixelFormat_RGB24, 5, 5, false);
+    ImageProcessing::Set(image, 0);
+    SetRGB24Pixel(image, 2, 2, 100, 100, 200);
+    ImageProcessing::SmoothGaussian5x5(image);
+
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 0, 0, 0));
+    ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 2, 0, 2, 2, 4));
+    ASSERT_TRUE(TestRGB24Pixel(image, 3, 0, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 4, 0, 0, 0, 0));
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 6, 6, 12));
+    ASSERT_TRUE(TestRGB24Pixel(image, 2, 1, 9, 9, 18));
+    ASSERT_TRUE(TestRGB24Pixel(image, 3, 1, 6, 6, 12));
+    ASSERT_TRUE(TestRGB24Pixel(image, 4, 1, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 2, 2, 2, 4));
+    ASSERT_TRUE(TestRGB24Pixel(image, 1, 2, 9, 9, 18));
+    ASSERT_TRUE(TestRGB24Pixel(image, 2, 2, 14, 14, 28));
+    ASSERT_TRUE(TestRGB24Pixel(image, 3, 2, 9, 9, 18));
+    ASSERT_TRUE(TestRGB24Pixel(image, 4, 2, 2, 2, 4));
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 3, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 1, 3, 6, 6, 12));
+    ASSERT_TRUE(TestRGB24Pixel(image, 2, 3, 9, 9, 18));
+    ASSERT_TRUE(TestRGB24Pixel(image, 3, 3, 6, 6, 12));
+    ASSERT_TRUE(TestRGB24Pixel(image, 4, 3, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 0, 4, 0, 0, 0));
+    ASSERT_TRUE(TestRGB24Pixel(image, 1, 4, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 2, 4, 2, 2, 4));
+    ASSERT_TRUE(TestRGB24Pixel(image, 3, 4, 1, 1, 3));
+    ASSERT_TRUE(TestRGB24Pixel(image, 4, 4, 0, 0, 0));
+  }
+}
+
+TEST(ImageProcessing, ApplyWindowingFloatToGrayScale8)
+{
+  {
+    Image image(PixelFormat_Float32, 6, 1, false);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, -5.0f, 0, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 0.0f, 1, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 5.0f, 2, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 10.0f, 3, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 1000.0f, 4, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 2.0f, 5, 0);
+
+    {
+      Image target(PixelFormat_Grayscale8, 6, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 128));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255*2/10));
+    }
+
+    {
+      Image target(PixelFormat_Grayscale8, 6, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true);
+
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 127));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255 - 255*2/10));
+    }
+
+    {
+      Image target(PixelFormat_Grayscale8, 6, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false);
+
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 128));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255*2/10));
+    }
+
+    {
+      Image target(PixelFormat_Grayscale8, 6, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true);
+
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 127));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 0));
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255 - 256*2/10));
+    }
+
+    {
+      Image target(PixelFormat_Grayscale8, 6, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false);
+
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));  // (-5 * 10) + 30 => pixel value = -20 => 0
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 256*30/100));  // ((0 * 10) + 30 => pixel value = 30 => 30%
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 256*80/100)); // ((5 * 10) + 30 => pixel value = 80 => 80%
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255)); // ((10 * 10) + 30 => pixel value = 130 => 100%
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255)); // ((1000 * 10) + 30 => pixel value = 10030 => 100%
+      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 128)); // ((2 * 10) + 30 => pixel value = 50 => 50%
+    }
+
+  }
+}
+
+TEST(ImageProcessing, ApplyWindowingFloatToGrayScale16)
+{
+  {
+    Image image(PixelFormat_Float32, 6, 1, false);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, -5.0f, 0, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 0.0f, 1, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 5.0f, 2, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 10.0f, 3, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 1000.0f, 4, 0);
+    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 2.0f, 5, 0);
+
+    {
+      Image target(PixelFormat_Grayscale16, 6, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 0));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 32768));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 5, 0, 65536*2/10));
+    }
+  }
+}
+
+TEST(ImageProcessing, ApplyWindowingGrayScale8ToGrayScale16)
+{
+  {
+    Image image(PixelFormat_Grayscale8, 5, 1, false);
+    SetGrayscale8Pixel(image, 0, 0, 0);
+    SetGrayscale8Pixel(image, 1, 0, 2);
+    SetGrayscale8Pixel(image, 2, 0, 5);
+    SetGrayscale8Pixel(image, 3, 0, 10);
+    SetGrayscale8Pixel(image, 4, 0, 255);
+
+    {
+      Image target(PixelFormat_Grayscale16, 5, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 65536*5/10));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535));
+    }
+  }
+}
+
+TEST(ImageProcessing, ApplyWindowingGrayScale16ToGrayScale16)
+{
+  {
+    Image image(PixelFormat_Grayscale16, 5, 1, false);
+    SetGrayscale16Pixel(image, 0, 0, 0);
+    SetGrayscale16Pixel(image, 1, 0, 2);
+    SetGrayscale16Pixel(image, 2, 0, 5);
+    SetGrayscale16Pixel(image, 3, 0, 10);
+    SetGrayscale16Pixel(image, 4, 0, 255);
+
+    {
+      Image target(PixelFormat_Grayscale16, 5, 1, false);
+      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
+
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 65536*5/10));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535));
+      ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535));
+    }
+  }
+}
+
+
+TEST(ImageProcessing, ShiftScaleGrayscale8)
+{
+  Image image(PixelFormat_Grayscale8, 5, 1, false);
+  SetGrayscale8Pixel(image, 0, 0, 0);
+  SetGrayscale8Pixel(image, 1, 0, 2);
+  SetGrayscale8Pixel(image, 2, 0, 5);
+  SetGrayscale8Pixel(image, 3, 0, 10);
+  SetGrayscale8Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, -1.1, 1.5, true);
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 6));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 13));
+  ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 255));
+}
+
+
+TEST(ImageProcessing, ShiftScaleGrayscale16)
+{
+  Image image(PixelFormat_Grayscale16, 5, 1, false);
+  SetGrayscale16Pixel(image, 0, 0, 0);
+  SetGrayscale16Pixel(image, 1, 0, 2);
+  SetGrayscale16Pixel(image, 2, 0, 5);
+  SetGrayscale16Pixel(image, 3, 0, 10);
+  SetGrayscale16Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, -1.1, 1.5, true);
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 0, 0, 0));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 1, 0, 1));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 2, 0, 6));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 3, 0, 13));
+  ASSERT_TRUE(TestGrayscale16Pixel(image, 4, 0, 381));
+}
+
+
+TEST(ImageProcessing, ShiftScaleSignedGrayscale16)
+{
+  Image image(PixelFormat_SignedGrayscale16, 5, 1, false);
+  SetSignedGrayscale16Pixel(image, 0, 0, 0);
+  SetSignedGrayscale16Pixel(image, 1, 0, 2);
+  SetSignedGrayscale16Pixel(image, 2, 0, 5);
+  SetSignedGrayscale16Pixel(image, 3, 0, 10);
+  SetSignedGrayscale16Pixel(image, 4, 0, 255);
+
+  ImageProcessing::ShiftScale(image, -17.1, 11.5, true);
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, -197));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, -174));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, -139));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, -82));
+  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 2736));
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/JpegLosslessTests.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
+
+#if ORTHANC_ENABLE_JPEG_LOSSLESS == 1
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/OrthancException.h"
+#include "../Core/Images/ImageBuffer.h"
+#include "../Core/Images/PngWriter.h"
+
+using namespace Orthanc;
+
+
+
+// TODO Write a test
+
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/LoggingTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,209 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+#include <boost/regex.hpp>
+#include <sstream>
+
+#include "../Core/Logging.h"
+
+using namespace Orthanc::Logging;
+
+static std::stringstream testErrorStream;
+void TestError(const char* message)
+{
+  testErrorStream << message;
+}
+
+static std::stringstream testWarningStream;
+void TestWarning(const char* message)
+{
+  testWarningStream << message;
+}
+
+static std::stringstream testInfoStream;
+void TestInfo(const char* message)
+{
+  testInfoStream << message;
+}
+
+/**
+   Extracts the log line payload
+
+   "E0423 16:55:43.001194 LoggingTests.cpp:102] Foo bar?\n"
+   -->
+   "Foo bar"
+
+   If the log line cannot be matched, the function returns false.
+*/
+
+#define EOLSTRING "\n"
+
+static bool GetLogLinePayload(std::string& payload,
+                              const std::string& logLine)
+{
+  const char* regexStr = "[A-Z][0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} "
+    "[a-zA-Z\\.\\-_]+:[0-9]+\\] (.*)" EOLSTRING "$";
+
+  boost::regex regexObj(regexStr);
+
+  //std::stringstream regexSStr;
+  //regexSStr << "E[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} "
+  //  "[a-zA-Z\\.\\-_]+:[0-9]+\\](.*)\r\n$";
+  //std::string regexStr = regexSStr.str();
+  boost::regex pattern(regexStr);
+  boost::cmatch what;
+  if (regex_match(logLine.c_str(), what, regexObj))
+  {
+    payload = what[1];
+    return true;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+namespace
+{
+  class LoggingMementoScope
+  {
+  public:
+    LoggingMementoScope()
+    {
+    }
+    
+    ~LoggingMementoScope()
+    {
+      Orthanc::Logging::Reset();
+    }
+  };
+
+
+  /**
+   * std::streambuf subclass used in FunctionCallingStream
+   **/
+  template<typename T>
+  class FuncStreamBuf : public std::stringbuf
+  {
+  public:
+    FuncStreamBuf(T func) : func_(func) {}
+
+    virtual int sync()
+    {
+      std::string text = this->str();
+      const char* buf = text.c_str();
+      func_(buf);
+      this->str("");
+      return 0;
+    }
+  private:
+    T func_;
+  };
+}
+
+
+TEST(FuncStreamBuf, BasicTest)
+{
+  LoggingMementoScope loggingConfiguration;
+
+  EnableTraceLevel(true);
+
+  typedef void(*LoggingFunctionFunc)(const char*);
+
+  FuncStreamBuf<LoggingFunctionFunc> errorStreamBuf(TestError);
+  std::ostream errorStream(&errorStreamBuf);
+
+  FuncStreamBuf<LoggingFunctionFunc> warningStreamBuf(TestWarning);
+  std::ostream warningStream(&warningStreamBuf);
+
+  FuncStreamBuf<LoggingFunctionFunc> infoStreamBuf(TestInfo);
+  std::ostream infoStream(&infoStreamBuf);
+
+  SetErrorWarnInfoLoggingStreams(errorStream, warningStream, infoStream);
+
+  {
+    const char* text = "E is the set of all sets that do not contain themselves. Does E contain itself?";
+    LOG(ERROR) << text;
+    std::string logLine = testErrorStream.str();
+    testErrorStream.str("");
+    testErrorStream.clear();
+    std::string payload;
+    bool ok = GetLogLinePayload(payload, logLine);
+    ASSERT_TRUE(ok);
+    ASSERT_STREQ(payload.c_str(), text);
+  }
+
+  // make sure loglines do not accumulate
+  {
+    const char* text = "some more nonsensical babblingiciously stupid gibberish";
+    LOG(ERROR) << text;
+    std::string logLine = testErrorStream.str();
+    testErrorStream.str("");
+    testErrorStream.clear();
+    std::string payload;
+    bool ok = GetLogLinePayload(payload, logLine);
+    ASSERT_TRUE(ok);
+    ASSERT_STREQ(payload.c_str(), text);
+  }
+
+  {
+    const char* text = "Trougoudou 53535345345353";
+    LOG(WARNING) << text;
+    std::string logLine = testWarningStream.str();
+    testWarningStream.str("");
+    testWarningStream.clear();
+    std::string payload;
+    bool ok = GetLogLinePayload(payload, logLine);
+    ASSERT_TRUE(ok);
+    ASSERT_STREQ(payload.c_str(), text);
+  }
+
+  {
+    const char* text = "Prout 111929";
+    LOG(INFO) << text;
+    std::string logLine = testInfoStream.str();
+    testInfoStream.str("");
+    testInfoStream.clear();
+    std::string payload;
+    bool ok = GetLogLinePayload(payload, logLine);
+    ASSERT_TRUE(ok);
+    ASSERT_STREQ(payload.c_str(), text);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/PluginsTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,121 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/OrthancException.h"
+#include "../Plugins/Engine/PluginsManager.h"
+
+using namespace Orthanc;
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+
+TEST(SharedLibrary, Enumerations)
+{
+  // The plugin engine cannot work if the size of an enumeration does
+  // not correspond to the size of "int32_t"
+  ASSERT_EQ(sizeof(int32_t), sizeof(OrthancPluginErrorCode));
+}
+
+
+TEST(SharedLibrary, Basic)
+{
+#if defined(_WIN32)
+  SharedLibrary l("kernel32.dll");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("GetVersionExW") != NULL);
+  ASSERT_TRUE(l.HasFunction("GetVersionExW"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#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_FALSE(l.HasFunction("world"));
+
+  /**
+   * On the Docker image "debian:buster-slim", the "libSegFault.so"
+   * library does exist, but does not contain any public symbol:
+   * 
+   *  $ sudo docker run -i -t --rm --entrypoint=bash debian:buster-slim
+   *  # apt-get update && apt-get install -y binutils
+   *  # nm -C /lib/x86_64-linux-gnu/libSegFault.so
+   *  nm: /lib/x86_64-linux-gnu/libSegFault.so: no symbols
+   *
+   * As a consequence, this part of the test is disabled since Orthanc
+   * 1.5.1, until we locate another shared library that is widely
+   * spread. Reference:
+   * https://groups.google.com/d/msg/orthanc-users/v-QFzpOzgJY/4Hm5NgxKBwAJ
+   **/
+  
+  //ASSERT_TRUE(l.GetFunction("_init") != NULL);
+  //ASSERT_TRUE(l.HasFunction("_init"));
+  
+#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__) || 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");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("pthread_create") != NULL);
+  ASSERT_TRUE(l.HasFunction("pthread_cancel"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#elif defined(__APPLE__) && defined(__MACH__)
+  SharedLibrary l("libdl.dylib");
+  ASSERT_THROW(l.GetFunction("world"), OrthancException);
+  ASSERT_TRUE(l.GetFunction("dlopen") != NULL);
+  ASSERT_TRUE(l.HasFunction("dlclose"));
+  ASSERT_FALSE(l.HasFunction("world"));
+
+#else
+#error Support your platform here
+#endif
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/RestApiTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,871 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#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/OrthancException.h"
+#include "../Core/Compression/ZlibCompressor.h"
+#include "../Core/RestApi/RestApiHierarchy.h"
+#include "../Core/HttpServer/HttpContentNegociation.h"
+#include "../Core/HttpServer/MultipartStreamReader.h"
+
+
+using namespace Orthanc;
+
+#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
+#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS"
+#endif
+
+
+
+TEST(HttpClient, Basic)
+{
+  HttpClient c;
+  ASSERT_FALSE(c.IsVerbose());
+  c.SetVerbose(true);
+  ASSERT_TRUE(c.IsVerbose());
+  c.SetVerbose(false);
+  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(BASE + "Product.json");
+
+  c.Apply(v);
+  ASSERT_TRUE(v.type() == Json::objectValue);
+  ASSERT_TRUE(v.isMember("Description"));
+#endif
+}
+
+
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 && ORTHANC_ENABLE_SSL == 1
+
+/**
+   The HTTPS CA certificates for BitBucket were extracted as follows:
+   
+   (1) We retrieve the certification chain of BitBucket:
+
+   # echo | openssl s_client -showcerts -connect www.bitbucket.org:443
+
+   (2) We see that the certification authority (CA) is
+   "www.digicert.com", and the root certificate is "DigiCert High
+   Assurance EV Root CA". As a consequence, we navigate to DigiCert to
+   find the URL to this CA certificate:
+
+   firefox https://www.digicert.com/digicert-root-certificates.htm
+
+   (3) Once we get the URL to the CA certificate, we convert it to a C
+   macro that can be used by libcurl:
+
+   # cd UnitTestsSources
+   # ../Resources/RetrieveCACertificates.py BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt > BitbucketCACertificates.h
+**/
+
+#include "BitbucketCACertificates.h"
+
+TEST(HttpClient, Ssl)
+{
+  SystemToolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert");
+
+  /*{
+    std::string s;
+    SystemToolbox::ReadFile(s, "/usr/share/ca-certificates/mozilla/WoSign.crt");
+    SystemToolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert");
+    }*/
+
+  HttpClient c;
+  c.SetHttpsVerifyPeers(true);
+  c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert");
+
+  // Test file modified on 2020-04-20, in order to use a git
+  // repository on BitBucket instead of a Mercurial repository
+  // (because Mercurial support disappears on 2020-05-31)
+  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
+
+  Json::Value v;
+  c.Apply(v);
+  ASSERT_TRUE(v.isMember("ServeFolders"));
+}
+
+TEST(HttpClient, SslNoVerification)
+{
+  HttpClient c;
+  c.SetHttpsVerifyPeers(false);
+  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
+
+  Json::Value v;
+  c.Apply(v);
+  ASSERT_TRUE(v.isMember("ServeFolders"));
+}
+
+#endif
+
+
+TEST(RestApi, ChunkedBuffer)
+{
+  ChunkedBuffer b;
+  ASSERT_EQ(0u, b.GetNumBytes());
+
+  b.AddChunk("hello", 5);
+  ASSERT_EQ(5u, b.GetNumBytes());
+
+  b.AddChunk("world", 5);
+  ASSERT_EQ(10u, b.GetNumBytes());
+
+  std::string s;
+  b.Flatten(s);
+  ASSERT_EQ("helloworld", s);
+}
+
+TEST(RestApi, ParseCookies)
+{
+  IHttpHandler::Arguments headers;
+  IHttpHandler::Arguments cookies;
+
+  headers["cookie"] = "a=b;c=d;;;e=f;;g=h;";
+  HttpToolbox::ParseCookies(cookies, headers);
+  ASSERT_EQ(4u, cookies.size());
+  ASSERT_EQ("b", cookies["a"]);
+  ASSERT_EQ("d", cookies["c"]);
+  ASSERT_EQ("f", cookies["e"]);
+  ASSERT_EQ("h", cookies["g"]);
+
+  headers["cookie"] = "  name =  value  ; name2=value2";
+  HttpToolbox::ParseCookies(cookies, headers);
+  ASSERT_EQ(2u, cookies.size());
+  ASSERT_EQ("value", cookies["name"]);
+  ASSERT_EQ("value2", cookies["name2"]);
+
+  headers["cookie"] = "  ;;;    ";
+  HttpToolbox::ParseCookies(cookies, headers);
+  ASSERT_EQ(0u, cookies.size());
+
+  headers["cookie"] = "  ;   n=v  ;;    ";
+  HttpToolbox::ParseCookies(cookies, headers);
+  ASSERT_EQ(1u, cookies.size());
+  ASSERT_EQ("v", cookies["n"]);
+}
+
+TEST(RestApi, RestApiPath)
+{
+  IHttpHandler::Arguments args;
+  UriComponents trail;
+
+  {
+    RestApiPath uri("/coucou/{abc}/d/*");
+    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
+    ASSERT_EQ(1u, args.size());
+    ASSERT_EQ(3u, trail.size());
+    ASSERT_EQ("moi", args["abc"]);
+    ASSERT_EQ("e", trail[0]);
+    ASSERT_EQ("f", trail[1]);
+    ASSERT_EQ("g", trail[2]);
+
+    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f"));
+    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/"));
+    ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d"));
+    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi"));
+
+    ASSERT_EQ(3u, uri.GetLevelCount());
+    ASSERT_TRUE(uri.IsUniversalTrailing());
+
+    ASSERT_EQ("coucou", uri.GetLevelName(0));
+    ASSERT_THROW(uri.GetWildcardName(0), OrthancException);
+
+    ASSERT_EQ("abc", uri.GetWildcardName(1));
+    ASSERT_THROW(uri.GetLevelName(1), OrthancException);
+
+    ASSERT_EQ("d", uri.GetLevelName(2));
+    ASSERT_THROW(uri.GetWildcardName(2), OrthancException);
+  }
+
+  {
+    RestApiPath uri("/coucou/{abc}/d");
+    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
+    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d"));
+    ASSERT_EQ(1u, args.size());
+    ASSERT_EQ(0u, trail.size());
+    ASSERT_EQ("moi", args["abc"]);
+
+    ASSERT_EQ(3u, uri.GetLevelCount());
+    ASSERT_FALSE(uri.IsUniversalTrailing());
+
+    ASSERT_EQ("coucou", uri.GetLevelName(0));
+    ASSERT_THROW(uri.GetWildcardName(0), OrthancException);
+
+    ASSERT_EQ("abc", uri.GetWildcardName(1));
+    ASSERT_THROW(uri.GetLevelName(1), OrthancException);
+
+    ASSERT_EQ("d", uri.GetLevelName(2));
+    ASSERT_THROW(uri.GetWildcardName(2), OrthancException);
+  }
+
+  {
+    RestApiPath uri("/*");
+    ASSERT_TRUE(uri.Match(args, trail, "/a/b/c"));
+    ASSERT_EQ(0u, args.size());
+    ASSERT_EQ(3u, trail.size());
+    ASSERT_EQ("a", trail[0]);
+    ASSERT_EQ("b", trail[1]);
+    ASSERT_EQ("c", trail[2]);
+
+    ASSERT_EQ(0u, uri.GetLevelCount());
+    ASSERT_TRUE(uri.IsUniversalTrailing());
+  }
+}
+
+
+
+
+
+
+static int testValue;
+
+template <int value>
+static void SetValue(RestApiGetCall& get)
+{
+  testValue = value;
+}
+
+
+static bool GetDirectory(Json::Value& target,
+                         RestApiHierarchy& hierarchy, 
+                         const std::string& uri)
+{
+  UriComponents p;
+  Toolbox::SplitUriComponents(p, uri);
+  return hierarchy.GetDirectory(target, p);
+}
+
+
+
+namespace
+{
+  class MyVisitor : public RestApiHierarchy::IVisitor
+  {
+  public:
+    virtual bool Visit(const RestApiHierarchy::Resource& resource,
+                       const UriComponents& uri,
+                       const IHttpHandler::Arguments& components,
+                       const UriComponents& trailing) ORTHANC_OVERRIDE
+    {
+      return resource.Handle(*(RestApiGetCall*) NULL);
+    }
+  };
+}
+
+
+static bool HandleGet(RestApiHierarchy& hierarchy, 
+                      const std::string& uri)
+{
+  UriComponents p;
+  Toolbox::SplitUriComponents(p, uri);
+  MyVisitor visitor;
+  return hierarchy.LookupResource(p, visitor);
+}
+
+
+TEST(RestApi, RestApiHierarchy)
+{
+  RestApiHierarchy root;
+  root.Register("/hello/world/test", SetValue<1>);
+  root.Register("/hello/world/test2", SetValue<2>);
+  root.Register("/hello/{world}/test3/test4", SetValue<3>);
+  root.Register("/hello2/*", SetValue<4>);
+
+  Json::Value m;
+  root.CreateSiteMap(m);
+  std::cout << m;
+
+  Json::Value d;
+  ASSERT_FALSE(GetDirectory(d, root, "/hello"));
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/a")); 
+  ASSERT_EQ(1u, d.size());
+  ASSERT_EQ("test3", d[0].asString());
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/world"));
+  ASSERT_EQ(2u, d.size());
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/a/test3"));
+  ASSERT_EQ(1u, d.size());
+  ASSERT_EQ("test4", d[0].asString());
+
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test"));
+  ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test2"));
+  ASSERT_FALSE(GetDirectory(d, root, "/hello2"));
+
+  testValue = 0;
+  ASSERT_TRUE(HandleGet(root, "/hello/world/test"));
+  ASSERT_EQ(testValue, 1);
+  ASSERT_TRUE(HandleGet(root, "/hello/world/test2"));
+  ASSERT_EQ(testValue, 2);
+  ASSERT_TRUE(HandleGet(root, "/hello/b/test3/test4"));
+  ASSERT_EQ(testValue, 3);
+  ASSERT_FALSE(HandleGet(root, "/hello/b/test3/test"));
+  ASSERT_EQ(testValue, 3);
+  ASSERT_TRUE(HandleGet(root, "/hello2/a/b"));
+  ASSERT_EQ(testValue, 4);
+}
+
+
+
+
+
+namespace
+{
+  class AcceptHandler : public 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) ORTHANC_OVERRIDE
+    {
+      type_ = type;
+      subtype_ = subtype;
+    }
+  };
+}
+
+
+TEST(RestApi, HttpContentNegociation)
+{
+  // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+  AcceptHandler h;
+
+  {
+    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";
+  
+  {
+    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());
+  }
+  
+  {
+    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());
+  }
+  
+  {
+    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");
+  }
+  
+  {
+    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());
+  }
+  
+  {
+    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"));
+  }
+}
+
+
+TEST(StringMatcher, Basic)
+{
+  StringMatcher matcher("---");
+
+  ASSERT_THROW(matcher.GetMatchBegin(), OrthancException);
+
+  {
+    const std::string s = "";
+    ASSERT_FALSE(matcher.Apply(s));
+  }
+
+  {
+    const std::string s = "abc----def";
+    ASSERT_TRUE(matcher.Apply(s));
+    ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin()));
+    ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd()));
+  }
+
+  {
+    const std::string s = "abc---";
+    ASSERT_TRUE(matcher.Apply(s));
+    ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin()));
+    ASSERT_EQ(s.end(), matcher.GetMatchEnd());
+    ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd()));
+    ASSERT_EQ("", std::string(matcher.GetMatchEnd(), s.end()));
+  }
+
+  {
+    const std::string s = "abc--def";
+    ASSERT_FALSE(matcher.Apply(s));
+    ASSERT_THROW(matcher.GetMatchBegin(), OrthancException);
+    ASSERT_THROW(matcher.GetMatchEnd(), OrthancException);
+  }
+
+  {
+    std::string s(10u, '\0');  // String with null values
+    ASSERT_EQ(10u, s.size());
+    ASSERT_EQ(10u, s.size());
+    ASSERT_FALSE(matcher.Apply(s));
+
+    s[9] = '-';
+    ASSERT_FALSE(matcher.Apply(s));
+
+    s[8] = '-';
+    ASSERT_FALSE(matcher.Apply(s));
+
+    s[7] = '-';
+    ASSERT_TRUE(matcher.Apply(s));
+    ASSERT_EQ(s.c_str() + 7, matcher.GetPointerBegin());
+    ASSERT_EQ(s.c_str() + 10, matcher.GetPointerEnd());
+    ASSERT_EQ(s.end() - 3, matcher.GetMatchBegin());
+    ASSERT_EQ(s.end(), matcher.GetMatchEnd());
+  }
+}
+
+
+
+class MultipartTester : public MultipartStreamReader::IHandler
+{
+private:
+  struct Part
+  {
+    MultipartStreamReader::HttpHeaders   headers_;
+    std::string  data_;
+
+    Part(const MultipartStreamReader::HttpHeaders& headers,
+         const void* part,
+         size_t size) :
+      headers_(headers),
+      data_(reinterpret_cast<const char*>(part), size)
+    {
+    }
+  };
+
+  std::vector<Part> parts_;
+
+public:
+  virtual void HandlePart(const MultipartStreamReader::HttpHeaders& headers,
+                          const void* part,
+                          size_t size)
+  {
+    parts_.push_back(Part(headers, part, size));
+  }
+
+  unsigned int GetCount() const
+  {
+    return parts_.size();
+  }
+
+  MultipartStreamReader::HttpHeaders& GetHeaders(size_t i)
+  {
+    return parts_[i].headers_;
+  }
+
+  const std::string& GetData(size_t i) const
+  {
+    return parts_[i].data_;
+  }
+};
+
+
+TEST(MultipartStreamReader, ParseHeaders)
+{
+  std::string ct, b, st, header;
+
+  {
+    MultipartStreamReader::HttpHeaders h;
+    h["hello"] = "world";
+    h["Content-Type"] = "world";  // Should be in lower-case
+    h["CONTENT-type"] = "world";  // Should be in lower-case
+    ASSERT_FALSE(MultipartStreamReader::GetMainContentType(header, h));
+  }
+
+  {
+    MultipartStreamReader::HttpHeaders h;
+    h["content-type"] = "world";
+    ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); 
+    ASSERT_EQ(header, "world");
+    ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header));
+  }
+
+  {
+    MultipartStreamReader::HttpHeaders h;
+    h["content-type"] = "multipart/related; dummy=value; boundary=1234; hello=world";
+    ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); 
+    ASSERT_EQ(header, h["content-type"]);
+    ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header));
+    ASSERT_EQ(ct, "multipart/related");
+    ASSERT_EQ(b, "1234");
+    ASSERT_TRUE(st.empty());
+  }
+
+  {
+    ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType
+                 (ct, st, b, "multipart/related; boundary="));  // Empty boundary
+  }
+
+  {
+    ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType
+                (ct, st, b, "Multipart/Related; TYPE=Application/Dicom; Boundary=heLLO"));
+    ASSERT_EQ(ct, "multipart/related");
+    ASSERT_EQ(b, "heLLO");
+    ASSERT_EQ(st, "application/dicom");
+  }
+
+  {
+    ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType
+                (ct, st, b, "Multipart/Related; type=\"application/DICOM\"; Boundary=a"));
+    ASSERT_EQ(ct, "multipart/related");
+    ASSERT_EQ(b, "a");
+    ASSERT_EQ(st, "application/dicom");
+  }
+}
+
+
+TEST(MultipartStreamReader, BytePerByte)
+{
+  std::string stream = "GARBAGE";
+
+  std::string boundary = "123456789123456789";
+
+  {
+    for (size_t i = 0; i < 10; i++)
+    {
+      std::string f = "hello " + boost::lexical_cast<std::string>(i);
+    
+      stream += "\r\n--" + boundary + "\r\n";
+      if (i % 2 == 0)
+        stream += "Content-Length: " + boost::lexical_cast<std::string>(f.size()) + "\r\n";
+      stream += "Content-Type: toto " + boost::lexical_cast<std::string>(i) + "\r\n\r\n";
+      stream += f;
+    }
+  
+    stream += "\r\n--" + boundary + "--";
+    stream += "GARBAGE";
+  }
+
+  for (unsigned int k = 0; k < 2; k++)
+  {
+    MultipartTester decoded;
+
+    MultipartStreamReader reader(boundary);
+    reader.SetBlockSize(1);
+    reader.SetHandler(decoded);
+
+    if (k == 0)
+    {
+      for (size_t i = 0; i < stream.size(); i++)
+      {
+        reader.AddChunk(&stream[i], 1);
+      }
+    }
+    else
+    {
+      reader.AddChunk(stream);
+    }
+
+    reader.CloseStream();
+
+    ASSERT_EQ(10u, decoded.GetCount());
+
+    for (size_t i = 0; i < 10; i++)
+    {
+      ASSERT_EQ("hello " + boost::lexical_cast<std::string>(i), decoded.GetData(i));
+      ASSERT_EQ("toto " + boost::lexical_cast<std::string>(i), decoded.GetHeaders(i)["content-type"]);
+
+      if (i % 2 == 0)
+      {
+        ASSERT_EQ(2u, decoded.GetHeaders(i).size());
+        ASSERT_TRUE(decoded.GetHeaders(i).find("content-length") != decoded.GetHeaders(i).end());
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/SQLiteChromiumTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,388 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Toolbox.h"
+#include "../Core/SQLite/Connection.h"
+#include "../Core/SQLite/Statement.h"
+#include "../Core/SQLite/Transaction.h"
+
+#include <sqlite3.h>
+
+
+using namespace Orthanc;
+using namespace Orthanc::SQLite;
+
+
+/********************************************************************
+ ** Tests from
+ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc
+ ********************************************************************/
+
+namespace
+{
+  class SQLConnectionTest : public testing::Test 
+  {
+  public:
+    SQLConnectionTest()
+    {
+    }
+
+    virtual ~SQLConnectionTest() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void SetUp() ORTHANC_OVERRIDE
+    {
+      db_.OpenInMemory();
+    }
+
+    virtual void TearDown() ORTHANC_OVERRIDE
+    {
+      db_.Close();
+    }
+
+    Connection& db() 
+    { 
+      return db_; 
+    }
+
+  private:
+    Connection db_;
+  };
+}
+
+
+
+TEST_F(SQLConnectionTest, Execute) 
+{
+  // Valid statement should return true.
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  EXPECT_EQ(SQLITE_OK, db().GetErrorCode());
+
+  // Invalid statement should fail.
+  ASSERT_EQ(SQLITE_ERROR,
+            db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b"));
+  EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode());
+}
+
+TEST_F(SQLConnectionTest, ExecuteWithErrorCode) {
+  ASSERT_EQ(SQLITE_OK,
+            db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)"));
+  ASSERT_EQ(SQLITE_ERROR,
+            db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE"));
+  ASSERT_EQ(SQLITE_ERROR,
+            db().ExecuteAndReturnErrorCode(
+              "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)"));
+}
+
+TEST_F(SQLConnectionTest, CachedStatement) {
+  StatementId id1("foo", 12);
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)"));
+
+  // Create a new cached statement.
+  {
+    Statement s(db(), id1, "SELECT a FROM foo");
+    ASSERT_TRUE(s.Step());
+    EXPECT_EQ(12, s.ColumnInt(0));
+  }
+
+  // The statement should be cached still.
+  EXPECT_TRUE(db().HasCachedStatement(id1));
+
+  {
+    // Get the same statement using different SQL. This should ignore our
+    // SQL and use the cached one (so it will be valid).
+    Statement s(db(), id1, "something invalid(");
+    ASSERT_TRUE(s.Step());
+    EXPECT_EQ(12, s.ColumnInt(0));
+  }
+
+  // Make sure other statements aren't marked as cached.
+  EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE));
+}
+
+TEST_F(SQLConnectionTest, IsSQLValidTest) {
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo"));
+  ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo"));
+}
+
+
+
+TEST_F(SQLConnectionTest, DoesStuffExist) {
+  // Test DoesTableExist.
+  EXPECT_FALSE(db().DoesTableExist("foo"));
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+  EXPECT_TRUE(db().DoesTableExist("foo"));
+
+  // Should be case sensitive.
+  EXPECT_FALSE(db().DoesTableExist("FOO"));
+
+  // Test DoesColumnExist.
+  EXPECT_FALSE(db().DoesColumnExist("foo", "bar"));
+  EXPECT_TRUE(db().DoesColumnExist("foo", "a"));
+
+  // Testing for a column on a nonexistent table.
+  EXPECT_FALSE(db().DoesColumnExist("bar", "b"));
+}
+
+TEST_F(SQLConnectionTest, GetLastInsertRowId) {
+  ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"));
+
+  ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)"));
+
+  // Last insert row ID should be valid.
+  int64_t row = db().GetLastInsertRowId();
+  EXPECT_LT(0, row);
+
+  // It should be the primary key of the row we just inserted.
+  Statement s(db(), "SELECT value FROM foo WHERE id=?");
+  s.BindInt64(0, row);
+  ASSERT_TRUE(s.Step());
+  EXPECT_EQ(12, s.ColumnInt(0));
+}
+
+TEST_F(SQLConnectionTest, Rollback) {
+  ASSERT_TRUE(db().BeginTransaction());
+  ASSERT_TRUE(db().BeginTransaction());
+  EXPECT_EQ(2, db().GetTransactionNesting());
+  db().RollbackTransaction();
+  EXPECT_FALSE(db().CommitTransaction());
+  EXPECT_TRUE(db().BeginTransaction());
+}
+
+
+
+
+/********************************************************************
+ ** Tests from
+ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc
+ ********************************************************************/
+
+namespace Orthanc
+{
+  namespace SQLite
+  {
+    class SQLStatementTest : public SQLConnectionTest
+    {
+    };
+
+    TEST_F(SQLStatementTest, Run) {
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
+
+      Statement s(db(), "SELECT b FROM foo WHERE a=?");
+      // Stepping it won't work since we haven't bound the value.
+      EXPECT_FALSE(s.Step());
+
+      // Run should fail since this produces output, and we should use Step(). This
+      // gets a bit wonky since sqlite says this is OK so succeeded is set.
+      s.Reset(true);
+      s.BindInt(0, 3);
+      EXPECT_FALSE(s.Run());
+      EXPECT_EQ(SQLITE_ROW, db().GetErrorCode());
+
+      // Resetting it should put it back to the previous state (not runnable).
+      s.Reset(true);
+
+      // Binding and stepping should produce one row.
+      s.BindInt(0, 3);
+      EXPECT_TRUE(s.Step());
+      EXPECT_EQ(12, s.ColumnInt(0));
+      EXPECT_FALSE(s.Step());
+    }
+
+    TEST_F(SQLStatementTest, BasicErrorCallback) {
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)"));
+      // Insert in the foo table the primary key. It is an error to insert
+      // something other than an number. This error causes the error callback
+      // handler to be called with SQLITE_MISMATCH as error code.
+      Statement s(db(), "INSERT INTO foo (a) VALUES (?)");
+      s.BindCString(0, "bad bad");
+      EXPECT_THROW(s.Run(), OrthancException);
+    }
+
+    TEST_F(SQLStatementTest, Reset) {
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
+      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)"));
+
+      Statement s(db(), "SELECT b FROM foo WHERE a = ? ");
+      s.BindInt(0, 3);
+      ASSERT_TRUE(s.Step());
+      EXPECT_EQ(12, s.ColumnInt(0));
+      ASSERT_FALSE(s.Step());
+
+      s.Reset(false);
+      // Verify that we can get all rows again.
+      ASSERT_TRUE(s.Step());
+      EXPECT_EQ(12, s.ColumnInt(0));
+      EXPECT_FALSE(s.Step());
+
+      s.Reset(true);
+      ASSERT_FALSE(s.Step());
+    }
+  }
+}
+
+
+
+
+
+
+/********************************************************************
+ ** Tests from
+ ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc
+ ********************************************************************/
+
+namespace
+{
+  class SQLTransactionTest : public SQLConnectionTest
+  {
+  public:
+    virtual void SetUp() ORTHANC_OVERRIDE
+    {
+      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);
+    }
+  };
+}
+
+
+TEST_F(SQLTransactionTest, Commit) {
+  {
+    Transaction t(db());
+    EXPECT_FALSE(t.IsOpen());
+    t.Begin();
+    EXPECT_TRUE(t.IsOpen());
+
+    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+
+    t.Commit();
+    EXPECT_FALSE(t.IsOpen());
+  }
+
+  EXPECT_EQ(1, CountFoo());
+}
+
+TEST_F(SQLTransactionTest, Rollback) {
+  // Test some basic initialization, and that rollback runs when you exit the
+  // scope.
+  {
+    Transaction t(db());
+    EXPECT_FALSE(t.IsOpen());
+    t.Begin();
+    EXPECT_TRUE(t.IsOpen());
+
+    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+  }
+
+  // Nothing should have been committed since it was implicitly rolled back.
+  EXPECT_EQ(0, CountFoo());
+
+  // Test explicit rollback.
+  Transaction t2(db());
+  EXPECT_FALSE(t2.IsOpen());
+  t2.Begin();
+
+  EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+  t2.Rollback();
+  EXPECT_FALSE(t2.IsOpen());
+
+  // Nothing should have been committed since it was explicitly rolled back.
+  EXPECT_EQ(0, CountFoo());
+}
+
+// Rolling back any part of a transaction should roll back all of them.
+TEST_F(SQLTransactionTest, NestedRollback) {
+  EXPECT_EQ(0, db().GetTransactionNesting());
+
+  // Outermost transaction.
+  {
+    Transaction outer(db());
+    outer.Begin();
+    EXPECT_EQ(1, db().GetTransactionNesting());
+
+    // The first inner one gets committed.
+    {
+      Transaction inner1(db());
+      inner1.Begin();
+      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+      EXPECT_EQ(2, db().GetTransactionNesting());
+
+      inner1.Commit();
+      EXPECT_EQ(1, db().GetTransactionNesting());
+    }
+
+    // One row should have gotten inserted.
+    EXPECT_EQ(1, CountFoo());
+
+    // The second inner one gets rolled back.
+    {
+      Transaction inner2(db());
+      inner2.Begin();
+      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
+      EXPECT_EQ(2, db().GetTransactionNesting());
+
+      inner2.Rollback();
+      EXPECT_EQ(1, db().GetTransactionNesting());
+    }
+
+    // A third inner one will fail in Begin since one has already been rolled
+    // back.
+    EXPECT_EQ(1, db().GetTransactionNesting());
+    {
+      Transaction inner3(db());
+      EXPECT_THROW(inner3.Begin(), OrthancException);
+      EXPECT_EQ(1, db().GetTransactionNesting());
+    }
+  }
+  EXPECT_EQ(0, db().GetTransactionNesting());
+  EXPECT_EQ(0, CountFoo());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/SQLiteTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,346 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/SystemToolbox.h"
+#include "../Core/SQLite/Connection.h"
+#include "../Core/SQLite/Statement.h"
+#include "../Core/SQLite/Transaction.h"
+
+#include <sqlite3.h>
+
+using namespace Orthanc;
+
+
+TEST(SQLite, Configuration)
+{
+  /**
+   * The system-wide version of SQLite under OS X uses
+   * SQLITE_THREADSAFE==2 (SQLITE_CONFIG_SERIALIZED), whereas the
+   * static builds of Orthanc use SQLITE_THREADSAFE==1
+   * (SQLITE_CONFIG_MULTITHREAD). In any case, we wish to ensure that
+   * SQLITE_THREADSAFE!=0 (SQLITE_CONFIG_SINGLETHREAD).
+   **/
+  ASSERT_NE(0, sqlite3_threadsafe());
+}
+
+
+TEST(SQLite, Connection)
+{
+  SystemToolbox::RemoveFile("UnitTestsResults/coucou");
+  SQLite::Connection c;
+  c.Open("UnitTestsResults/coucou");
+  c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
+  c.Execute("INSERT INTO c VALUES(NULL, 42);");
+}
+
+
+TEST(SQLite, StatementReferenceBasic)
+{
+  sqlite3* db;
+  sqlite3_open(":memory:", &db);
+
+  {
+    SQLite::StatementReference r(db, "SELECT * FROM sqlite_master");
+    ASSERT_EQ(0u, r.GetReferenceCount());
+
+    {
+      SQLite::StatementReference r1(r);
+      ASSERT_EQ(1u, r.GetReferenceCount());
+      ASSERT_EQ(0u, r1.GetReferenceCount());
+
+      {
+        SQLite::StatementReference r2(r);
+        ASSERT_EQ(2u, r.GetReferenceCount());
+        ASSERT_EQ(0u, r1.GetReferenceCount());
+        ASSERT_EQ(0u, r2.GetReferenceCount());
+
+        SQLite::StatementReference r3(r2);
+        ASSERT_EQ(3u, r.GetReferenceCount());
+        ASSERT_EQ(0u, r1.GetReferenceCount());
+        ASSERT_EQ(0u, r2.GetReferenceCount());
+        ASSERT_EQ(0u, r3.GetReferenceCount());
+      }
+
+      ASSERT_EQ(1u, r.GetReferenceCount());
+      ASSERT_EQ(0u, r1.GetReferenceCount());
+
+      {
+        SQLite::StatementReference r2(r);
+        ASSERT_EQ(2u, r.GetReferenceCount());
+        ASSERT_EQ(0u, r1.GetReferenceCount());
+        ASSERT_EQ(0u, r2.GetReferenceCount());
+      }
+
+      ASSERT_EQ(1u, r.GetReferenceCount());
+      ASSERT_EQ(0u, r1.GetReferenceCount());
+    }
+
+    ASSERT_EQ(0u, r.GetReferenceCount());
+  }
+
+  sqlite3_close(db);
+}
+
+TEST(SQLite, StatementBasic)
+{
+  SQLite::Connection c;
+  c.OpenInMemory();
+  
+  SQLite::Statement s(c, "SELECT * from sqlite_master");
+  s.Run();
+
+  for (unsigned int i = 0; i < 5; i++)
+  {
+    SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master");
+    cs.Step();
+  }
+}
+
+
+namespace
+{
+  static bool destroyed;
+
+  class MyFunc : public SQLite::IScalarFunction
+  {
+  public:
+    MyFunc()
+    {
+      destroyed = false;
+    }
+
+    virtual ~MyFunc() ORTHANC_OVERRIDE
+    {
+      destroyed = true;
+    }
+
+    virtual const char* GetName() const ORTHANC_OVERRIDE
+    {
+      return "MYFUNC";
+    }
+
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
+    {
+      return 2;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
+    {
+      context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1));
+    }
+  };
+
+  class MyDelete : public SQLite::IScalarFunction
+  {
+  public:
+    std::set<int> deleted_;
+
+    virtual const char* GetName() const ORTHANC_OVERRIDE
+    {
+      return "MYDELETE";
+    }
+
+    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
+    {
+      return 1;
+    }
+
+    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
+    {
+      deleted_.insert(context.GetIntValue(0));
+      context.SetNullResult();
+    }
+  };
+}
+
+TEST(SQLite, ScalarFunction)
+{
+  {
+    SQLite::Connection c;
+    c.OpenInMemory();
+    c.Register(new MyFunc());
+    c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);");
+    c.Execute("INSERT INTO t VALUES(NULL, 2, 3);");
+    c.Execute("INSERT INTO t VALUES(NULL, 4, 4);");
+    c.Execute("INSERT INTO t VALUES(NULL, 6, 5);");
+    SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t");
+    int i = 0;
+    while (t.Step())
+    {
+      ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2));
+      i++;
+    }
+    ASSERT_EQ(3, i);
+    ASSERT_FALSE(destroyed);
+  }
+  ASSERT_TRUE(destroyed);
+}
+
+TEST(SQLite, CascadedDeleteCallback)
+{
+  SQLite::Connection c;
+  c.OpenInMemory();
+  MyDelete *func = new MyDelete();
+  c.Register(func);
+  c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);");
+  c.Execute("CREATE TABLE child("
+            "  id INTEGER PRIMARY KEY, "
+            "  parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, "
+            "  value INTEGER);");
+  c.Execute("CREATE TRIGGER childRemoved "
+            "AFTER DELETE ON child "
+            "FOR EACH ROW BEGIN "
+            "  SELECT MYDELETE(old.value); "
+            "END;");
+
+  c.Execute("INSERT INTO parent VALUES(42, 100);");
+  c.Execute("INSERT INTO parent VALUES(43, 101);");
+
+  c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);");
+  c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);");
+
+  c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);");
+  c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);");
+
+  // The following command deletes "parent(43, 101)", then in turns
+  // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and
+  // 4301
+  c.Execute("DELETE FROM parent WHERE dummy=101");
+
+  ASSERT_EQ(2u, func->deleted_.size());
+  ASSERT_TRUE(func->deleted_.find(4300) != func->deleted_.end());
+  ASSERT_TRUE(func->deleted_.find(4301) != func->deleted_.end());
+}
+
+
+TEST(SQLite, EmptyTransactions)
+{
+  try
+  {
+    SQLite::Connection c;
+    c.OpenInMemory();
+
+    c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY);");
+    c.Execute("INSERT INTO a VALUES(NULL)");
+      
+    {
+      SQLite::Transaction t(c);
+      t.Begin();
+      {
+        SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
+        s.Step();
+      }
+      //t.Commit();
+    }
+
+    {
+      SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
+      s.Step();
+    }
+  }
+  catch (OrthancException& e)
+  {
+    fprintf(stderr, "Exception: [%s]\n", e.What());
+    throw e;
+  }
+}
+
+
+TEST(SQLite, Types)
+{
+  SQLite::Connection c;
+  c.OpenInMemory();
+  c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)");
+
+  {
+    SQLite::Statement s(c, std::string("SELECT * FROM a"));
+    ASSERT_EQ(2, s.ColumnCount());
+    ASSERT_FALSE(s.Step());
+  }
+
+  {
+    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
+    ASSERT_FALSE(s.Step());
+    ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement());
+  }
+
+  {
+    SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);");
+    s.BindNull(0);             ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindBool(0, true);       ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindInt(0, 42);          ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindInt64(0, 42ll);      ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindDouble(0, 42.5);     ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset();
+    s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset();
+  }
+
+  {
+    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1));
+    ASSERT_TRUE(s.ColumnIsNull(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
+    ASSERT_TRUE(s.ColumnBool(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
+    ASSERT_EQ(42, s.ColumnInt(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
+    ASSERT_EQ(42ll, s.ColumnInt64(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1));
+    ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1));
+    ASSERT_EQ("Hello", s.ColumnString(1));
+    ASSERT_TRUE(s.Step());
+    ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1));
+    ASSERT_EQ(5, s.ColumnByteLength(1));
+    ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5));
+
+    std::string t;
+    ASSERT_TRUE(s.ColumnBlobAsString(1, &t));
+    ASSERT_EQ("Hello", t);
+
+    ASSERT_FALSE(s.Step());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/StreamTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,335 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/SystemToolbox.h"
+#include "../Core/Toolbox.h"
+#include "../Core/OrthancException.h"
+#include "../Core/HttpServer/BufferHttpSender.h"
+#include "../Core/HttpServer/FilesystemHttpSender.h"
+#include "../Core/HttpServer/HttpStreamTranscoder.h"
+#include "../Core/Compression/ZlibCompressor.h"
+#include "../Core/Compression/GzipCompressor.h"
+
+
+using namespace Orthanc;
+
+
+TEST(Gzip, Basic)
+{
+  std::string s = "Hello world";
+ 
+  std::string compressed;
+  GzipCompressor c;
+  ASSERT_FALSE(c.HasPrefixWithUncompressedSize());
+  IBufferCompressor::Compress(compressed, c, s);
+
+  std::string uncompressed;
+  IBufferCompressor::Uncompress(uncompressed, c, compressed);
+  ASSERT_EQ(s.size(), uncompressed.size());
+  ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size()));
+}
+
+
+TEST(Gzip, Empty)
+{
+  std::string s;
+ 
+  std::string compressed;
+  GzipCompressor c;
+  ASSERT_FALSE(c.HasPrefixWithUncompressedSize());
+  c.SetPrefixWithUncompressedSize(false);
+  IBufferCompressor::Compress(compressed, c, s);
+
+  std::string uncompressed;
+  IBufferCompressor::Uncompress(uncompressed, c, compressed);
+  ASSERT_TRUE(uncompressed.empty());
+}
+
+
+TEST(Gzip, BasicWithPrefix)
+{
+  std::string s = "Hello world";
+ 
+  std::string compressed;
+  GzipCompressor c;
+  c.SetPrefixWithUncompressedSize(true);
+  ASSERT_TRUE(c.HasPrefixWithUncompressedSize());
+  IBufferCompressor::Compress(compressed, c, s);
+
+  std::string uncompressed;
+  IBufferCompressor::Uncompress(uncompressed, c, compressed);
+  ASSERT_EQ(s.size(), uncompressed.size());
+  ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size()));
+}
+
+
+TEST(Gzip, EmptyWithPrefix)
+{
+  std::string s;
+ 
+  std::string compressed;
+  GzipCompressor c;
+  c.SetPrefixWithUncompressedSize(true);
+  ASSERT_TRUE(c.HasPrefixWithUncompressedSize());
+  IBufferCompressor::Compress(compressed, c, s);
+
+  std::string uncompressed;
+  IBufferCompressor::Uncompress(uncompressed, c, compressed);
+  ASSERT_TRUE(uncompressed.empty());
+}
+
+
+TEST(Zlib, Basic)
+{
+  std::string s = Toolbox::GenerateUuid();
+  s = s + s + s + s;
+ 
+  std::string compressed, compressed2;
+  ZlibCompressor c;
+  ASSERT_TRUE(c.HasPrefixWithUncompressedSize());
+  IBufferCompressor::Compress(compressed, c, s);
+
+  std::string uncompressed;
+  IBufferCompressor::Uncompress(uncompressed, c, compressed);
+  ASSERT_EQ(s.size(), uncompressed.size());
+  ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size()));
+}
+
+
+TEST(Zlib, Level)
+{
+  std::string s = Toolbox::GenerateUuid();
+  s = s + s + s + s;
+ 
+  std::string compressed, compressed2;
+  ZlibCompressor c;
+  c.SetCompressionLevel(9);
+  IBufferCompressor::Compress(compressed, c, s);
+
+  c.SetCompressionLevel(0);
+  IBufferCompressor::Compress(compressed2, c, s);
+
+  ASSERT_TRUE(compressed.size() < compressed2.size());
+}
+
+
+TEST(Zlib, DISABLED_Corrupted)  // Disabled because it may result in a crash
+{
+  std::string s = Toolbox::GenerateUuid();
+  s = s + s + s + s;
+ 
+  std::string compressed;
+  ZlibCompressor c;
+  IBufferCompressor::Compress(compressed, c, s);
+
+  ASSERT_FALSE(compressed.empty());
+  compressed[compressed.size() - 1] = 'a';
+  std::string u;
+
+  ASSERT_THROW(IBufferCompressor::Uncompress(u, c, compressed), OrthancException);
+}
+
+
+TEST(Zlib, Empty)
+{
+  std::string s = "";
+ 
+  std::string compressed, compressed2;
+  ZlibCompressor c;
+  IBufferCompressor::Compress(compressed, c, s);
+  ASSERT_EQ(compressed, compressed2);
+
+  std::string uncompressed;
+  IBufferCompressor::Uncompress(uncompressed, c, compressed);
+  ASSERT_TRUE(uncompressed.empty());
+}
+
+
+static bool ReadAllStream(std::string& result,
+                          IHttpStreamAnswer& stream,
+                          bool allowGzip = false,
+                          bool allowDeflate = false)
+{
+  stream.SetupHttpCompression(allowGzip, allowDeflate);
+
+  result.resize(static_cast<size_t>(stream.GetContentLength()));
+
+  size_t pos = 0;
+  while (stream.ReadNextChunk())
+  {
+    size_t s = stream.GetChunkSize();
+    if (pos + s > result.size())
+    {
+      return false;
+    }
+
+    memcpy(&result[pos], stream.GetChunkContent(), s);
+    pos += s;
+  }
+
+  return pos == result.size();
+}
+
+
+TEST(BufferHttpSender, Basic)
+{
+  const std::string s = "Hello world";
+  std::string t;
+
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(1);
+    ASSERT_TRUE(ReadAllStream(t, sender));
+    ASSERT_EQ(0u, t.size());
+  }
+
+  for (int cs = 0; cs < 5; cs++)
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(cs);
+    sender.GetBuffer() = s;
+    ASSERT_TRUE(ReadAllStream(t, sender));
+    ASSERT_EQ(s, t);
+  }
+}
+
+
+TEST(FilesystemHttpSender, Basic)
+{
+  const std::string& path = "UnitTestsResults/stream";
+  const std::string s = "Hello world";
+  std::string t;
+
+  {
+    SystemToolbox::WriteFile(s, path);
+    FilesystemHttpSender sender(path);
+    ASSERT_TRUE(ReadAllStream(t, sender));
+    ASSERT_EQ(s, t);
+  }
+
+  {
+    SystemToolbox::WriteFile("", path);
+    FilesystemHttpSender sender(path);
+    ASSERT_TRUE(ReadAllStream(t, sender));
+    ASSERT_EQ(0u, t.size());
+  }
+}
+
+
+TEST(HttpStreamTranscoder, Basic)
+{
+  ZlibCompressor compressor;
+
+  const std::string s = "Hello world " + Toolbox::GenerateUuid();
+
+  std::string t;
+  IBufferCompressor::Compress(t, compressor, s);
+
+  for (int cs = 0; cs < 5; cs++)
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(cs);
+    sender.GetBuffer() = t;
+    std::string u;
+    ASSERT_TRUE(ReadAllStream(u, sender));
+
+    std::string v;
+    IBufferCompressor::Uncompress(v, compressor, u);
+    ASSERT_EQ(s, v);
+  }
+
+  // Pass-through test, no decompression occurs
+  for (int cs = 0; cs < 5; cs++)
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(cs);
+    sender.GetBuffer() = t;
+
+    HttpStreamTranscoder transcode(sender, CompressionType_None);
+    
+    std::string u;
+    ASSERT_TRUE(ReadAllStream(u, transcode));
+    
+    ASSERT_EQ(t, u);
+  }
+
+  // Pass-through test, decompression occurs
+  for (int cs = 0; cs < 5; cs++)
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(cs);
+    sender.GetBuffer() = t;
+
+    HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize);
+    
+    std::string u;
+    ASSERT_TRUE(ReadAllStream(u, transcode, false, false));
+    
+    ASSERT_EQ(s, u);
+  }
+
+  // Pass-through test with zlib, no decompression occurs but deflate is sent
+  for (int cs = 0; cs < 16; cs++)
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(cs);
+    sender.GetBuffer() = t;
+
+    HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize);
+    
+    std::string u;
+    ASSERT_TRUE(ReadAllStream(u, transcode, false, true));
+    
+    ASSERT_EQ(t.size() - sizeof(uint64_t), u.size());
+    ASSERT_EQ(t.substr(sizeof(uint64_t)), u);
+  }
+
+  for (int cs = 0; cs < 3; cs++)
+  {
+    BufferHttpSender sender;
+    sender.SetChunkSize(cs);
+
+    HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize);
+    std::string u;
+    ASSERT_TRUE(ReadAllStream(u, transcode, false, true));
+    
+    ASSERT_EQ(0u, u.size());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/ToolboxTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,175 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+#include "../Core/Compatibility.h"
+#include "../Core/IDynamicObject.h"
+#include "../Core/OrthancException.h"
+#include "../Core/Toolbox.h"
+
+using namespace Orthanc;
+
+TEST(Toolbox, Base64_allByteValues)
+{
+  std::string toEncode;
+  std::string base64Result;
+  std::string decodedResult;
+
+  size_t size = 2*256;
+  toEncode.reserve(size);
+  for (size_t i = 0; i < size; i++)
+    toEncode.push_back(i % 256);
+
+  Toolbox::EncodeBase64(base64Result, toEncode);
+  Toolbox::DecodeBase64(decodedResult, base64Result);
+
+  ASSERT_EQ(toEncode, decodedResult);
+}
+
+TEST(Toolbox, Base64_multipleSizes)
+{
+  std::string toEncode;
+  std::string base64Result;
+  std::string decodedResult;
+
+  for (size_t size = 0; size <= 5; size++)
+  {
+    printf("base64, testing size %zu\n", size);
+    toEncode.clear();
+    toEncode.reserve(size);
+    for (size_t i = 0; i < size; i++)
+      toEncode.push_back(i % 256);
+
+    Toolbox::EncodeBase64(base64Result, toEncode);
+    Toolbox::DecodeBase64(decodedResult, base64Result);
+
+    ASSERT_EQ(toEncode, decodedResult);
+  }
+}
+
+static std::string EncodeBase64Bis(const std::string& s)
+{
+  std::string result;
+  Toolbox::EncodeBase64(result, s);
+  return result;
+}
+
+
+TEST(Toolbox, Base64)
+{
+  ASSERT_EQ("", EncodeBase64Bis(""));
+  ASSERT_EQ("YQ==", EncodeBase64Bis("a"));
+
+  const std::string hello = "SGVsbG8gd29ybGQ=";
+  ASSERT_EQ(hello, EncodeBase64Bis("Hello world"));
+
+  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+/=");
+}
+
+
+#if 0 // enable only when compiling in Release with a C++ 11 compiler
+#include <chrono> // I had troubles to link with boost::chrono ...
+
+TEST(Toolbox, Base64_largeString)
+{
+  std::string toEncode;
+  std::string base64Result;
+  std::string decodedResult;
+
+  size_t size = 10 * 1024 * 1024;
+  toEncode.reserve(size);
+  for (size_t i = 0; i < size; i++)
+    toEncode.push_back(i % 256);
+
+  std::chrono::high_resolution_clock::time_point start;
+  std::chrono::high_resolution_clock::time_point afterEncoding;
+  std::chrono::high_resolution_clock::time_point afterDecoding;
+
+  start = std::chrono::high_resolution_clock::now();
+  Orthanc::Toolbox::EncodeBase64(base64Result, toEncode);
+  afterEncoding = std::chrono::high_resolution_clock::now();
+  Orthanc::Toolbox::DecodeBase64(decodedResult, base64Result);
+  afterDecoding = std::chrono::high_resolution_clock::now();
+
+  ASSERT_EQ(toEncode, decodedResult);
+
+  printf("encoding took %zu ms\n", (std::chrono::duration_cast<std::chrono::milliseconds>(afterEncoding - start)));
+  printf("decoding took %zu ms\n", (std::chrono::duration_cast<std::chrono::milliseconds>(afterDecoding - afterEncoding)));
+}
+#endif
+
+
+TEST(Toolbox, LargeHexadecimalToDecimal)
+{
+  // https://stackoverflow.com/a/16967286/881731
+  ASSERT_EQ(
+    "166089946137986168535368849184301740204613753693156360462575217560130904921953976324839782808018277000296027060873747803291797869684516494894741699267674246881622658654267131250470956587908385447044319923040838072975636163137212887824248575510341104029461758594855159174329892125993844566497176102668262139513",
+    Toolbox::LargeHexadecimalToDecimal("EC851A69B8ACD843164E10CFF70CF9E86DC2FEE3CF6F374B43C854E3342A2F1AC3E30C741CC41E679DF6D07CE6FA3A66083EC9B8C8BF3AF05D8BDBB0AA6Cb3ef8c5baa2a5e531ba9e28592f99e0fe4f95169a6c63f635d0197e325c5ec76219b907e4ebdcd401fb1986e4e3ca661ff73e7e2b8fd9988e753b7042b2bbca76679"));
+
+  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal(""));
+  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0"));
+  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0000"));
+  ASSERT_EQ("255", Toolbox::LargeHexadecimalToDecimal("00000ff"));
+
+  ASSERT_THROW(Toolbox::LargeHexadecimalToDecimal("g"), Orthanc::OrthancException);
+}
+
+
+TEST(Toolbox, GenerateDicomPrivateUniqueIdentifier)
+{
+  std::string s = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+  ASSERT_EQ("2.25.", s.substr(0, 5));
+}
+
+
+TEST(Toolbox, UniquePtr)
+{
+  std::unique_ptr<int> i(new int(42));
+  ASSERT_EQ(42, *i);
+
+  std::unique_ptr<SingleValueObject<int> > j(new SingleValueObject<int>(42));
+  ASSERT_EQ(42, j->GetValue());
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancFramework/UnitTestsSources/ZipTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,193 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/Compression/ZipWriter.h"
+#include "../Core/Compression/HierarchicalZipWriter.h"
+#include "../Core/Toolbox.h"
+
+
+using namespace Orthanc;
+
+TEST(ZipWriter, Basic)
+{
+  Orthanc::ZipWriter w;
+  w.SetOutputPath("UnitTestsResults/hello.zip");
+  w.Open();
+  w.OpenFile("world/hello");
+  w.Write("Hello world");
+}
+
+
+TEST(ZipWriter, Basic64)
+{
+  Orthanc::ZipWriter w;
+  w.SetOutputPath("UnitTestsResults/hello64.zip");
+  w.SetZip64(true);
+  w.Open();
+  w.OpenFile("world/hello");
+  w.Write("Hello world");
+}
+
+
+TEST(ZipWriter, Exceptions)
+{
+  Orthanc::ZipWriter w;
+  ASSERT_THROW(w.Open(), Orthanc::OrthancException);
+  w.SetOutputPath("UnitTestsResults/hello3.zip");
+  w.Open();
+  ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException);
+}
+
+
+TEST(ZipWriter, Append)
+{
+  {
+    Orthanc::ZipWriter w;
+    w.SetAppendToExisting(false);
+    w.SetOutputPath("UnitTestsResults/append.zip");
+    w.Open();
+    w.OpenFile("world/hello");
+    w.Write("Hello world 1");
+  }
+
+  {
+    Orthanc::ZipWriter w;
+    w.SetAppendToExisting(true);
+    w.SetOutputPath("UnitTestsResults/append.zip");
+    w.Open();
+    w.OpenFile("world/appended");
+    w.Write("Hello world 2");
+  }
+}
+
+
+
+
+
+namespace Orthanc
+{
+  // The namespace is necessary
+  // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members
+
+  TEST(HierarchicalZipWriter, Index)
+  {
+    HierarchicalZipWriter::Index i;
+    ASSERT_EQ("hello", i.OpenFile("hello"));
+    ASSERT_EQ("hello-2", i.OpenFile("hello"));
+    ASSERT_EQ("coucou", i.OpenFile("coucou"));
+    ASSERT_EQ("hello-3", i.OpenFile("hello"));
+
+    i.OpenDirectory("coucou");
+
+    ASSERT_EQ("coucou-2/world", i.OpenFile("world"));
+    ASSERT_EQ("coucou-2/world-2", i.OpenFile("world"));
+
+    i.OpenDirectory("world");
+  
+    ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello"));
+    ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello"));
+
+    i.CloseDirectory();
+
+    ASSERT_EQ("coucou-2/world-4", i.OpenFile("world"));
+
+    i.CloseDirectory();
+
+    ASSERT_EQ("coucou-3", i.OpenFile("coucou"));
+
+    ASSERT_THROW(i.CloseDirectory(), OrthancException);
+  }
+
+
+  TEST(HierarchicalZipWriter, Filenames)
+  {
+    ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric("    ÊtrE hellô  "));
+
+    // The "^" character is considered as a space in DICOM
+    ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric("    Hel^^  ^\r\n\t^^lo  \t  <world>  "));
+  }
+}
+
+
+TEST(HierarchicalZipWriter, Basic)
+{
+  static const std::string SPACES = "                             ";
+
+  HierarchicalZipWriter w("UnitTestsResults/hello2.zip");
+
+  w.SetCompressionLevel(0);
+
+  // Inside "/"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello\n");
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello-2\n");
+  w.OpenDirectory("hello");
+
+  // Inside "/hello-3"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello\n");
+  w.OpenDirectory("hello");
+
+  w.SetCompressionLevel(9);
+
+  // Inside "/hello-3/hello-2"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello\n");
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello-2\n");
+  w.CloseDirectory();
+
+  // Inside "/hello-3"
+  w.OpenFile("hello");
+  w.Write(SPACES + "hello-3\n");
+
+  /**
+
+     TO CHECK THE CONTENT OF THE "hello2.zip" FILE:
+
+     # unzip -v hello2.zip 
+
+     => There must be 6 files. The first 3 files must have a negative
+     compression ratio.
+
+  **/
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/COPYING	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU 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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- a/OrthancServer/Database/Compatibility/DatabaseLookup.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,420 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DatabaseLookup.h"
-
-#include "../../../Core/OrthancException.h"
-#include "../../Search/DicomTagConstraint.h"
-#include "../../ServerToolbox.h"
-#include "SetOfResources.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    namespace
-    {
-      // Anonymous namespace to avoid clashes between compiler modules
-      class MainTagsConstraints : boost::noncopyable
-      {
-      private:
-        std::vector<DicomTagConstraint*>  constraints_;
-
-      public:
-        ~MainTagsConstraints()
-        {
-          for (size_t i = 0; i < constraints_.size(); i++)
-          {
-            assert(constraints_[i] != NULL);
-            delete constraints_[i];
-          }
-        }
-
-        void Reserve(size_t n)
-        {
-          constraints_.reserve(n);
-        }
-
-        size_t GetSize() const
-        {
-          return constraints_.size();
-        }
-
-        DicomTagConstraint& GetConstraint(size_t i) const
-        {
-          if (i >= constraints_.size())
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-          }
-          else
-          {
-            assert(constraints_[i] != NULL);
-            return *constraints_[i];
-          }
-        }
-        
-        void Add(const DatabaseConstraint& constraint)
-        {
-          constraints_.push_back(new DicomTagConstraint(constraint));
-        }          
-      };
-    }
-    
-    
-    static void ApplyIdentifierConstraint(SetOfResources& candidates,
-                                          ILookupResources& compatibility,
-                                          const DatabaseConstraint& constraint,
-                                          ResourceType level)
-    {
-      std::list<int64_t> matches;
-
-      switch (constraint.GetConstraintType())
-      {
-        case ConstraintType_Equal:
-          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
-                                    IdentifierConstraintType_Equal, constraint.GetSingleValue());
-          break;
-          
-        case ConstraintType_SmallerOrEqual:
-          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
-                                    IdentifierConstraintType_SmallerOrEqual, constraint.GetSingleValue());
-          break;
-          
-        case ConstraintType_GreaterOrEqual:
-          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
-                                    IdentifierConstraintType_GreaterOrEqual, constraint.GetSingleValue());
-
-          break;
-          
-        case ConstraintType_Wildcard:
-          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
-                                    IdentifierConstraintType_Wildcard, constraint.GetSingleValue());
-
-          break;
-          
-        case ConstraintType_List:
-          for (size_t i = 0; i < constraint.GetValuesCount(); i++)
-          {
-            std::list<int64_t> tmp;
-            compatibility.LookupIdentifier(tmp, level, constraint.GetTag(),
-                                      IdentifierConstraintType_Wildcard, constraint.GetValue(i));
-            matches.splice(matches.end(), tmp);
-          }
-
-          break;
-          
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      candidates.Intersect(matches);
-    }
-
-    
-    static void ApplyIdentifierRange(SetOfResources& candidates,
-                                     ILookupResources& compatibility,
-                                     const DatabaseConstraint& smaller,
-                                     const DatabaseConstraint& greater,
-                                     ResourceType level)
-    {
-      assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
-             greater.GetConstraintType() == ConstraintType_GreaterOrEqual &&
-             smaller.GetTag() == greater.GetTag() &&
-             ServerToolbox::IsIdentifier(smaller.GetTag(), level));
-
-      std::list<int64_t> matches;
-      compatibility.LookupIdentifierRange(matches, level, smaller.GetTag(),
-                                     greater.GetSingleValue(), smaller.GetSingleValue());
-      candidates.Intersect(matches);
-    }
-
-    
-    static void ApplyLevel(SetOfResources& candidates,
-                           IDatabaseWrapper& database,
-                           ILookupResources& compatibility,
-                           const std::vector<DatabaseConstraint>& lookup,
-                           ResourceType level)
-    {
-      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
-      typedef std::map<DicomTag, SetOfConstraints> Identifiers;
-
-      // (1) Select which constraints apply to this level, and split
-      // them between "identifier tags" constraints and "main DICOM
-      // tags" constraints
-
-      Identifiers       identifiers;
-      SetOfConstraints  mainTags;
-      
-      for (size_t i = 0; i < lookup.size(); i++)
-      {
-        if (lookup[i].GetLevel() == level)
-        {
-          if (lookup[i].IsIdentifier())
-          {
-            identifiers[lookup[i].GetTag()].insert(&lookup[i]);
-          }
-          else
-          {
-            mainTags.insert(&lookup[i]);
-          }
-        }
-      }
-
-      
-      // (2) Apply the constraints over the identifiers
-      
-      for (Identifiers::const_iterator it = identifiers.begin();
-           it != identifiers.end(); ++it)
-      {
-        // Check whether some range constraint over identifiers is
-        // present at this level
-        const DatabaseConstraint* smaller = NULL;
-        const DatabaseConstraint* greater = NULL;
-        
-        for (SetOfConstraints::const_iterator it2 = it->second.begin();
-             it2 != it->second.end(); ++it2)
-        {
-          assert(*it2 != NULL);
-        
-          if ((*it2)->GetConstraintType() == ConstraintType_SmallerOrEqual)
-          {
-            smaller = *it2;
-          }
-
-          if ((*it2)->GetConstraintType() == ConstraintType_GreaterOrEqual)
-          {
-            greater = *it2;
-          }
-        }
-
-        if (smaller != NULL &&
-            greater != NULL)
-        {
-          // There is a range constraint: Apply it, as it is more efficient
-          ApplyIdentifierRange(candidates, compatibility, *smaller, *greater, level);
-        }
-        else
-        {
-          smaller = NULL;
-          greater = NULL;
-        }
-
-        for (SetOfConstraints::const_iterator it2 = it->second.begin();
-             it2 != it->second.end(); ++it2)
-        {
-          // Check to avoid applying twice the range constraint
-          if (*it2 != smaller &&
-              *it2 != greater)
-          {
-            ApplyIdentifierConstraint(candidates, compatibility, **it2, level);
-          }
-        }
-      }
-
-
-      // (3) Apply the constraints over the main DICOM tags (no index
-      // here, so this is less efficient than filtering over the
-      // identifiers)
-      if (!mainTags.empty())
-      {
-        MainTagsConstraints c;
-        c.Reserve(mainTags.size());
-        
-        for (SetOfConstraints::const_iterator it = mainTags.begin();
-             it != mainTags.end(); ++it)
-        {
-          assert(*it != NULL);
-          c.Add(**it);
-        }
-
-        std::list<int64_t>  source;
-        candidates.Flatten(compatibility, source);
-        candidates.Clear();
-
-        std::list<int64_t>  filtered;
-        for (std::list<int64_t>::const_iterator candidate = source.begin(); 
-             candidate != source.end(); ++candidate)
-        {
-          DicomMap tags;
-          database.GetMainDicomTags(tags, *candidate);
-
-          bool match = true;
-
-          for (size_t i = 0; i < c.GetSize(); i++)
-          {
-            if (!c.GetConstraint(i).IsMatch(tags))
-            {
-              match = false;
-              break;
-            }
-          }
-        
-          if (match)
-          {
-            filtered.push_back(*candidate);
-          }
-        }
-
-        candidates.Intersect(filtered);
-      }
-    }
-
-
-    static std::string GetOneInstance(IDatabaseWrapper& compatibility,
-                                      int64_t resource,
-                                      ResourceType level)
-    {
-      for (int i = level; i < ResourceType_Instance; i++)
-      {
-        assert(compatibility.GetResourceType(resource) == static_cast<ResourceType>(i));
-
-        std::list<int64_t> children;
-        compatibility.GetChildrenInternalId(children, resource);
-          
-        if (children.empty())
-        {
-          throw OrthancException(ErrorCode_Database);
-        }
-          
-        resource = children.front();
-      }
-
-      return compatibility.GetPublicId(resource);
-    }
-                           
-
-    void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
-                                              std::list<std::string>* instancesId,
-                                              const std::vector<DatabaseConstraint>& lookup,
-                                              ResourceType queryLevel,
-                                              size_t limit)
-    {
-      // This is a re-implementation of
-      // "../../../Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp"
-
-      assert(ResourceType_Patient < ResourceType_Study &&
-             ResourceType_Study < ResourceType_Series &&
-             ResourceType_Series < ResourceType_Instance);
-    
-      ResourceType upperLevel = queryLevel;
-      ResourceType lowerLevel = queryLevel;
-
-      for (size_t i = 0; i < lookup.size(); i++)
-      {
-        ResourceType level = lookup[i].GetLevel();
-
-        if (level < upperLevel)
-        {
-          upperLevel = level;
-        }
-
-        if (level > lowerLevel)
-        {
-          lowerLevel = level;
-        }
-      }
-
-      assert(upperLevel <= queryLevel &&
-             queryLevel <= lowerLevel);
-
-      SetOfResources candidates(database_, upperLevel);
-
-      for (int level = upperLevel; level <= lowerLevel; level++)
-      {
-        ApplyLevel(candidates, database_, compatibility_, lookup, static_cast<ResourceType>(level));
-
-        if (level != lowerLevel)
-        {
-          candidates.GoDown();
-        }
-      }
-
-      std::list<int64_t> resources;
-      candidates.Flatten(compatibility_, resources);
-
-      // Climb up, up to queryLevel
-
-      for (int level = lowerLevel; level > queryLevel; level--)
-      {
-        std::list<int64_t> parents;
-        for (std::list<int64_t>::const_iterator
-               it = resources.begin(); it != resources.end(); ++it)
-        {
-          int64_t parent;
-          if (database_.LookupParent(parent, *it))
-          {
-            parents.push_back(parent);
-          }
-        }
-
-        resources.swap(parents);
-      }
-
-      // Apply the limit, if given
-
-      if (limit != 0 &&
-          resources.size() > limit)
-      {
-        resources.resize(limit);
-      }
-
-      // Get the public ID of all the selected resources
-
-      size_t pos = 0;
-
-      for (std::list<int64_t>::const_iterator
-             it = resources.begin(); it != resources.end(); ++it, pos++)
-      {
-        assert(database_.GetResourceType(*it) == queryLevel);
-
-        const std::string resource = database_.GetPublicId(*it);
-        resourcesId.push_back(resource);
-
-        if (instancesId != NULL)
-        {
-          if (queryLevel == ResourceType_Instance)
-          {
-            // The resource is itself the instance
-            instancesId->push_back(resource);
-          }
-          else
-          {
-            // Collect one child instance for each of the selected resources
-            instancesId->push_back(GetOneInstance(database_, *it, queryLevel));
-          }
-        }
-      }
-    }
-  }
-}
--- a/OrthancServer/Database/Compatibility/DatabaseLookup.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../IDatabaseWrapper.h"
-#include "ILookupResources.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class DatabaseLookup : public boost::noncopyable
-    {
-    private:
-      IDatabaseWrapper&  database_;
-      ILookupResources&  compatibility_;
-
-    public:
-      DatabaseLookup(IDatabaseWrapper& database,
-                     ILookupResources& compatibility) :
-        database_(database),
-        compatibility_(compatibility)
-      {
-      }
-
-      void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                std::list<std::string>* instancesId,
-                                const std::vector<DatabaseConstraint>& lookup,
-                                ResourceType queryLevel,
-                                size_t limit);
-    };
-  }
-}
--- a/OrthancServer/Database/Compatibility/ICreateInstance.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ICreateInstance.h"
-
-#include "../../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    bool ICreateInstance::Apply(ICreateInstance& database,
-                                IDatabaseWrapper::CreateInstanceResult& result,
-                                int64_t& instanceId,
-                                const std::string& hashPatient,
-                                const std::string& hashStudy,
-                                const std::string& hashSeries,
-                                const std::string& hashInstance)
-    {
-      {
-        ResourceType type;
-        int64_t tmp;
-        
-        if (database.LookupResource(tmp, type, hashInstance))
-        {
-          // The instance already exists
-          assert(type == ResourceType_Instance);
-          instanceId = tmp;
-          return false;
-        }
-      }
-
-      instanceId = database.CreateResource(hashInstance, ResourceType_Instance);
-
-      result.isNewPatient_ = false;
-      result.isNewStudy_ = false;
-      result.isNewSeries_ = false;
-      result.patientId_ = -1;
-      result.studyId_ = -1;
-      result.seriesId_ = -1;
-      
-      // Detect up to which level the patient/study/series/instance
-      // hierarchy must be created
-
-      {
-        ResourceType dummy;
-
-        if (database.LookupResource(result.seriesId_, dummy, hashSeries))
-        {
-          assert(dummy == ResourceType_Series);
-          // The patient, the study and the series already exist
-
-          bool ok = (database.LookupResource(result.patientId_, dummy, hashPatient) &&
-                     database.LookupResource(result.studyId_, dummy, hashStudy));
-          assert(ok);
-        }
-        else if (database.LookupResource(result.studyId_, dummy, hashStudy))
-        {
-          assert(dummy == ResourceType_Study);
-
-          // New series: The patient and the study already exist
-          result.isNewSeries_ = true;
-
-          bool ok = database.LookupResource(result.patientId_, dummy, hashPatient);
-          assert(ok);
-        }
-        else if (database.LookupResource(result.patientId_, dummy, hashPatient))
-        {
-          assert(dummy == ResourceType_Patient);
-
-          // New study and series: The patient already exist
-          result.isNewStudy_ = true;
-          result.isNewSeries_ = true;
-        }
-        else
-        {
-          // New patient, study and series: Nothing exists
-          result.isNewPatient_ = true;
-          result.isNewStudy_ = true;
-          result.isNewSeries_ = true;
-        }
-      }
-
-      // Create the series if needed
-      if (result.isNewSeries_)
-      {
-        result.seriesId_ = database.CreateResource(hashSeries, ResourceType_Series);
-      }
-
-      // Create the study if needed
-      if (result.isNewStudy_)
-      {
-        result.studyId_ = database.CreateResource(hashStudy, ResourceType_Study);
-      }
-
-      // Create the patient if needed
-      if (result.isNewPatient_)
-      {
-        result.patientId_ = database.CreateResource(hashPatient, ResourceType_Patient);
-      }
-
-      // Create the parent-to-child links
-      database.AttachChild(result.seriesId_, instanceId);
-
-      if (result.isNewSeries_)
-      {
-        database.AttachChild(result.studyId_, result.seriesId_);
-      }
-
-      if (result.isNewStudy_)
-      {
-        database.AttachChild(result.patientId_, result.studyId_);
-      }
-
-      database.TagMostRecentPatient(result.patientId_);
-      
-      // Sanity checks
-      assert(result.patientId_ != -1);
-      assert(result.studyId_ != -1);
-      assert(result.seriesId_ != -1);
-      assert(instanceId != -1);
-
-      return true;
-    }
-  }
-}
--- a/OrthancServer/Database/Compatibility/ICreateInstance.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../IDatabaseWrapper.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class ICreateInstance : public boost::noncopyable
-    {
-    public:
-      virtual bool LookupResource(int64_t& id,
-                                  ResourceType& type,
-                                  const std::string& publicId) = 0;
-
-      virtual int64_t CreateResource(const std::string& publicId,
-                                     ResourceType type) = 0;
-
-      virtual void AttachChild(int64_t parent,
-                               int64_t child) = 0;
-
-      virtual void TagMostRecentPatient(int64_t patientId) = 0;
-      
-      static bool Apply(ICreateInstance& database,
-                        IDatabaseWrapper::CreateInstanceResult& result,
-                        int64_t& instanceId,
-                        const std::string& patient,
-                        const std::string& study,
-                        const std::string& series,
-                        const std::string& instance);
-    };
-  }
-}
--- a/OrthancServer/Database/Compatibility/IGetChildrenMetadata.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IGetChildrenMetadata.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    void IGetChildrenMetadata::Apply(IGetChildrenMetadata& database,
-                                     std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata)
-    {
-      // This function comes from an optimization of
-      // "ServerIndex::GetSeriesStatus()" in Orthanc <= 1.5.1
-      // Loop over the instances of this series
-
-      target.clear();
-      
-      std::list<int64_t> children;
-      database.GetChildrenInternalId(children, resourceId);
-
-      for (std::list<int64_t>::const_iterator 
-             it = children.begin(); it != children.end(); ++it)
-      {
-        std::string value;
-        if (database.LookupMetadata(value, *it, metadata))
-        {
-          target.push_back(value);
-        }
-      }
-    }
-  }
-}
--- a/OrthancServer/Database/Compatibility/IGetChildrenMetadata.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <boost/noncopyable.hpp>
-#include <list>
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class IGetChildrenMetadata : public boost::noncopyable
-    {
-    public:
-      virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                         int64_t id) = 0;
-
-      virtual bool LookupMetadata(std::string& target,
-                                  int64_t id,
-                                  MetadataType type) = 0;
-
-      static void Apply(IGetChildrenMetadata& database,
-                        std::list<std::string>& target,
-                        int64_t resourceId,
-                        MetadataType metadata);
-    };
-  }
-}
--- a/OrthancServer/Database/Compatibility/ILookupResourceAndParent.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ILookupResourceAndParent.h"
-
-#include "../../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    bool ILookupResourceAndParent::Apply(ILookupResourceAndParent& database,
-                                         int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId)
-    {
-      if (!database.LookupResource(id, type, publicId))
-      {
-        return false;
-      }
-      else if (type == ResourceType_Patient)
-      {
-        parentPublicId.clear();
-        return true;
-      }
-      else
-      {
-        int64_t parentId;
-        if (!database.LookupParent(parentId, id))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        parentPublicId = database.GetPublicId(parentId);
-        return true;
-      }
-    }
-  }
-}
--- a/OrthancServer/Database/Compatibility/ILookupResourceAndParent.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <boost/noncopyable.hpp>
-#include <list>
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class ILookupResourceAndParent : public boost::noncopyable
-    {
-    public:
-      virtual bool LookupResource(int64_t& id,
-                                  ResourceType& type,
-                                  const std::string& publicId) = 0;
-
-      virtual bool LookupParent(int64_t& parentId,
-                                int64_t resourceId) = 0;
-
-      virtual std::string GetPublicId(int64_t resourceId) = 0;
-
-      static bool Apply(ILookupResourceAndParent& database,
-                        int64_t& id,
-                        ResourceType& type,
-                        std::string& parentPublicId,
-                        const std::string& publicId);
-    };
-  }
-}
--- a/OrthancServer/Database/Compatibility/ILookupResources.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ILookupResources.h"
-
-#include "DatabaseLookup.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    void ILookupResources::Apply(
-      IDatabaseWrapper& database,
-      ILookupResources& compatibility,
-      std::list<std::string>& resourcesId,
-      std::list<std::string>* instancesId,
-      const std::vector<DatabaseConstraint>& lookup,
-      ResourceType queryLevel,
-      size_t limit)
-    {
-      Compatibility::DatabaseLookup compat(database, compatibility);
-      compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
-    }
-  }
-}
--- a/OrthancServer/Database/Compatibility/ILookupResources.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,78 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../IDatabaseWrapper.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    /**
-     * This is a compatibility class that contains database primitives
-     * that were used in Orthanc <= 1.5.1, and that have been removed
-     * during the optimization of the database engine.
-     **/
-    class ILookupResources : public boost::noncopyable
-    {     
-    public:
-      virtual ~ILookupResources()
-      {
-      }
-      
-      virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                     ResourceType resourceType) = 0;
-      
-      virtual void LookupIdentifier(std::list<int64_t>& result,
-                                    ResourceType level,
-                                    const DicomTag& tag,
-                                    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;
-
-      static void Apply(IDatabaseWrapper& database,
-                        ILookupResources& compatibility,
-                        std::list<std::string>& resourcesId,
-                        std::list<std::string>* instancesId,
-                        const std::vector<DatabaseConstraint>& lookup,
-                        ResourceType queryLevel,
-                        size_t limit);
-    };
-  }
-}
--- a/OrthancServer/Database/Compatibility/ISetResourcesContent.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../ResourcesContent.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class ISetResourcesContent : public boost::noncopyable
-    {
-    public:
-      virtual ~ISetResourcesContent()
-      {
-      }
-      
-      virtual void SetMainDicomTag(int64_t id,
-                                   const DicomTag& tag,
-                                   const std::string& value) = 0;
-
-      virtual void SetIdentifierTag(int64_t id,
-                                    const DicomTag& tag,
-                                    const std::string& value) = 0;
-
-      virtual void SetMetadata(int64_t id,
-                               MetadataType type,
-                               const std::string& value) = 0;
-
-      static void Apply(ISetResourcesContent& that,
-                        const ResourcesContent& content)
-      {
-        content.Store(that);
-      }
-    };
-  }
-}
--- a/OrthancServer/Database/Compatibility/SetOfResources.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,169 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "SetOfResources.h"
-
-#include "../../../Core/OrthancException.h"
-
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    void SetOfResources::Intersect(const std::list<int64_t>& resources)
-    {
-      if (resources_.get() == NULL)
-      {
-        resources_.reset(new Resources);
-
-        for (std::list<int64_t>::const_iterator
-               it = resources.begin(); it != resources.end(); ++it)
-        {
-          resources_->insert(*it);
-        }
-      }
-      else
-      {
-        std::unique_ptr<Resources> filtered(new Resources);
-
-        for (std::list<int64_t>::const_iterator
-               it = resources.begin(); it != resources.end(); ++it)
-        {
-          if (resources_->find(*it) != resources_->end())
-          {
-            filtered->insert(*it);
-          }
-        }
-
-#if __cplusplus < 201103L
-        resources_.reset(filtered.release());
-#else
-        resources_ = std::move(filtered);
-#endif
-      }
-    }
-
-
-    void SetOfResources::GoDown()
-    {
-      if (level_ == ResourceType_Instance)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      if (resources_.get() != NULL)
-      {
-        std::unique_ptr<Resources> children(new Resources);
-
-        for (Resources::const_iterator it = resources_->begin(); 
-             it != resources_->end(); ++it)
-        {
-          std::list<int64_t> tmp;
-          database_.GetChildrenInternalId(tmp, *it);
-
-          for (std::list<int64_t>::const_iterator
-                 child = tmp.begin(); child != tmp.end(); ++child)
-          {
-            children->insert(*child);
-          }
-        }
-
-#if __cplusplus < 201103L
-        resources_.reset(children.release());
-#else
-        resources_ = std::move(children);
-#endif
-      }
-
-      switch (level_)
-      {
-        case ResourceType_Patient:
-          level_ = ResourceType_Study;
-          break;
-
-        case ResourceType_Study:
-          level_ = ResourceType_Series;
-          break;
-
-        case ResourceType_Series:
-          level_ = ResourceType_Instance;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-
-    void SetOfResources::Flatten(std::list<std::string>& result)
-    {
-      result.clear();
-      
-      if (resources_.get() == NULL)
-      {
-        // All the resources of this level are part of the filter
-        database_.GetAllPublicIds(result, level_);
-      }
-      else
-      {
-        for (Resources::const_iterator it = resources_->begin(); 
-             it != resources_->end(); ++it)
-        {
-          result.push_back(database_.GetPublicId(*it));
-        }
-      }
-    }
-
-
-    void SetOfResources::Flatten(ILookupResources& compatibility,
-                                 std::list<int64_t>& result)
-    {
-      result.clear();
-      
-      if (resources_.get() == NULL)
-      {
-        // All the resources of this level are part of the filter
-        compatibility.GetAllInternalIds(result, level_);
-      }
-      else
-      {
-        for (Resources::const_iterator it = resources_->begin(); 
-             it != resources_->end(); ++it)
-        {
-          result.push_back(*it);
-        }
-      }
-    }
-  }
-}
--- a/OrthancServer/Database/Compatibility/SetOfResources.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../IDatabaseWrapper.h"
-#include "ILookupResources.h"
-
-#include <set>
-#include <memory>
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class SetOfResources : public boost::noncopyable
-    {
-    private:
-      typedef std::set<int64_t>  Resources;
-
-      IDatabaseWrapper&           database_;
-      ResourceType                level_;
-      std::unique_ptr<Resources>  resources_;
-    
-    public:
-      SetOfResources(IDatabaseWrapper& database,
-                     ResourceType level) : 
-        database_(database),
-        level_(level)
-      {
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      void Intersect(const std::list<int64_t>& resources);
-
-      void GoDown();
-
-      void Flatten(ILookupResources& compatibility,
-                   std::list<int64_t>& result);
-
-      void Flatten(std::list<std::string>& result);
-
-      void Clear()
-      {
-        resources_.reset(NULL);
-      }
-    };
-  }
-}
--- a/OrthancServer/Database/IDatabaseListener.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../ServerIndexChange.h"
-
-#include <string>
-
-namespace Orthanc
-{
-  class IDatabaseListener : public boost::noncopyable
-  {
-  public:
-    virtual ~IDatabaseListener()
-    {
-    }
-
-    virtual void SignalRemainingAncestor(ResourceType parentType,
-                                         const std::string& publicId) = 0;
-
-    virtual void SignalFileDeleted(const FileInfo& info) = 0;
-
-    virtual void SignalChange(const ServerIndexChange& change) = 0;
-  };
-}
--- a/OrthancServer/Database/IDatabaseWrapper.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,256 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/FileStorage/FileInfo.h"
-#include "../../Core/FileStorage/IStorageArea.h"
-#include "../../Core/SQLite/ITransaction.h"
-
-#include "../ExportedResource.h"
-#include "IDatabaseListener.h"
-
-#include <list>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class DatabaseConstraint;
-  class ResourcesContent;
-
-  
-  class IDatabaseWrapper : public boost::noncopyable
-  {
-  public:
-    class ITransaction : public boost::noncopyable
-    {
-    public:
-      virtual ~ITransaction()
-      {
-      }
-
-      virtual void Begin() = 0;
-
-      virtual void Rollback() = 0;
-
-      virtual void Commit(int64_t fileSizeDelta) = 0;
-    };
-
-
-    struct CreateInstanceResult
-    {
-      bool     isNewPatient_;
-      bool     isNewStudy_;
-      bool     isNewSeries_;
-      int64_t  patientId_;
-      int64_t  studyId_;
-      int64_t  seriesId_;
-    };
-
-    virtual ~IDatabaseWrapper()
-    {
-    }
-
-    virtual void Open() = 0;
-
-    virtual void Close() = 0;
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) = 0;
-
-    virtual void ClearChanges() = 0;
-
-    virtual void ClearExportedResources() = 0;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment) = 0;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type) = 0;
-
-    virtual void DeleteResource(int64_t id) = 0;
-
-    virtual void FlushToDisk() = 0;
-
-    virtual bool HasFlushToDisk() const = 0;
-
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType) = 0;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) = 0;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) = 0;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id) = 0;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id) = 0;
-
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) = 0;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id) = 0;
-
-    virtual std::string GetPublicId(int64_t resourceId) = 0;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType) = 0;
-
-    virtual ResourceType 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 ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id) = 0;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) = 0;
-
-    virtual void LogExportedResource(const ExportedResource& resource) = 0;
-    
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType) = 0;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property) = 0;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type) = 0;
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId) = 0;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid) = 0;
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value) = 0;
-
-    virtual void ClearMainDicomTags(int64_t id) = 0;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value) = 0;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected) = 0;
-
-    virtual ITransaction* StartTransaction() = 0;
-
-    virtual void SetListener(IDatabaseListener& listener) = 0;
-
-    virtual unsigned int GetDatabaseVersion() = 0;
-
-    virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) = 0;
-
-
-    /**
-     * Primitives introduced in Orthanc 1.5.2
-     **/
-    
-    virtual bool IsDiskSizeAbove(uint64_t threshold) = 0;
-    
-    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                      std::list<std::string>* instancesId, // Can be NULL if not needed
-                                      const std::vector<DatabaseConstraint>& lookup,
-                                      ResourceType queryLevel,
-                                      size_t limit) = 0;
-
-    // Returns "true" iff. the instance is new and has been inserted
-    // into the database. If "false" is returned, the content of
-    // "result" is undefined, but "instanceId" must be properly
-    // set. This method must also tag the parent patient as the most
-    // recent in the patient recycling order if it is not protected
-    // (so as to fix issue #58).
-    virtual bool CreateInstance(CreateInstanceResult& result, /* out */
-                                int64_t& instanceId,          /* out */
-                                const std::string& patient,
-                                const std::string& study,
-                                const std::string& series,
-                                const std::string& instance) = 0;
-
-    // It is guaranteed that the resources to be modified have no main
-    // DICOM tags, and no DICOM identifiers associated with
-    // them. However, some metadata might be already existing, and
-    // have to be overwritten.
-    virtual void SetResourcesContent(const ResourcesContent& content) = 0;
-
-    virtual void GetChildrenMetadata(std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata) = 0;
-
-    virtual int64_t GetLastChangeIndex() = 0;
-
-
-    /**
-     * Primitives introduced in Orthanc 1.5.4
-     **/
-
-    virtual bool LookupResourceAndParent(int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId) = 0;
-  };
-}
--- a/OrthancServer/Database/InstallTrackAttachmentsSize.sql	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-CREATE TABLE GlobalIntegers(
-       key INTEGER PRIMARY KEY,
-       value INTEGER);
-
-INSERT INTO GlobalProperties VALUES (6, 1);  -- GlobalProperty_GetTotalSizeIsFast
-
-INSERT INTO GlobalIntegers SELECT 0, IFNULL(SUM(compressedSize), 0) FROM AttachedFiles;
-INSERT INTO GlobalIntegers SELECT 1, IFNULL(SUM(uncompressedSize), 0) FROM AttachedFiles;
-
-CREATE TRIGGER AttachedFileIncrementSize
-AFTER INSERT ON AttachedFiles
-BEGIN
-  UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0;
-  UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1;
-END;
-
-CREATE TRIGGER AttachedFileDecrementSize
-AFTER DELETE ON AttachedFiles
-BEGIN
-  UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0;
-  UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1;
-END;
--- a/OrthancServer/Database/PrepareDatabase.sql	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-CREATE TABLE GlobalProperties(
-       property INTEGER PRIMARY KEY,
-       value TEXT
-       );
-
-CREATE TABLE Resources(
-       internalId INTEGER PRIMARY KEY AUTOINCREMENT,
-       resourceType INTEGER,
-       publicId TEXT,
-       parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
-       );
-
-CREATE TABLE MainDicomTags(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       tagGroup INTEGER,
-       tagElement INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, tagGroup, tagElement)
-       );
-
--- The following table was added in Orthanc 0.8.5 (database v5)
-CREATE TABLE DicomIdentifiers(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       tagGroup INTEGER,
-       tagElement INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, tagGroup, tagElement)
-       );
-
-CREATE TABLE Metadata(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       type INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, type)
-       );
-
-CREATE TABLE AttachedFiles(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       fileType INTEGER,
-       uuid TEXT,
-       compressedSize INTEGER,
-       uncompressedSize INTEGER,
-       compressionType INTEGER,
-       uncompressedMD5 TEXT,  -- New in Orthanc 0.7.3 (database v4)
-       compressedMD5 TEXT,    -- New in Orthanc 0.7.3 (database v4)
-       PRIMARY KEY(id, fileType)
-       );              
-
-CREATE TABLE Changes(
-       seq INTEGER PRIMARY KEY AUTOINCREMENT,
-       changeType INTEGER,
-       internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       resourceType INTEGER,
-       date TEXT
-       );
-
-CREATE TABLE ExportedResources(
-       seq INTEGER PRIMARY KEY AUTOINCREMENT,
-       resourceType INTEGER,
-       publicId TEXT,
-       remoteModality TEXT,
-       patientId TEXT,
-       studyInstanceUid TEXT,
-       seriesInstanceUid TEXT,
-       sopInstanceUid TEXT,
-       date TEXT
-       ); 
-
-CREATE TABLE PatientRecyclingOrder(
-       seq INTEGER PRIMARY KEY AUTOINCREMENT,
-       patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
-       );
-
-CREATE INDEX ChildrenIndex ON Resources(parentId);
-CREATE INDEX PublicIndex ON Resources(publicId);
-CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
-CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
-
-CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
--- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up
--- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
--- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
-
--- The 3 following indexes were added in Orthanc 0.8.5 (database v5)
-CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
-CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
-CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
-
-CREATE INDEX ChangesIndex ON Changes(internalId);
-
-CREATE TRIGGER AttachedFileDeleted
-AFTER DELETE ON AttachedFiles
-BEGIN
-  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
-                           old.compressionType, old.compressedSize,
-                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
-END;
-
-CREATE TRIGGER ResourceDeleted
-AFTER DELETE ON Resources
-BEGIN
-  SELECT SignalResourceDeleted(old.publicId, old.resourceType);  -- New in Orthanc 0.8.5 (db v5)
-  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
-    FROM Resources AS parent WHERE internalId = old.parentId;
-END;
-
--- Delete a parent resource when its unique child is deleted 
-CREATE TRIGGER ResourceDeletedParentCleaning
-AFTER DELETE ON Resources
-FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
-BEGIN
-  DELETE FROM Resources WHERE internalId = old.parentId;
-END;
-
-CREATE TRIGGER PatientAdded
-AFTER INSERT ON Resources
-FOR EACH ROW WHEN new.resourceType = 1  -- "1" corresponds to "ResourceType_Patient" in C++
-BEGIN
-  INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
-END;
-
-
--- Set the version of the database schema
--- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-INSERT INTO GlobalProperties VALUES (1, "6");
--- a/OrthancServer/Database/ResourcesContent.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ResourcesContent.h"
-
-#include "Compatibility/ISetResourcesContent.h"
-
-#include <cassert>
-
-
-namespace Orthanc
-{
-  void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const
-  {
-    for (std::list<TagValue>::const_iterator
-           it = tags_.begin(); it != tags_.end(); ++it)
-    {
-      if (it->isIdentifier_)
-      {
-        compatibility.SetIdentifierTag(it->resourceId_, it->tag_,  it->value_);
-      }
-      else
-      {
-        compatibility.SetMainDicomTag(it->resourceId_, it->tag_,  it->value_);
-      }
-    }
-
-    for (std::list<Metadata>::const_iterator
-           it = metadata_.begin(); it != metadata_.end(); ++it)
-    {
-      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_);
-    }
-  }
-}
--- a/OrthancServer/Database/ResourcesContent.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,134 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../ServerEnumerations.h"
-
-#include <boost/noncopyable.hpp>
-#include <list>
-
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    class ISetResourcesContent;
-  }
-  
-  class ResourcesContent : public boost::noncopyable
-  {
-  public:
-    struct TagValue
-    {
-      int64_t      resourceId_;
-      bool         isIdentifier_;
-      DicomTag     tag_;
-      std::string  value_;
-
-      TagValue(int64_t resourceId,
-               bool isIdentifier,
-               const DicomTag& tag,
-               const std::string& value) :
-        resourceId_(resourceId),
-        isIdentifier_(isIdentifier),
-        tag_(tag),
-        value_(value)
-      {
-      }
-    };
-
-    struct Metadata
-    {
-      int64_t       resourceId_;
-      MetadataType  metadata_;
-      std::string   value_;
-
-      Metadata(int64_t  resourceId,
-               MetadataType metadata,
-               const std::string& value) :
-        resourceId_(resourceId),
-        metadata_(metadata),
-        value_(value)
-      {
-      }
-    };
-
-    typedef std::list<TagValue>  ListTags;
-    typedef std::list<Metadata>  ListMetadata;
-    
-  private:
-    ListTags       tags_;
-    ListMetadata   metadata_;
-
-  public:
-    void AddMainDicomTag(int64_t resourceId,
-                         const DicomTag& tag,
-                         const std::string& value)
-    {
-      tags_.push_back(TagValue(resourceId, false, tag, value));
-    }
-
-    void AddIdentifierTag(int64_t resourceId,
-                          const DicomTag& tag,
-                          const std::string& value)
-    {
-      tags_.push_back(TagValue(resourceId, true, tag, value));
-    }
-
-    void AddMetadata(int64_t resourceId,
-                     MetadataType metadata,
-                     const std::string& value)
-    {
-      metadata_.push_back(Metadata(resourceId, metadata, value));
-    }
-
-    void AddResource(int64_t resource,
-                     ResourceType level,
-                     const DicomMap& dicomSummary);
-
-    // WARNING: The database should be locked with a transaction!
-    void Store(Compatibility::ISetResourcesContent& target) const;
-
-    const ListTags& GetListTags() const
-    {
-      return tags_;
-    }
-
-    const ListMetadata& GetListMetadata() const
-    {
-      return metadata_;
-    }
-  };
-}
--- a/OrthancServer/Database/SQLiteDatabaseWrapper.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1353 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "SQLiteDatabaseWrapper.h"
-
-#include "../../Core/DicomFormat/DicomArray.h"
-#include "../../Core/Logging.h"
-#include "../../Core/SQLite/Transaction.h"
-#include "../Search/ISqlLookupFormatter.h"
-#include "../ServerToolbox.h"
-
-#include <OrthancServerResources.h>
-
-#include <stdio.h>
-#include <boost/lexical_cast.hpp>
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    class SignalFileDeleted : public SQLite::IScalarFunction
-    {
-    private:
-      IDatabaseListener& listener_;
-
-    public:
-      SignalFileDeleted(IDatabaseListener& listener) :
-        listener_(listener)
-      {
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalFileDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 7;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        std::string uncompressedMD5, compressedMD5;
-
-        if (!context.IsNullValue(5))
-        {
-          uncompressedMD5 = context.GetStringValue(5);
-        }
-
-        if (!context.IsNullValue(6))
-        {
-          compressedMD5 = context.GetStringValue(6);
-        }
-
-        FileInfo info(context.GetStringValue(0),
-                      static_cast<FileContentType>(context.GetIntValue(1)),
-                      static_cast<uint64_t>(context.GetInt64Value(2)),
-                      uncompressedMD5,
-                      static_cast<CompressionType>(context.GetIntValue(3)),
-                      static_cast<uint64_t>(context.GetInt64Value(4)),
-                      compressedMD5);
-        
-        listener_.SignalFileDeleted(info);
-      }
-    };
-
-    class SignalResourceDeleted : public SQLite::IScalarFunction
-    {
-    private:
-      IDatabaseListener& listener_;
-
-    public:
-      SignalResourceDeleted(IDatabaseListener& listener) :
-        listener_(listener)
-      {
-      }
-
-      virtual const char* GetName() const
-      {
-        return "SignalResourceDeleted";
-      }
-
-      virtual unsigned int GetCardinality() const
-      {
-        return 2;
-      }
-
-      virtual void Compute(SQLite::FunctionContext& context)
-      {
-        ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
-        ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
-        listener_.SignalChange(change);
-      }
-    };
-
-    class SignalRemainingAncestor : public SQLite::IScalarFunction
-    {
-    private:
-      bool hasRemainingAncestor_;
-      std::string remainingPublicId_;
-      ResourceType 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(SQLite::FunctionContext& context)
-      {
-        VLOG(1) << "There exists a remaining ancestor with public ID \""
-                << context.GetStringValue(0)
-                << "\" of type "
-                << context.GetIntValue(1);
-
-        if (!hasRemainingAncestor_ ||
-            remainingType_ >= context.GetIntValue(1))
-        {
-          hasRemainingAncestor_ = true;
-          remainingPublicId_ = context.GetStringValue(0);
-          remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
-        }
-      }
-
-      bool HasRemainingAncestor() const
-      {
-        return hasRemainingAncestor_;
-      }
-
-      const std::string& GetRemainingAncestorId() const
-      {
-        assert(hasRemainingAncestor_);
-        return remainingPublicId_;
-      }
-
-      ResourceType GetRemainingAncestorType() const
-      {
-        assert(hasRemainingAncestor_);
-        return remainingType_;
-      }
-    };
-  }
-
-
-  void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
-                                          int64_t id)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
-    s.BindInt64(0, id);
-
-    childrenPublicIds.clear();
-    while (s.Step())
-    {
-      childrenPublicIds.push_back(s.ColumnString(0));
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::DeleteResource(int64_t id)
-  {
-    signalRemainingAncestor_->Reset();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
-    s.BindInt64(0, id);
-    s.Run();
-
-    if (signalRemainingAncestor_->HasRemainingAncestor() &&
-        listener_ != NULL)
-    {
-      listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
-                                         signalRemainingAncestor_->GetRemainingAncestorId());
-    }
-  }
-
-
-  bool SQLiteDatabaseWrapper::GetParentPublicId(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.internalId = b.parentId AND b.internalId = ?");     
-    s.BindInt64(0, id);
-
-    if (s.Step())
-    {
-      target = s.ColumnString(0);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table)
-  {
-    /**
-     * "Generally one cannot use SQL parameters/placeholders for
-     * database identifiers (tables, columns, views, schemas, etc.) or
-     * database functions (e.g., CURRENT_DATE), but instead only for
-     * binding literal values." => To avoid any SQL injection, we
-     * check that the "table" parameter has only alphabetic
-     * characters.
-     * https://stackoverflow.com/a/1274764/881731
-     **/
-    for (size_t i = 0; i < table.size(); i++)
-    {
-      if (!isalpha(table[i]))
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-    // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached
-    SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table);
-
-    if (s.Step())
-    {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-    
-  SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : 
-    listener_(NULL), 
-    signalRemainingAncestor_(NULL),
-    version_(0)
-  {
-    db_.Open(path);
-  }
-
-
-  SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : 
-    listener_(NULL), 
-    signalRemainingAncestor_(NULL),
-    version_(0)
-  {
-    db_.OpenInMemory();
-  }
-
-
-  int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property,
-                                                      int defaultValue)
-  {
-    std::string tmp;
-
-    if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel))
-    {
-      return defaultValue;
-    }
-    else
-    {
-      try
-      {
-        return boost::lexical_cast<int>(tmp);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange,
-                               "Global property " + boost::lexical_cast<std::string>(property) +
-                               " should be an integer, but found: " + tmp);
-      }
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::Open()
-  {
-    db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
-
-    // Performance tuning of SQLite with PRAGMAs
-    // 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");
-
-    // Make "LIKE" case-sensitive in SQLite 
-    db_.Execute("PRAGMA case_sensitive_like = true;");
-    
-    {
-      SQLite::Transaction t(db_);
-      t.Begin();
-
-      if (!db_.DoesTableExist("GlobalProperties"))
-      {
-        LOG(INFO) << "Creating the database";
-        std::string query;
-        ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE);
-        db_.Execute(query);
-      }
-
-      // Check the version of the database
-      std::string tmp;
-      if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion))
-      {
-        tmp = "Unknown";
-      }
-
-      bool ok = false;
-      try
-      {
-        LOG(INFO) << "Version of the Orthanc database: " << tmp;
-        version_ = boost::lexical_cast<unsigned int>(tmp);
-        ok = true;
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-      }
-
-      if (!ok)
-      {
-        throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
-                               "Incompatible version of the Orthanc database: " + tmp);
-      }
-
-      // New in Orthanc 1.5.1
-      if (version_ == 6)
-      {
-        if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) ||
-            tmp != "1")
-        {
-          LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
-          std::string query;
-          ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
-          db_.Execute(query);
-        }
-      }
-
-      t.Commit();
-    }
-
-    signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
-    db_.Register(signalRemainingAncestor_);
-  }
-
-
-  static void ExecuteUpgradeScript(SQLite::Connection& db,
-                                   ServerResources::FileResourceId script)
-  {
-    std::string upgrade;
-    ServerResources::GetFileResource(upgrade, script);
-    db.BeginTransaction();
-    db.Execute(upgrade);
-    db.CommitTransaction();    
-  }
-
-
-  void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
-                                      IStorageArea& storageArea)
-  {
-    if (targetVersion != 6)
-    {
-      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
-    }
-
-    // This version of Orthanc is only compatible with versions 3, 4,
-    // 5 and 6 of the DB schema
-    if (version_ != 3 &&
-        version_ != 4 &&
-        version_ != 5 &&
-        version_ != 6)
-    {
-      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
-    }
-
-    if (version_ == 3)
-    {
-      LOG(WARNING) << "Upgrading database version from 3 to 4";
-      ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_3_TO_4);
-      version_ = 4;
-    }
-
-    if (version_ == 4)
-    {
-      LOG(WARNING) << "Upgrading database version from 4 to 5";
-      ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_4_TO_5);
-      version_ = 5;
-    }
-
-    if (version_ == 5)
-    {
-      LOG(WARNING) << "Upgrading database version from 5 to 6";
-      // No change in the DB schema, the step from version 5 to 6 only
-      // consists in reconstructing the main DICOM tags information
-      // (as more tags got included).
-      db_.BeginTransaction();
-      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();
-      version_ = 6;
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener)
-  {
-    listener_ = &listener;
-    db_.Register(new Internals::SignalFileDeleted(listener));
-    db_.Register(new Internals::SignalResourceDeleted(listener));
-  }
-
-
-  void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName)
-  {
-    db_.Execute("DELETE FROM " + tableName);    
-  }
-
-
-  bool SQLiteDatabaseWrapper::LookupParent(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())
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (s.ColumnIsNull(0))
-    {
-      return false;
-    }
-    else
-    {
-      parentId = s.ColumnInt(0);
-      return true;
-    }
-  }
-
-
-  ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT resourceType FROM Resources WHERE internalId=?");
-    s.BindInt64(0, resourceId);
-    
-    if (s.Step())
-    {
-      return static_cast<ResourceType>(s.ColumnInt(0));
-    }
-    else
-    { 
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId)
-  {
-    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
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                                         bool& done /*out*/,
-                                         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);
-    GetChangesInternal(target, done, s, maxResults);
-  }
-
-
-  void SQLiteDatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-  {
-    bool done;  // Ignored
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
-    GetChangesInternal(target, done, s, 1);
-  }
-
-
-  class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction
-  {
-  private:
-    SQLiteDatabaseWrapper&                that_;
-    std::unique_ptr<SQLite::Transaction>  transaction_;
-    int64_t                               initialDiskSize_;
-
-  public:
-    Transaction(SQLiteDatabaseWrapper& that) :
-      that_(that),
-      transaction_(new SQLite::Transaction(that_.db_))
-    {
-#if defined(NDEBUG)
-      // Release mode
-      initialDiskSize_ = 0;
-#else
-      // Debug mode
-      initialDiskSize_ = static_cast<int64_t>(that_.GetTotalCompressedSize());
-#endif
-    }
-
-    virtual void Begin()
-    {
-      transaction_->Begin();
-    }
-
-    virtual void Rollback() 
-    {
-      transaction_->Rollback();
-    }
-
-    virtual void Commit(int64_t fileSizeDelta /* only used in debug */)
-    {
-      transaction_->Commit();
-
-      assert(initialDiskSize_ + fileSizeDelta >= 0 &&
-             initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(that_.GetTotalCompressedSize()));
-    }
-  };
-
-
-  IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction()
-  {
-    return new Transaction(*this);
-  }
-
-
-  void SQLiteDatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                             int64_t id)
-  {
-    target.clear();
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
-    s.BindInt64(0, id);
-
-    while (s.Step())
-    {
-      MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
-      target[key] = s.ColumnString(1);
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::GetTotalCompressedSize()
-  {
-    // Old SQL query that was used in Orthanc <= 1.5.0:
-    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-    
-  uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize()
-  {
-    // Old SQL query that was used in Orthanc <= 1.5.0:
-    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
-
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
-    s.Run();
-    return static_cast<uint64_t>(s.ColumnInt64(0));
-  }
-
-
-  uint64_t SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
-    s.BindInt64(0, internalId);
-    return !s.Step();
-  }
-
-
-  void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId)
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT * FROM Resources WHERE internalId=?");
-    s.BindInt64(0, internalId);
-    return s.Step();
-  }
-
-
-  bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold)
-  {
-    return GetTotalCompressedSize() > threshold;
-  }
-
-
-
-  class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
-  {
-  private:
-    std::list<std::string>  values_;
-
-  public:
-    virtual std::string GenerateParameter(const std::string& value)
-    {
-      values_.push_back(value);
-      return "?";
-    }
-    
-    virtual std::string FormatResourceType(ResourceType level)
-    {
-      return boost::lexical_cast<std::string>(level);
-    }
-
-    virtual std::string FormatWildcardEscape()
-    {
-      return "ESCAPE '\\'";
-    }
-
-    void Bind(SQLite::Statement& statement) const
-    {
-      size_t pos = 0;
-      
-      for (std::list<std::string>::const_iterator
-             it = values_.begin(); it != values_.end(); ++it, pos++)
-      {
-        statement.BindString(pos, *it);
-      }
-    }
-  };
-
-  
-  static void AnswerLookup(std::list<std::string>& resourcesId,
-                           std::list<std::string>& instancesId,
-                           SQLite::Connection& db,
-                           ResourceType level)
-  {
-    resourcesId.clear();
-    instancesId.clear();
-    
-    std::unique_ptr<SQLite::Statement> statement;
-    
-    switch (level)
-    {
-      case ResourceType_Patient:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE,
-            "SELECT patients.publicId, instances.publicID FROM Lookup AS patients "
-            "INNER JOIN Resources studies ON patients.internalId=studies.parentId "
-            "INNER JOIN Resources series ON studies.internalId=series.parentId "
-            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
-            "GROUP BY patients.publicId"));
-      
-        break;
-      }
-
-      case ResourceType_Study:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE,
-            "SELECT studies.publicId, instances.publicID FROM Lookup AS studies "
-            "INNER JOIN Resources series ON studies.internalId=series.parentId "
-            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
-            "GROUP BY studies.publicId"));
-      
-        break;
-      }
-
-      case ResourceType_Series:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE,
-            "SELECT series.publicId, instances.publicID FROM Lookup AS series "
-            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
-            "GROUP BY series.publicId"));
-      
-        break;
-      }
-
-      case ResourceType_Instance:
-      {
-        statement.reset(
-          new SQLite::Statement(
-            db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup"));
-        
-        break;
-      }
-      
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    assert(statement.get() != NULL);
-      
-    while (statement->Step())
-    {
-      resourcesId.push_back(statement->ColumnString(0));
-      instancesId.push_back(statement->ColumnString(1));
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::ApplyLookupResources(std::list<std::string>& resourcesId,
-                                                   std::list<std::string>* instancesId,
-                                                   const std::vector<DatabaseConstraint>& lookup,
-                                                   ResourceType queryLevel,
-                                                   size_t limit)
-  {
-    LookupFormatter formatter;
-
-    std::string sql;
-    LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
-
-    sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
-    
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
-      s.Run();
-    }
-
-    {
-      SQLite::Statement statement(db_, sql);
-      formatter.Bind(statement);
-      statement.Run();
-    }
-
-    if (instancesId != NULL)
-    {
-      AnswerLookup(resourcesId, *instancesId, db_, queryLevel);
-    }
-    else
-    {
-      resourcesId.clear();
-    
-      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup");
-        
-      while (s.Step())
-      {
-        resourcesId.push_back(s.ColumnString(0));
-      }
-    }
-  }
-
-
-  int64_t SQLiteDatabaseWrapper::GetLastChangeIndex()
-  {
-    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
-                        "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
-
-    if (s.Step())
-    {
-      int64_t c = s.ColumnInt(0);
-      assert(!s.Step());
-      return c;
-    }
-    else
-    {
-      // No change has been recorded so far in the database
-      return 0;
-    }
-  }
-
-
-  void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient)
-  {
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                          "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
-      s.BindInt64(0, patient);
-      s.Run();
-
-      assert(db_.GetLastChangeCount() == 0 ||
-             db_.GetLastChangeCount() == 1);
-      
-      if (db_.GetLastChangeCount() == 0)
-      {
-        // The patient was protected, there was nothing to delete from the recycling order
-        return;
-      }
-    }
-
-    {
-      SQLite::Statement s(db_, SQLITE_FROM_HERE,
-                          "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
-      s.BindInt64(0, patient);
-      s.Run();
-    }
-  }
-}
--- a/OrthancServer/Database/SQLiteDatabaseWrapper.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,372 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IDatabaseWrapper.h"
-
-#include "../../Core/SQLite/Connection.h"
-#include "Compatibility/ICreateInstance.h"
-#include "Compatibility/IGetChildrenMetadata.h"
-#include "Compatibility/ILookupResourceAndParent.h"
-#include "Compatibility/ISetResourcesContent.h"
-
-namespace Orthanc
-{
-  namespace Internals
-  {
-    class SignalRemainingAncestor;
-  }
-
-  /**
-   * This class manages an instance of the Orthanc SQLite database. It
-   * translates low-level requests into SQL statements. Mutual
-   * exclusion MUST be implemented at a higher level.
-   **/
-  class SQLiteDatabaseWrapper :
-    public IDatabaseWrapper,
-    public Compatibility::ICreateInstance,
-    public Compatibility::IGetChildrenMetadata,
-    public Compatibility::ILookupResourceAndParent,
-    public Compatibility::ISetResourcesContent
-  {
-  private:
-    class Transaction;
-    class LookupFormatter;
-
-    IDatabaseListener* listener_;
-    SQLite::Connection db_;
-    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);
-
-    // Unused => could be removed
-    int GetGlobalIntegerProperty(GlobalProperty property,
-                                 int defaultValue);
-
-  public:
-    SQLiteDatabaseWrapper(const std::string& path);
-
-    SQLiteDatabaseWrapper();
-
-    virtual void Open()
-      ORTHANC_OVERRIDE;
-
-    virtual void Close()
-      ORTHANC_OVERRIDE
-    {
-      db_.Close();
-    }
-
-    virtual void SetListener(IDatabaseListener& listener)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId)
-      ORTHANC_OVERRIDE;
-
-    virtual std::string GetPublicId(int64_t resourceId)
-      ORTHANC_OVERRIDE;
-
-    virtual ResourceType GetResourceType(int64_t resourceId)
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteResource(int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-      ORTHANC_OVERRIDE;
-
-    virtual IDatabaseWrapper::ITransaction* StartTransaction()
-      ORTHANC_OVERRIDE;
-
-    virtual void FlushToDisk()
-      ORTHANC_OVERRIDE
-    {
-      db_.FlushToDisk();
-    }
-
-    virtual bool HasFlushToDisk() const
-      ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual void ClearChanges()
-      ORTHANC_OVERRIDE
-    {
-      ClearTable("Changes");
-    }
-
-    virtual void ClearExportedResources()
-      ORTHANC_OVERRIDE
-    {
-      ClearTable("ExportedResources");
-    }
-
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual unsigned int GetDatabaseVersion()
-      ORTHANC_OVERRIDE
-    {
-      return version_;
-    }
-
-    virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea)
-      ORTHANC_OVERRIDE;
-
-
-    /**
-     * The methods declared below are for unit testing only!
-     **/
-
-    const char* GetErrorMessage() const
-    {
-      return db_.GetErrorMessage();
-    }
-
-    void GetChildren(std::list<std::string>& childrenPublicIds,
-                     int64_t id);
-
-    int64_t GetTableRecordCount(const std::string& table);
-    
-    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)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property)
-      ORTHANC_OVERRIDE;
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId)
-      ORTHANC_OVERRIDE;
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type)
-      ORTHANC_OVERRIDE;
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment)
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment)
-      ORTHANC_OVERRIDE;
-
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType)
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearMainDicomTags(int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  const DicomTag& tag,
-                                  const std::string& value)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id)
-      ORTHANC_OVERRIDE;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change)
-      ORTHANC_OVERRIDE;
-
-    virtual void LogExportedResource(const ExportedResource& resource)
-      ORTHANC_OVERRIDE;
-    
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetTotalCompressedSize()
-      ORTHANC_OVERRIDE;
-    
-    virtual uint64_t GetTotalUncompressedSize()
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit)
-      ORTHANC_OVERRIDE;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId)
-      ORTHANC_OVERRIDE;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid)
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsProtectedPatient(int64_t internalId)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected)
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsExistingResource(int64_t internalId)
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsDiskSizeAbove(uint64_t threshold)
-      ORTHANC_OVERRIDE;
-
-    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                      std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
-                                      ResourceType queryLevel,
-                                      size_t limit)
-      ORTHANC_OVERRIDE;
-
-    virtual bool CreateInstance(CreateInstanceResult& result,
-                                int64_t& instanceId,
-                                const std::string& patient,
-                                const std::string& study,
-                                const std::string& series,
-                                const std::string& instance)
-      ORTHANC_OVERRIDE
-    {
-      return ICreateInstance::Apply
-        (*this, result, instanceId, patient, study, series, instance);
-    }
-
-    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
-      ORTHANC_OVERRIDE
-    {
-      ISetResourcesContent::Apply(*this, content);
-    }
-
-    virtual void GetChildrenMetadata(std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata)
-      ORTHANC_OVERRIDE
-    {
-      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
-    }
-
-    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
-
-    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
-
-    virtual bool LookupResourceAndParent(int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId)
-      ORTHANC_OVERRIDE
-    {
-      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
-    }
-  };
-}
--- a/OrthancServer/Database/Upgrade3To4.sql	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
--- This SQLite script updates the version of the Orthanc database from 3 to 4.
-
--- Add 2 new columns at "AttachedFiles"
-
-ALTER TABLE AttachedFiles ADD COLUMN uncompressedMD5 TEXT;
-ALTER TABLE AttachedFiles ADD COLUMN compressedMD5 TEXT;
-
--- Update the "AttachedFileDeleted" trigger
-
-DROP TRIGGER AttachedFileDeleted;
-
-CREATE TRIGGER AttachedFileDeleted
-AFTER DELETE ON AttachedFiles
-BEGIN
-  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
-                           old.compressionType, old.compressedSize,
-                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
-                           old.uncompressedMD5, old.compressedMD5);
-END;
-
--- Change the database version
--- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-
-UPDATE GlobalProperties SET value="4" WHERE property=1;
--- a/OrthancServer/Database/Upgrade4To5.sql	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
--- This SQLite script updates the version of the Orthanc database from 4 to 5.
-
-
--- Remove 2 indexes to speed up
-
-DROP INDEX MainDicomTagsIndex2;
-DROP INDEX MainDicomTagsIndexValues;
-
-
--- Add a new table to index the DICOM identifiers
-
-CREATE TABLE DicomIdentifiers(
-       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
-       tagGroup INTEGER,
-       tagElement INTEGER,
-       value TEXT,
-       PRIMARY KEY(id, tagGroup, tagElement)
-       );
-
-CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
-CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
-CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
-
-
--- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags
-
-INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags
-       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
-              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
-              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
-              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
-              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
-
-DELETE FROM MainDicomTags
-       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
-              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
-              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
-              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
-              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
-
-
--- Upgrade the "ResourceDeleted" trigger
-
-DROP TRIGGER ResourceDeleted;
-DROP TRIGGER ResourceDeletedParentCleaning;
-
-CREATE TRIGGER ResourceDeleted
-AFTER DELETE ON Resources
-BEGIN
-  SELECT SignalResourceDeleted(old.publicId, old.resourceType);
-  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
-    FROM Resources AS parent WHERE internalId = old.parentId;
-END;
-
-CREATE TRIGGER ResourceDeletedParentCleaning
-AFTER DELETE ON Resources
-FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
-BEGIN
-  DELETE FROM Resources WHERE internalId = old.parentId;
-END;
-
-
--- Change the database version
--- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
-
-UPDATE GlobalProperties SET value="5" WHERE property=1;
--- a/OrthancServer/DicomInstanceOrigin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-  }
-}
--- a/OrthancServer/DicomInstanceOrigin.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,508 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomInstanceToStore.h"
-
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-
-
-namespace Orthanc
-{
-  // Anonymous namespace to avoid clashes between compilation modules
-  namespace
-  {
-    template <typename T>
-    class SmartContainer
-    {
-    private:
-      T* content_;
-      bool toDelete_;
-      bool isReadOnly_;
-
-      void Deallocate()
-      {
-        if (content_ && toDelete_)
-        {
-          delete content_;
-          toDelete_ = false;
-          content_ = NULL;
-        }
-      }
-
-    public:
-      SmartContainer() : content_(NULL), toDelete_(false), isReadOnly_(true)
-      {
-      }
-
-      ~SmartContainer()
-      {
-        Deallocate();
-      }
-
-      void Allocate()
-      {
-        Deallocate();
-        content_ = new T;
-        toDelete_ = true;
-        isReadOnly_ = false;
-      }
-
-      void TakeOwnership(T* content)
-      {
-        if (content == NULL)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-
-        Deallocate();
-        content_ = content;
-        toDelete_ = true;
-        isReadOnly_ = false;
-      }
-
-      void SetReference(T& content)   // Read and write assign, without transfering ownership
-      {
-        Deallocate();
-        content_ = &content;
-        toDelete_ = false;
-        isReadOnly_ = false;
-      }
-
-      void SetConstReference(const T& content)   // Read-only assign, without transfering ownership
-      {
-        Deallocate();
-        content_ = &const_cast<T&>(content);
-        toDelete_ = false;
-        isReadOnly_ = true;
-      }
-
-      bool HasContent() const
-      {
-        return content_ != NULL;
-      }
-
-      T& GetContent()
-      {
-        if (content_ == NULL)
-        {
-          throw OrthancException(ErrorCode_BadSequenceOfCalls);
-        }
-
-        if (isReadOnly_)
-        {
-          throw OrthancException(ErrorCode_ReadOnly);
-        }
-
-        return *content_;
-      }
-
-      const T& GetConstContent() const
-      {
-        if (content_ == NULL)
-        {
-          throw OrthancException(ErrorCode_BadSequenceOfCalls);
-        }
-
-        return *content_;
-      }
-    };
-  }
-
-
-  class DicomInstanceToStore::PImpl
-  {
-  public:
-    DicomInstanceOrigin                  origin_;
-    bool                                 hasBuffer_;
-    std::unique_ptr<std::string>         ownBuffer_;
-    const void*                          bufferData_;
-    size_t                               bufferSize_;
-    SmartContainer<ParsedDicomFile>      parsed_;
-    SmartContainer<DicomMap>             summary_;
-    SmartContainer<Json::Value>          json_;
-    MetadataMap                          metadata_;
-
-    PImpl() :
-      hasBuffer_(false),
-      bufferData_(NULL),
-      bufferSize_(0)
-    {
-    }
-
-  private:
-    std::unique_ptr<DicomInstanceHasher>  hasher_;
-
-    void ParseDicomFile()
-    {
-      if (!parsed_.HasContent())
-      {
-        if (!hasBuffer_)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-      
-        if (ownBuffer_.get() != NULL)
-        {
-          parsed_.TakeOwnership(new ParsedDicomFile(*ownBuffer_));
-        }
-        else
-        {
-          parsed_.TakeOwnership(new ParsedDicomFile(bufferData_, bufferSize_));
-        }
-      }
-    }
-
-    void ComputeMissingInformation()
-    {
-      if (hasBuffer_ &&
-          summary_.HasContent() &&
-          json_.HasContent())
-      {
-        // Fine, everything is available
-        return; 
-      }
-    
-      if (!hasBuffer_)
-      {
-        if (!parsed_.HasContent())
-        {
-          if (!summary_.HasContent())
-          {
-            throw OrthancException(ErrorCode_NotImplemented);
-          }
-          else
-          {
-            parsed_.TakeOwnership(new ParsedDicomFile(summary_.GetConstContent(),
-                                                      GetDefaultDicomEncoding(),
-                                                      false /* be strict */));
-          }                                
-        }
-
-        // Serialize the parsed DICOM file
-        ownBuffer_.reset(new std::string);
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(*ownBuffer_,
-                                                 *parsed_.GetContent().GetDcmtkObject().getDataset()))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Unable to serialize a DICOM file to a memory buffer");
-        }
-
-        hasBuffer_ = true;
-      }
-
-      if (summary_.HasContent() &&
-          json_.HasContent())
-      {
-        return;
-      }
-
-      // At this point, we know that the DICOM file is available as a
-      // memory buffer, but that its summary or its JSON version is
-      // missing
-
-      ParseDicomFile();
-      assert(parsed_.HasContent());
-
-      // At this point, we have parsed the DICOM file
-    
-      if (!summary_.HasContent())
-      {
-        summary_.Allocate();
-        FromDcmtkBridge::ExtractDicomSummary(summary_.GetContent(), 
-                                             *parsed_.GetContent().GetDcmtkObject().getDataset());
-      }
-    
-      if (!json_.HasContent())
-      {
-        json_.Allocate();
-
-        std::set<DicomTag> ignoreTagLength;
-        FromDcmtkBridge::ExtractDicomAsJson(json_.GetContent(), 
-                                            *parsed_.GetContent().GetDcmtkObject().getDataset(),
-                                            ignoreTagLength);
-      }
-    }
-
-
-  public:
-    void SetBuffer(const void* data,
-                   size_t size)
-    {
-      ownBuffer_.reset(NULL);
-      bufferData_ = data;
-      bufferSize_ = size;
-      hasBuffer_ = true;
-    }
-    
-    const void* GetBufferData()
-    {
-      ComputeMissingInformation();
-
-      if (!hasBuffer_)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (ownBuffer_.get() != NULL)
-      {
-        if (ownBuffer_->empty())
-        {
-          return NULL;
-        }
-        else
-        {
-          return ownBuffer_->c_str();
-        }
-      }
-      else
-      {
-        return bufferData_;
-      }
-    }
-
-
-    size_t GetBufferSize()
-    {
-      ComputeMissingInformation();
-    
-      if (!hasBuffer_)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (ownBuffer_.get() != NULL)
-      {
-        return ownBuffer_->size();
-      }
-      else
-      {
-        return bufferSize_;
-      }
-    }
-
-
-    const DicomMap& GetSummary()
-    {
-      ComputeMissingInformation();
-    
-      if (!summary_.HasContent())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      return summary_.GetConstContent();
-    }
-
-    
-    const Json::Value& GetJson()
-    {
-      ComputeMissingInformation();
-    
-      if (!json_.HasContent())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      return json_.GetConstContent();
-    }
-
-
-    DicomInstanceHasher& GetHasher()
-    {
-      if (hasher_.get() == NULL)
-      {
-        hasher_.reset(new DicomInstanceHasher(GetSummary()));
-      }
-
-      if (hasher_.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      return *hasher_;
-    }
-
-    
-    bool LookupTransferSyntax(std::string& result)
-    {
-      ComputeMissingInformation();
-
-      DicomMap header;
-      if (DicomMap::ParseDicomMetaInformation(header, GetBufferData(), GetBufferSize()))
-      {
-        const DicomValue* value = header.TestAndGetValue(DICOM_TAG_TRANSFER_SYNTAX_UID);
-        if (value != NULL &&
-            !value->IsBinary() &&
-            !value->IsNull())
-        {
-          result = Toolbox::StripSpaces(value->GetContent());
-          return true;
-        }
-      }
-
-      return false;
-    }
-
-
-    ParsedDicomFile& GetParsedDicomFile()
-    {
-      ComputeMissingInformation();
-      ParseDicomFile();
-      
-      if (parsed_.HasContent())
-      {
-        return parsed_.GetContent();
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  };
-
-
-  DicomInstanceToStore::DicomInstanceToStore() :
-    pimpl_(new PImpl)
-  {
-  }
-
-
-  void DicomInstanceToStore::SetOrigin(const DicomInstanceOrigin& origin)
-  {
-    pimpl_->origin_ = origin;
-  }
-
-    
-  const DicomInstanceOrigin& DicomInstanceToStore::GetOrigin() const
-  {
-    return pimpl_->origin_;
-  }
-
-    
-  void DicomInstanceToStore::SetBuffer(const void* dicom,
-                                       size_t size)
-  {
-    pimpl_->SetBuffer(dicom, size);
-  }
-
-
-  void DicomInstanceToStore::SetParsedDicomFile(ParsedDicomFile& parsed)
-  {
-    pimpl_->parsed_.SetReference(parsed);
-  }
-
-
-  void DicomInstanceToStore::SetSummary(const DicomMap& summary)
-  {
-    pimpl_->summary_.SetConstReference(summary);
-  }
-
-
-  void DicomInstanceToStore::SetJson(const Json::Value& json)
-  {
-    pimpl_->json_.SetConstReference(json);
-  }
-
-
-  const DicomInstanceToStore::MetadataMap& DicomInstanceToStore::GetMetadata() const
-  {
-    return pimpl_->metadata_;
-  }
-
-
-  DicomInstanceToStore::MetadataMap& DicomInstanceToStore::GetMetadata()
-  {
-    return pimpl_->metadata_;
-  }
-
-
-  void DicomInstanceToStore::AddMetadata(ResourceType level,
-                                         MetadataType metadata,
-                                         const std::string& value)
-  {
-    pimpl_->metadata_[std::make_pair(level, metadata)] = value;
-  }
-
-
-  const void* DicomInstanceToStore::GetBufferData() const
-  {
-    return const_cast<PImpl&>(*pimpl_).GetBufferData();
-  }
-
-
-  size_t DicomInstanceToStore::GetBufferSize() const
-  {
-    return const_cast<PImpl&>(*pimpl_).GetBufferSize();
-  }
-
-
-  const DicomMap& DicomInstanceToStore::GetSummary()
-  {
-    return pimpl_->GetSummary();
-  }
-
-    
-  const Json::Value& DicomInstanceToStore::GetJson() const
-  {
-    return const_cast<PImpl&>(*pimpl_).GetJson();
-  }
-
-
-  bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) const
-  {
-    return const_cast<PImpl&>(*pimpl_).LookupTransferSyntax(result);
-  }
-
-
-  DicomInstanceHasher& DicomInstanceToStore::GetHasher()
-  {
-    return pimpl_->GetHasher();
-  }
-
-  bool DicomInstanceToStore::HasPixelData() const
-  {
-    return const_cast<PImpl&>(*pimpl_).GetParsedDicomFile().HasTag(DICOM_TAG_PIXEL_DATA);
-  }
-
-  ParsedDicomFile& DicomInstanceToStore::GetParsedDicomFile() const
-  {
-    return const_cast<PImpl&>(*pimpl_).GetParsedDicomFile();
-  }
-}
--- a/OrthancServer/DicomInstanceToStore.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomFormat/DicomMap.h"
-#include "DicomInstanceOrigin.h"
-#include "ServerEnumerations.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class ParsedDicomFile;
-
-  class DicomInstanceToStore : public boost::noncopyable
-  {
-  public:
-    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
-
-  private:
-    class PImpl;
-    boost::shared_ptr<PImpl>  pimpl_;
-
-  public:
-    DicomInstanceToStore();
-
-    void SetOrigin(const DicomInstanceOrigin& origin);
-    
-    const DicomInstanceOrigin& GetOrigin() const;
-
-    // WARNING: The buffer is not copied, it must not be removed as
-    // long as the "DicomInstanceToStore" object is alive
-    void SetBuffer(const void* dicom,
-                   size_t size);
-
-    void SetParsedDicomFile(ParsedDicomFile& parsed);
-
-    void SetSummary(const DicomMap& summary);
-
-    void SetJson(const Json::Value& json);
-
-    const MetadataMap& GetMetadata() const;
-
-    MetadataMap& GetMetadata();
-
-    void AddMetadata(ResourceType level,
-                     MetadataType metadata,
-                     const std::string& value);
-
-    const void* GetBufferData() const;
-
-    size_t GetBufferSize() const;
-
-    const DicomMap& GetSummary();
-    
-    const Json::Value& GetJson() const;
-
-    bool LookupTransferSyntax(std::string& result) const;
-
-    DicomInstanceHasher& GetHasher();
-
-    bool HasPixelData() const;
-
-    ParsedDicomFile& GetParsedDicomFile() const;
-  };
-}
--- a/OrthancServer/EmbeddedResourceHttpHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "EmbeddedResourceHttpHandler.h"
-
-#include "../Core/HttpServer/HttpOutput.h"
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-#include "../Core/SystemToolbox.h"
-
-
-namespace Orthanc
-{
-  EmbeddedResourceHttpHandler::EmbeddedResourceHttpHandler(
-    const std::string& baseUri,
-    ServerResources::DirectoryResourceId resourceId)
-  {
-    Toolbox::SplitUriComponents(baseUri_, baseUri);
-    resourceId_ = resourceId;
-  }
-
-
-  bool EmbeddedResourceHttpHandler::Handle(
-    HttpOutput& output,
-    RequestOrigin /*origin*/,
-    const char* /*remoteIp*/,
-    const char* /*username*/,
-    HttpMethod method,
-    const UriComponents& uri,
-    const Arguments& headers,
-    const GetArguments& arguments,
-    const void* /*bodyData*/,
-    size_t /*bodySize*/)
-  {
-    if (!Toolbox::IsChildUri(baseUri_, uri))
-    {
-      // This URI is not served by this handler
-      return false;
-    }
-
-    if (method != HttpMethod_Get)
-    {
-      output.SendMethodNotAllowed("GET");
-      return true;
-    }
-
-    std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size());
-    MimeType contentType = SystemToolbox::AutodetectMimeType(resourcePath);
-
-    try
-    {
-      const void* buffer = ServerResources::GetDirectoryResourceBuffer(resourceId_, resourcePath.c_str());
-      size_t size = ServerResources::GetDirectoryResourceSize(resourceId_, resourcePath.c_str());
-
-      output.SetContentType(contentType);
-      output.Answer(buffer, size);
-    }
-    catch (OrthancException&)
-    {
-      LOG(WARNING) << "Unable to find HTTP resource: " << resourcePath;
-      output.SendStatus(HttpStatus_404_NotFound);
-    }
-
-    return true;
-  } 
-}
--- a/OrthancServer/EmbeddedResourceHttpHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/HttpServer/IHttpHandler.h"
-
-#include <OrthancServerResources.h>   // Autogenerated file
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class EmbeddedResourceHttpHandler : public IHttpHandler
-  {
-  private:
-    UriComponents baseUri_;
-    ServerResources::DirectoryResourceId resourceId_;
-
-  public:
-    EmbeddedResourceHttpHandler(
-      const std::string& baseUri,
-      ServerResources::DirectoryResourceId resourceId);
-
-    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                            RequestOrigin origin,
-                                            const char* remoteIp,
-                                            const char* username,
-                                            HttpMethod method,
-                                            const UriComponents& uri,
-                                            const Arguments& headers)
-    {
-      return false;
-    }
-
-    virtual bool Handle(HttpOutput& output,
-                        RequestOrigin origin,
-                        const char* remoteIp,
-                        const char* username,
-                        HttpMethod method,
-                        const UriComponents& uri,
-                        const Arguments& headers,
-                        const GetArguments& arguments,
-                        const void* /*bodyData*/,
-                        size_t /*bodySize*/);
-  };
-}
--- a/OrthancServer/ExportedResource.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ExportedResource.h"
-
-#include "../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  void ExportedResource::Format(Json::Value& item) const
-  {
-    item = Json::objectValue;
-    item["Seq"] = static_cast<int>(seq_);
-    item["ResourceType"] = EnumerationToString(resourceType_);
-    item["ID"] = publicId_;
-    item["Path"] = GetBasePath(resourceType_, publicId_);
-    item["RemoteModality"] = modality_;
-    item["Date"] = date_;
-
-    // WARNING: Do not add "break" below and do not reorder the case items!
-    switch (resourceType_)
-    {
-      case ResourceType_Instance:
-        item["SOPInstanceUID"] = sopInstanceUid_;
-
-      case ResourceType_Series:
-        item["SeriesInstanceUID"] = seriesInstanceUid_;
-
-      case ResourceType_Study:
-        item["StudyInstanceUID"] = studyInstanceUid_;
-
-      case ResourceType_Patient:
-        item["PatientID"] = patientId_;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-}
--- a/OrthancServer/ExportedResource.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Toolbox.h"
-
-#include <string>
-#include <json/value.h>
-
-namespace Orthanc
-{
-  class ExportedResource
-  {
-  private:
-    int64_t      seq_;
-    ResourceType resourceType_;
-    std::string  publicId_;
-    std::string  modality_;
-    std::string  date_;
-    std::string  patientId_;
-    std::string  studyInstanceUid_;
-    std::string  seriesInstanceUid_;
-    std::string  sopInstanceUid_;
-
-  public:
-    ExportedResource(int64_t seq,
-                     ResourceType 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) :
-      seq_(seq),
-      resourceType_(resourceType),
-      publicId_(publicId),
-      modality_(modality),
-      date_(date),
-      patientId_(patientId),
-      studyInstanceUid_(studyInstanceUid),
-      seriesInstanceUid_(seriesInstanceUid),
-      sopInstanceUid_(sopInstanceUid)
-    {
-    }
-
-    int64_t  GetSeq() const
-    {
-      return seq_;
-    }
-
-    ResourceType  GetResourceType() const
-    {
-      return resourceType_;
-    }
-
-    const std::string&  GetPublicId() const
-    {
-      return publicId_;
-    }
-
-    const std::string& GetModality() const
-    {
-      return modality_;
-    }
-
-    const std::string& GetDate() const
-    {
-      return date_;
-    }
-
-    const std::string& GetPatientId() const
-    {
-      return patientId_;
-    }
-
-    const std::string& GetStudyInstanceUid() const
-    {
-      return studyInstanceUid_;
-    }
-
-    const std::string& GetSeriesInstanceUid() const
-    {
-      return seriesInstanceUid_;
-    }
-
-    const std::string& GetSopInstanceUid() const
-    {
-      return sopInstanceUid_;
-    }
-
-    void Format(Json::Value& item) const;
-  };
-}
--- a/OrthancServer/IDicomImageDecoder.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomInstanceToStore.h"
-#include "ServerIndexChange.h"
-
-#include <json/value.h>
-
-namespace Orthanc
-{
-  class IServerListener : public boost::noncopyable
-  {
-  public:
-    virtual ~IServerListener()
-    {
-    }
-
-    virtual void SignalStoredInstance(const std::string& publicId,
-                                      const DicomInstanceToStore& instance,
-                                      const Json::Value& simplifiedTags) = 0;
-    
-    virtual void SignalChange(const ServerIndexChange& change) = 0;
-
-    virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                        const Json::Value& simplified) = 0;
-  };
-}
--- a/OrthancServer/LuaScripting.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,950 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "LuaScripting.h"
-
-#include "OrthancConfiguration.h"
-#include "OrthancRestApi/OrthancRestApi.h"
-#include "ServerContext.h"
-
-#include "../Core/HttpServer/StringHttpOutput.h"
-#include "../Core/Logging.h"
-#include "../Core/Lua/LuaFunctionCall.h"
-
-#include <OrthancServerResources.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;
-      
-      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()))
-      {
-        std::map<MetadataType, std::string> metadata;
-        that.context_.GetIndex().GetAllMetadata(metadata, change_.GetPublicId());
-        
-        Json::Value formattedMetadata = Json::objectValue;
-
-        for (std::map<MetadataType, std::string>::const_iterator 
-               it = metadata.begin(); it != metadata.end(); ++it)
-        {
-          std::string key = EnumerationToString(it->first);
-          formattedMetadata[key] = it->second;
-        }      
-
-        {
-          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(formattedMetadata);
-            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();
-        }
-      }
-    }
-  };
-
-
-  class LuaScripting::DeleteEvent : public LuaScripting::IEvent
-  {
-  private:
-    ResourceType  level_;
-    std::string   publicId_;
-
-  public:
-    DeleteEvent(ResourceType level,
-                const std::string& publicId) :
-      level_(level),
-      publicId_(publicId)
-    {
-    }
-
-    virtual void Apply(LuaScripting& that)
-    {
-      std::string functionName;
-      
-      switch (level_)
-      {
-        case ResourceType_Patient:
-          functionName = "OnDeletedPatient";
-          break;
-
-        case ResourceType_Study:
-          functionName = "OnDeletedStudy";
-          break;
-
-        case ResourceType_Series:
-          functionName = "OnDeletedSeries";
-          break;
-
-        case ResourceType_Instance:
-          functionName = "OnDeletedInstance";
-          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(publicId_);
-          call.Execute();
-        }
-      }
-    }
-  };
-
-
-  class LuaScripting::UpdateEvent : public LuaScripting::IEvent
-  {
-  private:
-    ResourceType  level_;
-    std::string   publicId_;
-
-  public:
-    UpdateEvent(ResourceType level,
-                const std::string& publicId) :
-      level_(level),
-      publicId_(publicId)
-    {
-    }
-
-    virtual void Apply(LuaScripting& that)
-    {
-      std::string functionName;
-      
-      switch (level_)
-      {
-        case ResourceType_Patient:
-          functionName = "OnUpdatedPatient";
-          break;
-
-        case ResourceType_Study:
-          functionName = "OnUpdatedStudy";
-          break;
-
-        case ResourceType_Series:
-          functionName = "OnUpdatedSeries";
-          break;
-
-        case ResourceType_Instance:
-          functionName = "OnUpdatedInstance";
-          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(publicId_);
-          call.Execute();
-        }
-      }
-    }
-  };
-
-
-  ServerContext* LuaScripting::GetServerContext(lua_State *state)
-  {
-    const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext");
-    return const_cast<ServerContext*>(reinterpret_cast<const ServerContext*>(value));
-  }
-
-
-  // Syntax in Lua: RestApiGet(uri, builtin)
-  int LuaScripting::RestApiGet(lua_State *state)
-  {
-    ServerContext* serverContext = GetServerContext(state);
-    if (serverContext == NULL)
-    {
-      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
-      lua_pushnil(state);
-      return 1;
-    }
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    if (nArgs < 1 || nArgs > 3 || 
-        !lua_isstring(state, 1) ||                 // URI
-        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
-    {
-      LOG(ERROR) << "Lua: Bad parameters to RestApiGet()";
-      lua_pushnil(state);
-      return 1;
-    }
-
-    const char* uri = lua_tostring(state, 1);
-    bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);
-
-    std::map<std::string, std::string> headers;
-    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
-
-    try
-    {
-      std::string result;
-      if (HttpToolbox::SimpleGet(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                 RequestOrigin_Lua, uri, headers))
-      {
-        lua_pushlstring(state, result.c_str(), result.size());
-        return 1;
-      }
-    }
-    catch (OrthancException& e)
-    {
-      LOG(ERROR) << "Lua: " << e.What();
-    }
-
-    LOG(ERROR) << "Lua: Error in RestApiGet() for URI: " << uri;
-    lua_pushnil(state);
-    return 1;
-  }
-
-
-  int LuaScripting::RestApiPostOrPut(lua_State *state,
-                                     bool isPost)
-  {
-    ServerContext* serverContext = GetServerContext(state);
-    if (serverContext == NULL)
-    {
-      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
-      lua_pushnil(state);
-      return 1;
-    }
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    if (nArgs < 2 || nArgs > 4 || 
-        !lua_isstring(state, 1) ||                 // URI
-        !lua_isstring(state, 2) ||                 // Body
-        (nArgs >= 3 && !lua_isboolean(state, 3)))  // Restrict to built-in API?
-    {
-      LOG(ERROR) << "Lua: Bad parameters to " << (isPost ? "RestApiPost()" : "RestApiPut()");
-      lua_pushnil(state);
-      return 1;
-    }
-
-    const char* uri = lua_tostring(state, 1);
-    size_t bodySize = 0;
-    const char* bodyData = lua_tolstring(state, 2, &bodySize);
-    bool builtin = (nArgs == 3 ? lua_toboolean(state, 3) != 0 : false);
-
-    std::map<std::string, std::string> headers;
-    LuaContext::GetDictionaryArgument(headers, state, 4, true /* HTTP header key to lower case */);
-        
-    try
-    {
-      std::string result;
-      if (isPost ?
-          HttpToolbox::SimplePost(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                  RequestOrigin_Lua, uri, bodyData, bodySize, headers) :
-          HttpToolbox::SimplePut(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                 RequestOrigin_Lua, uri, bodyData, bodySize, headers))
-      {
-        lua_pushlstring(state, result.c_str(), result.size());
-        return 1;
-      }
-    }
-    catch (OrthancException& e)
-    {
-      LOG(ERROR) << "Lua: " << e.What();
-    }
-
-    LOG(ERROR) << "Lua: Error in " << (isPost ? "RestApiPost()" : "RestApiPut()") << " for URI: " << uri;
-    lua_pushnil(state);
-    return 1;
-  }
-
-
-  // Syntax in Lua: RestApiPost(uri, body, builtin)
-  int LuaScripting::RestApiPost(lua_State *state)
-  {
-    return RestApiPostOrPut(state, true);
-  }
-
-
-  // Syntax in Lua: RestApiPut(uri, body, builtin)
-  int LuaScripting::RestApiPut(lua_State *state)
-  {
-    return RestApiPostOrPut(state, false);
-  }
-
-
-  // Syntax in Lua: RestApiDelete(uri, builtin)
-  int LuaScripting::RestApiDelete(lua_State *state)
-  {
-    ServerContext* serverContext = GetServerContext(state);
-    if (serverContext == NULL)
-    {
-      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
-      lua_pushnil(state);
-      return 1;
-    }
-
-    // Check the types of the arguments
-    int nArgs = lua_gettop(state);
-    if (nArgs < 1 || nArgs > 3 ||
-        !lua_isstring(state, 1) ||                 // URI
-        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
-    {
-      LOG(ERROR) << "Lua: Bad parameters to RestApiDelete()";
-      lua_pushnil(state);
-      return 1;
-    }
-
-    const char* uri = lua_tostring(state, 1);
-    bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);
-
-    std::map<std::string, std::string> headers;
-    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
-    
-    try
-    {
-      if (HttpToolbox::SimpleDelete(serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                    RequestOrigin_Lua, uri, headers))
-      {
-        lua_pushboolean(state, 1);
-        return 1;
-      }
-    }
-    catch (OrthancException& e)
-    {
-      LOG(ERROR) << "Lua: " << e.What();
-    }
-
-    LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri;
-    lua_pushnil(state);
-
-    return 1;
-  }
-
-
-  // Syntax in Lua: GetOrthancConfiguration()
-  int LuaScripting::GetOrthancConfiguration(lua_State *state)
-  {
-    Json::Value configuration;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      configuration = lock.GetJson();
-    }
-
-    LuaContext::GetLuaContext(state).PushJson(configuration);
-
-    return 1;
-  }
-
-
-  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 lock.AddDeleteResourceOperation(context_);
-    }
-
-    if (operation == "store-scu")
-    {
-      std::string localAet;
-      if (parameters.isMember("LocalAet"))
-      {
-        localAet = parameters["LocalAet"].asString();
-      }
-      else
-      {
-        localAet = context_.GetDefaultLocalApplicationEntityTitle();
-      }
-
-      std::string name = parameters["Modality"].asString();
-      RemoteModalityParameters modality;
-
-      {
-        OrthancConfiguration::ReaderLock configLock;
-        modality = configLock.GetConfiguration().GetModalityUsingSymbolicName(name);
-      }
-
-      // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()"
-      return lock.AddStoreScuOperation(context_, localAet, modality);
-    }
-
-    if (operation == "store-peer")
-    {
-      OrthancConfiguration::ReaderLock configLock;
-      std::string name = parameters["Peer"].asString();
-
-      WebServiceParameters peer;
-      if (configLock.GetConfiguration().LookupOrthancPeer(peer, name))
-      {
-        return lock.AddStorePeerOperation(peer);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_UnknownResource,
-                               "No peer with symbolic name: " + name);
-      }
-    }
-
-    if (operation == "modify")
-    {
-      std::unique_ptr<DicomModification> modification(new DicomModification);
-      modification->ParseModifyRequest(parameters);
-
-      return lock.AddModifyInstanceOperation(context_, modification.release());
-    }
-
-    if (operation == "call-system")
-    {
-      LOG(INFO) << "Lua script to call system command on " << parameters["Resource"].asString();
-
-      const Json::Value& argsIn = parameters["Arguments"];
-      if (argsIn.type() != Json::arrayValue)
-      {
-        throw OrthancException(ErrorCode_BadParameterType);
-      }
-
-      std::vector<std::string> args;
-      args.reserve(argsIn.size());
-      for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i)
-      {
-        // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e
-        switch (argsIn[i].type())
-        {
-          case Json::stringValue:
-            args.push_back(argsIn[i].asString());
-            break;
-
-          case Json::intValue:
-            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt()));
-            break;
-
-          case Json::uintValue:
-            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt()));
-            break;
-
-          case Json::realValue:
-            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat()));
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_BadParameterType);
-        }
-      }
-
-      std::string command = parameters["Command"].asString();
-      std::vector<std::string> postArgs;
-
-      return lock.AddSystemCallOperation(command, args, postArgs);
-    }
-
-    throw OrthancException(ErrorCode_ParameterOutOfRange);
-  }
-
-
-  void LuaScripting::InitializeJob()
-  {
-    lua_.Execute("_InitializeJob()");
-  }
-
-
-  void LuaScripting::SubmitJob()
-  {
-    Json::Value operations;
-    LuaFunctionCall call2(lua_, "_AccessJob");
-    call2.ExecuteToJson(operations, false);
-     
-    if (operations.type() != Json::arrayValue)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    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)
-    {
-      if (operations[i].type() != Json::objectValue ||
-          !operations[i].isMember("Operation"))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      const Json::Value& parameters = 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())
-      {
-        lock.AddDicomInstanceInput(index, context_, resource);
-      }
-      else if (!isFirst)
-      {
-        lock.Connect(previous, index);
-      }
-
-      isFirst = false;
-      previous = index;
-    }
-  }
-
-
-  LuaScripting::LuaScripting(ServerContext& context) : 
-    context_(context),
-    state_(State_Setup)
-  {
-    lua_.SetGlobalVariable("_ServerContext", &context);
-    lua_.RegisterFunction("RestApiGet", RestApiGet);
-    lua_.RegisterFunction("RestApiPost", RestApiPost);
-    lua_.RegisterFunction("RestApiPut", RestApiPut);
-    lua_.RegisterFunction("RestApiDelete", RestApiDelete);
-    lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
-
-    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::EventThread(LuaScripting* that)
-  {
-    for (;;)
-    {
-      std::unique_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 (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();
-        }
-      }
-
-      that->jobManager_.GetDicomConnectionManager().CloseIfInactive();
-    }
-  }
-
-
-  void LuaScripting::Start()
-  {
-    boost::recursive_mutex::scoped_lock lock(mutex_);
-
-    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;
-    }
-  }
-
-
-  void LuaScripting::Stop()
-  {
-    {
-      boost::recursive_mutex::scoped_lock lock(mutex_);
-
-      if (state_ != State_Running)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      state_ = State_Done;
-    }
-
-    jobManager_.AwakeTrailingSleep();
-
-    if (eventThread_.joinable())
-    {
-      LOG(INFO) << "Stopping the Lua engine";
-      eventThread_.join();
-      LOG(INFO) << "The Lua engine has stopped";
-    }
-  }
-
-
-  void LuaScripting::SignalStoredInstance(const std::string& publicId,
-                                          const DicomInstanceToStore& instance,
-                                          const Json::Value& simplifiedTags)
-  {
-    Json::Value metadata = Json::objectValue;
-
-    for (ServerIndex::MetadataMap::const_iterator 
-           it = instance.GetMetadata().begin(); 
-         it != instance.GetMetadata().end(); ++it)
-    {
-      if (it->first.first == ResourceType_Instance)
-      {
-        metadata[EnumerationToString(it->first.second)] = it->second;
-      }
-    }
-
-    pendingEvents_.Enqueue(new OnStoredInstanceEvent(publicId, simplifiedTags, metadata, instance));
-  }
-
-
-  void LuaScripting::SignalChange(const ServerIndexChange& change)
-  {
-    if (change.GetChangeType() == ChangeType_StablePatient ||
-        change.GetChangeType() == ChangeType_StableStudy ||
-        change.GetChangeType() == ChangeType_StableSeries)
-    {
-      pendingEvents_.Enqueue(new StableResourceEvent(change));
-    }
-    else if (change.GetChangeType() == ChangeType_Deleted)
-    {
-      pendingEvents_.Enqueue(new DeleteEvent(change.GetResourceType(), change.GetPublicId()));
-    }
-    else if (change.GetChangeType() == ChangeType_UpdatedAttachment ||
-             change.GetChangeType() == ChangeType_UpdatedMetadata)
-    {
-      pendingEvents_.Enqueue(new UpdateEvent(change.GetResourceType(), change.GetPublicId()));
-    }
-  }
-
-
-  bool LuaScripting::FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                            const Json::Value& simplified)
-  {
-    static const char* NAME = "ReceivedInstanceFilter";
-
-    boost::recursive_mutex::scoped_lock lock(mutex_);
-
-    if (lua_.IsExistingFunction(NAME))
-    {
-      LuaFunctionCall call(lua_, NAME);
-      call.PushJson(simplified);
-
-      Json::Value origin;
-      instance.GetOrigin().Format(origin);
-      call.PushJson(origin);
-
-      Json::Value info = Json::objectValue;
-      info["HasPixelData"] = instance.HasPixelData();
-
-      std::string s;
-      if (instance.LookupTransferSyntax(s))
-      {
-        info["TransferSyntaxUID"] = s;
-      }
-
-      call.PushJson(info);
-
-      if (!call.ExecutePredicate())
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  void LuaScripting::Execute(const std::string& command)
-  {
-    pendingEvents_.Enqueue(new ExecuteEvent(command));
-  }
-
-
-  void LuaScripting::LoadGlobalConfiguration()
-  {
-    OrthancConfiguration::ReaderLock configLock;
-
-    {
-      std::string command;
-      Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
-      lua_.Execute(command);
-    }    
-
-    std::list<std::string> luaScripts;
-    configLock.GetConfiguration().GetListOfStringsParameter(luaScripts, "LuaScripts");
-
-    LuaScripting::Lock lock(*this);
-
-    for (std::list<std::string>::const_iterator
-           it = luaScripts.begin(); it != luaScripts.end(); ++it)
-    {
-      std::string path = configLock.GetConfiguration().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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomInstanceToStore.h"
-#include "ServerIndexChange.h"
-#include "ServerJobs/LuaJobManager.h"
-
-#include "../Core/MultiThreading/SharedMessageQueue.h"
-#include "../Core/Lua/LuaContext.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-
-  class LuaScripting : public boost::noncopyable
-  {
-  private:
-    enum State
-    {
-      State_Setup,
-      State_Running,
-      State_Done
-    };
-    
-    class ExecuteEvent;
-    class IEvent;
-    class OnStoredInstanceEvent;
-    class StableResourceEvent;
-    class JobEvent;
-    class DeleteEvent;
-    class UpdateEvent;
-
-    static ServerContext* GetServerContext(lua_State *state);
-
-    static int RestApiPostOrPut(lua_State *state,
-                                bool isPost);
-    static int RestApiGet(lua_State *state);
-    static int RestApiPost(lua_State *state);
-    static int RestApiPut(lua_State *state);
-    static int RestApiDelete(lua_State *state);
-    static int GetOrthancConfiguration(lua_State *state);
-
-    size_t ParseOperation(LuaJobManager::Lock& lock,
-                          const std::string& operation,
-                          const Json::Value& parameters);
-
-    void InitializeJob();
-
-    void SubmitJob();
-
-    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 Lock : public boost::noncopyable
-    {
-    private:
-      LuaScripting&                        that_;
-      boost::recursive_mutex::scoped_lock  lock_;
-
-    public:
-      explicit Lock(LuaScripting& that) : 
-        that_(that), 
-        lock_(that.mutex_)
-      {
-      }
-
-      LuaContext& GetLua()
-      {
-        return that_.lua_;
-      }
-    };
-
-    LuaScripting(ServerContext& context);
-
-    ~LuaScripting();
-
-    void Start();
-
-    void Stop();
-    
-    void SignalStoredInstance(const std::string& publicId,
-                              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);
-
-    TimeoutDicomConnectionManager& GetDicomConnectionManager()
-    {
-      return jobManager_.GetDicomConnectionManager();
-    }
-  };
-}
--- a/OrthancServer/OrthancConfiguration.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,899 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancConfiguration.h"
-
-#include "../Core/HttpServer/HttpServer.h"
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-#include "../Core/SystemToolbox.h"
-#include "../Core/TemporaryFile.h"
-#include "../Core/Toolbox.h"
-
-#include "ServerIndex.h"
-
-
-static const char* const DICOM_MODALITIES = "DicomModalities";
-static const char* const DICOM_MODALITIES_IN_DB = "DicomModalitiesInDatabase";
-static const char* const ORTHANC_PEERS = "OrthancPeers";
-static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase";
-static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory";
-
-namespace Orthanc
-{
-  static void AddFileToConfiguration(Json::Value& target,
-                                     const boost::filesystem::path& path)
-  {
-    std::map<std::string, std::string> env;
-    SystemToolbox::GetEnvironmentVariables(env);
-    
-    LOG(WARNING) << "Reading the configuration from: " << path;
-
-    Json::Value config;
-
-    {
-      std::string content;
-      SystemToolbox::ReadFile(content, path.string());
-
-      content = Toolbox::SubstituteVariables(content, env);
-
-      Json::Value tmp;
-      Json::Reader reader;
-      if (!reader.parse(content, tmp) ||
-          tmp.type() != Json::objectValue)
-      {
-        throw OrthancException(ErrorCode_BadJson,
-                               "The configuration file does not follow the JSON syntax: " + path.string());
-      }
-
-      Toolbox::CopyJsonWithoutComments(config, tmp);
-    }
-
-    if (target.size() == 0)
-    {
-      target = config;
-    }
-    else
-    {
-      // Merge the newly-added file with the previous content of "target"
-      Json::Value::Members members = config.getMemberNames();
-      for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
-      {
-        if (target.isMember(members[i]))
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "The configuration section \"" + members[i] +
-                                 "\" is defined in 2 different configuration files");
-        }
-        else
-        {
-          target[members[i]] = config[members[i]];
-        }
-      }
-    }
-  }
-
-    
-  static void ScanFolderForConfiguration(Json::Value& target,
-                                         const char* folder)
-  {
-    using namespace boost::filesystem;
-
-    LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files";
-
-    directory_iterator end_it; // default construction yields past-the-end
-    for (directory_iterator it(folder);
-         it != end_it;
-         ++it)
-    {
-      if (!is_directory(it->status()))
-      {
-        std::string extension = boost::filesystem::extension(it->path());
-        Toolbox::ToLowerCase(extension);
-
-        if (extension == ".json")
-        {
-          AddFileToConfiguration(target, it->path().string());
-        }
-      }
-    }
-  }
-
-    
-  static void ReadConfiguration(Json::Value& target,
-                                const char* configurationFile)
-  {
-    target = Json::objectValue;
-
-    if (configurationFile != NULL)
-    {
-      if (!boost::filesystem::exists(configurationFile))
-      {
-        throw OrthancException(ErrorCode_InexistentFile,
-                               "Inexistent path to configuration: " +
-                               std::string(configurationFile));
-      }
-      
-      if (boost::filesystem::is_directory(configurationFile))
-      {
-        ScanFolderForConfiguration(target, configurationFile);
-      }
-      else
-      {
-        AddFileToConfiguration(target, configurationFile);
-      }
-    }
-    else
-    {
-#if ORTHANC_STANDALONE == 1
-      // No default path for the standalone configuration
-      LOG(WARNING) << "Using the default Orthanc configuration";
-      return;
-
-#else
-      // 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";
-
-      AddFileToConfiguration(target, p);
-#endif
-    }
-  }
-
-
-  static void CheckAlphanumeric(const std::string& s)
-  {
-    for (size_t j = 0; j < s.size(); j++)
-    {
-      if (!isalnum(s[j]) && 
-          s[j] != '-')
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Only alphanumeric and dash characters are allowed "
-                               "in the names of modalities/peers, but found: " + s);
-      }
-    }
-  }
-
-
-  void OrthancConfiguration::LoadModalitiesFromJson(const Json::Value& source)
-  {
-    modalities_.clear();
-
-    if (source.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Bad format of the \"" + std::string(DICOM_MODALITIES) +
-                             "\" configuration section");
-    }
-
-    Json::Value::Members members = source.getMemberNames();
-
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const std::string& name = members[i];
-      CheckAlphanumeric(name);
-
-      RemoteModalityParameters modality;
-      modality.Unserialize(source[name]);
-      modalities_[name] = modality;
-    }
-  }
-
-
-  void OrthancConfiguration::LoadPeersFromJson(const Json::Value& source)
-  {
-    peers_.clear();
-
-    if (source.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Bad format of the \"" + std::string(ORTHANC_PEERS) +
-                             "\" configuration section");
-    }
-
-    Json::Value::Members members = source.getMemberNames();
-
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const std::string& name = members[i];
-      CheckAlphanumeric(name);
-
-      WebServiceParameters peer;
-      peer.Unserialize(source[name]);
-      peers_[name] = peer;
-    }
-  }
-
-
-  void OrthancConfiguration::LoadModalities()
-  {
-    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
-    {
-      // Modalities are stored in the database
-      if (serverIndex_ == NULL)
-      {
-        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, "{}");
-
-        Json::Reader reader;
-        Json::Value modalities;
-        if (reader.parse(property, modalities))
-        {
-          LoadModalitiesFromJson(modalities);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Cannot unserialize the list of modalities from the Orthanc database");
-        }
-      }
-    }
-    else
-    {
-      // Modalities are stored in the configuration files
-      if (json_.isMember(DICOM_MODALITIES))
-      {
-        LoadModalitiesFromJson(json_[DICOM_MODALITIES]);
-      }
-      else
-      {
-        modalities_.clear();
-      }
-    }
-  }
-
-  void OrthancConfiguration::LoadPeers()
-  {
-    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
-    {
-      // Peers are stored in the database
-      if (serverIndex_ == NULL)
-      {
-        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, "{}");
-
-        Json::Reader reader;
-        Json::Value peers;
-        if (reader.parse(property, peers))
-        {
-          LoadPeersFromJson(peers);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Cannot unserialize the list of peers from the Orthanc database");
-        }
-      }
-    }
-    else
-    {
-      // Peers are stored in the configuration files
-      if (json_.isMember(ORTHANC_PEERS))
-      {
-        LoadPeersFromJson(json_[ORTHANC_PEERS]);
-      }
-      else
-      {
-        peers_.clear();
-      }
-    }
-  }
-
-
-  void OrthancConfiguration::SaveModalitiesToJson(Json::Value& target)
-  {
-    target = Json::objectValue;
-
-    for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it)
-    {
-      Json::Value modality;
-      it->second.Serialize(modality, true /* force advanced format */);
-
-      target[it->first] = modality;
-    }
-  }
-
-    
-  void OrthancConfiguration::SavePeersToJson(Json::Value& target)
-  {
-    target = Json::objectValue;
-
-    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
-    {
-      Json::Value peer;
-      it->second.Serialize(peer, 
-                           false /* use simple format if possible */, 
-                           true  /* include passwords */);
-
-      target[it->first] = peer;
-    }
-  }  
-    
-    
-  void OrthancConfiguration::SaveModalities()
-  {
-    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
-    {
-      // Modalities are stored in the database
-      if (serverIndex_ == NULL)
-      {
-        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        Json::Value modalities;
-        SaveModalitiesToJson(modalities);
-
-        Json::FastWriter writer;
-        std::string s = writer.write(modalities);
-
-        serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, s);
-      }
-    }
-    else
-    {
-      // Modalities are stored in the configuration files
-      if (!modalities_.empty() ||
-          json_.isMember(DICOM_MODALITIES))
-      {
-        SaveModalitiesToJson(json_[DICOM_MODALITIES]);
-      }
-    }
-  }
-
-
-  void OrthancConfiguration::SavePeers()
-  {
-    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
-    {
-      // Peers are stored in the database
-      if (serverIndex_ == NULL)
-      {
-        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        Json::Value peers;
-        SavePeersToJson(peers);
-
-        Json::FastWriter writer;
-        std::string s = writer.write(peers);
-
-        serverIndex_->SetGlobalProperty(GlobalProperty_Peers, s);
-      }
-    }
-    else
-    {
-      // Peers are stored in the configuration files
-      if (!peers_.empty() ||
-          json_.isMember(ORTHANC_PEERS))
-      {
-        SavePeersToJson(json_[ORTHANC_PEERS]);
-      }
-    }
-  }
-
-
-  OrthancConfiguration& OrthancConfiguration::GetInstance()
-  {
-    static OrthancConfiguration configuration;
-    return configuration;
-  }
-
-
-  bool OrthancConfiguration::LookupStringParameter(std::string& target,
-                                                   const std::string& parameter) const
-  {
-    if (json_.isMember(parameter))
-    {
-      if (json_[parameter].type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_BadParameterType,
-                               "The configuration option \"" + parameter + "\" must be a string");
-      }
-      else
-      {
-        target = json_[parameter].asString();
-        return true;
-      }
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter,
-                                                       const std::string& defaultValue) const
-  {
-    std::string value;
-    if (LookupStringParameter(value, parameter))
-    {
-      return value;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-    
-  int OrthancConfiguration::GetIntegerParameter(const std::string& parameter,
-                                                int defaultValue) const
-  {
-    if (json_.isMember(parameter))
-    {
-      if (json_[parameter].type() != Json::intValue)
-      {
-        throw OrthancException(ErrorCode_BadParameterType,
-                               "The configuration option \"" + parameter + "\" must be an integer");
-      }
-      else
-      {
-        return json_[parameter].asInt();
-      }
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-    
-  unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(
-    const std::string& parameter,
-    unsigned int defaultValue) const
-  {
-    int v = GetIntegerParameter(parameter, defaultValue);
-
-    if (v < 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "The configuration option \"" + parameter + "\" must be a positive integer");
-    }
-    else
-    {
-      return static_cast<unsigned int>(v);
-    }
-  }
-
-
-  bool OrthancConfiguration::LookupBooleanParameter(bool& target,
-                                                    const std::string& parameter) const
-  {
-    if (json_.isMember(parameter))
-    {
-      if (json_[parameter].type() != Json::booleanValue)
-      {
-        throw OrthancException(ErrorCode_BadParameterType,
-                               "The configuration option \"" + parameter +
-                               "\" must be a Boolean (true or false)");
-      }
-      else
-      {
-        target = json_[parameter].asBool();
-        return true;
-      }
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter,
-                                                 bool defaultValue) const
-  {
-    bool value;
-    if (LookupBooleanParameter(value, parameter))
-    {
-      return value;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  void OrthancConfiguration::Read(const char* configurationFile)
-  {
-    // Read the content of the configuration
-    configurationFileArg_ = configurationFile;
-    ReadConfiguration(json_, configurationFile);
-
-    // Adapt the paths to the configurations
-    defaultDirectory_ = boost::filesystem::current_path();
-    configurationAbsolutePath_ = "";
-
-    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
-    }
-  }
-
-
-  void OrthancConfiguration::LoadModalitiesAndPeers()
-  {
-    LoadModalities();
-    LoadPeers();
-  }
-
-
-  void OrthancConfiguration::RegisterFont(ServerResources::FileResourceId resource)
-  {
-    std::string content;
-    ServerResources::GetFileResource(content, resource);
-    fontRegistry_.AddFromMemory(content);
-  }
-
-
-  void OrthancConfiguration::GetDicomModalityUsingSymbolicName(
-    RemoteModalityParameters& modality,
-    const std::string& name) const
-  {
-    Modalities::const_iterator found = modalities_.find(name);
-
-    if (found == modalities_.end())
-    {
-      throw OrthancException(ErrorCode_InexistentItem,
-                             "No modality with symbolic name: " + name);
-    }
-    else
-    {
-      modality = found->second;
-    }
-  }
-
-
-  bool OrthancConfiguration::LookupOrthancPeer(WebServiceParameters& peer,
-                                               const std::string& name) const
-  {
-    Peers::const_iterator found = peers_.find(name);
-
-    if (found == peers_.end())
-    {
-      LOG(ERROR) << "No peer with symbolic name: " << name;
-      return false;
-    }
-    else
-    {
-      peer = found->second;
-      return true;
-    }
-  }
-
-
-  void OrthancConfiguration::GetListOfDicomModalities(std::set<std::string>& target) const
-  {
-    target.clear();
-
-    for (Modalities::const_iterator 
-           it = modalities_.begin(); it != modalities_.end(); ++it)
-    {
-      target.insert(it->first);
-    }
-  }
-
-
-  void OrthancConfiguration::GetListOfOrthancPeers(std::set<std::string>& target) const
-  {
-    target.clear();
-
-    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
-    {
-      target.insert(it->first);
-    }
-  }
-
-
-  bool OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const
-  {
-    httpServer.ClearUsers();
-
-    if (!json_.isMember("RegisteredUsers"))
-    {
-      return false;
-    }
-
-    const Json::Value& users = json_["RegisteredUsers"];
-    if (users.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users");
-    }
-
-    bool hasUser = false;
-    Json::Value::Members usernames = users.getMemberNames();
-    for (size_t i = 0; i < usernames.size(); i++)
-    {
-      const std::string& username = usernames[i];
-      std::string password = users[username].asString();
-      httpServer.RegisterUser(username.c_str(), password.c_str());
-      hasUser = true;
-    }
-
-    return hasUser;
-  }
-    
-
-  std::string OrthancConfiguration::InterpretStringParameterAsPath(
-    const std::string& parameter) const
-  {
-    return SystemToolbox::InterpretRelativePath(defaultDirectory_.string(), parameter);
-  }
-
-    
-  void OrthancConfiguration::GetListOfStringsParameter(std::list<std::string>& target,
-                                                       const std::string& key) const
-  {
-    target.clear();
-  
-    if (!json_.isMember(key))
-    {
-      return;
-    }
-
-    const Json::Value& lst = json_[key];
-
-    if (lst.type() != Json::arrayValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings");
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
-    {
-      target.push_back(lst[i].asString());
-    }    
-  }
-
-    
-  bool OrthancConfiguration::IsSameAETitle(const std::string& aet1,
-                                           const std::string& aet2) const
-  {
-    if (GetBooleanParameter("StrictAetComparison", false))
-    {
-      // Case-sensitive matching
-      return aet1 == aet2;
-    }
-    else
-    {
-      // Case-insensitive matching (default)
-      std::string tmp1, tmp2;
-      Toolbox::ToLowerCase(tmp1, aet1);
-      Toolbox::ToLowerCase(tmp2, aet2);
-      return tmp1 == tmp2;
-    }
-  }
-
-
-  bool OrthancConfiguration::LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
-                                                             const std::string& aet) const
-  {
-    for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it)
-    {
-      if (IsSameAETitle(aet, it->second.GetApplicationEntityTitle()))
-      {
-        modality = it->second;
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  bool OrthancConfiguration::IsKnownAETitle(const std::string& aet,
-                                            const std::string& ip) const
-  {
-    RemoteModalityParameters modality;
-    
-    if (!LookupDicomModalityUsingAETitle(modality, aet))
-    {
-      LOG(WARNING) << "Modality \"" << aet
-                   << "\" is not listed in the \"DicomModalities\" configuration option";
-      return false;
-    }
-    else if (!GetBooleanParameter("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;
-    }
-  }
-
-
-  RemoteModalityParameters 
-  OrthancConfiguration::GetModalityUsingSymbolicName(const std::string& name) const
-  {
-    RemoteModalityParameters modality;
-    GetDicomModalityUsingSymbolicName(modality, name);
-
-    return modality;
-  }
-
-    
-  RemoteModalityParameters 
-  OrthancConfiguration::GetModalityUsingAet(const std::string& aet) const
-  {
-    RemoteModalityParameters modality;
-      
-    if (LookupDicomModalityUsingAETitle(modality, aet))
-    {
-      return modality;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InexistentItem,
-                             "Unknown modality for AET: " + aet);
-    }
-  }
-
-    
-  void OrthancConfiguration::UpdateModality(const std::string& symbolicName,
-                                            const RemoteModalityParameters& modality)
-  {
-    CheckAlphanumeric(symbolicName);
-    
-    modalities_[symbolicName] = modality;
-    SaveModalities();
-  }
-
-
-  void OrthancConfiguration::RemoveModality(const std::string& symbolicName)
-  {
-    modalities_.erase(symbolicName);
-    SaveModalities();
-  }
-
-    
-  void OrthancConfiguration::UpdatePeer(const std::string& symbolicName,
-                                        const WebServiceParameters& peer)
-  {
-    CheckAlphanumeric(symbolicName);
-    
-    peer.CheckClientCertificate();
-
-    peers_[symbolicName] = peer;
-    SavePeers();
-  }
-
-
-  void OrthancConfiguration::RemovePeer(const std::string& symbolicName)
-  {
-    peers_.erase(symbolicName);
-    SavePeers();
-  }
-
-
-  void OrthancConfiguration::Format(std::string& result) const
-  {
-    Json::StyledWriter w;
-    result = w.write(json_);
-  }
-
-
-  void OrthancConfiguration::SetDefaultEncoding(Encoding encoding)
-  {
-    SetDefaultDicomEncoding(encoding);
-
-    // Propagate the encoding to the configuration file that is
-    // stored in memory
-    json_["DefaultEncoding"] = EnumerationToString(encoding);
-  }
-
-
-  bool OrthancConfiguration::HasConfigurationChanged() const
-  {
-    Json::Value current;
-    ReadConfiguration(current, configurationFileArg_);
-
-    Json::FastWriter writer;
-    std::string a = writer.write(json_);
-    std::string b = writer.write(current);
-
-    return a != b;
-  }
-
-
-  void OrthancConfiguration::SetServerIndex(ServerIndex& index)
-  {
-    serverIndex_ = &index;
-  }
-
-
-  void OrthancConfiguration::ResetServerIndex()
-  {
-    serverIndex_ = NULL;
-  }
-
-  
-  TemporaryFile* OrthancConfiguration::CreateTemporaryFile() const
-  {
-    if (json_.isMember(TEMPORARY_DIRECTORY))
-    {
-      return new TemporaryFile(InterpretStringParameterAsPath(GetStringParameter(TEMPORARY_DIRECTORY, ".")), "");
-    }
-    else
-    {
-      return new TemporaryFile;
-    }
-  }
-
-
-  std::string OrthancConfiguration::GetDefaultPrivateCreator() const
-  {
-    // New configuration option in Orthanc 1.6.0
-    return GetStringParameter("DefaultPrivateCreator", "");
-  }
-}
--- a/OrthancServer/OrthancConfiguration.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,237 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/FontRegistry.h"
-#include "../Core/WebServiceParameters.h"
-#include "../Core/DicomNetworking/RemoteModalityParameters.h"
-
-#include <OrthancServerResources.h>
-
-#include <boost/filesystem.hpp>
-#include <boost/thread/shared_mutex.hpp>
-#include <boost/thread/lock_types.hpp>
-
-namespace Orthanc
-{
-  class HttpServer;
-  class ServerIndex;
-  class TemporaryFile;
-  
-  class OrthancConfiguration : public boost::noncopyable
-  {
-  private:
-    typedef std::map<std::string, RemoteModalityParameters>   Modalities;
-    typedef std::map<std::string, WebServiceParameters>       Peers;
-
-    boost::shared_mutex      mutex_;
-    Json::Value              json_;
-    boost::filesystem::path  defaultDirectory_;
-    std::string              configurationAbsolutePath_;
-    FontRegistry             fontRegistry_;
-    const char*              configurationFileArg_;
-    Modalities               modalities_;
-    Peers                    peers_;
-    ServerIndex*             serverIndex_;
-
-    OrthancConfiguration() :
-      configurationFileArg_(NULL)
-    {
-    }
-
-    void LoadModalitiesFromJson(const Json::Value& source);
-    
-    void LoadPeersFromJson(const Json::Value& source);
-    
-    void LoadModalities();
-    
-    void LoadPeers();
-    
-    void SaveModalitiesToJson(Json::Value& target);
-    
-    void SavePeersToJson(Json::Value& target);
-    
-    void SaveModalities();
-    
-    void SavePeers();
-
-    static OrthancConfiguration& GetInstance();
-
-  public:
-    class ReaderLock : public boost::noncopyable
-    {
-    private:
-      OrthancConfiguration&                    configuration_;
-      boost::shared_lock<boost::shared_mutex>  lock_;
-
-    public:
-      ReaderLock() :
-        configuration_(GetInstance()),
-        lock_(configuration_.mutex_)
-      {
-      }
-
-      const OrthancConfiguration& GetConfiguration() const
-      {
-        return configuration_;
-      }
-
-      const Json::Value& GetJson() const
-      {
-        return configuration_.json_;
-      }
-    };
-
-
-    class WriterLock : public boost::noncopyable
-    {
-    private:
-      OrthancConfiguration&                    configuration_;
-      boost::unique_lock<boost::shared_mutex>  lock_;
-
-    public:
-      WriterLock() :
-        configuration_(GetInstance()),
-        lock_(configuration_.mutex_)
-      {
-      }
-
-      OrthancConfiguration& GetConfiguration()
-      {
-        return configuration_;
-      }
-
-      const OrthancConfiguration& GetConfiguration() const
-      {
-        return configuration_;
-      }
-
-      const Json::Value& GetJson() const
-      {
-        return configuration_.json_;
-      }
-    };
-
-
-    const std::string& GetConfigurationAbsolutePath() const
-    {
-      return configurationAbsolutePath_;
-    }
-
-    const FontRegistry& GetFontRegistry() const
-    {
-      return fontRegistry_;
-    }
-
-    void Read(const char* configurationFile);
-
-    void LoadModalitiesAndPeers();
-    
-    void RegisterFont(ServerResources::FileResourceId resource);
-
-    bool LookupStringParameter(std::string& target,
-                               const std::string& parameter) const;
-
-    std::string GetStringParameter(const std::string& parameter,
-                                   const std::string& defaultValue) const;
-    
-    int GetIntegerParameter(const std::string& parameter,
-                            int defaultValue) const;
-    
-    unsigned int GetUnsignedIntegerParameter(const std::string& parameter,
-                                             unsigned int defaultValue) const;
-
-    bool LookupBooleanParameter(bool& target,
-                                const std::string& parameter) const;
-
-    bool GetBooleanParameter(const std::string& parameter,
-                             bool defaultValue) const;
-
-    void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
-                                           const std::string& name) const;
-
-    bool LookupOrthancPeer(WebServiceParameters& peer,
-                           const std::string& name) const;
-
-    void GetListOfDicomModalities(std::set<std::string>& target) const;
-
-    void GetListOfOrthancPeers(std::set<std::string>& target) const;
-
-    // Returns "true" iff. at least one user is registered
-    bool SetupRegisteredUsers(HttpServer& httpServer) const;
-
-    std::string InterpretStringParameterAsPath(const std::string& parameter) const;
-    
-    void GetListOfStringsParameter(std::list<std::string>& target,
-                                   const std::string& key) const;
-    
-    bool IsSameAETitle(const std::string& aet1,
-                       const std::string& aet2) const;
-
-    bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
-                                         const std::string& aet) const;
-
-    bool IsKnownAETitle(const std::string& aet,
-                        const std::string& ip) const;
-
-    RemoteModalityParameters GetModalityUsingSymbolicName(const std::string& name) const;
-    
-    RemoteModalityParameters GetModalityUsingAet(const std::string& aet) const;
-    
-    void UpdateModality(const std::string& symbolicName,
-                        const RemoteModalityParameters& modality);
-
-    void RemoveModality(const std::string& symbolicName);
-    
-    void UpdatePeer(const std::string& symbolicName,
-                    const WebServiceParameters& peer);
-
-    void RemovePeer(const std::string& symbolicName);
-
-
-    void Format(std::string& result) const;
-    
-    void SetDefaultEncoding(Encoding encoding);
-
-    bool HasConfigurationChanged() const;
-
-    void SetServerIndex(ServerIndex& index);
-
-    void ResetServerIndex();
-
-    TemporaryFile* CreateTemporaryFile() const;
-
-    std::string GetDefaultPrivateCreator() const;
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/explorer.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,43 @@
+ul.tree ul {
+    margin-left: 36px; 
+}
+
+#progress { 
+    position: relative; 
+    /*height: 2em; */
+    width: 100%; 
+    background-color: grey; 
+    height: 2.5em;
+}
+
+#progress .label {
+    z-index: 10; 
+    position: absolute; 
+    left:0; 
+    top: 0; 
+    width: 100%; 
+    font-weight: bold; 
+    text-align: center;  
+    text-shadow: none; 
+    padding: .5em;
+    color: white;
+}
+
+#progress .bar { 
+    z-index: 0; 
+    position: absolute; 
+    left:0; 
+    top: 0; 
+    height: 100%; 
+    width: 0%; 
+    background-color: green; 
+}
+
+.ui-title a {
+    text-decoration: none;
+    color: white !important;
+}
+
+.switch-container .ui-slider-switch {
+    width: 100%;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/explorer.html	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,665 @@
+<!DOCTYPE html>
+
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>Orthanc Explorer</title>
+
+  <link rel="stylesheet" href="libs/jquery.mobile.min.css" />
+  <link rel="stylesheet" href="libs/jqtree.css" />
+  <link rel="stylesheet" href="libs/jquery.mobile.simpledialog.min.css" />
+  <link rel="stylesheet" href="libs/jquery-file-upload/css/style.css" />
+  <link rel="stylesheet" href="libs/jquery-file-upload/css/jquery.fileupload-ui.css" />
+  <link rel="stylesheet" href="libs/slimbox2/slimbox2.css" />
+
+  <script src="libs/jquery.min.js"></script>
+  <script src="libs/jquery.mobile.min.js"></script>
+  <script src="libs/jqm.page.params.js"></script>
+  <script src="libs/tree.jquery.js"></script>
+  <script src="libs/date.js"></script>
+  <script src="libs/jquery.mobile.simpledialog2.js"></script>
+  <script src="libs/slimbox2.js"></script>
+  <script src="libs/jquery.blockui.js"></script>
+
+  <!-- https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin -->
+  <script src="libs/jquery-file-upload/js/vendor/jquery.ui.widget.js"></script>
+  <script src="libs/jquery-file-upload/js/jquery.iframe-transport.js"></script>
+  <script src="libs/jquery-file-upload/js/jquery.fileupload.js"></script>
+
+  <link rel="stylesheet" href="explorer.css" />
+  <script src="file-upload.js"></script>
+  <script src="explorer.js"></script>
+  <script src="query-retrieve.js"></script>
+  <script src="../plugins/explorer.js"></script>
+</head>
+
+<body>
+  <div data-role="page" id="lookup">
+    <div data-role="header">
+      <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">
+      <div data-role="content" id="content" style="padding:0px">
+        <p align="center">
+          <a href="http://www.orthanc-server.com/" target="_blank" alt="Orthanc homepage">
+            <img src="orthanc-logo.png" alt="Orthanc" style="max-width:100%" />
+          </a>
+        </p>
+      </div>
+      
+      <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="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>
+      <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>
+        <!-- It's very difficult to style a "file" input so we
+        actually hide it and create a "proxy" button that is
+        forwarding its click to the "file" input -->
+        <input id="fileupload" type="file" name="files[]" data-url="../instances/" style="display:none" multiple>
+      </div>
+      <p>
+        <ul data-role="listview" data-inset="true">
+          <li id="fileupload-proxy" onclick="$('#fileupload').click()" data-icon="arrow-r" data-theme="e">
+            <a href="#">Select files to upload ...</a>
+          </li>
+          <li data-icon="arrow-r" data-theme="e"><a href="#" id="upload-button">Start the upload</a></li>
+          <!--li data-icon="gear" data-theme="e"><a href="#" id="upload-abort" class="ui-disabled">Abort the current upload</a></li-->
+          <li data-icon="delete" data-theme="d"><a href="#" id="upload-clear">Clear the pending uploads</a></li>
+        </ul>
+        <div id="progress" class="ui-corner-all">
+          <span class="bar ui-corner-all"></span>
+          <div class="label"></div>
+        </div>
+      </p>
+      <div class="ui-bar ui-bar-e" id="issue-21-warning">
+        <h3>Warning:</h3> Orthanc issue #21: On Firefox, especially on
+        Linux & OSX systems, files might be missing if using
+        drag-and-drop. Please use the "Select files to upload" button
+        instead, or use the command-line "ImportDicomFiles.py" script.
+      </div>
+      <ul id="upload-list" data-role="listview" data-inset="true">
+        <li data-role="list-divider">Drag and drop DICOM files here</li>
+      </ul>
+    </div>
+  </div>
+
+  <div data-role="page" id="patient">
+    <div data-role="header">
+      <h1><span class="orthanc-name"></span>Patient</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 class="ui-grid-a">
+        <div class="ui-block-a" style="width:30%">
+          <div style="padding-right:10px">
+            <ul data-role="listview" data-inset="true" data-theme="a" id="patient-info">
+            </ul>
+            <p>
+              <div class="switch-container">
+                <select name="protection" id="protection" data-role="slider">
+                  <option value="off">Unprotected</option>
+                  <option value="on">Protected</option>
+                </select>
+              </div>
+            </p>
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Interact</li>
+              <li data-icon="delete"><a href="#" id="patient-delete">Delete this patient</a></li>
+              <li data-icon="forward"><a href="#" id="patient-store">Send to remote modality</a></li>
+              <li data-icon="star"><a href="#" id="patient-anonymize">Anonymize</a></li>
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Access</li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="patient-anonymized-from">Before anonymization</a>
+              </li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="patient-modified-from">Before modification</a>
+              </li>
+              <li data-icon="gear"><a href="#" id="patient-archive">Download ZIP</a></li>
+              <li data-icon="gear"><a href="#" id="patient-media">Download DICOMDIR</a></li>
+            </ul>
+          </div>
+        </div>
+        <div class="ui-block-b" style="width:70%">
+          <div style="padding:10px">
+            <ul id="list-studies" data-role="listview" data-inset="true" data-filter="true">
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div data-role="page" id="study">
+    <div data-role="header">
+      <h1>
+        <span class="orthanc-name"></span>
+        <a href="#" class="patient-link">Patient</a> &raquo;
+        Study
+      </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 class="ui-grid-a">
+        <div class="ui-block-a" style="width:30%">
+          <div style="padding-right:10px">
+            <ul data-role="listview" data-inset="true" data-theme="a" id="study-info">
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Interact</li>
+              <li data-icon="delete"><a href="#" id="study-delete">Delete this study</a></li>
+              <li data-icon="forward"><a href="#" id="study-store">Send to DICOM modality</a></li>
+              <li data-icon="star"><a href="#" id="study-anonymize">Anonymize</a></li>
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Access</li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="study-anonymized-from">Before anonymization</a>
+              </li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="study-modified-from">Before modification</a>
+              </li>
+              <li data-icon="gear"><a href="#" id="study-archive">Download ZIP</a></li>
+              <li data-icon="gear"><a href="#" id="study-media">Download DICOMDIR</a></li>
+            </ul>
+          </div>
+        </div>
+        <div class="ui-block-b" style="width:70%">
+          <div style="padding:10px">
+            <ul id="list-series" data-role="listview" data-inset="true" data-filter="true">
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div data-role="page" id="series">
+    <div data-role="header">
+      <h1>
+        <span class="orthanc-name"></span>
+        <a href="#" class="patient-link">Patient</a> &raquo;
+        <a href="#" class="study-link">Study</a> &raquo;
+        Series
+      </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 class="ui-grid-a">
+        <div class="ui-block-a" style="width:30%">
+          <div style="padding-right:10px">
+            <ul data-role="listview" data-inset="true" data-theme="a" id="series-info">
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Interact</li>
+              <li data-icon="delete"><a href="#" id="series-delete">Delete this series</a></li>
+              <li data-icon="forward"><a href="#" id="series-store">Send to DICOM modality</a></li>
+              <li data-icon="star"><a href="#" id="series-anonymize">Anonymize</a></li>
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Access</li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="series-anonymized-from">Before anonymization</a>
+              </li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="series-modified-from">Before modification</a>
+              </li>
+              <li data-icon="search"><a href="#" id="series-preview">Preview this series</a></li>
+              <li data-icon="gear"><a href="#" id="series-archive">Download ZIP</a></li>
+              <li data-icon="gear"><a href="#" id="series-media">Download DICOMDIR</a></li>
+            </ul>
+          </div>
+        </div>
+        <div class="ui-block-b" style="width:70%">
+          <div style="padding:10px">
+            <ul id="list-instances" data-role="listview" data-inset="true" data-filter="true">
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div data-role="page" id="instance">
+    <div data-role="header">
+      <h1>
+        <span class="orthanc-name"></span>
+        <a href="#" class="patient-link">Patient</a> &raquo;
+        <a href="#" class="study-link">Study</a> &raquo;
+        <a href="#" class="series-link">Series</a> &raquo;
+        Instance
+      </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 class="ui-grid-a">
+        <div class="ui-block-a" style="width:30%">
+          <div style="padding-right:10px">
+            <ul data-role="listview" data-inset="true" data-theme="a" id="instance-info">
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Interact</li>
+              <li data-icon="delete"><a href="#" id="instance-delete">Delete this instance</a></li>
+              <li data-icon="forward"><a href="#" id="instance-store">Send to DICOM modality</a></li>
+            </ul>
+
+            <ul data-role="listview" data-inset="true" data-theme="d" data-divider-theme="c">
+              <li data-role="list-divider">Access</li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="instance-anonymized-from">Before anonymization</a>
+              </li>
+              <li data-icon="info" data-theme="e" style="display:none">
+                <a href="#" id="instance-modified-from">Before modification</a>
+              </li>
+              <li data-icon="arrow-d"><a href="#" id="instance-download-dicom">Download the DICOM file</a></li>
+              <li data-icon="arrow-d"><a href="#" id="instance-download-json">Download the JSON file</a></li>
+              <li data-icon="search"><a href="#" id="instance-preview">Preview the instance</a></li>
+            </ul>
+          </div>
+        </div>
+        <div class="ui-block-b" style="width:70%">
+          <div style="padding:10px">
+            <div class="ui-body ui-body-b">
+              <h1>DICOM Tags</h1>
+              <p align="right">
+                <input type="checkbox" id="show-tag-name" checked="checked" class="custom" data-mini="true" />
+                <label for="show-tag-name">Show tag description</label>
+              </p>
+              <div id="dicom-tree"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div data-role="page" id="plugins">
+    <div data-role="header">
+      <h1><span class="orthanc-name"></span>Plugins</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-plugins" data-role="listview" data-inset="true" data-filter="true">
+      </ul>
+    </div>
+  </div>
+
+  <div data-role="page" id="query-retrieve">
+    <div data-role="header">
+      <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">
+        <div data-role="fieldcontain">
+          <label for="qr-server">DICOM server:</label>
+          <select name="qr-server" id="qr-server">
+          </select>
+        </div>
+
+        <div data-role="fieldcontain" id="qr-fields">
+          <fieldset data-role="controlgroup">
+            <legend>Field of interest:</legend>
+            <input type="radio" name="qr-field" id="qr-patient-id" value="PatientID" checked="checked" />
+            <label for="qr-patient-id">Patient ID</label>
+            <input type="radio" name="qr-field" id="qr-patient-name" value="PatientName" />
+            <label for="qr-patient-name">Patient Name</label>
+            <input type="radio" name="qr-field" id="qr-accession-number" value="AccessionNumber" />
+            <label for="qr-accession-number">Accession Number</label>
+            <input type="radio" name="qr-field" id="qr-study-description" value="StudyDescription" />
+            <label for="qr-study-description">Study Description</label>
+          </fieldset>
+        </div>
+
+        <div data-role="fieldcontain">
+          <label for="qr-value">Value for this field:</label>
+          <input type="text" name="qr-value" id="qr-value" value="*" />
+        </div>
+
+        <div data-role="fieldcontain">
+          <label for="qr-date">Study date:</label>
+          <select name="qr-date" id="qr-date">
+          </select>
+        </div>
+
+        <div data-role="fieldcontain" id="qr-modalities">
+          <div data-role="fieldcontain">
+            <fieldset data-role="controlgroup" data-type="horizontal">
+              <legend>Modalities:</legend>
+              <input type="checkbox" name="CR" id="qr-cr" class="custom" /> <label for="qr-cr">CR</label>
+              <input type="checkbox" name="CT" id="qr-ct" class="custom" /> <label for="qr-ct">CT</label>
+              <input type="checkbox" name="MR" id="qr-mr" class="custom" /> <label for="qr-mr">MR</label>
+              <input type="checkbox" name="NM" id="qr-nm" class="custom" /> <label for="qr-nm">NM</label>
+              <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>
+              <input type="checkbox" name="MG" id="qr-mg" class="custom" /> <label for="qr-mg">MG</label>
+            </fieldset>
+          </div>
+        </div>
+
+        <fieldset class="ui-grid-a">
+          <div class="ui-block-a">
+            <button id="qr-echo" data-theme="a">Test Echo</button>
+          </div>
+          <div class="ui-block-b">
+            <button id="qr-submit" type="submit" data-theme="b">Search studies</button>
+          </div>
+        </fieldset>
+      </form>
+    </div>
+  </div>
+
+
+  <div data-role="page" id="query-retrieve-2">
+    <div data-role="header">
+      <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">
+      <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
+      </ul>
+    </div>
+  </div>
+
+
+  <div data-role="page" id="query-retrieve-3">
+    <div data-role="header">
+      <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">
+      </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>
+  </div>
+
+  <div id="dicom-store" style="display:none;" class="ui-body-c">
+    <p align="center"><b>Sending to DICOM modality...</b></p>
+    <p><img src="libs/images/ajax-loader.gif" alt="" /></p>
+  </div>
+
+  <div id="info-retrieve" style="display:none;" class="ui-body-c">
+    <p align="center"><b>Retrieving images from DICOM modality...</b></p>
+    <p><img src="libs/images/ajax-loader.gif" alt="" /></p>
+  </div>
+
+  <div id="dialog" style="display:none">
+  </div>
+
+  <div id="template-insecure" style="display:none">
+    <div class="warning-insecure ui-body ui-body-e">
+      <h1>Insecure setup</h1>
+      <p>
+        Your Orthanc server is accepting remote connections, but is
+	using the default username and password, or has user
+	authentication explicitly turned off. Please carefully read
+	your logs and review your configuration, especially
+	options <tt>RemoteAccessAllowed</tt>, <tt>AuthenticationEnabled</tt>,
+	and <tt>RegisteredUsers</tt>.
+      </p>
+    </div>
+  </div>
+</body>
+
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/explorer.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1570 @@
+// http://stackoverflow.com/questions/1663741/is-there-a-good-jquery-drag-and-drop-file-upload-plugin
+
+
+// Forbid the access to IE
+if ($.browser.msie)
+{
+  alert("Please use Mozilla Firefox or Google Chrome. Microsoft Internet Explorer is not supported.");
+}
+
+// http://jquerymobile.com/demos/1.1.0/docs/api/globalconfig.html
+//$.mobile.ajaxEnabled = false;
+//$.mobile.page.prototype.options.addBackBtn = true;
+//$.mobile.defaultPageTransition = 'slide';
+
+
+var LIMIT_RESOURCES = 100;
+
+var currentPage = '';
+var currentUuid = '';
+
+
+function DeepCopy(obj)
+{
+  return jQuery.extend(true, {}, obj);
+}
+
+
+function ChangePage(page, options)
+{
+  var first = true;
+  var value;
+
+  if (options) {
+    for (var key in options) {
+      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()
+{
+  if (currentPage == 'patient')
+    RefreshPatient();
+  else if (currentPage == 'study')
+    RefreshStudy();
+  else if (currentPage == 'series')
+    RefreshSeries();
+  else if (currentPage == 'instance')
+    RefreshInstance();
+}
+
+
+$(document).ready(function() {
+  var $tree = $('#dicom-tree');
+  $tree.tree({
+    autoEscape: false
+  });
+
+  $('#dicom-tree').bind(
+    'tree.click',
+    function(event) {
+      if (event.node.is_open)
+        $tree.tree('closeNode', event.node, true);
+      else
+        $tree.tree('openNode', event.node, true);
+    }
+  );
+
+  // Inject the template of the warning about insecure setup as the
+  // first child of each page
+  var insecure = $('#template-insecure').html();
+  $('[data-role="page"]>[data-role="content"]').prepend(insecure);
+  
+  currentPage = $.mobile.pageData.active;
+  currentUuid = $.mobile.pageData.uuid;
+  if (!(typeof currentPage === 'undefined') &&
+      !(typeof currentUuid === 'undefined') &&
+      currentPage.length > 0 && 
+      currentUuid.length > 0)
+  {
+    Refresh();
+  }
+});
+
+function GetAuthorizationTokensFromUrl() {
+  var urlVariables = window.location.search.substring(1).split('&');
+  var dict = {};
+
+  for (var i = 0; i < urlVariables.length; i++) {
+      var split = urlVariables[i].split('=');
+
+      if (split.length == 2 && (split[0] == "token" || split[0] == "auth-token" || split[0] == "authorization")) {
+        dict[split[0]] = split[1];
+      }
+  }
+  return dict;
+};
+
+var authorizationTokens = GetAuthorizationTokensFromUrl();
+
+/* Copy the authoziation toekn from the url search parameters into HTTP headers in every request to the Rest API.  
+Thanks to this behaviour, you may specify a ?token=xxx in your url and this will be passed 
+as the "token" header in every request to the API allowing you to use the authorization plugin */
+$.ajaxSetup(
+  {
+    headers : authorizationTokens
+  }
+);
+
+
+function SplitLongUid(s)
+{
+  return '<span>' + s.substr(0, s.length / 2) + '</span> <span>' + s.substr(s.length / 2, s.length - s.length / 2) + '</span>';
+}
+
+
+function ParseDicomDate(s)
+{
+  y = parseInt(s.substr(0, 4), 10);
+  m = parseInt(s.substr(4, 2), 10) - 1;
+  d = parseInt(s.substr(6, 2), 10);
+
+  if (y == null || m == null || d == null ||
+      !isFinite(y) || !isFinite(m) || !isFinite(d))
+  {
+    return null;
+  }
+
+  if (y < 1900 || y > 2100 ||
+      m < 0 || m >= 12 ||
+      d <= 0 || d >= 32)
+  {
+    return null;
+  }
+
+  return new Date(y, m, d);
+}
+
+
+function FormatDicomDate(s)
+{
+  if (s == undefined)
+    return "No date";
+
+  var d = ParseDicomDate(s);
+  if (d == null)
+    return '?';
+  else
+    return d.toString('dddd, MMMM d, yyyy');
+}
+
+function FormatFloatSequence(s)
+{
+  if (s == undefined || s.length == 0)
+    return "-";
+
+  if (s.indexOf("\\") == -1)
+    return s;
+
+  var oldValues = s.split("\\");
+  var newValues = [];
+  for (var i = 0; i < oldValues.length; i++)
+  {
+    newValues.push(parseFloat(oldValues[i]).toFixed(3));
+  }
+  return newValues.join("\\");
+}
+
+function Sort(arr, fieldExtractor, isInteger, reverse)
+{
+  var defaultValue;
+  if (isInteger)
+    defaultValue = 0;
+  else
+    defaultValue = '';
+
+  arr.sort(function(a, b) {
+    var ta = fieldExtractor(a);
+    var tb = fieldExtractor(b);
+    var order;
+
+    if (ta == undefined)
+      ta = defaultValue;
+
+    if (tb == undefined)
+      tb = defaultValue;
+
+    if (isInteger)
+    {
+      ta = parseInt(ta, 10);
+      tb = parseInt(tb, 10);
+      order = ta - tb;
+    }
+    else
+    {
+      if (ta < tb)
+        order = -1;
+      else if (ta > tb)
+        order = 1;
+      else
+        order = 0;
+    }
+
+    if (reverse)
+      return -order;
+    else
+      return order;
+  });
+}
+
+
+function SortOnDicomTag(arr, tag, isInteger, reverse)
+{
+  return Sort(arr, function(a) { 
+    return a.MainDicomTags[tag];
+  }, isInteger, reverse);
+}
+
+
+
+function GetResource(uri, callback)
+{
+  $.ajax({
+    url: '..' + uri,
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(s) {
+      callback(s);
+    }
+  });
+}
+
+
+function CompleteFormatting(node, link, isReverse, count)
+{
+  if (count != null)
+  {
+    node = node.add($('<span>')
+                    .addClass('ui-li-count')
+                    .text(count));
+  }
+  
+  if (link != null)
+  {
+    node = $('<a>').attr('href', link).append(node);
+
+    if (isReverse)
+      node.attr('data-direction', 'reverse')
+  }
+
+  node = $('<li>').append(node);
+
+  if (isReverse)
+    node.attr('data-icon', 'back');
+
+  return node;
+}
+
+
+function FormatMainDicomTags(target, tags, tagsToIgnore)
+{
+  var v;
+  
+  for (var i in tags)
+  {
+    if (tagsToIgnore.indexOf(i) == -1)
+    {
+      v = tags[i];
+
+      if (i == "PatientBirthDate" ||
+          i == "StudyDate" ||
+          i == "SeriesDate")
+      {
+        v = FormatDicomDate(v);
+      }
+      else if (i == "DicomStudyInstanceUID" ||
+               i == "DicomSeriesInstanceUID")
+      {
+        v = SplitLongUid(v);
+      }
+      else if (i == "ImagePositionPatient" ||
+               i == "ImageOrientationPatient")
+      {
+        v = FormatFloatSequence(v);
+      }
+      
+      target.append($('<p>')
+                    .text(i + ': ')
+                    .append($('<strong>').text(v)));
+    }
+  }
+}
+
+
+function FormatPatient(patient, link, isReverse)
+{
+  var node = $('<div>').append($('<h3>').text(patient.MainDicomTags.PatientName));
+
+  FormatMainDicomTags(node, patient.MainDicomTags, [ 
+    "PatientName"
+    // "OtherPatientIDs"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, patient.Studies.length);
+}
+
+
+
+function FormatStudy(study, link, isReverse, includePatient)
+{
+  var label;
+  var node;
+
+  if (includePatient) {
+    label = study.Label;
+  } else {
+    label = study.MainDicomTags.StudyDescription;
+  }
+
+  node = $('<div>').append($('<h3>').text(label));
+
+  if (includePatient) {
+    FormatMainDicomTags(node, study.PatientMainDicomTags, [ 
+      'PatientName'
+    ]);
+  }
+    
+  FormatMainDicomTags(node, study.MainDicomTags, [ 
+     'StudyDescription', 
+     'StudyTime' 
+  ]);
+
+  return CompleteFormatting(node, link, isReverse, study.Series.length);
+}
+
+
+
+function FormatSeries(series, link, isReverse)
+{
+  var c;
+  var node;
+
+  if (series.ExpectedNumberOfInstances == null ||
+      series.Instances.length == series.ExpectedNumberOfInstances)
+  {
+    c = series.Instances.length;
+  }
+  else
+  {
+    c = series.Instances.length + '/' + series.ExpectedNumberOfInstances;
+  }
+  
+  node = $('<div>')
+      .append($('<h3>').text(series.MainDicomTags.SeriesDescription))
+      .append($('<p>').append($('<em>')
+                           .text('Status: ')
+                           .append($('<strong>').text(series.Status))));
+
+  FormatMainDicomTags(node, series.MainDicomTags, [ 
+     "SeriesDescription", 
+     "SeriesTime", 
+     "Manufacturer",
+     "ImagesInAcquisition",
+     "SeriesDate",
+     "ImageOrientationPatient"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse, c);
+}
+
+
+function FormatInstance(instance, link, isReverse)
+{
+  var node = $('<div>').append($('<h3>').text('Instance: ' + instance.IndexInSeries));
+
+  FormatMainDicomTags(node, instance.MainDicomTags, [
+    "AcquisitionNumber", 
+    "InstanceNumber", 
+    "InstanceCreationDate", 
+    "InstanceCreationTime"
+  ]);
+    
+  return CompleteFormatting(node, link, isReverse);
+}
+
+
+$('[data-role="page"]').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../system',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(s) {
+      if (s.Name != "") {
+        $('.orthanc-name').html($('<a>')
+                                .addClass('ui-link')
+                                .attr('href', 'explorer.html')
+                                .text(s.Name)
+                                .append(' &raquo; '));
+      }
+
+      // New in Orthanc 1.5.8
+      if ('IsHttpServerSecure' in s &&
+          !s.IsHttpServerSecure) {
+        $('.warning-insecure').show();
+      } else {
+        $('.warning-insecure').hide();
+      }
+    }
+  });
+});
+
+
+
+$('#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() {
+  var lookup;
+
+  $('#lookup-result').hide();
+
+  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&since=0&limit=' + (LIMIT_RESOURCES + 1), function(patients) {
+    var target = $('#all-patients');
+    var count, showAlert, p;
+
+
+    $('li', target).remove();
+    
+    SortOnDicomTag(patients, 'PatientName', false, false);
+
+    if (patients.length <= LIMIT_RESOURCES) {
+      count = patients.length;
+      showAlert = false;
+    }
+    else {
+      count = LIMIT_RESOURCES;
+      showAlert = true;
+    }
+
+    for (var i = 0; i < count; i++) {
+      p = FormatPatient(patients[i], '#patient?uuid=' + patients[i].ID);
+      target.append(p);
+    }
+
+    target.listview('refresh'); 
+
+    if (showAlert) {
+      $('#count-patients').text(LIMIT_RESOURCES);
+      $('#alert-patients').show();
+    } else {
+      $('#alert-patients').hide();
+    }
+  });
+});
+
+
+
+function FormatListOfStudies(targetId, alertId, countId, studies)
+{
+  var target = $(targetId);
+  var patient, study, s;
+  var count, showAlert;
+
+  $('li', target).remove();
+
+  for (var i = 0; i < studies.length; i++) {
+    patient = studies[i].PatientMainDicomTags.PatientName;
+    study = studies[i].MainDicomTags.StudyDescription;
+
+    s = "";
+    if (typeof patient === 'string') {
+      s = patient;
+    }
+
+    if (typeof study === 'string') {
+      if (s.length > 0) {
+        s += ' - ';
+      }
+
+      s += study;
+    }
+
+    studies[i]['Label'] = s;
+  }
+
+  Sort(studies, function(a) { return a.Label }, false, false);
+
+  if (studies.length <= LIMIT_RESOURCES) {
+    count = studies.length;
+    showAlert = false;
+  }
+  else {
+    count = LIMIT_RESOURCES;
+    showAlert = true;
+  }
+
+  for (var i = 0; i < count; i++) {
+    s = FormatStudy(studies[i], '#study?uuid=' + studies[i].ID, false, true);
+    target.append(s);
+  }
+
+  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);
+  });
+});
+
+
+
+function SetupAnonymizedOrModifiedFrom(buttonSelector, resource, resourceType, field)
+{
+  if (field in resource)
+  {
+    $(buttonSelector).closest('li').show();
+    $(buttonSelector).click(function(e) {
+      window.location.assign('explorer.html#' + resourceType + '?uuid=' + resource[field]);
+    });
+  }
+  else
+  {
+    $(buttonSelector).closest('li').hide();
+  }
+}
+
+
+
+function RefreshPatient()
+{
+  var pageData, target, v;
+
+  if ($.mobile.pageData) {
+    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();
+        $('#patient-info')
+          .append('<li data-role="list-divider">Patient</li>')
+          .append(FormatPatient(patient))
+          .listview('refresh');
+
+        target = $('#list-studies');
+        $('li', target).remove();
+        
+        for (var i = 0; i < studies.length; i++) {
+          if (i == 0 || studies[i].MainDicomTags.StudyDate != studies[i - 1].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));
+        }
+
+        SetupAnonymizedOrModifiedFrom('#patient-anonymized-from', patient, 'patient', 'AnonymizedFrom');
+        SetupAnonymizedOrModifiedFrom('#patient-modified-from', patient, 'patient', 'ModifiedFrom');
+
+        target.listview('refresh');
+
+        // Check whether this patient is protected
+        $.ajax({
+          url: '../patients/' + pageData.uuid + '/protected',
+          type: 'GET',
+          dataType: 'text',
+          async: false,
+          cache: false,
+          success: function (s) {
+            v = (s == '1') ? 'on' : 'off';
+            $('#protection').val(v).slider('refresh');
+          }
+        });
+
+        currentPage = 'patient';
+        currentUuid = pageData.uuid;
+      });
+    });
+  }
+}
+
+
+function RefreshStudy()
+{
+  var pageData, target;
+
+  if ($.mobile.pageData) {
+    pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/studies/' + pageData.uuid, function(study) {
+      GetResource('/patients/' + study.ParentPatient, function(patient) {
+        GetResource('/studies/' + pageData.uuid + '/series', function(series) {
+          SortOnDicomTag(series, 'SeriesDate', false, true);
+
+          $('#study .patient-link').attr('href', '#patient?uuid=' + patient.ID);
+          $('#study-info li').remove();
+          $('#study-info')
+            .append('<li data-role="list-divider">Patient</li>')
+            .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
+            .append('<li data-role="list-divider">Study</li>')
+            .append(FormatStudy(study))
+            .listview('refresh');
+
+          SetupAnonymizedOrModifiedFrom('#study-anonymized-from', study, 'study', 'AnonymizedFrom');
+          SetupAnonymizedOrModifiedFrom('#study-modified-from', study, 'study', 'ModifiedFrom');
+
+          target = $('#list-series');
+          $('li', target).remove();
+          for (var i = 0; i < series.length; i++) {
+            if (i == 0 || series[i].MainDicomTags.SeriesDate != series[i - 1].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 = pageData.uuid;
+        });
+      });
+    });
+  }
+}
+  
+
+function RefreshSeries() 
+{
+  var pageData, target;
+
+  if ($.mobile.pageData) {
+    pageData = DeepCopy($.mobile.pageData);
+
+    GetResource('/series/' + pageData.uuid, function(series) {
+      GetResource('/studies/' + series.ParentStudy, function(study) {
+        GetResource('/patients/' + study.ParentPatient, function(patient) {
+          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);
+            $('#series .study-link').attr('href', '#study?uuid=' + study.ID);
+
+            $('#series-info li').remove();
+            $('#series-info')
+              .append('<li data-role="list-divider">Patient</li>')
+              .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
+              .append('<li data-role="list-divider">Study</li>')
+              .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
+              .append('<li data-role="list-divider">Series</li>')
+              .append(FormatSeries(series))
+              .listview('refresh');
+
+            SetupAnonymizedOrModifiedFrom('#series-anonymized-from', series, 'series', 'AnonymizedFrom');
+            SetupAnonymizedOrModifiedFrom('#series-modified-from', series, 'series', 'ModifiedFrom');
+
+            target = $('#list-instances');
+            $('li', target).remove();
+            for (var i = 0; i < instances.length; i++) {
+              target.append(FormatInstance(instances[i], '#instance?uuid=' + instances[i].ID));
+            }
+            target.listview('refresh');
+
+            currentPage = 'series';
+            currentUuid = pageData.uuid;
+          });
+        });
+      });
+    });
+  }
+}
+
+
+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)
+{
+  var result = [];
+  var label, c;
+
+  for (var i in dicom) {
+    if (dicom[i] != null) {
+      label = (i + '<span class="tag-name"> (<i>' +
+                   EscapeHtml(dicom[i]["Name"]) +
+                   '</i>)</span>: ');
+
+      if (dicom[i]["Type"] == 'String')
+      {
+        result.push({
+          label: label + '<strong>' + EscapeHtml(dicom[i]["Value"]) + '</strong>',
+          children: []
+        });
+      }
+      else if (dicom[i]["Type"] == 'TooLong')
+      {
+        result.push({
+          label: label + '<i>Too long</i>',
+          children: []
+        });
+      }
+      else if (dicom[i]["Type"] == 'Null')
+      {
+        result.push({
+          label: label + '<i>Null</i>',
+          children: []
+        });
+      }
+      else if (dicom[i]["Type"] == 'Sequence')
+      {
+        c = [];
+        for (var j = 0; j < dicom[i]["Value"].length; j++) {
+          c.push({
+            label: 'Item ' + j,
+            children: ConvertForTree(dicom[i]["Value"][j])
+          });
+        }
+
+        result.push({
+          label: label + '[]',
+          children: c
+        });
+      }
+    }
+  }
+
+  return result;
+}
+
+
+function RefreshInstance()
+{
+  var pageData;
+
+  if ($.mobile.pageData) {
+    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) {
+
+            $('#instance .patient-link').attr('href', '#patient?uuid=' + patient.ID);
+            $('#instance .study-link').attr('href', '#study?uuid=' + study.ID);
+            $('#instance .series-link').attr('href', '#series?uuid=' + series.ID);
+            
+            $('#instance-info li').remove();
+            $('#instance-info')
+              .append('<li data-role="list-divider">Patient</li>')
+              .append(FormatPatient(patient, '#patient?uuid=' + patient.ID, true))
+              .append('<li data-role="list-divider">Study</li>')
+              .append(FormatStudy(study, '#study?uuid=' + study.ID, true))
+              .append('<li data-role="list-divider">Series</li>')
+              .append(FormatSeries(series, '#series?uuid=' + series.ID, true))
+              .append('<li data-role="list-divider">Instance</li>')
+              .append(FormatInstance(instance))
+              .listview('refresh');
+
+            GetResource('/instances/' + instance.ID + '/tags', function(s) {
+              $('#dicom-tree').tree('loadData', ConvertForTree(s));
+            });
+
+            SetupAnonymizedOrModifiedFrom('#instance-anonymized-from', instance, 'instance', 'AnonymizedFrom');
+            SetupAnonymizedOrModifiedFrom('#instance-modified-from', instance, 'instance', 'ModifiedFrom');
+
+            currentPage = 'instance';
+            currentUuid = pageData.uuid;
+          });
+        });
+      });
+    });
+  }
+}
+
+$(document).live('pagebeforehide', function() {
+  currentPage = '';
+  currentUuid = '';
+});
+
+
+
+$('#patient').live('pagebeforeshow', RefreshPatient);
+$('#study').live('pagebeforeshow', RefreshStudy);
+$('#series').live('pagebeforeshow', RefreshSeries);
+$('#instance').live('pagebeforeshow', RefreshInstance);
+
+$(function() {
+  $(window).hashchange(function(e, data) {
+    // This fixes the navigation with the back button and with the anonymization
+    if ('uuid' in $.mobile.pageData &&
+        currentPage == $.mobile.pageData.active &&
+        currentUuid != $.mobile.pageData.uuid) {
+      Refresh();
+    }
+  });
+});
+
+
+
+
+
+function DeleteResource(path)
+{
+  $.ajax({
+    url: path,
+    type: 'DELETE',
+    dataType: 'json',
+    async: false,
+    success: function(s) {
+      var ancestor = s.RemainingAncestor;
+      if (ancestor == null)
+        $.mobile.changePage('#lookup');
+      else
+        $.mobile.changePage('#' + ancestor.Type.toLowerCase() + '?uuid=' + ancestor.ID);
+    }
+  });
+}
+
+
+
+function OpenDeleteResourceDialog(path, title)
+{
+  $(document).simpledialog2({ 
+    // http://dev.jtsage.com/jQM-SimpleDialog/demos2/
+    // http://dev.jtsage.com/jQM-SimpleDialog/demos2/options.html
+    mode: 'button',
+    animate: false,
+    headerText: title,
+    headerClose: true,
+    width: '500px',
+    buttons : {
+      'OK': {
+        click: function () { 
+          DeleteResource(path);
+        },
+        icon: "delete",
+        theme: "c"
+      },
+      'Cancel': {
+        click: function () { 
+        }
+      }
+    }
+  });
+}
+
+
+
+$('#instance-delete').live('click', function() {
+  OpenDeleteResourceDialog('../instances/' + $.mobile.pageData.uuid,
+                           'Delete this instance?');
+});
+
+$('#study-delete').live('click', function() {
+  OpenDeleteResourceDialog('../studies/' + $.mobile.pageData.uuid,
+                           'Delete this study?');
+});
+
+$('#series-delete').live('click', function() {
+  OpenDeleteResourceDialog('../series/' + $.mobile.pageData.uuid,
+                           'Delete this series?');
+});
+
+$('#patient-delete').live('click', function() {
+  OpenDeleteResourceDialog('../patients/' + $.mobile.pageData.uuid,
+                           'Delete this patient?');
+});
+
+
+$('#instance-download-dicom').live('click', function(e) {
+  // http://stackoverflow.com/a/1296101
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../instances/' + $.mobile.pageData.uuid + '/file';
+});
+
+$('#instance-download-json').live('click', function(e) {
+  // http://stackoverflow.com/a/1296101
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../instances/' + $.mobile.pageData.uuid + '/tags';
+});
+
+
+
+$('#instance-preview').live('click', function(e) {
+  var pageData, pdf, images;
+
+  if ($.mobile.pageData) {
+    pageData = DeepCopy($.mobile.pageData);
+
+    pdf = '../instances/' + pageData.uuid + '/pdf';
+    $.ajax({
+      url: pdf,
+      cache: false,
+      success: function(s) {
+        window.location.assign(pdf);
+      },
+      error: function() {
+        GetResource('/instances/' + pageData.uuid + '/frames', function(frames) {
+          if (frames.length == 1)
+          {
+            // Viewing a single-frame image
+            jQuery.slimbox('../instances/' + pageData.uuid + '/preview', '', {
+              overlayFadeDuration : 1,
+              resizeDuration : 1,
+              imageFadeDuration : 1
+            });
+          }
+          else
+          {
+            // Viewing a multi-frame image
+
+            images = [];
+            for (var i = 0; i < frames.length; i++) {
+              images.push([ '../instances/' + pageData.uuid + '/frames/' + i + '/preview' ]);
+            }
+
+            jQuery.slimbox(images, 0, {
+              overlayFadeDuration : 1,
+              resizeDuration : 1,
+              imageFadeDuration : 1,
+              loop : true
+            });
+          }
+        });
+      }
+    });
+  }
+});
+
+
+
+$('#series-preview').live('click', function(e) {
+  var pageData, images;
+
+  if ($.mobile.pageData) {
+    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);
+
+        images = [];
+        for (var i = 0; i < instances.length; i++) {
+          images.push([ '../instances/' + instances[i].ID + '/preview',
+                        (i + 1).toString() + '/' + instances.length.toString() ])
+        }
+
+        jQuery.slimbox(images, 0, {
+          overlayFadeDuration : 1,
+          resizeDuration : 1,
+          imageFadeDuration : 1,
+          loop : true
+        });
+      });
+    });
+  }
+});
+
+
+
+
+
+function ChooseDicomModality(callback)
+{
+  var clickedModality = '';
+  var clickedPeer = '';
+  var items = $('<ul>')
+    .attr('data-divider-theme', 'd')
+    .attr('data-role', 'listview');
+
+  // Retrieve the list of the known DICOM modalities
+  $.ajax({
+    url: '../modalities',
+    type: 'GET',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(modalities) {
+      var name, item;
+      
+      if (modalities.length > 0)
+      {
+        items.append('<li data-role="list-divider">DICOM modalities</li>');
+
+        for (var i = 0; i < modalities.length; i++) {
+          name = modalities[i];
+          item = $('<li>')
+            .html('<a href="#" rel="close">' + name + '</a>')
+            .attr('name', name)
+            .click(function() { 
+              clickedModality = $(this).attr('name');
+            });
+          items.append(item);
+        }
+      }
+
+      // Retrieve the list of the known Orthanc peers
+      $.ajax({
+        url: '../peers',
+        type: 'GET',
+        dataType: 'json',
+        async: false,
+        cache: false,
+        success: function(peers) {
+          var name, item;
+
+          if (peers.length > 0)
+          {
+            items.append('<li data-role="list-divider">Orthanc peers</li>');
+
+            for (var i = 0; i < peers.length; i++) {
+              name = peers[i];
+              item = $('<li>')
+                .html('<a href="#" rel="close">' + name + '</a>')
+                .attr('name', name)
+                .click(function() { 
+                  clickedPeer = $(this).attr('name');
+                });
+              items.append(item);
+            }
+          }
+
+          // Launch the dialog
+          $('#dialog').simpledialog2({
+            mode: 'blank',
+            animate: false,
+            headerText: 'Choose target',
+            headerClose: true,
+            forceInput: false,
+            width: '100%',
+            blankContent: items,
+            callbackClose: function() {
+              var timer;
+              function WaitForDialogToClose() {
+                if (!$('#dialog').is(':visible')) {
+                  clearInterval(timer);
+                  callback(clickedModality, clickedPeer);
+                }
+              }
+              timer = setInterval(WaitForDialogToClose, 100);
+            }
+          });
+        }
+      });
+    }
+  });
+}
+
+
+$('#instance-store,#series-store,#study-store,#patient-store').live('click', function(e) {
+  ChooseDicomModality(function(modality, peer) {
+    var pageData = DeepCopy($.mobile.pageData);
+    var url, loading;
+
+    if (modality != '')
+    {
+      url = '../modalities/' + modality + '/store';
+      loading = '#dicom-store';
+    }
+
+    if (peer != '')
+    {
+      url = '../peers/' + peer + '/store';
+      loading = '#peer-store';
+    }
+
+    if (url != '') {
+      $.ajax({
+        url: url,
+        type: 'POST',
+        dataType: 'text',
+        data: pageData.uuid,
+        async: true,  // Necessary to block UI
+        beforeSend: function() {
+          $.blockUI({ message: $(loading) });
+        },
+        complete: function(s) {
+          $.unblockUI();
+        },
+        success: function(s) {
+        },
+        error: function() {
+          alert('Error during store');
+        }
+      });      
+    }
+  });
+});
+
+
+$('#show-tag-name').live('change', function(e) {
+  var checked = e.currentTarget.checked;
+  if (checked)
+    $('.tag-name').show();
+  else
+    $('.tag-name').hide();
+});
+
+
+$('#patient-archive').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../patients/' + $.mobile.pageData.uuid + '/archive';
+});
+
+$('#study-archive').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../studies/' + $.mobile.pageData.uuid + '/archive';
+});
+
+$('#series-archive').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../series/' + $.mobile.pageData.uuid + '/archive';
+});
+
+
+$('#patient-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../patients/' + $.mobile.pageData.uuid + '/media';
+});
+
+$('#study-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../studies/' + $.mobile.pageData.uuid + '/media';
+});
+
+$('#series-media').live('click', function(e) {
+  e.preventDefault();  //stop the browser from following
+  window.location.href = '../series/' + $.mobile.pageData.uuid + '/media';
+});
+
+
+
+$('#protection').live('change', function(e) {
+  var isProtected = e.target.value == "on";
+  $.ajax({
+    url: '../patients/' + $.mobile.pageData.uuid + '/protected',
+    type: 'PUT',
+    dataType: 'text',
+    data: isProtected ? '1' : '0',
+    async: false
+  });
+});
+
+
+
+function OpenAnonymizeResourceDialog(path, title)
+{
+  $(document).simpledialog2({ 
+    mode: 'button',
+    animate: false,
+    headerText: title,
+    headerClose: true,
+    width: '500px',
+    buttons : {
+      'OK': {
+        click: function () { 
+          $.ajax({
+            url: path + '/anonymize',
+            type: 'POST',
+            data: '{ "Keep" : [ "SeriesDescription", "StudyDescription" ] }',
+            dataType: 'json',
+            async: false,
+            cache: false,
+            success: function(s) {
+              // The following line does not work...
+              //$.mobile.changePage('explorer.html#patient?uuid=' + s.PatientID);
+
+              window.location.assign('explorer.html#patient?uuid=' + s.PatientID);
+              //window.location.reload();
+            }
+          });
+        },
+        icon: "delete",
+        theme: "c"
+      },
+      'Cancel': {
+        click: function () { 
+        }
+      }
+    }
+  });
+}
+
+$('#instance-anonymize').live('click', function() {
+  OpenAnonymizeResourceDialog('../instances/' + $.mobile.pageData.uuid,
+                              'Anonymize this instance?');
+});
+
+$('#study-anonymize').live('click', function() {
+  OpenAnonymizeResourceDialog('../studies/' + $.mobile.pageData.uuid,
+                              'Anonymize this study?');
+});
+
+$('#series-anonymize').live('click', function() {
+  OpenAnonymizeResourceDialog('../series/' + $.mobile.pageData.uuid,
+                              'Anonymize this series?');
+});
+
+$('#patient-anonymize').live('click', function() {
+  OpenAnonymizeResourceDialog('../patients/' + $.mobile.pageData.uuid,
+                              'Anonymize this patient?');
+});
+
+
+$('#plugins').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../plugins',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(plugins) {
+      var target = $('#all-plugins');
+      $('li', target).remove();
+
+      plugins.map(function(id) {
+        return $.ajax({
+          url: '../plugins/' + id,
+          dataType: 'json',
+          async: false,
+          cache: false,
+          success: function(plugin) {
+            var li = $('<li>');
+            var item = li;
+
+            if ('RootUri' in plugin)
+            {
+              item = $('<a>');
+              li.append(item);
+              item.click(function() {
+                window.open(plugin.RootUri);
+              });
+            }
+
+            item.append($('<h1>').text(plugin.ID));
+            item.append($('<p>').text(plugin.Description));
+            item.append($('<span>').addClass('ui-li-count').text(plugin.Version));
+            target.append(li);
+          }
+        });
+      });
+
+      target.listview('refresh');
+    }
+  });
+});
+
+
+
+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');
+      var running, pending, inactive;
+
+      $('li', target).remove();
+
+      running = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Currently running');
+
+      pending = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Pending jobs');
+
+      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() {
+  var pageData, target;
+
+  if ($.mobile.pageData) {
+    pageData = DeepCopy($.mobile.pageData);
+
+    $.ajax({
+      url: '../jobs/' + pageData.uuid,
+      dataType: 'json',
+      async: false,
+      cache: false,
+      success: function(job) {
+        var block, value;
+        
+        target = $('#job-info');
+        $('li', target).remove();
+
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('General information about the job'));
+
+        {                       
+          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'));
+
+        {
+          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-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');
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/file-upload.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,116 @@
+var pendingUploads = [];
+var currentUpload = 0;
+var totalUpload = 0;
+var alreadyInitialized = false; // trying to debug Orthanc issue #1
+
+$(document).ready(function() {
+  if (alreadyInitialized) {
+    console.log("Orthanc issue #1: the fileupload has been initialized twice !");
+  } else {
+    alreadyInitialized = true;
+  }
+  
+  // Initialize the jQuery File Upload widget:
+  $('#fileupload').fileupload({
+    //dataType: 'json',
+    //maxChunkSize: 500,
+    //sequentialUploads: true,
+    limitConcurrentUploads: 3,
+    add: function (e, data) {
+      pendingUploads.push(data);
+    }
+  })
+    .bind('fileuploadstop', function(e, data) {
+      $('#upload-button').removeClass('ui-disabled');
+      //$('#upload-abort').addClass('ui-disabled');
+      $('#progress .bar').css('width', '100%');
+      if ($('#progress .label').text() != 'Failure')
+        $('#progress .label').text('Done');
+    })
+    .bind('fileuploadfail', function(e, data) {
+      $('#progress .bar')
+        .css('width', '100%')
+        .css('background-color', 'red');
+      $('#progress .label').text('Failure');
+    })
+    .bind('fileuploaddrop', function (e, data) {
+      console.log("dropped " + data.files.length + " files: ", data);
+      appendFilesToUploadList(data.files);
+    })
+    .bind('fileuploadsend', function (e, data) {
+      // Update the progress bar. Note: for some weird reason, the
+      // "fileuploadprogressall" does not work under Firefox.
+      var progress = parseInt(currentUpload / totalUploads * 100, 10);
+      currentUpload += 1;
+      $('#progress .label').text('Uploading: ' + progress + '%');
+      $('#progress .bar')
+        .css('width', progress + '%')
+        .css('background-color', 'green');
+    });
+});
+
+function appendFilesToUploadList(files) {
+  var target = $('#upload-list');
+  $.each(files, function (index, file) {
+    target.append('<li class="pending-file">' + file.name + '</li>');
+  });
+  target.listview('refresh');
+}
+
+$('#fileupload').live('change', function (e) {
+  appendFilesToUploadList(e.target.files);
+})
+
+
+function ClearUploadProgress()
+{
+  $('#progress .label').text('');
+  $('#progress .bar').css('width', '0%').css('background-color', '#333');
+}
+
+$('#upload').live('pagebeforeshow', function() {
+  if (navigator.userAgent.toLowerCase().indexOf('firefox') == -1) {
+    $("#issue-21-warning").css('display', 'none');
+  }
+
+  ClearUploadProgress();
+});
+
+$('#upload').live('pageshow', function() {
+  $('#fileupload').fileupload('enable');
+});
+
+$('#upload').live('pagehide', function() {
+  $('#fileupload').fileupload('disable');
+});
+
+
+$('#upload-button').live('click', function() {
+  var pu = pendingUploads;
+  pendingUploads = [];
+
+  $('.pending-file').remove();
+  $('#upload-list').listview('refresh');
+  ClearUploadProgress();
+
+  currentUpload = 1;
+  totalUploads = pu.length + 1;
+  if (pu.length > 0) {
+    $('#upload-button').addClass('ui-disabled');
+    //$('#upload-abort').removeClass('ui-disabled');
+  }
+
+  for (var i = 0; i < pu.length; i++) {
+    pu[i].submit();
+  }
+});
+
+$('#upload-clear').live('click', function() {
+  pendingUploads = [];
+  $('.pending-file').remove();
+  $('#upload-list').listview('refresh');
+});
+
+/*$('#upload-abort').live('click', function() {
+  $('#fileupload').fileupload().abort();
+  });*/
Binary file OrthancServer/OrthancExplorer/images/unsupported.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/date.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,104 @@
+/**
+ * Version: 1.0 Alpha-1 
+ * Build Date: 13-Nov-2007
+ * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved.
+ * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. 
+ * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/
+ */
+Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}};
+Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
+return-1;};Date.getDayNumberFromName=function(name){var n=Date.CultureInfo.dayNames,m=Date.CultureInfo.abbreviatedDayNames,o=Date.CultureInfo.shortestDayNames,s=name.toLowerCase();for(var i=0;i<n.length;i++){if(n[i].toLowerCase()==s||m[i].toLowerCase()==s){return i;}}
+return-1;};Date.isLeapYear=function(year){return(((year%4===0)&&(year%100!==0))||(year%400===0));};Date.getDaysInMonth=function(year,month){return[31,(Date.isLeapYear(year)?29:28),31,30,31,30,31,31,30,31,30,31][month];};Date.getTimezoneOffset=function(s,dst){return(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST[s.toUpperCase()]:Date.CultureInfo.abbreviatedTimeZoneStandard[s.toUpperCase()];};Date.getTimezoneAbbreviation=function(offset,dst){var n=(dst||false)?Date.CultureInfo.abbreviatedTimeZoneDST:Date.CultureInfo.abbreviatedTimeZoneStandard,p;for(p in n){if(n[p]===offset){return p;}}
+return null;};Date.prototype.clone=function(){return new Date(this.getTime());};Date.prototype.compareTo=function(date){if(isNaN(this)){throw new Error(this);}
+if(date instanceof Date&&!isNaN(date)){return(this>date)?1:(this<date)?-1:0;}else{throw new TypeError(date);}};Date.prototype.equals=function(date){return(this.compareTo(date)===0);};Date.prototype.between=function(start,end){var t=this.getTime();return t>=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;}
+var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);}
+if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);}
+if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);}
+if(x.hour||x.hours){this.addHours(x.hour||x.hours);}
+if(x.month||x.months){this.addMonths(x.month||x.months);}
+if(x.year||x.years){this.addYears(x.year||x.years);}
+if(x.day||x.days){this.addDays(x.day||x.days);}
+return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(value<min||value>max){throw new RangeError(value+" is not a valid value for "+name+".");}
+return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;}
+if(!x.second&&x.second!==0){x.second=-1;}
+if(!x.minute&&x.minute!==0){x.minute=-1;}
+if(!x.hour&&x.hour!==0){x.hour=-1;}
+if(!x.day&&x.day!==0){x.day=-1;}
+if(!x.month&&x.month!==0){x.month=-1;}
+if(!x.year&&x.year!==0){x.year=-1;}
+if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());}
+if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());}
+if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());}
+if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());}
+if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());}
+if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());}
+if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());}
+if(x.timezone){this.setTimezone(x.timezone);}
+if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);}
+return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;}
+var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}}
+return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();};
+Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;}
+return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i<dx.length;i++){$D[dx[i]]=$D[dx[i].substring(0,3)]=df(i);}
+var mf=function(n){return function(){if(this._is){this._is=false;return this.getMonth()===n;}
+return this.moveToMonth(n,this._orient);};};for(var j=0;j<mx.length;j++){$D[mx[j]]=$D[mx[j].substring(0,3)]=mf(j);}
+var ef=function(j){return function(){if(j.substring(j.length-1)!="s"){j+="s";}
+return this["add"+j](this._orient);};};var nf=function(n){return function(){this._dateElement=n;return this;};};for(var k=0;k<px.length;k++){de=px[k].toLowerCase();$D[de]=$D[de+"s"]=ef(px[k]);$N[de]=$N[de+"s"]=nf(de);}}());Date.prototype.toJSONString=function(){return this.toString("yyyy-MM-ddThh:mm:ssZ");};Date.prototype.toShortDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortDatePattern);};Date.prototype.toLongDateString=function(){return this.toString(Date.CultureInfo.formatPatterns.longDatePattern);};Date.prototype.toShortTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.shortTimePattern);};Date.prototype.toLongTimeString=function(){return this.toString(Date.CultureInfo.formatPatterns.longTimePattern);};Date.prototype.getOrdinal=function(){switch(this.getDate()){case 1:case 21:case 31:return"st";case 2:case 22:return"nd";case 3:case 23:return"rd";default:return"th";}};
+(function(){Date.Parsing={Exception:function(s){this.message="Parse error at '"+s.substring(0,10)+" ...'";}};var $P=Date.Parsing;var _=$P.Operators={rtoken:function(r){return function(s){var mx=s.match(r);if(mx){return([mx[0],s.substring(mx[0].length)]);}else{throw new $P.Exception(s);}};},token:function(s){return function(s){return _.rtoken(new RegExp("^\s*"+s+"\s*"))(s);};},stoken:function(s){return _.rtoken(new RegExp("^"+s));},until:function(p){return function(s){var qx=[],rx=null;while(s.length){try{rx=p.call(this,s);}catch(e){qx.push(rx[0]);s=rx[1];continue;}
+break;}
+return[qx,s];};},many:function(p){return function(s){var rx=[],r=null;while(s.length){try{r=p.call(this,s);}catch(e){return[rx,s];}
+rx.push(r[0]);s=r[1];}
+return[rx,s];};},optional:function(p){return function(s){var r=null;try{r=p.call(this,s);}catch(e){return[null,s];}
+return[r[0],r[1]];};},not:function(p){return function(s){try{p.call(this,s);}catch(e){return[null,s];}
+throw new $P.Exception(s);};},ignore:function(p){return p?function(s){var r=null;r=p.call(this,s);return[null,r[1]];}:null;},product:function(){var px=arguments[0],qx=Array.prototype.slice.call(arguments,1),rx=[];for(var i=0;i<px.length;i++){rx.push(_.each(px[i],qx));}
+return rx;},cache:function(rule){var cache={},r=null;return function(s){try{r=cache[s]=(cache[s]||rule.call(this,s));}catch(e){r=cache[s]=e;}
+if(r instanceof $P.Exception){throw r;}else{return r;}};},any:function(){var px=arguments;return function(s){var r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
+try{r=(px[i].call(this,s));}catch(e){r=null;}
+if(r){return r;}}
+throw new $P.Exception(s);};},each:function(){var px=arguments;return function(s){var rx=[],r=null;for(var i=0;i<px.length;i++){if(px[i]==null){continue;}
+try{r=(px[i].call(this,s));}catch(e){throw new $P.Exception(s);}
+rx.push(r[0]);s=r[1];}
+return[rx,s];};},all:function(){var px=arguments,_=_;return _.each(_.optional(px));},sequence:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;if(px.length==1){return px[0];}
+return function(s){var r=null,q=null;var rx=[];for(var i=0;i<px.length;i++){try{r=px[i].call(this,s);}catch(e){break;}
+rx.push(r[0]);try{q=d.call(this,r[1]);}catch(ex){q=null;break;}
+s=q[1];}
+if(!r){throw new $P.Exception(s);}
+if(q){throw new $P.Exception(q[1]);}
+if(c){try{r=c.call(this,r[1]);}catch(ey){throw new $P.Exception(r[1]);}}
+return[rx,(r?r[1]:s)];};},between:function(d1,p,d2){d2=d2||d1;var _fn=_.each(_.ignore(d1),p,_.ignore(d2));return function(s){var rx=_fn.call(this,s);return[[rx[0][0],r[0][2]],rx[1]];};},list:function(p,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return(p instanceof Array?_.each(_.product(p.slice(0,-1),_.ignore(d)),p.slice(-1),_.ignore(c)):_.each(_.many(_.each(p,_.ignore(d))),px,_.ignore(c)));},set:function(px,d,c){d=d||_.rtoken(/^\s*/);c=c||null;return function(s){var r=null,p=null,q=null,rx=null,best=[[],s],last=false;for(var i=0;i<px.length;i++){q=null;p=null;r=null;last=(px.length==1);try{r=px[i].call(this,s);}catch(e){continue;}
+rx=[[r[0]],r[1]];if(r[1].length>0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;}
+if(!last&&q[1].length===0){last=true;}
+if(!last){var qx=[];for(var j=0;j<px.length;j++){if(i!=j){qx.push(px[j]);}}
+p=_.set(qx,d).call(this,q[1]);if(p[0].length>0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}}
+if(rx[1].length<best[1].length){best=rx;}
+if(best[1].length===0){break;}}
+if(best[0].length===0){return best;}
+if(c){try{q=c.call(this,best[1]);}catch(ey){throw new $P.Exception(best[1]);}
+best[1]=q[1];}
+return best;};},forward:function(gr,fname){return function(s){return gr[fname].call(this,s);};},replace:function(rule,repl){return function(s){var r=rule.call(this,s);return[repl,r[1]];};},process:function(rule,fn){return function(s){var r=rule.call(this,s);return[fn.call(this,r[0]),r[1]];};},min:function(min,rule){return function(s){var rx=rule.call(this,s);if(rx[0].length<min){throw new $P.Exception(s);}
+return rx;};}};var _generator=function(op){return function(){var args=null,rx=[];if(arguments.length>1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];}
+if(args){for(var i=0,px=args.shift();i<px.length;i++){args.unshift(px[i]);rx.push(op.apply(null,args));args.shift();return rx;}}else{return op.apply(null,arguments);}};};var gx="optional not ignore cache".split(/\s/);for(var i=0;i<gx.length;i++){_[gx[i]]=_generator(_[gx[i]]);}
+var _vector=function(op){return function(){if(arguments[0]instanceof Array){return op.apply(null,arguments[0]);}else{return op.apply(null,arguments);}};};var vx="each any all".split(/\s/);for(var j=0;j<vx.length;j++){_[vx[j]]=_vector(_[vx[j]]);}}());(function(){var flattenAndCompact=function(ax){var rx=[];for(var i=0;i<ax.length;i++){if(ax[i]instanceof Array){rx=rx.concat(flattenAndCompact(ax[i]));}else{if(ax[i]){rx.push(ax[i]);}}}
+return rx;};Date.Grammar={};Date.Translator={hour:function(s){return function(){this.hour=Number(s);};},minute:function(s){return function(){this.minute=Number(s);};},second:function(s){return function(){this.second=Number(s);};},meridian:function(s){return function(){this.meridian=s.slice(0,1).toLowerCase();};},timezone:function(s){return function(){var n=s.replace(/[^\d\+\-]/g,"");if(n.length){this.timezoneOffset=Number(n);}else{this.timezone=s.toLowerCase();}};},day:function(x){var s=x[0];return function(){this.day=Number(s.match(/\d+/)[0]);};},month:function(s){return function(){this.month=((s.length==3)?Date.getMonthNumberFromName(s):(Number(s)-1));};},year:function(s){return function(){var n=Number(s);this.year=((s.length>2)?n:(n+(((n+2000)<Date.CultureInfo.twoDigitYearMax)?2000:1900)));};},rday:function(s){return function(){switch(s){case"yesterday":this.days=-1;break;case"tomorrow":this.days=1;break;case"today":this.days=0;break;case"now":this.days=0;this.now=true;break;}};},finishExact:function(x){x=(x instanceof Array)?x:[x];var now=new Date();this.year=now.getFullYear();this.month=now.getMonth();this.day=1;this.hour=0;this.minute=0;this.second=0;for(var i=0;i<x.length;i++){if(x[i]){x[i].call(this);}}
+this.hour=(this.meridian=="p"&&this.hour<13)?this.hour+12:this.hour;if(this.day>Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");}
+var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});}
+return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;}
+for(var i=0;i<x.length;i++){if(typeof x[i]=="function"){x[i].call(this);}}
+if(this.now){return new Date();}
+var today=Date.today();var method=null;var expression=!!(this.days!=null||this.orient||this.operator);if(expression){var gap,mod,orient;orient=((this.orient=="past"||this.operator=="subtract")?-1:1);if(this.weekday){this.unit="day";gap=(Date.getDayNumberFromName(this.weekday)-today.getDay());mod=7;this.days=gap?((gap+(orient*mod))%mod):(orient*mod);}
+if(this.month){this.unit="month";gap=(this.month-today.getMonth());mod=12;this.months=gap?((gap+(orient*mod))%mod):(orient*mod);this.month=null;}
+if(!this.unit){this.unit="day";}
+if(this[this.unit+"s"]==null||this.operator!=null){if(!this.value){this.value=1;}
+if(this.unit=="week"){this.unit="day";this.value=this.value*7;}
+this[this.unit+"s"]=this.value*orient;}
+return today.add(this);}else{if(this.meridian&&this.hour){this.hour=(this.hour<13&&this.meridian=="p")?this.hour+12:this.hour;}
+if(this.weekday&&!this.day){this.day=(today.addDays((Date.getDayNumberFromName(this.weekday)-today.getDay()))).getDate();}
+if(this.month&&!this.day){this.day=1;}
+return today.set(this);}}};var _=Date.Parsing.Operators,g=Date.Grammar,t=Date.Translator,_fn;g.datePartDelimiter=_.rtoken(/^([\s\-\.\,\/\x27]+)/);g.timePartDelimiter=_.stoken(":");g.whiteSpace=_.rtoken(/^\s*/);g.generalDelimiter=_.rtoken(/^(([\s\,]|at|on)+)/);var _C={};g.ctoken=function(keys){var fn=_C[keys];if(!fn){var c=Date.CultureInfo.regexPatterns;var kx=keys.split(/\s+/),px=[];for(var i=0;i<kx.length;i++){px.push(_.replace(_.rtoken(c[kx[i]]),kx[i]));}
+fn=_C[keys]=_.any.apply(null,px);}
+return fn;};g.ctoken2=function(key){return _.rtoken(Date.CultureInfo.regexPatterns[key]);};g.h=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2]|[1-9])/),t.hour));g.hh=_.cache(_.process(_.rtoken(/^(0[0-9]|1[0-2])/),t.hour));g.H=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3]|[0-9])/),t.hour));g.HH=_.cache(_.process(_.rtoken(/^([0-1][0-9]|2[0-3])/),t.hour));g.m=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.minute));g.mm=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.minute));g.s=_.cache(_.process(_.rtoken(/^([0-5][0-9]|[0-9])/),t.second));g.ss=_.cache(_.process(_.rtoken(/^[0-5][0-9]/),t.second));g.hms=_.cache(_.sequence([g.H,g.mm,g.ss],g.timePartDelimiter));g.t=_.cache(_.process(g.ctoken2("shortMeridian"),t.meridian));g.tt=_.cache(_.process(g.ctoken2("longMeridian"),t.meridian));g.z=_.cache(_.process(_.rtoken(/^(\+|\-)?\s*\d\d\d\d?/),t.timezone));g.zz=_.cache(_.process(_.rtoken(/^(\+|\-)\s*\d\d\d\d/),t.timezone));g.zzz=_.cache(_.process(g.ctoken2("timezone"),t.timezone));g.timeSuffix=_.each(_.ignore(g.whiteSpace),_.set([g.tt,g.zzz]));g.time=_.each(_.optional(_.ignore(_.stoken("T"))),g.hms,g.timeSuffix);g.d=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1]|\d)/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.dd=_.cache(_.process(_.each(_.rtoken(/^([0-2]\d|3[0-1])/),_.optional(g.ctoken2("ordinalSuffix"))),t.day));g.ddd=g.dddd=_.cache(_.process(g.ctoken("sun mon tue wed thu fri sat"),function(s){return function(){this.weekday=s;};}));g.M=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d|\d)/),t.month));g.MM=_.cache(_.process(_.rtoken(/^(1[0-2]|0\d)/),t.month));g.MMM=g.MMMM=_.cache(_.process(g.ctoken("jan feb mar apr may jun jul aug sep oct nov dec"),t.month));g.y=_.cache(_.process(_.rtoken(/^(\d\d?)/),t.year));g.yy=_.cache(_.process(_.rtoken(/^(\d\d)/),t.year));g.yyy=_.cache(_.process(_.rtoken(/^(\d\d?\d?\d?)/),t.year));g.yyyy=_.cache(_.process(_.rtoken(/^(\d\d\d\d)/),t.year));_fn=function(){return _.each(_.any.apply(null,arguments),_.not(g.ctoken2("timeContext")));};g.day=_fn(g.d,g.dd);g.month=_fn(g.M,g.MMM);g.year=_fn(g.yyyy,g.yy);g.orientation=_.process(g.ctoken("past future"),function(s){return function(){this.orient=s;};});g.operator=_.process(g.ctoken("add subtract"),function(s){return function(){this.operator=s;};});g.rday=_.process(g.ctoken("yesterday tomorrow today now"),t.rday);g.unit=_.process(g.ctoken("minute hour day week month year"),function(s){return function(){this.unit=s;};});g.value=_.process(_.rtoken(/^\d\d?(st|nd|rd|th)?/),function(s){return function(){this.value=s.replace(/\D/g,"");};});g.expression=_.set([g.rday,g.operator,g.value,g.unit,g.orientation,g.ddd,g.MMM]);_fn=function(){return _.set(arguments,g.datePartDelimiter);};g.mdy=_fn(g.ddd,g.month,g.day,g.year);g.ymd=_fn(g.ddd,g.year,g.month,g.day);g.dmy=_fn(g.ddd,g.day,g.month,g.year);g.date=function(s){return((g[Date.CultureInfo.dateElementOrder]||g.mdy).call(this,s));};g.format=_.process(_.many(_.any(_.process(_.rtoken(/^(dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?)/),function(fmt){if(g[fmt]){return g[fmt];}else{throw Date.Parsing.Exception(fmt);}}),_.process(_.rtoken(/^[^dMyhHmstz]+/),function(s){return _.ignore(_.stoken(s));}))),function(rules){return _.process(_.each.apply(null,rules),t.finishExact);});var _F={};var _get=function(f){return _F[f]=(_F[f]||g.format(f)[0]);};g.formats=function(fx){if(fx instanceof Array){var rx=[];for(var i=0;i<fx.length;i++){rx.push(_get(fx[i]));}
+return _.any.apply(null,rx);}else{return _get(fx);}};g._formats=g.formats(["yyyy-MM-ddTHH:mm:ss","ddd, MMM dd, yyyy H:mm:ss tt","ddd MMM d yyyy HH:mm:ss zzz","d"]);g._start=_.process(_.set([g.date,g.time,g.expression],g.generalDelimiter,g.whiteSpace),t.finish);g.start=function(s){try{var r=g._formats.call({},s);if(r[1].length===0){return r;}}catch(e){}
+return g._start.call({},s);};}());Date._parse=Date.parse;Date.parse=function(s){var r=null;if(!s){return null;}
+try{r=Date.Grammar.start.call({},s);}catch(e){return null;}
+return((r[1].length===0)?r[0]:null);};Date.getParseFunction=function(fx){var fn=Date.Grammar.formats(fx);return function(s){var r=null;try{r=fn.call({},s);}catch(e){return null;}
+return((r[1].length===0)?r[0]:null);};};Date.parseExact=function(s,fx){return Date.getParseFunction(fx)(s);};
Binary file OrthancServer/OrthancExplorer/libs/images/ajax-loader.gif has changed
Binary file OrthancServer/OrthancExplorer/libs/images/icons-18-black.png has changed
Binary file OrthancServer/OrthancExplorer/libs/images/icons-18-white.png has changed
Binary file OrthancServer/OrthancExplorer/libs/images/icons-36-black.png has changed
Binary file OrthancServer/OrthancExplorer/libs/images/icons-36-white.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/images/notes.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,5 @@
+* The "ajax-loader.gif" was generated by the http://www.ajaxload.info/
+  Web site. "The Generated gifs can be used, modified and distributed
+  under the terms of the ​Do What The Fuck You Want To Public License."
+
+* The images "icons*" come from jQuery Mobile.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jqm.page.params.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,113 @@
+// jqm.page.params.js - version 0.1
+// Copyright (c) 2011, Kin Blas
+// 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 the <organization> 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 <COPYRIGHT HOLDER> 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.
+
+(function( $, window, undefined ) {
+
+// Given a query string, convert all the name/value pairs
+// into a property/value object. If a name appears more than
+// once in a query string, the value is automatically turned
+// into an array.
+function queryStringToObject( qstr )
+{
+	var result = {},
+		nvPairs = ( ( qstr || "" ).replace( /^\?/, "" ).split( /&/ ) ),
+		i, pair, n, v;
+
+	for ( i = 0; i < nvPairs.length; i++ ) {
+		var pstr = nvPairs[ i ];
+		if ( pstr ) {
+			pair = pstr.split( /=/ );
+			n = pair[ 0 ];
+			v = pair[ 1 ];
+			if ( result[ n ] === undefined ) {
+				result[ n ] = v;
+			} else {
+				if ( typeof result[ n ] !== "object" ) {
+					result[ n ] = [ result[ n ] ];
+				}
+				result[ n ].push( v );
+			}
+		}
+	}
+
+	return result;
+}
+
+// The idea here is to listen for any pagebeforechange notifications from
+// jQuery Mobile, and then muck with the toPage and options so that query
+// params can be passed to embedded/internal pages. So for example, if a
+// changePage() request for a URL like:
+//
+//    http://mycompany.com/myapp/#page-1?foo=1&bar=2
+//
+// is made, the page that will actually get shown is:
+//
+//    http://mycompany.com/myapp/#page-1
+//
+// The browser's location will still be updated to show the original URL.
+// The query params for the embedded page are also added as a property/value
+// object on the options object. You can access it from your page notifications
+// via data.options.pageData.
+$( document ).bind( "pagebeforechange", function( e, data ) {
+
+	// We only want to handle the case where we are being asked
+	// to go to a page by URL, and only if that URL is referring
+	// to an internal page by id.
+
+	if ( typeof data.toPage === "string" ) {
+		var u = $.mobile.path.parseUrl( data.toPage );
+		if ( $.mobile.path.isEmbeddedPage( u ) ) {
+			// The request is for an internal page, if the hash
+			// contains query (search) params, strip them off the
+			// toPage URL and then set options.dataUrl appropriately
+			// so the location.hash shows the originally requested URL
+			// that hash the query params in the hash.
+
+			var u2 = $.mobile.path.parseUrl( u.hash.replace( /^#/, "" ) );
+			if ( u2.search ) {
+				if ( !data.options.dataUrl ) {
+					data.options.dataUrl = data.toPage;
+				}
+				data.options.pageData = queryStringToObject( u2.search );
+				data.toPage = u.hrefNoHash + "#" + u2.pathname;
+			}
+		}
+	}
+});
+
+})( jQuery, window );
+
+
+
+
+// http://stackoverflow.com/a/8295488
+$(document).bind("pagebeforechange", function( event, data ) {
+  $.mobile.pageData = (data && data.options && data.options.pageData)
+    ? data.options.pageData
+    : {};
+
+  $.mobile.pageData.active = data.toPage[0].id;
+});
Binary file OrthancServer/OrthancExplorer/libs/jqtree-icons.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jqtree.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,143 @@
+ul.tree {
+    margin-left: 12px;
+}
+
+ul.tree,
+ul.tree ul {
+    list-style: none outside;
+    margin-bottom: 0;
+    padding: 0;
+}
+
+ul.tree ul {
+    display: block;
+    margin-left: 12px;
+    margin-right: 0;
+}
+
+ul.tree li.closed > ul {
+    display: none;
+}
+
+ul.tree li {
+    clear: both;
+}
+
+ul.tree .toggler {
+    background-image: url(jqtree-icons.png);
+    background-repeat: no-repeat;
+    background-position: -8px 0;
+    width: 8px;
+    height: 8px;
+    display: block;
+    position: absolute;
+    left: -12px;
+    top: 30%;
+    text-indent: -9999px;
+    border-bottom: none;
+}
+
+ul.tree div {
+    cursor: pointer;
+}
+
+ul.tree .title {
+    color: #1C4257;
+    vertical-align: middle;
+}
+
+ul.tree li.folder {
+    margin-bottom: 4px;
+}
+
+ul.tree li.folder.closed {
+    margin-bottom: 1px;
+}
+
+ul.tree li.folder .title {
+    margin-left: 0;
+}
+
+ul.tree .toggler.closed {
+    background-position: 0 0;
+}
+
+span.tree-dragging {
+    color: #fff;
+    background: #000;
+    opacity: 0.6;
+    cursor: pointer;
+    padding: 2px 8px;
+}
+
+ul.tree li.ghost {
+    position: relative;
+    z-index: 10;
+    margin-right: 10px;
+}
+
+ul.tree li.ghost span {
+    display: block;
+}
+
+ul.tree li.ghost span.circle {
+    background-image: url(jqtree-icons.png);
+    background-repeat: no-repeat;
+    background-position: 0 -8px;
+    height: 8px;
+    width: 8px;
+    position: absolute;
+    top: -4px;
+    left: 2px;
+}
+
+ul.tree li.ghost span.line {
+    background-color: #0000ff;
+    height: 2px;
+    padding: 0;
+    position: absolute;
+    top: -1px;
+    left: 10px;
+    width: 100%;
+}
+
+ul.tree li.ghost.inside {
+    margin-left: 48px;
+}
+
+ul.tree span.tree-hit {
+    position: absolute;
+    display: block;
+}
+
+ul.tree span.border {
+    position: absolute;
+    display: block;
+    left: -2px;
+    top: 0;
+    border: solid 2px #0000ff;
+    -webkit-border-radius: 6px;
+    -moz-border-radius: 6px;
+    border-radius: 6px;
+    margin: 0;
+}
+
+ul.tree div {
+    width: 100%; /* todo: why is this in here? */
+    *width: auto; /* ie7 fix; issue 41 */
+    position: relative;
+}
+
+ul.tree li.selected > div,
+ul.tree li.selected > div:hover {
+    background-color: #97BDD6;
+    background: -webkit-gradient(linear, left top, left bottom, from(#BEE0F5), to(#89AFCA));
+    background: -moz-linear-gradient(top, #BEE0F5, #89AFCA);
+    background: -ms-linear-gradient(top, #BEE0F5, #89AFCA);
+    background: -o-linear-gradient(top, #BEE0F5, #89AFCA);
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
+}
+
+ul.tree .moving > div .title {
+    outline: dashed 1px #0000ff;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/css/jquery.fileupload-ui.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,84 @@
+@charset 'UTF-8';
+/*
+ * jQuery File Upload UI Plugin CSS 6.3
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+.fileinput-button {
+  position: relative;
+  overflow: hidden;
+  float: left;
+  margin-right: 4px;
+}
+.fileinput-button input {
+  position: absolute;
+  top: 0;
+  right: 0;
+  margin: 0;
+  border: solid transparent;
+  border-width: 0 0 100px 200px;
+  opacity: 0;
+  filter: alpha(opacity=0);
+  -moz-transform: translate(-300px, 0) scale(4);
+  direction: ltr;
+  cursor: pointer;
+}
+.fileupload-buttonbar .btn,
+.fileupload-buttonbar .toggle {
+  margin-bottom: 5px;
+}
+.files .progress {
+  width: 200px;
+}
+.progress-animated .bar {
+  background: url(../img/progressbar.gif) !important;
+  filter: none;
+}
+.fileupload-loading {
+  position: absolute;
+  left: 50%;
+  width: 128px;
+  height: 128px;
+  background: url(../img/loading.gif) center no-repeat;
+  display: none;
+}
+.fileupload-processing .fileupload-loading {
+  display: block;
+}
+
+/* Fix for IE 6: */
+*html .fileinput-button {
+  line-height: 22px;
+  margin: 1px -3px 0 0;
+}
+
+/* Fix for IE 7: */
+*+html .fileinput-button {
+  margin: 1px 0 0 0;
+}
+
+@media (max-width: 480px) {
+  .files .btn span {
+    display: none;
+  }
+  .files .preview * {
+    width: 40px;
+  }
+  .files .name * {
+    width: 80px;
+    display: inline-block;
+    word-wrap: break-word;
+  }
+  .files .progress {
+    width: 20px;
+  }
+  .files .delete {
+    width: 60px;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/css/style.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,15 @@
+@charset 'UTF-8';
+/*
+ * jQuery File Upload Plugin CSS Example 1.0
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2012, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+body{
+  padding-top: 60px;
+}
Binary file OrthancServer/OrthancExplorer/libs/jquery-file-upload/img/loading.gif has changed
Binary file OrthancServer/OrthancExplorer/libs/jquery-file-upload/img/progressbar.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.postmessage-transport.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,117 @@
+/*
+ * jQuery postMessage Transport Plugin 1.1
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/*jslint unparam: true, nomen: true */
+/*global define, window, document */
+
+(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define(['jquery'], factory);
+    } else {
+        // Browser globals:
+        factory(window.jQuery);
+    }
+}(function ($) {
+    'use strict';
+
+    var counter = 0,
+        names = [
+            'accepts',
+            'cache',
+            'contents',
+            'contentType',
+            'crossDomain',
+            'data',
+            'dataType',
+            'headers',
+            'ifModified',
+            'mimeType',
+            'password',
+            'processData',
+            'timeout',
+            'traditional',
+            'type',
+            'url',
+            'username'
+        ],
+        convert = function (p) {
+            return p;
+        };
+
+    $.ajaxSetup({
+        converters: {
+            'postmessage text': convert,
+            'postmessage json': convert,
+            'postmessage html': convert
+        }
+    });
+
+    $.ajaxTransport('postmessage', function (options) {
+        if (options.postMessage && window.postMessage) {
+            var iframe,
+                loc = $('<a>').prop('href', options.postMessage)[0],
+                target = loc.protocol + '//' + loc.host,
+                xhrUpload = options.xhr().upload;
+            return {
+                send: function (_, completeCallback) {
+                    var message = {
+                            id: 'postmessage-transport-' + (counter += 1)
+                        },
+                        eventName = 'message.' + message.id;
+                    iframe = $(
+                        '<iframe style="display:none;" src="' +
+                            options.postMessage + '" name="' +
+                            message.id + '"></iframe>'
+                    ).bind('load', function () {
+                        $.each(names, function (i, name) {
+                            message[name] = options[name];
+                        });
+                        message.dataType = message.dataType.replace('postmessage ', '');
+                        $(window).bind(eventName, function (e) {
+                            e = e.originalEvent;
+                            var data = e.data,
+                                ev;
+                            if (e.origin === target && data.id === message.id) {
+                                if (data.type === 'progress') {
+                                    ev = document.createEvent('Event');
+                                    ev.initEvent(data.type, false, true);
+                                    $.extend(ev, data);
+                                    xhrUpload.dispatchEvent(ev);
+                                } else {
+                                    completeCallback(
+                                        data.status,
+                                        data.statusText,
+                                        {postmessage: data.result},
+                                        data.headers
+                                    );
+                                    iframe.remove();
+                                    $(window).unbind(eventName);
+                                }
+                            }
+                        });
+                        iframe[0].contentWindow.postMessage(
+                            message,
+                            target
+                        );
+                    }).appendTo(document.body);
+                },
+                abort: function () {
+                    if (iframe) {
+                        iframe.remove();
+                    }
+                }
+            };
+        }
+    });
+
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/cors/jquery.xdr-transport.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,85 @@
+/*
+ * jQuery XDomainRequest Transport Plugin 1.1.2
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ *
+ * Based on Julian Aubourg's ajaxHooks xdr.js:
+ * https://github.com/jaubourg/ajaxHooks/
+ */
+
+/*jslint unparam: true */
+/*global define, window, XDomainRequest */
+
+(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define(['jquery'], factory);
+    } else {
+        // Browser globals:
+        factory(window.jQuery);
+    }
+}(function ($) {
+    'use strict';
+    if (window.XDomainRequest && !$.support.cors) {
+        $.ajaxTransport(function (s) {
+            if (s.crossDomain && s.async) {
+                if (s.timeout) {
+                    s.xdrTimeout = s.timeout;
+                    delete s.timeout;
+                }
+                var xdr;
+                return {
+                    send: function (headers, completeCallback) {
+                        function callback(status, statusText, responses, responseHeaders) {
+                            xdr.onload = xdr.onerror = xdr.ontimeout = $.noop;
+                            xdr = null;
+                            completeCallback(status, statusText, responses, responseHeaders);
+                        }
+                        xdr = new XDomainRequest();
+                        // XDomainRequest only supports GET and POST:
+                        if (s.type === 'DELETE') {
+                            s.url = s.url + (/\?/.test(s.url) ? '&' : '?') +
+                                '_method=DELETE';
+                            s.type = 'POST';
+                        } else if (s.type === 'PUT') {
+                            s.url = s.url + (/\?/.test(s.url) ? '&' : '?') +
+                                '_method=PUT';
+                            s.type = 'POST';
+                        }
+                        xdr.open(s.type, s.url);
+                        xdr.onload = function () {
+                            callback(
+                                200,
+                                'OK',
+                                {text: xdr.responseText},
+                                'Content-Type: ' + xdr.contentType
+                            );
+                        };
+                        xdr.onerror = function () {
+                            callback(404, 'Not Found');
+                        };
+                        if (s.xdrTimeout) {
+                            xdr.ontimeout = function () {
+                                callback(0, 'timeout');
+                            };
+                            xdr.timeout = s.xdrTimeout;
+                        }
+                        xdr.send((s.hasContent && s.data) || null);
+                    },
+                    abort: function () {
+                        if (xdr) {
+                            xdr.onerror = $.noop();
+                            xdr.abort();
+                        }
+                    }
+                };
+            }
+        });
+    }
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-fp.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,219 @@
+/*
+ * jQuery File Upload File Processing Plugin 1.0
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2012, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/*jslint nomen: true, unparam: true, regexp: true */
+/*global define, window, document */
+
+(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define([
+            'jquery',
+            'load-image',
+            'canvas-to-blob',
+            './jquery.fileupload'
+        ], factory);
+    } else {
+        // Browser globals:
+        factory(
+            window.jQuery,
+            window.loadImage
+        );
+    }
+}(function ($, loadImage) {
+    'use strict';
+
+    // The File Upload IP version extends the basic fileupload widget
+    // with file processing functionality:
+    $.widget('blueimpFP.fileupload', $.blueimp.fileupload, {
+
+        options: {
+            // The list of file processing actions:
+            process: [
+            /*
+                {
+                    action: 'load',
+                    fileTypes: /^image\/(gif|jpeg|png)$/,
+                    maxFileSize: 20000000 // 20MB
+                },
+                {
+                    action: 'resize',
+                    maxWidth: 1920,
+                    maxHeight: 1200,
+                    minWidth: 800,
+                    minHeight: 600
+                },
+                {
+                    action: 'save'
+                }
+            */
+            ],
+
+            // The add callback is invoked as soon as files are added to the
+            // fileupload widget (via file input selection, drag & drop or add
+            // API call). See the basic file upload widget for more information:
+            add: function (e, data) {
+                $(this).fileupload('process', data).done(function () {
+                    data.submit();
+                });
+            }
+        },
+
+        processActions: {
+            // Loads the image given via data.files and data.index
+            // as canvas element.
+            // Accepts the options fileTypes (regular expression)
+            // and maxFileSize (integer) to limit the files to load:
+            load: function (data, options) {
+                var that = this,
+                    file = data.files[data.index],
+                    dfd = $.Deferred();
+                if (window.HTMLCanvasElement &&
+                        window.HTMLCanvasElement.prototype.toBlob &&
+                        ($.type(options.maxFileSize) !== 'number' ||
+                            file.size < options.maxFileSize) &&
+                        (!options.fileTypes ||
+                            options.fileTypes.test(file.type))) {
+                    loadImage(
+                        file,
+                        function (canvas) {
+                            data.canvas = canvas;
+                            dfd.resolveWith(that, [data]);
+                        },
+                        {canvas: true}
+                    );
+                } else {
+                    dfd.rejectWith(that, [data]);
+                }
+                return dfd.promise();
+            },
+            // Resizes the image given as data.canvas and updates
+            // data.canvas with the resized image.
+            // Accepts the options maxWidth, maxHeight, minWidth and
+            // minHeight to scale the given image:
+            resize: function (data, options) {
+                if (data.canvas) {
+                    var canvas = loadImage.scale(data.canvas, options);
+                    if (canvas.width !== data.canvas.width ||
+                            canvas.height !== data.canvas.height) {
+                        data.canvas = canvas;
+                        data.processed = true;
+                    }
+                }
+                return data;
+            },
+            // Saves the processed image given as data.canvas
+            // inplace at data.index of data.files:
+            save: function (data, options) {
+                // Do nothing if no processing has happened:
+                if (!data.canvas || !data.processed) {
+                    return data;
+                }
+                var that = this,
+                    file = data.files[data.index],
+                    name = file.name,
+                    dfd = $.Deferred(),
+                    callback = function (blob) {
+                        if (!blob.name) {
+                            if (file.type === blob.type) {
+                                blob.name = file.name;
+                            } else if (file.name) {
+                                blob.name = file.name.replace(
+                                    /\..+$/,
+                                    '.' + blob.type.substr(6)
+                                );
+                            }
+                        }
+                        // Store the created blob at the position
+                        // of the original file in the files list:
+                        data.files[data.index] = blob;
+                        dfd.resolveWith(that, [data]);
+                    };
+                // Use canvas.mozGetAsFile directly, to retain the filename, as
+                // Gecko doesn't support the filename option for FormData.append:
+                if (data.canvas.mozGetAsFile) {
+                    callback(data.canvas.mozGetAsFile(
+                        (/^image\/(jpeg|png)$/.test(file.type) && name) ||
+                            ((name && name.replace(/\..+$/, '')) ||
+                                'blob') + '.png',
+                        file.type
+                    ));
+                } else {
+                    data.canvas.toBlob(callback, file.type);
+                }
+                return dfd.promise();
+            }
+        },
+
+        // Resizes the file at the given index and stores the created blob at
+        // the original position of the files list, returns a Promise object:
+        _processFile: function (files, index, options) {
+            var that = this,
+                dfd = $.Deferred().resolveWith(that, [{
+                    files: files,
+                    index: index
+                }]),
+                chain = dfd.promise();
+            that._processing += 1;
+            $.each(options.process, function (i, settings) {
+                chain = chain.pipe(function (data) {
+                    return that.processActions[settings.action]
+                        .call(this, data, settings);
+                });
+            });
+            chain.always(function () {
+                that._processing -= 1;
+                if (that._processing === 0) {
+                    that.element
+                        .removeClass('fileupload-processing');
+                }
+            });
+            if (that._processing === 1) {
+                that.element.addClass('fileupload-processing');
+            }
+            return chain;
+        },
+
+        // Processes the files given as files property of the data parameter,
+        // returns a Promise object that allows one to bind a done handler, which
+        // will be invoked after processing all files (inplace) is done:
+        process: function (data) {
+            var that = this,
+                options = $.extend({}, this.options, data);
+            if (options.process && options.process.length &&
+                    this._isXHRUpload(options)) {
+                $.each(data.files, function (index, file) {
+                    that._processingQueue = that._processingQueue.pipe(
+                        function () {
+                            var dfd = $.Deferred();
+                            that._processFile(data.files, index, options)
+                                .always(function () {
+                                    dfd.resolveWith(that);
+                                });
+                            return dfd.promise();
+                        }
+                    );
+                });
+            }
+            return this._processingQueue;
+        },
+
+        _create: function () {
+            $.blueimp.fileupload.prototype._create.call(this);
+            this._processing = 0;
+            this._processingQueue = $.Deferred().resolveWith(this)
+                .promise();
+        }
+
+    });
+
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload-ui.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,702 @@
+/*
+ * jQuery File Upload User Interface Plugin 6.9.1
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/*jslint nomen: true, unparam: true, regexp: true */
+/*global define, window, document, URL, webkitURL, FileReader */
+
+(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define([
+            'jquery',
+            'tmpl',
+            'load-image',
+            './jquery.fileupload-fp'
+        ], factory);
+    } else {
+        // Browser globals:
+        factory(
+            window.jQuery,
+            window.tmpl,
+            window.loadImage
+        );
+    }
+}(function ($, tmpl, loadImage) {
+    'use strict';
+
+    // The UI version extends the FP (file processing) version or the basic
+    // file upload widget and adds complete user interface interaction:
+    var parentWidget = ($.blueimpFP || $.blueimp).fileupload;
+    $.widget('blueimpUI.fileupload', parentWidget, {
+
+        options: {
+            // By default, files added to the widget are uploaded as soon
+            // as the user clicks on the start buttons. To enable automatic
+            // uploads, set the following option to true:
+            autoUpload: false,
+            // The following option limits the number of files that are
+            // allowed to be uploaded using this widget:
+            maxNumberOfFiles: undefined,
+            // The maximum allowed file size:
+            maxFileSize: undefined,
+            // The minimum allowed file size:
+            minFileSize: undefined,
+            // The regular expression for allowed file types, matches
+            // against either file type or file name:
+            acceptFileTypes:  /.+$/i,
+            // The regular expression to define for which files a preview
+            // image is shown, matched against the file type:
+            previewSourceFileTypes: /^image\/(gif|jpeg|png)$/,
+            // The maximum file size of images that are to be displayed as preview:
+            previewSourceMaxFileSize: 5000000, // 5MB
+            // The maximum width of the preview images:
+            previewMaxWidth: 80,
+            // The maximum height of the preview images:
+            previewMaxHeight: 80,
+            // By default, preview images are displayed as canvas elements
+            // if supported by the browser. Set the following option to false
+            // to always display preview images as img elements:
+            previewAsCanvas: true,
+            // The ID of the upload template:
+            uploadTemplateId: 'template-upload',
+            // The ID of the download template:
+            downloadTemplateId: 'template-download',
+            // The container for the list of files. If undefined, it is set to
+            // an element with class "files" inside of the widget element:
+            filesContainer: undefined,
+            // By default, files are appended to the files container.
+            // Set the following option to true, to prepend files instead:
+            prependFiles: false,
+            // The expected data type of the upload response, sets the dataType
+            // option of the $.ajax upload requests:
+            dataType: 'json',
+
+            // The add callback is invoked as soon as files are added to the fileupload
+            // widget (via file input selection, drag & drop or add API call).
+            // See the basic file upload widget for more information:
+            add: function (e, data) {
+                var that = $(this).data('fileupload'),
+                    options = that.options,
+                    files = data.files;
+                $(this).fileupload('process', data).done(function () {
+                    that._adjustMaxNumberOfFiles(-files.length);
+                    data.isAdjusted = true;
+                    data.files.valid = data.isValidated = that._validate(files);
+                    data.context = that._renderUpload(files).data('data', data);
+                    options.filesContainer[
+                        options.prependFiles ? 'prepend' : 'append'
+                    ](data.context);
+                    that._renderPreviews(files, data.context);
+                    that._forceReflow(data.context);
+                    that._transition(data.context).done(
+                        function () {
+                            if ((that._trigger('added', e, data) !== false) &&
+                                    (options.autoUpload || data.autoUpload) &&
+                                    data.autoUpload !== false && data.isValidated) {
+                                data.submit();
+                            }
+                        }
+                    );
+                });
+            },
+            // Callback for the start of each file upload request:
+            send: function (e, data) {
+                var that = $(this).data('fileupload');
+                if (!data.isValidated) {
+                    if (!data.isAdjusted) {
+                        that._adjustMaxNumberOfFiles(-data.files.length);
+                    }
+                    if (!that._validate(data.files)) {
+                        return false;
+                    }
+                }
+                if (data.context && data.dataType &&
+                        data.dataType.substr(0, 6) === 'iframe') {
+                    // Iframe Transport does not support progress events.
+                    // In lack of an indeterminate progress bar, we set
+                    // the progress to 100%, showing the full animated bar:
+                    data.context
+                        .find('.progress').addClass(
+                            !$.support.transition && 'progress-animated'
+                        )
+                        .attr('aria-valuenow', 100)
+                        .find('.bar').css(
+                            'width',
+                            '100%'
+                        );
+                }
+                return that._trigger('sent', e, data);
+            },
+            // Callback for successful uploads:
+            done: function (e, data) {
+                var that = $(this).data('fileupload'),
+                    template;
+                if (data.context) {
+                    data.context.each(function (index) {
+                        var file = ($.isArray(data.result) &&
+                                data.result[index]) || {error: 'emptyResult'};
+                        if (file.error) {
+                            that._adjustMaxNumberOfFiles(1);
+                        }
+                        that._transition($(this)).done(
+                            function () {
+                                var node = $(this);
+                                template = that._renderDownload([file])
+                                    .css('height', node.height())
+                                    .replaceAll(node);
+                                that._forceReflow(template);
+                                that._transition(template).done(
+                                    function () {
+                                        data.context = $(this);
+                                        that._trigger('completed', e, data);
+                                    }
+                                );
+                            }
+                        );
+                    });
+                } else {
+                    template = that._renderDownload(data.result)
+                        .appendTo(that.options.filesContainer);
+                    that._forceReflow(template);
+                    that._transition(template).done(
+                        function () {
+                            data.context = $(this);
+                            that._trigger('completed', e, data);
+                        }
+                    );
+                }
+            },
+            // Callback for failed (abort or error) uploads:
+            fail: function (e, data) {
+                var that = $(this).data('fileupload'),
+                    template;
+                that._adjustMaxNumberOfFiles(data.files.length);
+                if (data.context) {
+                    data.context.each(function (index) {
+                        if (data.errorThrown !== 'abort') {
+                            var file = data.files[index];
+                            file.error = file.error || data.errorThrown ||
+                                true;
+                            that._transition($(this)).done(
+                                function () {
+                                    var node = $(this);
+                                    template = that._renderDownload([file])
+                                        .replaceAll(node);
+                                    that._forceReflow(template);
+                                    that._transition(template).done(
+                                        function () {
+                                            data.context = $(this);
+                                            that._trigger('failed', e, data);
+                                        }
+                                    );
+                                }
+                            );
+                        } else {
+                            that._transition($(this)).done(
+                                function () {
+                                    $(this).remove();
+                                    that._trigger('failed', e, data);
+                                }
+                            );
+                        }
+                    });
+                } else if (data.errorThrown !== 'abort') {
+                    that._adjustMaxNumberOfFiles(-data.files.length);
+                    data.context = that._renderUpload(data.files)
+                        .appendTo(that.options.filesContainer)
+                        .data('data', data);
+                    that._forceReflow(data.context);
+                    that._transition(data.context).done(
+                        function () {
+                            data.context = $(this);
+                            that._trigger('failed', e, data);
+                        }
+                    );
+                } else {
+                    that._trigger('failed', e, data);
+                }
+            },
+            // Callback for upload progress events:
+            progress: function (e, data) {
+                if (data.context) {
+                    var progress = parseInt(data.loaded / data.total * 100, 10);
+                    data.context.find('.progress')
+                        .attr('aria-valuenow', progress)
+                        .find('.bar').css(
+                            'width',
+                            progress + '%'
+                        );
+                }
+            },
+            // Callback for global upload progress events:
+            progressall: function (e, data) {
+                var $this = $(this),
+                    progress = parseInt(data.loaded / data.total * 100, 10),
+                    globalProgressNode = $this.find('.fileupload-progress'),
+                    extendedProgressNode = globalProgressNode
+                        .find('.progress-extended');
+                if (extendedProgressNode.length) {
+                    extendedProgressNode.html(
+                        $this.data('fileupload')._renderExtendedProgress(data)
+                    );
+                }
+                globalProgressNode
+                    .find('.progress')
+                    .attr('aria-valuenow', progress)
+                    .find('.bar').css(
+                        'width',
+                        progress + '%'
+                    );
+            },
+            // Callback for uploads start, equivalent to the global ajaxStart event:
+            start: function (e) {
+                var that = $(this).data('fileupload');
+                that._transition($(this).find('.fileupload-progress')).done(
+                    function () {
+                        that._trigger('started', e);
+                    }
+                );
+            },
+            // Callback for uploads stop, equivalent to the global ajaxStop event:
+            stop: function (e) {
+                var that = $(this).data('fileupload');
+                that._transition($(this).find('.fileupload-progress')).done(
+                    function () {
+                        $(this).find('.progress')
+                            .attr('aria-valuenow', '0')
+                            .find('.bar').css('width', '0%');
+                        $(this).find('.progress-extended').html('&nbsp;');
+                        that._trigger('stopped', e);
+                    }
+                );
+            },
+            // Callback for file deletion:
+            destroy: function (e, data) {
+                var that = $(this).data('fileupload');
+                if (data.url) {
+                    $.ajax(data);
+                    that._adjustMaxNumberOfFiles(1);
+                }
+                that._transition(data.context).done(
+                    function () {
+                        $(this).remove();
+                        that._trigger('destroyed', e, data);
+                    }
+                );
+            }
+        },
+
+        // Link handler, that allows one to download files
+        // by drag & drop of the links to the desktop:
+        _enableDragToDesktop: function () {
+            var link = $(this),
+                url = link.prop('href'),
+                name = link.prop('download'),
+                type = 'application/octet-stream';
+            link.bind('dragstart', function (e) {
+                try {
+                    e.originalEvent.dataTransfer.setData(
+                        'DownloadURL',
+                        [type, name, url].join(':')
+                    );
+                } catch (err) {}
+            });
+        },
+
+        _adjustMaxNumberOfFiles: function (operand) {
+            if (typeof this.options.maxNumberOfFiles === 'number') {
+                this.options.maxNumberOfFiles += operand;
+                if (this.options.maxNumberOfFiles < 1) {
+                    this._disableFileInputButton();
+                } else {
+                    this._enableFileInputButton();
+                }
+            }
+        },
+
+        _formatFileSize: function (bytes) {
+            if (typeof bytes !== 'number') {
+                return '';
+            }
+            if (bytes >= 1000000000) {
+                return (bytes / 1000000000).toFixed(2) + ' GB';
+            }
+            if (bytes >= 1000000) {
+                return (bytes / 1000000).toFixed(2) + ' MB';
+            }
+            return (bytes / 1000).toFixed(2) + ' KB';
+        },
+
+        _formatBitrate: function (bits) {
+            if (typeof bits !== 'number') {
+                return '';
+            }
+            if (bits >= 1000000000) {
+                return (bits / 1000000000).toFixed(2) + ' Gbit/s';
+            }
+            if (bits >= 1000000) {
+                return (bits / 1000000).toFixed(2) + ' Mbit/s';
+            }
+            if (bits >= 1000) {
+                return (bits / 1000).toFixed(2) + ' kbit/s';
+            }
+            return bits + ' bit/s';
+        },
+
+        _formatTime: function (seconds) {
+            var date = new Date(seconds * 1000),
+                days = parseInt(seconds / 86400, 10);
+            days = days ? days + 'd ' : '';
+            return days +
+                ('0' + date.getUTCHours()).slice(-2) + ':' +
+                ('0' + date.getUTCMinutes()).slice(-2) + ':' +
+                ('0' + date.getUTCSeconds()).slice(-2);
+        },
+
+        _formatPercentage: function (floatValue) {
+            return (floatValue * 100).toFixed(2) + ' %';
+        },
+
+        _renderExtendedProgress: function (data) {
+            return this._formatBitrate(data.bitrate) + ' | ' +
+                this._formatTime(
+                    (data.total - data.loaded) * 8 / data.bitrate
+                ) + ' | ' +
+                this._formatPercentage(
+                    data.loaded / data.total
+                ) + ' | ' +
+                this._formatFileSize(data.loaded) + ' / ' +
+                this._formatFileSize(data.total);
+        },
+
+        _hasError: function (file) {
+            if (file.error) {
+                return file.error;
+            }
+            // The number of added files is subtracted from
+            // maxNumberOfFiles before validation, so we check if
+            // maxNumberOfFiles is below 0 (instead of below 1):
+            if (this.options.maxNumberOfFiles < 0) {
+                return 'maxNumberOfFiles';
+            }
+            // Files are accepted if either the file type or the file name
+            // matches against the acceptFileTypes regular expression, as
+            // only browsers with support for the File API report the type:
+            if (!(this.options.acceptFileTypes.test(file.type) ||
+                    this.options.acceptFileTypes.test(file.name))) {
+                return 'acceptFileTypes';
+            }
+            if (this.options.maxFileSize &&
+                    file.size > this.options.maxFileSize) {
+                return 'maxFileSize';
+            }
+            if (typeof file.size === 'number' &&
+                    file.size < this.options.minFileSize) {
+                return 'minFileSize';
+            }
+            return null;
+        },
+
+        _validate: function (files) {
+            var that = this,
+                valid = !!files.length;
+            $.each(files, function (index, file) {
+                file.error = that._hasError(file);
+                if (file.error) {
+                    valid = false;
+                }
+            });
+            return valid;
+        },
+
+        _renderTemplate: function (func, files) {
+            if (!func) {
+                return $();
+            }
+            var result = func({
+                files: files,
+                formatFileSize: this._formatFileSize,
+                options: this.options
+            });
+            if (result instanceof $) {
+                return result;
+            }
+            return $(this.options.templatesContainer).html(result).children();
+        },
+
+        _renderPreview: function (file, node) {
+            var that = this,
+                options = this.options,
+                dfd = $.Deferred();
+            return ((loadImage && loadImage(
+                file,
+                function (img) {
+                    node.append(img);
+                    that._forceReflow(node);
+                    that._transition(node).done(function () {
+                        dfd.resolveWith(node);
+                    });
+                    if (!$.contains(document.body, node[0])) {
+                        // If the element is not part of the DOM,
+                        // transition events are not triggered,
+                        // so we have to resolve manually:
+                        dfd.resolveWith(node);
+                    }
+                },
+                {
+                    maxWidth: options.previewMaxWidth,
+                    maxHeight: options.previewMaxHeight,
+                    canvas: options.previewAsCanvas
+                }
+            )) || dfd.resolveWith(node)) && dfd;
+        },
+
+        _renderPreviews: function (files, nodes) {
+            var that = this,
+                options = this.options;
+            nodes.find('.preview span').each(function (index, element) {
+                var file = files[index];
+                if (options.previewSourceFileTypes.test(file.type) &&
+                        ($.type(options.previewSourceMaxFileSize) !== 'number' ||
+                        file.size < options.previewSourceMaxFileSize)) {
+                    that._processingQueue = that._processingQueue.pipe(function () {
+                        var dfd = $.Deferred();
+                        that._renderPreview(file, $(element)).done(
+                            function () {
+                                dfd.resolveWith(that);
+                            }
+                        );
+                        return dfd.promise();
+                    });
+                }
+            });
+            return this._processingQueue;
+        },
+
+        _renderUpload: function (files) {
+            return this._renderTemplate(
+                this.options.uploadTemplate,
+                files
+            );
+        },
+
+        _renderDownload: function (files) {
+            return this._renderTemplate(
+                this.options.downloadTemplate,
+                files
+            ).find('a[download]').each(this._enableDragToDesktop).end();
+        },
+
+        _startHandler: function (e) {
+            e.preventDefault();
+            var button = $(this),
+                template = button.closest('.template-upload'),
+                data = template.data('data');
+            if (data && data.submit && !data.jqXHR && data.submit()) {
+                button.prop('disabled', true);
+            }
+        },
+
+        _cancelHandler: function (e) {
+            e.preventDefault();
+            var template = $(this).closest('.template-upload'),
+                data = template.data('data') || {};
+            if (!data.jqXHR) {
+                data.errorThrown = 'abort';
+                e.data.fileupload._trigger('fail', e, data);
+            } else {
+                data.jqXHR.abort();
+            }
+        },
+
+        _deleteHandler: function (e) {
+            e.preventDefault();
+            var button = $(this);
+            e.data.fileupload._trigger('destroy', e, {
+                context: button.closest('.template-download'),
+                url: button.attr('data-url'),
+                type: button.attr('data-type') || 'DELETE',
+                dataType: e.data.fileupload.options.dataType
+            });
+        },
+
+        _forceReflow: function (node) {
+            return $.support.transition && node.length &&
+                node[0].offsetWidth;
+        },
+
+        _transition: function (node) {
+            var dfd = $.Deferred();
+            if ($.support.transition && node.hasClass('fade')) {
+                node.bind(
+                    $.support.transition.end,
+                    function (e) {
+                        // Make sure we don't respond to other transitions events
+                        // in the container element, e.g. from button elements:
+                        if (e.target === node[0]) {
+                            node.unbind($.support.transition.end);
+                            dfd.resolveWith(node);
+                        }
+                    }
+                ).toggleClass('in');
+            } else {
+                node.toggleClass('in');
+                dfd.resolveWith(node);
+            }
+            return dfd;
+        },
+
+        _initButtonBarEventHandlers: function () {
+            var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'),
+                filesList = this.options.filesContainer,
+                ns = this.options.namespace;
+            fileUploadButtonBar.find('.start')
+                .bind('click.' + ns, function (e) {
+                    e.preventDefault();
+                    filesList.find('.start button').click();
+                });
+            fileUploadButtonBar.find('.cancel')
+                .bind('click.' + ns, function (e) {
+                    e.preventDefault();
+                    filesList.find('.cancel button').click();
+                });
+            fileUploadButtonBar.find('.delete')
+                .bind('click.' + ns, function (e) {
+                    e.preventDefault();
+                    filesList.find('.delete input:checked')
+                        .siblings('button').click();
+                    fileUploadButtonBar.find('.toggle')
+                        .prop('checked', false);
+                });
+            fileUploadButtonBar.find('.toggle')
+                .bind('change.' + ns, function (e) {
+                    filesList.find('.delete input').prop(
+                        'checked',
+                        $(this).is(':checked')
+                    );
+                });
+        },
+
+        _destroyButtonBarEventHandlers: function () {
+            this.element.find('.fileupload-buttonbar button')
+                .unbind('click.' + this.options.namespace);
+            this.element.find('.fileupload-buttonbar .toggle')
+                .unbind('change.' + this.options.namespace);
+        },
+
+        _initEventHandlers: function () {
+            parentWidget.prototype._initEventHandlers.call(this);
+            var eventData = {fileupload: this};
+            this.options.filesContainer
+                .delegate(
+                    '.start button',
+                    'click.' + this.options.namespace,
+                    eventData,
+                    this._startHandler
+                )
+                .delegate(
+                    '.cancel button',
+                    'click.' + this.options.namespace,
+                    eventData,
+                    this._cancelHandler
+                )
+                .delegate(
+                    '.delete button',
+                    'click.' + this.options.namespace,
+                    eventData,
+                    this._deleteHandler
+                );
+            this._initButtonBarEventHandlers();
+        },
+
+        _destroyEventHandlers: function () {
+            var options = this.options;
+            this._destroyButtonBarEventHandlers();
+            options.filesContainer
+                .undelegate('.start button', 'click.' + options.namespace)
+                .undelegate('.cancel button', 'click.' + options.namespace)
+                .undelegate('.delete button', 'click.' + options.namespace);
+            parentWidget.prototype._destroyEventHandlers.call(this);
+        },
+
+        _enableFileInputButton: function () {
+            this.element.find('.fileinput-button input')
+                .prop('disabled', false)
+                .parent().removeClass('disabled');
+        },
+
+        _disableFileInputButton: function () {
+            this.element.find('.fileinput-button input')
+                .prop('disabled', true)
+                .parent().addClass('disabled');
+        },
+
+        _initTemplates: function () {
+            var options = this.options;
+            options.templatesContainer = document.createElement(
+                options.filesContainer.prop('nodeName')
+            );
+            if (tmpl) {
+                if (options.uploadTemplateId) {
+                    options.uploadTemplate = tmpl(options.uploadTemplateId);
+                }
+                if (options.downloadTemplateId) {
+                    options.downloadTemplate = tmpl(options.downloadTemplateId);
+                }
+            }
+        },
+
+        _initFilesContainer: function () {
+            var options = this.options;
+            if (options.filesContainer === undefined) {
+                options.filesContainer = this.element.find('.files');
+            } else if (!(options.filesContainer instanceof $)) {
+                options.filesContainer = $(options.filesContainer);
+            }
+        },
+
+        _initSpecialOptions: function () {
+            parentWidget.prototype._initSpecialOptions.call(this);
+            this._initFilesContainer();
+            this._initTemplates();
+        },
+
+        _create: function () {
+            parentWidget.prototype._create.call(this);
+            this._refreshOptionsList.push(
+                'filesContainer',
+                'uploadTemplateId',
+                'downloadTemplateId'
+            );
+            if (!$.blueimpFP) {
+                this._processingQueue = $.Deferred().resolveWith(this).promise();
+                this.process = function () {
+                    return this._processingQueue;
+                };
+            }
+        },
+
+        enable: function () {
+            parentWidget.prototype.enable.call(this);
+            this.element.find('input, button').prop('disabled', false);
+            this._enableFileInputButton();
+        },
+
+        disable: function () {
+            this.element.find('input, button').prop('disabled', true);
+            this._disableFileInputButton();
+            parentWidget.prototype.disable.call(this);
+        }
+
+    });
+
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.fileupload.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,968 @@
+/*
+ * jQuery File Upload Plugin 5.12
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2010, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/*jslint nomen: true, unparam: true, regexp: true */
+/*global define, window, document, Blob, FormData, location */
+
+(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define([
+            'jquery',
+            'jquery.ui.widget'
+        ], factory);
+    } else {
+        // Browser globals:
+        factory(window.jQuery);
+    }
+}(function ($) {
+    'use strict';
+
+    // The FileReader API is not actually used, but works as feature detection,
+    // as e.g. Safari supports XHR file uploads via the FormData API,
+    // but not non-multipart XHR file uploads:
+    $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
+    $.support.xhrFormDataFileUpload = !!window.FormData;
+
+    // The fileupload widget listens for change events on file input fields defined
+    // via fileInput setting and paste or drop events of the given dropZone.
+    // In addition to the default jQuery Widget methods, the fileupload widget
+    // exposes the "add" and "send" methods, to add or directly send files using
+    // the fileupload API.
+    // By default, files added via file input selection, paste, drag & drop or
+    // "add" method are uploaded immediately, but it is possible to override
+    // the "add" callback option to queue file uploads.
+    $.widget('blueimp.fileupload', {
+
+        options: {
+            // The namespace used for event handler binding on the dropZone and
+            // fileInput collections.
+            // If not set, the name of the widget ("fileupload") is used.
+            namespace: undefined,
+            // The drop target collection, by the default the complete document.
+            // Set to null or an empty collection to disable drag & drop support:
+            dropZone: $(document),
+            // The file input field collection, that is listened for change events.
+            // If undefined, it is set to the file input fields inside
+            // of the widget element on plugin initialization.
+            // Set to null or an empty collection to disable the change listener.
+            fileInput: undefined,
+            // By default, the file input field is replaced with a clone after
+            // each input field change event. This is required for iframe transport
+            // queues and allows change events to be fired for the same file
+            // selection, but can be disabled by setting the following option to false:
+            replaceFileInput: true,
+            // The parameter name for the file form data (the request argument name).
+            // If undefined or empty, the name property of the file input field is
+            // used, or "files[]" if the file input name property is also empty,
+            // can be a string or an array of strings:
+            paramName: undefined,
+            // By default, each file of a selection is uploaded using an individual
+            // request for XHR type uploads. Set to false to upload file
+            // selections in one request each:
+            singleFileUploads: true,
+            // To limit the number of files uploaded with one XHR request,
+            // set the following option to an integer greater than 0:
+            limitMultiFileUploads: undefined,
+            // Set the following option to true to issue all file upload requests
+            // in a sequential order:
+            sequentialUploads: false,
+            // To limit the number of concurrent uploads,
+            // set the following option to an integer greater than 0:
+            limitConcurrentUploads: undefined,
+            // Set the following option to true to force iframe transport uploads:
+            forceIframeTransport: false,
+            // Set the following option to the location of a redirect url on the
+            // origin server, for cross-domain iframe transport uploads:
+            redirect: undefined,
+            // The parameter name for the redirect url, sent as part of the form
+            // data and set to 'redirect' if this option is empty:
+            redirectParamName: undefined,
+            // Set the following option to the location of a postMessage window,
+            // to enable postMessage transport uploads:
+            postMessage: undefined,
+            // By default, XHR file uploads are sent as multipart/form-data.
+            // The iframe transport is always using multipart/form-data.
+            // Set to false to enable non-multipart XHR uploads:
+            multipart: true,
+            // To upload large files in smaller chunks, set the following option
+            // to a preferred maximum chunk size. If set to 0, null or undefined,
+            // or the browser does not support the required Blob API, files will
+            // be uploaded as a whole.
+            maxChunkSize: undefined,
+            // When a non-multipart upload or a chunked multipart upload has been
+            // aborted, this option can be used to resume the upload by setting
+            // it to the size of the already uploaded bytes. This option is most
+            // useful when modifying the options object inside of the "add" or
+            // "send" callbacks, as the options are cloned for each file upload.
+            uploadedBytes: undefined,
+            // By default, failed (abort or error) file uploads are removed from the
+            // global progress calculation. Set the following option to false to
+            // prevent recalculating the global progress data:
+            recalculateProgress: true,
+            // Interval in milliseconds to calculate and trigger progress events:
+            progressInterval: 100,
+            // Interval in milliseconds to calculate progress bitrate:
+            bitrateInterval: 500,
+
+            // Additional form data to be sent along with the file uploads can be set
+            // using this option, which accepts an array of objects with name and
+            // value properties, a function returning such an array, a FormData
+            // object (for XHR file uploads), or a simple object.
+            // The form of the first fileInput is given as parameter to the function:
+            formData: function (form) {
+                return form.serializeArray();
+            },
+
+            // The add callback is invoked as soon as files are added to the fileupload
+            // widget (via file input selection, drag & drop, paste or add API call).
+            // If the singleFileUploads option is enabled, this callback will be
+            // called once for each file in the selection for XHR file uploads, else
+            // once for each file selection.
+            // The upload starts when the submit method is invoked on the data parameter.
+            // The data object contains a files property holding the added files
+            // and allows one to override plugin options as well as define ajax settings.
+            // Listeners for this callback can also be bound the following way:
+            // .bind('fileuploadadd', func);
+            // data.submit() returns a Promise object and allows one to attach additional
+            // handlers using jQuery's Deferred callbacks:
+            // data.submit().done(func).fail(func).always(func);
+            add: function (e, data) {
+                data.submit();
+            },
+
+            // Other callbacks:
+            // Callback for the submit event of each file upload:
+            // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
+            // Callback for the start of each file upload request:
+            // send: function (e, data) {}, // .bind('fileuploadsend', func);
+            // Callback for successful uploads:
+            // done: function (e, data) {}, // .bind('fileuploaddone', func);
+            // Callback for failed (abort or error) uploads:
+            // fail: function (e, data) {}, // .bind('fileuploadfail', func);
+            // Callback for completed (success, abort or error) requests:
+            // always: function (e, data) {}, // .bind('fileuploadalways', func);
+            // Callback for upload progress events:
+            // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
+            // Callback for global upload progress events:
+            // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
+            // Callback for uploads start, equivalent to the global ajaxStart event:
+            // start: function (e) {}, // .bind('fileuploadstart', func);
+            // Callback for uploads stop, equivalent to the global ajaxStop event:
+            // stop: function (e) {}, // .bind('fileuploadstop', func);
+            // Callback for change events of the fileInput collection:
+            // change: function (e, data) {}, // .bind('fileuploadchange', func);
+            // Callback for paste events to the dropZone collection:
+            // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
+            // Callback for drop events of the dropZone collection:
+            // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
+            // Callback for dragover events of the dropZone collection:
+            // dragover: function (e) {}, // .bind('fileuploaddragover', func);
+
+            // The plugin options are used as settings object for the ajax calls.
+            // The following are jQuery ajax settings required for the file uploads:
+            processData: false,
+            contentType: false,
+            cache: false
+        },
+
+        // A list of options that require a refresh after assigning a new value:
+        _refreshOptionsList: [
+            'namespace',
+            'dropZone',
+            'fileInput',
+            'multipart',
+            'forceIframeTransport'
+        ],
+
+        _BitrateTimer: function () {
+            this.timestamp = +(new Date());
+            this.loaded = 0;
+            this.bitrate = 0;
+            this.getBitrate = function (now, loaded, interval) {
+                var timeDiff = now - this.timestamp;
+                if (!this.bitrate || !interval || timeDiff > interval) {
+                    this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
+                    this.loaded = loaded;
+                    this.timestamp = now;
+                }
+                return this.bitrate;
+            };
+        },
+
+        _isXHRUpload: function (options) {
+            return !options.forceIframeTransport &&
+                ((!options.multipart && $.support.xhrFileUpload) ||
+                $.support.xhrFormDataFileUpload);
+        },
+
+        _getFormData: function (options) {
+            var formData;
+            if (typeof options.formData === 'function') {
+                return options.formData(options.form);
+            }
+			if ($.isArray(options.formData)) {
+                return options.formData;
+            }
+			if (options.formData) {
+                formData = [];
+                $.each(options.formData, function (name, value) {
+                    formData.push({name: name, value: value});
+                });
+                return formData;
+            }
+            return [];
+        },
+
+        _getTotal: function (files) {
+            var total = 0;
+            $.each(files, function (index, file) {
+                total += file.size || 1;
+            });
+            return total;
+        },
+
+        _onProgress: function (e, data) {
+            if (e.lengthComputable) {
+                var now = +(new Date()),
+                    total,
+                    loaded;
+                if (data._time && data.progressInterval &&
+                        (now - data._time < data.progressInterval) &&
+                        e.loaded !== e.total) {
+                    return;
+                }
+                data._time = now;
+                total = data.total || this._getTotal(data.files);
+                loaded = parseInt(
+                    e.loaded / e.total * (data.chunkSize || total),
+                    10
+                ) + (data.uploadedBytes || 0);
+                this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
+                data.lengthComputable = true;
+                data.loaded = loaded;
+                data.total = total;
+                data.bitrate = data._bitrateTimer.getBitrate(
+                    now,
+                    loaded,
+                    data.bitrateInterval
+                );
+                // Trigger a custom progress event with a total data property set
+                // to the file size(s) of the current upload and a loaded data
+                // property calculated accordingly:
+                this._trigger('progress', e, data);
+                // Trigger a global progress event for all current file uploads,
+                // including ajax calls queued for sequential file uploads:
+                this._trigger('progressall', e, {
+                    lengthComputable: true,
+                    loaded: this._loaded,
+                    total: this._total,
+                    bitrate: this._bitrateTimer.getBitrate(
+                        now,
+                        this._loaded,
+                        data.bitrateInterval
+                    )
+                });
+            }
+        },
+
+        _initProgressListener: function (options) {
+            var that = this,
+                xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
+            // Accesss to the native XHR object is required to add event listeners
+            // for the upload progress event:
+            if (xhr.upload) {
+                $(xhr.upload).bind('progress', function (e) {
+                    var oe = e.originalEvent;
+                    // Make sure the progress event properties get copied over:
+                    e.lengthComputable = oe.lengthComputable;
+                    e.loaded = oe.loaded;
+                    e.total = oe.total;
+                    that._onProgress(e, options);
+                });
+                options.xhr = function () {
+                    return xhr;
+                };
+            }
+        },
+
+        _initXHRData: function (options) {
+            var formData,
+                file = options.files[0],
+                // Ignore non-multipart setting if not supported:
+                multipart = options.multipart || !$.support.xhrFileUpload,
+                paramName = options.paramName[0];
+            if (!multipart || options.blob) {
+                // For non-multipart uploads and chunked uploads,
+                // file meta data is not part of the request body,
+                // so we transmit this data as part of the HTTP headers.
+                // For cross domain requests, these headers must be allowed
+                // via Access-Control-Allow-Headers or removed using
+                // the beforeSend callback:
+                options.headers = $.extend(options.headers, {
+                    'X-File-Name': file.name,
+                    'X-File-Type': file.type,
+                    'X-File-Size': file.size
+                });
+                if (!options.blob) {
+                    // Non-chunked non-multipart upload:
+                    options.contentType = file.type;
+                    options.data = file;
+                } else if (!multipart) {
+                    // Chunked non-multipart upload:
+                    options.contentType = 'application/octet-stream';
+                    options.data = options.blob;
+                }
+            }
+            if (multipart && $.support.xhrFormDataFileUpload) {
+                if (options.postMessage) {
+                    // window.postMessage does not allow sending FormData
+                    // objects, so we just add the File/Blob objects to
+                    // the formData array and let the postMessage window
+                    // create the FormData object out of this array:
+                    formData = this._getFormData(options);
+                    if (options.blob) {
+                        formData.push({
+                            name: paramName,
+                            value: options.blob
+                        });
+                    } else {
+                        $.each(options.files, function (index, file) {
+                            formData.push({
+                                name: options.paramName[index] || paramName,
+                                value: file
+                            });
+                        });
+                    }
+                } else {
+                    if (options.formData instanceof FormData) {
+                        formData = options.formData;
+                    } else {
+                        formData = new FormData();
+                        $.each(this._getFormData(options), function (index, field) {
+                            formData.append(field.name, field.value);
+                        });
+                    }
+                    if (options.blob) {
+                        formData.append(paramName, options.blob, file.name);
+                    } else {
+                        $.each(options.files, function (index, file) {
+                            // File objects are also Blob instances.
+                            // This check allows the tests to run with
+                            // dummy objects:
+                            if (file instanceof Blob) {
+                                formData.append(
+                                    options.paramName[index] || paramName,
+                                    file,
+                                    file.name
+                                );
+                            }
+                        });
+                    }
+                }
+                options.data = formData;
+            }
+            // Blob reference is not needed anymore, free memory:
+            options.blob = null;
+        },
+
+        _initIframeSettings: function (options) {
+            // Setting the dataType to iframe enables the iframe transport:
+            options.dataType = 'iframe ' + (options.dataType || '');
+            // The iframe transport accepts a serialized array as form data:
+            options.formData = this._getFormData(options);
+            // Add redirect url to form data on cross-domain uploads:
+            if (options.redirect && $('<a></a>').prop('href', options.url)
+                    .prop('host') !== location.host) {
+                options.formData.push({
+                    name: options.redirectParamName || 'redirect',
+                    value: options.redirect
+                });
+            }
+        },
+
+        _initDataSettings: function (options) {
+            if (this._isXHRUpload(options)) {
+                if (!this._chunkedUpload(options, true)) {
+                    if (!options.data) {
+                        this._initXHRData(options);
+                    }
+                    this._initProgressListener(options);
+                }
+                if (options.postMessage) {
+                    // Setting the dataType to postmessage enables the
+                    // postMessage transport:
+                    options.dataType = 'postmessage ' + (options.dataType || '');
+                }
+            } else {
+                this._initIframeSettings(options, 'iframe');
+            }
+        },
+
+        _getParamName: function (options) {
+            var fileInput = $(options.fileInput),
+                paramName = options.paramName;
+            if (!paramName) {
+                paramName = [];
+                fileInput.each(function () {
+                    var input = $(this),
+                        name = input.prop('name') || 'files[]',
+                        i = (input.prop('files') || [1]).length;
+                    while (i) {
+                        paramName.push(name);
+                        i -= 1;
+                    }
+                });
+                if (!paramName.length) {
+                    paramName = [fileInput.prop('name') || 'files[]'];
+                }
+            } else if (!$.isArray(paramName)) {
+                paramName = [paramName];
+            }
+            return paramName;
+        },
+
+        _initFormSettings: function (options) {
+            // Retrieve missing options from the input field and the
+            // associated form, if available:
+            if (!options.form || !options.form.length) {
+                options.form = $(options.fileInput.prop('form'));
+            }
+            options.paramName = this._getParamName(options);
+            if (!options.url) {
+                options.url = options.form.prop('action') || location.href;
+            }
+            // The HTTP request method must be "POST" or "PUT":
+            options.type = (options.type || options.form.prop('method') || '')
+                .toUpperCase();
+            if (options.type !== 'POST' && options.type !== 'PUT') {
+                options.type = 'POST';
+            }
+        },
+
+        _getAJAXSettings: function (data) {
+            var options = $.extend({}, this.options, data);
+            this._initFormSettings(options);
+            this._initDataSettings(options);
+            return options;
+        },
+
+        // Maps jqXHR callbacks to the equivalent
+        // methods of the given Promise object:
+        _enhancePromise: function (promise) {
+            promise.success = promise.done;
+            promise.error = promise.fail;
+            promise.complete = promise.always;
+            return promise;
+        },
+
+        // Creates and returns a Promise object enhanced with
+        // the jqXHR methods abort, success, error and complete:
+        _getXHRPromise: function (resolveOrReject, context, args) {
+            var dfd = $.Deferred(),
+                promise = dfd.promise();
+            context = context || this.options.context || promise;
+            if (resolveOrReject === true) {
+                dfd.resolveWith(context, args);
+            } else if (resolveOrReject === false) {
+                dfd.rejectWith(context, args);
+            }
+            promise.abort = dfd.promise;
+            return this._enhancePromise(promise);
+        },
+
+        // Uploads a file in multiple, sequential requests
+        // by splitting the file up in multiple blob chunks.
+        // If the second parameter is true, only tests if the file
+        // should be uploaded in chunks, but does not invoke any
+        // upload requests:
+        _chunkedUpload: function (options, testOnly) {
+            var that = this,
+                file = options.files[0],
+                fs = file.size,
+                ub = options.uploadedBytes = options.uploadedBytes || 0,
+                mcs = options.maxChunkSize || fs,
+                // Use the Blob methods with the slice implementation
+                // according to the W3C Blob API specification:
+                slice = file.webkitSlice || file.mozSlice || file.slice,
+                upload,
+                n,
+                jqXHR,
+                pipe;
+            if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
+                    options.data) {
+                return false;
+            }
+            if (testOnly) {
+                return true;
+            }
+            if (ub >= fs) {
+                file.error = 'uploadedBytes';
+                return this._getXHRPromise(
+                    false,
+                    options.context,
+                    [null, 'error', file.error]
+                );
+            }
+            // n is the number of blobs to upload,
+            // calculated via filesize, uploaded bytes and max chunk size:
+            n = Math.ceil((fs - ub) / mcs);
+            // The chunk upload method accepting the chunk number as parameter:
+            upload = function (i) {
+                if (!i) {
+                    return that._getXHRPromise(true, options.context);
+                }
+                // Upload the blobs in sequential order:
+                return upload(i -= 1).pipe(function () {
+                    // Clone the options object for each chunk upload:
+                    var o = $.extend({}, options);
+                    o.blob = slice.call(
+                        file,
+                        ub + i * mcs,
+                        ub + (i + 1) * mcs
+                    );
+                    // Store the current chunk size, as the blob itself
+                    // will be dereferenced after data processing:
+                    o.chunkSize = o.blob.size;
+                    // Process the upload data (the blob and potential form data):
+                    that._initXHRData(o);
+                    // Add progress listeners for this chunk upload:
+                    that._initProgressListener(o);
+                    jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
+                        .done(function () {
+                            // Create a progress event if upload is done and
+                            // no progress event has been invoked for this chunk:
+                            if (!o.loaded) {
+                                that._onProgress($.Event('progress', {
+                                    lengthComputable: true,
+                                    loaded: o.chunkSize,
+                                    total: o.chunkSize
+                                }), o);
+                            }
+                            options.uploadedBytes = o.uploadedBytes +=
+                                o.chunkSize;
+                        });
+                    return jqXHR;
+                });
+            };
+            // Return the piped Promise object, enhanced with an abort method,
+            // which is delegated to the jqXHR object of the current upload,
+            // and jqXHR callbacks mapped to the equivalent Promise methods:
+            pipe = upload(n);
+            pipe.abort = function () {
+                return jqXHR.abort();
+            };
+            return this._enhancePromise(pipe);
+        },
+
+        _beforeSend: function (e, data) {
+            if (this._active === 0) {
+                // the start callback is triggered when an upload starts
+                // and no other uploads are currently running,
+                // equivalent to the global ajaxStart event:
+                this._trigger('start');
+                // Set timer for global bitrate progress calculation:
+                this._bitrateTimer = new this._BitrateTimer();
+            }
+            this._active += 1;
+            // Initialize the global progress values:
+            this._loaded += data.uploadedBytes || 0;
+            this._total += this._getTotal(data.files);
+        },
+
+        _onDone: function (result, textStatus, jqXHR, options) {
+            if (!this._isXHRUpload(options)) {
+                // Create a progress event for each iframe load:
+                this._onProgress($.Event('progress', {
+                    lengthComputable: true,
+                    loaded: 1,
+                    total: 1
+                }), options);
+            }
+            options.result = result;
+            options.textStatus = textStatus;
+            options.jqXHR = jqXHR;
+            this._trigger('done', null, options);
+        },
+
+        _onFail: function (jqXHR, textStatus, errorThrown, options) {
+            options.jqXHR = jqXHR;
+            options.textStatus = textStatus;
+            options.errorThrown = errorThrown;
+            this._trigger('fail', null, options);
+            if (options.recalculateProgress) {
+                // Remove the failed (error or abort) file upload from
+                // the global progress calculation:
+                this._loaded -= options.loaded || options.uploadedBytes || 0;
+                this._total -= options.total || this._getTotal(options.files);
+            }
+        },
+
+        _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
+            this._active -= 1;
+            options.textStatus = textStatus;
+            if (jqXHRorError && jqXHRorError.always) {
+                options.jqXHR = jqXHRorError;
+                options.result = jqXHRorResult;
+            } else {
+                options.jqXHR = jqXHRorResult;
+                options.errorThrown = jqXHRorError;
+            }
+            this._trigger('always', null, options);
+            if (this._active === 0) {
+                // The stop callback is triggered when all uploads have
+                // been completed, equivalent to the global ajaxStop event:
+                this._trigger('stop');
+                // Reset the global progress values:
+                this._loaded = this._total = 0;
+                this._bitrateTimer = null;
+            }
+        },
+
+        _onSend: function (e, data) {
+            var that = this,
+                jqXHR,
+                slot,
+                pipe,
+                options = that._getAJAXSettings(data),
+                send = function (resolve, args) {
+                    that._sending += 1;
+                    // Set timer for bitrate progress calculation:
+                    options._bitrateTimer = new that._BitrateTimer();
+                    jqXHR = jqXHR || (
+                        (resolve !== false &&
+                        that._trigger('send', e, options) !== false &&
+                        (that._chunkedUpload(options) || $.ajax(options))) ||
+                        that._getXHRPromise(false, options.context, args)
+                    ).done(function (result, textStatus, jqXHR) {
+                        that._onDone(result, textStatus, jqXHR, options);
+                    }).fail(function (jqXHR, textStatus, errorThrown) {
+                        that._onFail(jqXHR, textStatus, errorThrown, options);
+                    }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
+                        that._sending -= 1;
+                        that._onAlways(
+                            jqXHRorResult,
+                            textStatus,
+                            jqXHRorError,
+                            options
+                        );
+                        if (options.limitConcurrentUploads &&
+                                options.limitConcurrentUploads > that._sending) {
+                            // Start the next queued upload,
+                            // that has not been aborted:
+                            var nextSlot = that._slots.shift();
+                            while (nextSlot) {
+                                if (!nextSlot.isRejected()) {
+                                    nextSlot.resolve();
+                                    break;
+                                }
+                                nextSlot = that._slots.shift();
+                            }
+                        }
+                    });
+                    return jqXHR;
+                };
+            this._beforeSend(e, options);
+            if (this.options.sequentialUploads ||
+                    (this.options.limitConcurrentUploads &&
+                    this.options.limitConcurrentUploads <= this._sending)) {
+                if (this.options.limitConcurrentUploads > 1) {
+                    slot = $.Deferred();
+                    this._slots.push(slot);
+                    pipe = slot.pipe(send);
+                } else {
+                    pipe = (this._sequence = this._sequence.pipe(send, send));
+                }
+                // Return the piped Promise object, enhanced with an abort method,
+                // which is delegated to the jqXHR object of the current upload,
+                // and jqXHR callbacks mapped to the equivalent Promise methods:
+                pipe.abort = function () {
+                    var args = [undefined, 'abort', 'abort'];
+                    if (!jqXHR) {
+                        if (slot) {
+                            slot.rejectWith(args);
+                        }
+                        return send(false, args);
+                    }
+                    return jqXHR.abort();
+                };
+                return this._enhancePromise(pipe);
+            }
+            return send();
+        },
+
+        _onAdd: function (e, data) {
+            var that = this,
+                result = true,
+                options = $.extend({}, this.options, data),
+                limit = options.limitMultiFileUploads,
+                paramName = this._getParamName(options),
+                paramNameSet,
+                paramNameSlice,
+                fileSet,
+                i;
+            if (!(options.singleFileUploads || limit) ||
+                    !this._isXHRUpload(options)) {
+                fileSet = [data.files];
+                paramNameSet = [paramName];
+            } else if (!options.singleFileUploads && limit) {
+                fileSet = [];
+                paramNameSet = [];
+                for (i = 0; i < data.files.length; i += limit) {
+                    fileSet.push(data.files.slice(i, i + limit));
+                    paramNameSlice = paramName.slice(i, i + limit);
+                    if (!paramNameSlice.length) {
+                        paramNameSlice = paramName;
+                    }
+                    paramNameSet.push(paramNameSlice);
+                }
+            } else {
+                paramNameSet = paramName;
+            }
+            data.originalFiles = data.files;
+            $.each(fileSet || data.files, function (index, element) {
+                var newData = $.extend({}, data);
+                newData.files = fileSet ? element : [element];
+                newData.paramName = paramNameSet[index];
+                newData.submit = function () {
+                    newData.jqXHR = this.jqXHR =
+                        (that._trigger('submit', e, this) !== false) &&
+                        that._onSend(e, this);
+                    return this.jqXHR;
+                };
+                return (result = that._trigger('add', e, newData));
+            });
+            return result;
+        },
+
+        // File Normalization for Gecko 1.9.1 (Firefox 3.5) support:
+        _normalizeFile: function (index, file) {
+            if (file.name === undefined && file.size === undefined) {
+                file.name = file.fileName;
+                file.size = file.fileSize;
+            }
+        },
+
+        _replaceFileInput: function (input) {
+            var inputClone = input.clone(true);
+            $('<form></form>').append(inputClone)[0].reset();
+            // Detaching allows one to insert the fileInput on another form
+            // without loosing the file input value:
+            input.after(inputClone).detach();
+            // Avoid memory leaks with the detached file input:
+            $.cleanData(input.unbind('remove'));
+            // Replace the original file input element in the fileInput
+            // collection with the clone, which has been copied including
+            // event handlers:
+            this.options.fileInput = this.options.fileInput.map(function (i, el) {
+                if (el === input[0]) {
+                    return inputClone[0];
+                }
+                return el;
+            });
+            // If the widget has been initialized on the file input itself,
+            // override this.element with the file input clone:
+            if (input[0] === this.element[0]) {
+                this.element = inputClone;
+            }
+        },
+
+        _getFileInputFiles: function (fileInput) {
+            fileInput = $(fileInput);
+            var files = $.each($.makeArray(fileInput.prop('files')), this._normalizeFile),
+                value;
+            if (!files.length) {
+                value = fileInput.prop('value');
+                if (!value) {
+                    return [];
+                }
+                // If the files property is not available, the browser does not
+                // support the File API and we add a pseudo File object with
+                // the input value as name with path information removed:
+                files = [{name: value.replace(/^.*\\/, '')}];
+            }
+            return files;
+        },
+
+        _onChange: function (e) {
+            var that = e.data.fileupload,
+                data = {
+                    fileInput: $(e.target),
+                    form: $(e.target.form)
+                };
+            data.files = that._getFileInputFiles(data.fileInput);
+            if (that.options.replaceFileInput) {
+                that._replaceFileInput(data.fileInput);
+            }
+            if (that._trigger('change', e, data) === false ||
+                    that._onAdd(e, data) === false) {
+                return false;
+            }
+        },
+
+        _onPaste: function (e) {
+            var that = e.data.fileupload,
+                cbd = e.originalEvent.clipboardData,
+                items = (cbd && cbd.items) || [],
+                data = {files: []};
+            $.each(items, function (index, item) {
+                var file = item.getAsFile && item.getAsFile();
+                if (file) {
+                    data.files.push(file);
+                }
+            });
+            if (that._trigger('paste', e, data) === false ||
+                    that._onAdd(e, data) === false) {
+                return false;
+            }
+        },
+
+        _onDrop: function (e) {
+            var that = e.data.fileupload,
+                dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
+                data = {
+                    files: $.each(
+                        $.makeArray(dataTransfer && dataTransfer.files),
+                        that._normalizeFile
+                    )
+                };
+            if (that._trigger('drop', e, data) === false ||
+                    that._onAdd(e, data) === false) {
+                return false;
+            }
+            e.preventDefault();
+        },
+
+        _onDragOver: function (e) {
+            var that = e.data.fileupload,
+                dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
+            if (that._trigger('dragover', e) === false) {
+                return false;
+            }
+            if (dataTransfer) {
+                dataTransfer.dropEffect = 'copy';
+            }
+            e.preventDefault();
+        },
+
+        _initEventHandlers: function () {
+            var ns = this.options.namespace;
+            if (this._isXHRUpload(this.options)) {
+                this.options.dropZone
+                    .bind('dragover.' + ns, {fileupload: this}, this._onDragOver)
+                    .bind('drop.' + ns, {fileupload: this}, this._onDrop)
+                    .bind('paste.' + ns, {fileupload: this}, this._onPaste);
+            }
+            this.options.fileInput
+                .bind('change.' + ns, {fileupload: this}, this._onChange);
+        },
+
+        _destroyEventHandlers: function () {
+            var ns = this.options.namespace;
+            this.options.dropZone
+                .unbind('dragover.' + ns, this._onDragOver)
+                .unbind('drop.' + ns, this._onDrop)
+                .unbind('paste.' + ns, this._onPaste);
+            this.options.fileInput
+                .unbind('change.' + ns, this._onChange);
+        },
+
+        _setOption: function (key, value) {
+            var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
+            if (refresh) {
+                this._destroyEventHandlers();
+            }
+            $.Widget.prototype._setOption.call(this, key, value);
+            if (refresh) {
+                this._initSpecialOptions();
+                this._initEventHandlers();
+            }
+        },
+
+        _initSpecialOptions: function () {
+            var options = this.options;
+            if (options.fileInput === undefined) {
+                options.fileInput = this.element.is('input:file') ?
+                        this.element : this.element.find('input:file');
+            } else if (!(options.fileInput instanceof $)) {
+                options.fileInput = $(options.fileInput);
+            }
+            if (!(options.dropZone instanceof $)) {
+                options.dropZone = $(options.dropZone);
+            }
+        },
+
+        _create: function () {
+            var options = this.options;
+            // Initialize options set via HTML5 data-attributes:
+            $.extend(options, $(this.element[0].cloneNode(false)).data());
+            options.namespace = options.namespace || this.widgetName;
+            this._initSpecialOptions();
+            this._slots = [];
+            this._sequence = this._getXHRPromise(true);
+            this._sending = this._active = this._loaded = this._total = 0;
+            this._initEventHandlers();
+        },
+
+        destroy: function () {
+            this._destroyEventHandlers();
+            $.Widget.prototype.destroy.call(this);
+        },
+
+        enable: function () {
+            $.Widget.prototype.enable.call(this);
+            this._initEventHandlers();
+        },
+
+        disable: function () {
+            this._destroyEventHandlers();
+            $.Widget.prototype.disable.call(this);
+        },
+
+        // This method is exposed to the widget API and allows adding files
+        // using the fileupload API. The data parameter accepts an object which
+        // must have a files property and can contain additional options:
+        // .fileupload('add', {files: filesList});
+        add: function (data) {
+            if (!data || this.options.disabled) {
+                return;
+            }
+            if (data.fileInput && !data.files) {
+                data.files = this._getFileInputFiles(data.fileInput);
+            } else {
+                data.files = $.each($.makeArray(data.files), this._normalizeFile);
+            }
+            this._onAdd(null, data);
+        },
+
+        // This method is exposed to the widget API and allows sending files
+        // using the fileupload API. The data parameter accepts an object which
+        // must have a files property and can contain additional options:
+        // .fileupload('send', {files: filesList});
+        // The method returns a Promise object for the file upload call.
+        send: function (data) {
+            if (data && !this.options.disabled) {
+                if (data.fileInput && !data.files) {
+                    data.files = this._getFileInputFiles(data.fileInput);
+                } else {
+                    data.files = $.each($.makeArray(data.files), this._normalizeFile);
+                }
+                if (data.files.length) {
+                    return this._onSend(null, data);
+                }
+            }
+            return this._getXHRPromise(false, data && data.context);
+        }
+
+    });
+
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/jquery.iframe-transport.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,171 @@
+/*
+ * jQuery Iframe Transport Plugin 1.4
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/*jslint unparam: true, nomen: true */
+/*global define, window, document */
+
+(function (factory) {
+    'use strict';
+    if (typeof define === 'function' && define.amd) {
+        // Register as an anonymous AMD module:
+        define(['jquery'], factory);
+    } else {
+        // Browser globals:
+        factory(window.jQuery);
+    }
+}(function ($) {
+    'use strict';
+
+    // Helper variable to create unique names for the transport iframes:
+    var counter = 0;
+
+    // The iframe transport accepts three additional options:
+    // options.fileInput: a jQuery collection of file input fields
+    // options.paramName: the parameter name for the file form data,
+    //  overrides the name property of the file input field(s),
+    //  can be a string or an array of strings.
+    // options.formData: an array of objects with name and value properties,
+    //  equivalent to the return data of .serializeArray(), e.g.:
+    //  [{name: 'a', value: 1}, {name: 'b', value: 2}]
+    $.ajaxTransport('iframe', function (options) {
+        if (options.async && (options.type === 'POST' || options.type === 'GET')) {
+            var form,
+                iframe;
+            return {
+                send: function (_, completeCallback) {
+                    form = $('<form style="display:none;"></form>');
+                    // javascript:false as initial iframe src
+                    // prevents warning popups on HTTPS in IE6.
+                    // IE versions below IE8 cannot set the name property of
+                    // elements that have already been added to the DOM,
+                    // so we set the name along with the iframe HTML markup:
+                    iframe = $(
+                        '<iframe src="javascript:false;" name="iframe-transport-' +
+                            (counter += 1) + '"></iframe>'
+                    ).bind('load', function () {
+                        var fileInputClones,
+                            paramNames = $.isArray(options.paramName) ?
+                                    options.paramName : [options.paramName];
+                        iframe
+                            .unbind('load')
+                            .bind('load', function () {
+                                var response;
+                                // Wrap in a try/catch block to catch exceptions thrown
+                                // when trying to access cross-domain iframe contents:
+                                try {
+                                    response = iframe.contents();
+                                    // Google Chrome and Firefox do not throw an
+                                    // exception when calling iframe.contents() on
+                                    // cross-domain requests, so we unify the response:
+                                    if (!response.length || !response[0].firstChild) {
+                                        throw new Error();
+                                    }
+                                } catch (e) {
+                                    response = undefined;
+                                }
+                                // The complete callback returns the
+                                // iframe content document as response object:
+                                completeCallback(
+                                    200,
+                                    'success',
+                                    {'iframe': response}
+                                );
+                                // Fix for IE endless progress bar activity bug
+                                // (happens on form submits to iframe targets):
+                                $('<iframe src="javascript:false;"></iframe>')
+                                    .appendTo(form);
+                                form.remove();
+                            });
+                        form
+                            .prop('target', iframe.prop('name'))
+                            .prop('action', options.url)
+                            .prop('method', options.type);
+                        if (options.formData) {
+                            $.each(options.formData, function (index, field) {
+                                $('<input type="hidden"/>')
+                                    .prop('name', field.name)
+                                    .val(field.value)
+                                    .appendTo(form);
+                            });
+                        }
+                        if (options.fileInput && options.fileInput.length &&
+                                options.type === 'POST') {
+                            fileInputClones = options.fileInput.clone();
+                            // Insert a clone for each file input field:
+                            options.fileInput.after(function (index) {
+                                return fileInputClones[index];
+                            });
+                            if (options.paramName) {
+                                options.fileInput.each(function (index) {
+                                    $(this).prop(
+                                        'name',
+                                        paramNames[index] || options.paramName
+                                    );
+                                });
+                            }
+                            // Appending the file input fields to the hidden form
+                            // removes them from their original location:
+                            form
+                                .append(options.fileInput)
+                                .prop('enctype', 'multipart/form-data')
+                                // enctype must be set as encoding for IE:
+                                .prop('encoding', 'multipart/form-data');
+                        }
+                        form.submit();
+                        // Insert the file input fields at their original location
+                        // by replacing the clones with the originals:
+                        if (fileInputClones && fileInputClones.length) {
+                            options.fileInput.each(function (index, input) {
+                                var clone = $(fileInputClones[index]);
+                                $(input).prop('name', clone.prop('name'));
+                                clone.replaceWith(input);
+                            });
+                        }
+                    });
+                    form.append(iframe).appendTo(document.body);
+                },
+                abort: function () {
+                    if (iframe) {
+                        // javascript:false as iframe src aborts the request
+                        // and prevents warning popups on HTTPS in IE6.
+                        // concat is used to avoid the "Script URL" JSLint error:
+                        iframe
+                            .unbind('load')
+                            .prop('src', 'javascript'.concat(':false;'));
+                    }
+                    if (form) {
+                        form.remove();
+                    }
+                }
+            };
+        }
+    });
+
+    // The iframe transport returns the iframe content document as response.
+    // The following adds converters from iframe to text, json, html, and script:
+    $.ajaxSetup({
+        converters: {
+            'iframe text': function (iframe) {
+                return $(iframe[0].body).text();
+            },
+            'iframe json': function (iframe) {
+                return $.parseJSON($(iframe[0].body).text());
+            },
+            'iframe html': function (iframe) {
+                return $(iframe[0].body).html();
+            },
+            'iframe script': function (iframe) {
+                return $.globalEval($(iframe[0].body).text());
+            }
+        }
+    });
+
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/locale.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,29 @@
+/*
+ * jQuery File Upload Plugin Localization Example 6.5.1
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2012, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ */
+
+/*global window */
+
+window.locale = {
+    "fileupload": {
+        "errors": {
+            "maxFileSize": "File is too big",
+            "minFileSize": "File is too small",
+            "acceptFileTypes": "Filetype not allowed",
+            "maxNumberOfFiles": "Max number of files exceeded",
+            "uploadedBytes": "Uploaded bytes exceed file size",
+            "emptyResult": "Empty file upload result"
+        },
+        "error": "Error",
+        "start": "Start",
+        "cancel": "Cancel",
+        "destroy": "Delete"
+    }
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery-file-upload/js/vendor/jquery.ui.widget.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,282 @@
+/*
+ * jQuery UI Widget 1.8.18+amd
+ * https://github.com/blueimp/jQuery-File-Upload
+ *
+ * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Widget
+ */
+
+(function (factory) {
+    if (typeof define === "function" && define.amd) {
+        // Register as an anonymous AMD module:
+        define(["jquery"], factory);
+    } else {
+        // Browser globals:
+        factory(jQuery);
+    }
+}(function( $, undefined ) {
+
+// jQuery 1.4+
+if ( $.cleanData ) {
+	var _cleanData = $.cleanData;
+	$.cleanData = function( elems ) {
+		for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+			try {
+				$( elem ).triggerHandler( "remove" );
+			// http://bugs.jquery.com/ticket/8235
+			} catch( e ) {}
+		}
+		_cleanData( elems );
+	};
+} else {
+	var _remove = $.fn.remove;
+	$.fn.remove = function( selector, keepData ) {
+		return this.each(function() {
+			if ( !keepData ) {
+				if ( !selector || $.filter( selector, [ this ] ).length ) {
+					$( "*", this ).add( [ this ] ).each(function() {
+						try {
+							$( this ).triggerHandler( "remove" );
+						// http://bugs.jquery.com/ticket/8235
+						} catch( e ) {}
+					});
+				}
+			}
+			return _remove.call( $(this), selector, keepData );
+		});
+	};
+}
+
+$.widget = function( name, base, prototype ) {
+	var namespace = name.split( "." )[ 0 ],
+		fullName;
+	name = name.split( "." )[ 1 ];
+	fullName = namespace + "-" + name;
+
+	if ( !prototype ) {
+		prototype = base;
+		base = $.Widget;
+	}
+
+	// create selector for plugin
+	$.expr[ ":" ][ fullName ] = function( elem ) {
+		return !!$.data( elem, name );
+	};
+
+	$[ namespace ] = $[ namespace ] || {};
+	$[ namespace ][ name ] = function( options, element ) {
+		// allow instantiation without initializing for simple inheritance
+		if ( arguments.length ) {
+			this._createWidget( options, element );
+		}
+	};
+
+	var basePrototype = new base();
+	// we need to make the options hash a property directly on the new instance
+	// otherwise we'll modify the options hash on the prototype that we're
+	// inheriting from
+//	$.each( basePrototype, function( key, val ) {
+//		if ( $.isPlainObject(val) ) {
+//			basePrototype[ key ] = $.extend( {}, val );
+//		}
+//	});
+	basePrototype.options = $.extend( true, {}, basePrototype.options );
+	$[ namespace ][ name ].prototype = $.extend( true, basePrototype, {
+		namespace: namespace,
+		widgetName: name,
+		widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name,
+		widgetBaseClass: fullName
+	}, prototype );
+
+	$.widget.bridge( name, $[ namespace ][ name ] );
+};
+
+$.widget.bridge = function( name, object ) {
+	$.fn[ name ] = function( options ) {
+		var isMethodCall = typeof options === "string",
+			args = Array.prototype.slice.call( arguments, 1 ),
+			returnValue = this;
+
+		// allow multiple hashes to be passed on init
+		options = !isMethodCall && args.length ?
+			$.extend.apply( null, [ true, options ].concat(args) ) :
+			options;
+
+		// prevent calls to internal methods
+		if ( isMethodCall && options.charAt( 0 ) === "_" ) {
+			return returnValue;
+		}
+
+		if ( isMethodCall ) {
+			this.each(function() {
+				var instance = $.data( this, name ),
+					methodValue = instance && $.isFunction( instance[options] ) ?
+						instance[ options ].apply( instance, args ) :
+						instance;
+				// TODO: add this back in 1.9 and use $.error() (see #5972)
+//				if ( !instance ) {
+//					throw "cannot call methods on " + name + " prior to initialization; " +
+//						"attempted to call method '" + options + "'";
+//				}
+//				if ( !$.isFunction( instance[options] ) ) {
+//					throw "no such method '" + options + "' for " + name + " widget instance";
+//				}
+//				var methodValue = instance[ options ].apply( instance, args );
+				if ( methodValue !== instance && methodValue !== undefined ) {
+					returnValue = methodValue;
+					return false;
+				}
+			});
+		} else {
+			this.each(function() {
+				var instance = $.data( this, name );
+				if ( instance ) {
+					instance.option( options || {} )._init();
+				} else {
+					$.data( this, name, new object( options, this ) );
+				}
+			});
+		}
+
+		return returnValue;
+	};
+};
+
+$.Widget = function( options, element ) {
+	// allow instantiation without initializing for simple inheritance
+	if ( arguments.length ) {
+		this._createWidget( options, element );
+	}
+};
+
+$.Widget.prototype = {
+	widgetName: "widget",
+	widgetEventPrefix: "",
+	options: {
+		disabled: false
+	},
+	_createWidget: function( options, element ) {
+		// $.widget.bridge stores the plugin instance, but we do it anyway
+		// so that it's stored even before the _create function runs
+		$.data( element, this.widgetName, this );
+		this.element = $( element );
+		this.options = $.extend( true, {},
+			this.options,
+			this._getCreateOptions(),
+			options );
+
+		var self = this;
+		this.element.bind( "remove." + this.widgetName, function() {
+			self.destroy();
+		});
+
+		this._create();
+		this._trigger( "create" );
+		this._init();
+	},
+	_getCreateOptions: function() {
+		return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ];
+	},
+	_create: function() {},
+	_init: function() {},
+
+	destroy: function() {
+		this.element
+			.unbind( "." + this.widgetName )
+			.removeData( this.widgetName );
+		this.widget()
+			.unbind( "." + this.widgetName )
+			.removeAttr( "aria-disabled" )
+			.removeClass(
+				this.widgetBaseClass + "-disabled " +
+				"ui-state-disabled" );
+	},
+
+	widget: function() {
+		return this.element;
+	},
+
+	option: function( key, value ) {
+		var options = key;
+
+		if ( arguments.length === 0 ) {
+			// don't return a reference to the internal hash
+			return $.extend( {}, this.options );
+		}
+
+		if  (typeof key === "string" ) {
+			if ( value === undefined ) {
+				return this.options[ key ];
+			}
+			options = {};
+			options[ key ] = value;
+		}
+
+		this._setOptions( options );
+
+		return this;
+	},
+	_setOptions: function( options ) {
+		var self = this;
+		$.each( options, function( key, value ) {
+			self._setOption( key, value );
+		});
+
+		return this;
+	},
+	_setOption: function( key, value ) {
+		this.options[ key ] = value;
+
+		if ( key === "disabled" ) {
+			this.widget()
+				[ value ? "addClass" : "removeClass"](
+					this.widgetBaseClass + "-disabled" + " " +
+					"ui-state-disabled" )
+				.attr( "aria-disabled", value );
+		}
+
+		return this;
+	},
+
+	enable: function() {
+		return this._setOption( "disabled", false );
+	},
+	disable: function() {
+		return this._setOption( "disabled", true );
+	},
+
+	_trigger: function( type, event, data ) {
+		var prop, orig,
+			callback = this.options[ type ];
+
+		data = data || {};
+		event = $.Event( event );
+		event.type = ( type === this.widgetEventPrefix ?
+			type :
+			this.widgetEventPrefix + type ).toLowerCase();
+		// the original event may come from any element
+		// so we need to reset the target on the new event
+		event.target = this.element[ 0 ];
+
+		// copy original event properties over to the new event
+		orig = event.originalEvent;
+		if ( orig ) {
+			for ( prop in orig ) {
+				if ( !( prop in event ) ) {
+					event[ prop ] = orig[ prop ];
+				}
+			}
+		}
+
+		this.element.trigger( event, data );
+
+		return !( $.isFunction(callback) &&
+			callback.call( this.element[0], event, data ) === false ||
+			event.isDefaultPrevented() );
+	}
+};
+
+}));
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery.blockui.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,499 @@
+/*!
+ * jQuery blockUI plugin
+ * Version 2.39 (23-MAY-2011)
+ * @requires jQuery v1.2.3 or later
+ *
+ * Examples at: http://malsup.com/jquery/block/
+ * Copyright (c) 2007-2010 M. Alsup
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ *
+ * Thanks to Amir-Hossein Sobhi for some excellent contributions!
+ */
+
+;(function($) {
+
+if (/1\.(0|1|2)\.(0|1|2)/.test($.fn.jquery) || /^1.1/.test($.fn.jquery)) {
+	alert('blockUI requires jQuery v1.2.3 or later!  You are using v' + $.fn.jquery);
+	return;
+}
+
+$.fn._fadeIn = $.fn.fadeIn;
+
+var noOp = function() {};
+
+// this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
+// retarded userAgent strings on Vista)
+var mode = document.documentMode || 0;
+var setExpr = $.browser.msie && (($.browser.version < 8 && !mode) || mode < 8);
+var ie6 = $.browser.msie && /MSIE 6.0/.test(navigator.userAgent) && !mode;
+
+// global $ methods for blocking/unblocking the entire page
+$.blockUI   = function(opts) { install(window, opts); };
+$.unblockUI = function(opts) { remove(window, opts); };
+
+// convenience method for quick growl-like notifications  (http://www.google.com/search?q=growl)
+$.growlUI = function(title, message, timeout, onClose) {
+	var $m = $('<div class="growlUI"></div>');
+	if (title) $m.append('<h1>'+title+'</h1>');
+	if (message) $m.append('<h2>'+message+'</h2>');
+	if (timeout == undefined) timeout = 3000;
+	$.blockUI({
+		message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
+		timeout: timeout, showOverlay: false,
+		onUnblock: onClose, 
+		css: $.blockUI.defaults.growlCSS
+	});
+};
+
+// plugin method for blocking element content
+$.fn.block = function(opts) {
+	return this.unblock({ fadeOut: 0 }).each(function() {
+		if ($.css(this,'position') == 'static')
+			this.style.position = 'relative';
+		if ($.browser.msie)
+			this.style.zoom = 1; // force 'hasLayout'
+		install(this, opts);
+	});
+};
+
+// plugin method for unblocking element content
+$.fn.unblock = function(opts) {
+	return this.each(function() {
+		remove(this, opts);
+	});
+};
+
+$.blockUI.version = 2.39; // 2nd generation blocking at no extra cost!
+
+// override these in your code to change the default behavior and style
+$.blockUI.defaults = {
+	// message displayed when blocking (use null for no message)
+	message:  '<h1>Please wait...</h1>',
+
+	title: null,	  // title string; only used when theme == true
+	draggable: true,  // only used when theme == true (requires jquery-ui.js to be loaded)
+	
+	theme: false, // set to true to use with jQuery UI themes
+	
+	// styles for the message when blocking; if you wish to disable
+	// these and use an external stylesheet then do this in your code:
+	// $.blockUI.defaults.css = {};
+	css: {
+		padding:	0,
+		margin:		0,
+		width:		'30%',
+		top:		'40%',
+		left:		'35%',
+		textAlign:	'center',
+		color:		'#000',
+		border:		'3px solid #aaa',
+		backgroundColor:'#fff',
+		cursor:		'wait'
+	},
+	
+	// minimal style set used when themes are used
+	themedCSS: {
+		width:	'30%',
+		top:	'40%',
+		left:	'35%'
+	},
+
+	// styles for the overlay
+	overlayCSS:  {
+		backgroundColor: '#000',
+		opacity:	  	 0.6,
+		cursor:		  	 'wait'
+	},
+
+	// styles applied when using $.growlUI
+	growlCSS: {
+		width:  	'350px',
+		top:		'10px',
+		left:   	'',
+		right:  	'10px',
+		border: 	'none',
+		padding:	'5px',
+		opacity:	0.6,
+		cursor: 	'default',
+		color:		'#fff',
+		backgroundColor: '#000',
+		'-webkit-border-radius': '10px',
+		'-moz-border-radius':	 '10px',
+		'border-radius': 		 '10px'
+	},
+	
+	// IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
+	// (hat tip to Jorge H. N. de Vasconcelos)
+	iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',
+
+	// force usage of iframe in non-IE browsers (handy for blocking applets)
+	forceIframe: false,
+
+	// z-index for the blocking overlay
+	baseZ: 1000,
+
+	// set these to true to have the message automatically centered
+	centerX: true, // <-- only effects element blocking (page block controlled via css above)
+	centerY: true,
+
+	// allow body element to be stetched in ie6; this makes blocking look better
+	// on "short" pages.  disable if you wish to prevent changes to the body height
+	allowBodyStretch: true,
+
+	// enable if you want key and mouse events to be disabled for content that is blocked
+	bindEvents: true,
+
+	// be default blockUI will supress tab navigation from leaving blocking content
+	// (if bindEvents is true)
+	constrainTabKey: true,
+
+	// fadeIn time in millis; set to 0 to disable fadeIn on block
+	fadeIn:  200,
+
+	// fadeOut time in millis; set to 0 to disable fadeOut on unblock
+	fadeOut:  400,
+
+	// time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
+	timeout: 0,
+
+	// disable if you don't want to show the overlay
+	showOverlay: true,
+
+	// if true, focus will be placed in the first available input field when
+	// page blocking
+	focusInput: true,
+
+	// suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
+	applyPlatformOpacityRules: true,
+	
+	// callback method invoked when fadeIn has completed and blocking message is visible
+	onBlock: null,
+
+	// callback method invoked when unblocking has completed; the callback is
+	// passed the element that has been unblocked (which is the window object for page
+	// blocks) and the options that were passed to the unblock call:
+	//	 onUnblock(element, options)
+	onUnblock: null,
+
+	// don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
+	quirksmodeOffsetHack: 4,
+
+	// class name of the message block
+	blockMsgClass: 'blockMsg'
+};
+
+// private data and functions follow...
+
+var pageBlock = null;
+var pageBlockEls = [];
+
+function install(el, opts) {
+	var full = (el == window);
+	var msg = opts && opts.message !== undefined ? opts.message : undefined;
+	opts = $.extend({}, $.blockUI.defaults, opts || {});
+	opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
+	var css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
+	var themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
+	msg = msg === undefined ? opts.message : msg;
+
+	// remove the current block (if there is one)
+	if (full && pageBlock)
+		remove(window, {fadeOut:0});
+
+	// if an existing element is being used as the blocking content then we capture
+	// its current place in the DOM (and current display style) so we can restore
+	// it when we unblock
+	if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
+		var node = msg.jquery ? msg[0] : msg;
+		var data = {};
+		$(el).data('blockUI.history', data);
+		data.el = node;
+		data.parent = node.parentNode;
+		data.display = node.style.display;
+		data.position = node.style.position;
+		if (data.parent)
+			data.parent.removeChild(node);
+	}
+
+	$(el).data('blockUI.onUnblock', opts.onUnblock);
+	var z = opts.baseZ;
+
+	// blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
+	// layer1 is the iframe layer which is used to supress bleed through of underlying content
+	// layer2 is the overlay layer which has opacity and a wait cursor (by default)
+	// layer3 is the message content that is displayed while blocking
+
+	var lyr1 = ($.browser.msie || opts.forceIframe) 
+		? $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>')
+		: $('<div class="blockUI" style="display:none"></div>');
+	
+	var lyr2 = opts.theme 
+	 	? $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>')
+	 	: $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
+
+	var lyr3, s;
+	if (opts.theme && full) {
+		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">' +
+				'<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>' +
+				'<div class="ui-widget-content ui-dialog-content"></div>' +
+			'</div>';
+	}
+	else if (opts.theme) {
+		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">' +
+				'<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>' +
+				'<div class="ui-widget-content ui-dialog-content"></div>' +
+			'</div>';
+	}
+	else if (full) {
+		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
+	}			 
+	else {
+		s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
+	}
+	lyr3 = $(s);
+
+	// if we have a message, style it
+	if (msg) {
+		if (opts.theme) {
+			lyr3.css(themedCSS);
+			lyr3.addClass('ui-widget-content');
+		}
+		else 
+			lyr3.css(css);
+	}
+
+	// style the overlay
+	if (!opts.theme && (!opts.applyPlatformOpacityRules || !($.browser.mozilla && /Linux/.test(navigator.platform))))
+		lyr2.css(opts.overlayCSS);
+	lyr2.css('position', full ? 'fixed' : 'absolute');
+
+	// make iframe layer transparent in IE
+	if ($.browser.msie || opts.forceIframe)
+		lyr1.css('opacity',0.0);
+
+	//$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
+	var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el);
+	$.each(layers, function() {
+		this.appendTo($par);
+	});
+	
+	if (opts.theme && opts.draggable && $.fn.draggable) {
+		lyr3.draggable({
+			handle: '.ui-dialog-titlebar',
+			cancel: 'li'
+		});
+	}
+
+	// ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
+	var expr = setExpr && (!$.boxModel || $('object,embed', full ? null : el).length > 0);
+	if (ie6 || expr) {
+		// give body 100% height
+		if (full && opts.allowBodyStretch && $.boxModel)
+			$('html,body').css('height','100%');
+
+		// fix ie6 issue when blocked element has a border width
+		if ((ie6 || !$.boxModel) && !full) {
+			var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth');
+			var fixT = t ? '(0 - '+t+')' : 0;
+			var fixL = l ? '(0 - '+l+')' : 0;
+		}
+
+		// simulate fixed position
+		$.each([lyr1,lyr2,lyr3], function(i,o) {
+			var s = o[0].style;
+			s.position = 'absolute';
+			if (i < 2) {
+				full ? s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"')
+					 : s.setExpression('height','this.parentNode.offsetHeight + "px"');
+				full ? s.setExpression('width','jQuery.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"')
+					 : s.setExpression('width','this.parentNode.offsetWidth + "px"');
+				if (fixL) s.setExpression('left', fixL);
+				if (fixT) s.setExpression('top', fixT);
+			}
+			else if (opts.centerY) {
+				if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
+				s.marginTop = 0;
+			}
+			else if (!opts.centerY && full) {
+				var top = (opts.css && opts.css.top) ? parseInt(opts.css.top) : 0;
+				var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"';
+				s.setExpression('top',expression);
+			}
+		});
+	}
+
+	// show the message
+	if (msg) {
+		if (opts.theme)
+			lyr3.find('.ui-widget-content').append(msg);
+		else
+			lyr3.append(msg);
+		if (msg.jquery || msg.nodeType)
+			$(msg).show();
+	}
+
+	if (($.browser.msie || opts.forceIframe) && opts.showOverlay)
+		lyr1.show(); // opacity is zero
+	if (opts.fadeIn) {
+		var cb = opts.onBlock ? opts.onBlock : noOp;
+		var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
+		var cb2 = msg ? cb : noOp;
+		if (opts.showOverlay)
+			lyr2._fadeIn(opts.fadeIn, cb1);
+		if (msg)
+			lyr3._fadeIn(opts.fadeIn, cb2);
+	}
+	else {
+		if (opts.showOverlay)
+			lyr2.show();
+		if (msg)
+			lyr3.show();
+		if (opts.onBlock)
+			opts.onBlock();
+	}
+
+	// bind key and mouse events
+	bind(1, el, opts);
+
+	if (full) {
+		pageBlock = lyr3[0];
+		pageBlockEls = $(':input:enabled:visible',pageBlock);
+		if (opts.focusInput)
+			setTimeout(focus, 20);
+	}
+	else
+		center(lyr3[0], opts.centerX, opts.centerY);
+
+	if (opts.timeout) {
+		// auto-unblock
+		var to = setTimeout(function() {
+			full ? $.unblockUI(opts) : $(el).unblock(opts);
+		}, opts.timeout);
+		$(el).data('blockUI.timeout', to);
+	}
+};
+
+// remove the block
+function remove(el, opts) {
+	var full = (el == window);
+	var $el = $(el);
+	var data = $el.data('blockUI.history');
+	var to = $el.data('blockUI.timeout');
+	if (to) {
+		clearTimeout(to);
+		$el.removeData('blockUI.timeout');
+	}
+	opts = $.extend({}, $.blockUI.defaults, opts || {});
+	bind(0, el, opts); // unbind events
+
+	if (opts.onUnblock === null) {
+		opts.onUnblock = $el.data('blockUI.onUnblock');
+		$el.removeData('blockUI.onUnblock');
+	}
+
+	var els;
+	if (full) // crazy selector to handle odd field errors in ie6/7
+		els = $('body').children().filter('.blockUI').add('body > .blockUI');
+	else
+		els = $('.blockUI', el);
+
+	if (full)
+		pageBlock = pageBlockEls = null;
+
+	if (opts.fadeOut) {
+		els.fadeOut(opts.fadeOut);
+		setTimeout(function() { reset(els,data,opts,el); }, opts.fadeOut);
+	}
+	else
+		reset(els, data, opts, el);
+};
+
+// move blocking element back into the DOM where it started
+function reset(els,data,opts,el) {
+	els.each(function(i,o) {
+		// remove via DOM calls so we don't lose event handlers
+		if (this.parentNode)
+			this.parentNode.removeChild(this);
+	});
+
+	if (data && data.el) {
+		data.el.style.display = data.display;
+		data.el.style.position = data.position;
+		if (data.parent)
+			data.parent.appendChild(data.el);
+		$(el).removeData('blockUI.history');
+	}
+
+	if (typeof opts.onUnblock == 'function')
+		opts.onUnblock(el,opts);
+};
+
+// bind/unbind the handler
+function bind(b, el, opts) {
+	var full = el == window, $el = $(el);
+
+	// don't bother unbinding if there is nothing to unbind
+	if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
+		return;
+	if (!full)
+		$el.data('blockUI.isBlocked', b);
+
+	// don't bind events when overlay is not in use or if bindEvents is false
+	if (!opts.bindEvents || (b && !opts.showOverlay)) 
+		return;
+
+	// bind anchors and inputs for mouse and key events
+	var events = 'mousedown mouseup keydown keypress';
+	b ? $(document).bind(events, opts, handler) : $(document).unbind(events, handler);
+
+// former impl...
+//	   var $e = $('a,:input');
+//	   b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
+};
+
+// event handler to suppress keyboard/mouse events when blocking
+function handler(e) {
+	// allow tab navigation (conditionally)
+	if (e.keyCode && e.keyCode == 9) {
+		if (pageBlock && e.data.constrainTabKey) {
+			var els = pageBlockEls;
+			var fwd = !e.shiftKey && e.target === els[els.length-1];
+			var back = e.shiftKey && e.target === els[0];
+			if (fwd || back) {
+				setTimeout(function(){focus(back)},10);
+				return false;
+			}
+		}
+	}
+	var opts = e.data;
+	// allow events within the message content
+	if ($(e.target).parents('div.' + opts.blockMsgClass).length > 0)
+		return true;
+
+	// allow events for content that is not being blocked
+	return $(e.target).parents().children().filter('div.blockUI').length == 0;
+};
+
+function focus(back) {
+	if (!pageBlockEls)
+		return;
+	var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0];
+	if (e)
+		e.focus();
+};
+
+function center(el, x, y) {
+	var p = el.parentNode, s = el.style;
+	var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth');
+	var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth');
+	if (x) s.left = l > 0 ? (l+'px') : '0';
+	if (y) s.top  = t > 0 ? (t+'px') : '0';
+};
+
+function sz(el, p) {
+	return parseInt($.css(el,p))||0;
+};
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery.min.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,4 @@
+/*! jQuery v1.7.2 jquery.com | jquery.org/license */
+(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"<!doctype html>":"")+"<html><body>"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function ca(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function b_(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bD.test(a)?d(a,e):b_(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&f.type(b)==="object")for(var e in b)b_(a+"["+e+"]",b[e],c,d);else d(a,b)}function b$(a,c){var d,e,g=f.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((g[d]?a:e||(e={}))[d]=c[d]);e&&f.extend(!0,a,e)}function bZ(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bS,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bZ(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bZ(a,c,d,e,"*",g));return l}function bY(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bO),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bB(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?1:0,g=4;if(d>0){if(c!=="border")for(;e<g;e+=2)c||(d-=parseFloat(f.css(a,"padding"+bx[e]))||0),c==="margin"?d+=parseFloat(f.css(a,c+bx[e]))||0:d-=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0;return d+"px"}d=by(a,b);if(d<0||d==null)d=a.style[b];if(bt.test(d))return d;d=parseFloat(d)||0;if(c)for(;e<g;e+=2)d+=parseFloat(f.css(a,"padding"+bx[e]))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+bx[e]+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+bx[e]))||0);return d+"px"}function bo(a){var b=c.createElement("div");bh.appendChild(b),b.innerHTML=a.outerHTML;return b.firstChild}function bn(a){var b=(a.nodeName||"").toLowerCase();b==="input"?bm(a):b!=="script"&&typeof a.getElementsByTagName!="undefined"&&f.grep(a.getElementsByTagName("input"),bm)}function bm(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bl(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bk(a,b){var c;b.nodeType===1&&(b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?b.outerHTML=a.outerHTML:c!=="input"||a.type!=="checkbox"&&a.type!=="radio"?c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text):(a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value)),b.removeAttribute(f.expando),b.removeAttribute("_submit_attached"),b.removeAttribute("_change_attached"))}function bj(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c,d,e,g=f._data(a),h=f._data(b,g),i=g.events;if(i){delete h.handle,h.events={};for(c in i)for(d=0,e=i[c].length;d<e;d++)f.event.add(b,c,i[c][d])}h.data&&(h.data=f.extend({},h.data))}}function bi(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function U(a){var b=V.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function T(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(O.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c<d;c++)b[a[c]]=!0;return b}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b,c){var d;if(b){if(H)return H.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h,i){var j,k=d==null,l=0,m=a.length;if(d&&typeof d=="object"){for(l in d)e.access(a,c,l,d[l],1,h,f);g=1}else if(f!==b){j=i===b&&e.isFunction(f),k&&(j?(j=c,c=function(a,b,c){return j.call(e(a),c)}):(c.call(a,f),c=null));if(c)for(;l<m;l++)c(a[l],d,j?f.call(a[l],l,c(a[l],d)):f,i);g=1}return g?a:k?c.call(a):m?c(a[0],d):h},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=r.exec(a)||s.exec(a)||t.exec(a)||a.indexOf("compatible")<0&&u.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g={};f.Callbacks=function(a){a=a?g[a]||h(a):{};var c=[],d=[],e,i,j,k,l,m,n=function(b){var d,e,g,h,i;for(d=0,e=b.length;d<e;d++)g=b[d],h=f.type(g),h==="array"?n(g):h==="function"&&(!a.unique||!p.has(g))&&c.push(g)},o=function(b,f){f=f||[],e=!a.memory||[b,f],i=!0,j=!0,m=k||0,k=0,l=c.length;for(;c&&m<l;m++)if(c[m].apply(b,f)===!1&&a.stopOnFalse){e=!0;break}j=!1,c&&(a.once?e===!0?p.disable():c=[]:d&&d.length&&(e=d.shift(),p.fireWith(e[0],e[1])))},p={add:function(){if(c){var a=c.length;n(arguments),j?l=c.length:e&&e!==!0&&(k=a,o(e[0],e[1]))}return this},remove:function(){if(c){var b=arguments,d=0,e=b.length;for(;d<e;d++)for(var f=0;f<c.length;f++)if(b[d]===c[f]){j&&f<=l&&(l--,f<=m&&m--),c.splice(f--,1);if(a.unique)break}}return this},has:function(a){if(c){var b=0,d=c.length;for(;b<d;b++)if(a===c[b])return!0}return!1},empty:function(){c=[];return this},disable:function(){c=d=e=b;return this},disabled:function(){return!c},lock:function(){d=b,(!e||e===!0)&&p.disable();return this},locked:function(){return!d},fireWith:function(b,c){d&&(j?a.once||d.push([b,c]):(!a.once||!e)&&o(b,c));return this},fire:function(){p.fireWith(this,arguments);return this},fired:function(){return!!i}};return p};var i=[].slice;f.extend({Deferred:function(a){var b=f.Callbacks("once memory"),c=f.Callbacks("once memory"),d=f.Callbacks("memory"),e="pending",g={resolve:b,reject:c,notify:d},h={done:b.add,fail:c.add,progress:d.add,state:function(){return e},isResolved:b.fired,isRejected:c.fired,then:function(a,b,c){i.done(a).fail(b).progress(c);return this},always:function(){i.done.apply(i,arguments).fail.apply(i,arguments);return this},pipe:function(a,b,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[b,"reject"],progress:[c,"notify"]},function(a,b){var c=b[0],e=b[1],g;f.isFunction(c)?i[a](function(){g=c.apply(this,arguments),g&&f.isFunction(g.promise)?g.promise().then(d.resolve,d.reject,d.notify):d[e+"With"](this===i?d:this,[g])}):i[a](d[e])})}).promise()},promise:function(a){if(a==null)a=h;else for(var b in h)a[b]=h[b];return a}},i=h.promise({}),j;for(j in g)i[j]=g[j].fire,i[j+"With"]=g[j].fireWith;i.done(function(){e="resolved"},c.disable,d.lock).fail(function(){e="rejected"},b.disable,d.lock),a&&a.call(i,i);return i},when:function(a){function m(a){return function(b){e[a]=arguments.length>1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c<d;c++)b[c]&&b[c].promise&&f.isFunction(b[c].promise)?b[c].promise().then(l(c),j.reject,m(c)):--g;g||j.resolveWith(j,b)}else j!==a&&j.resolveWith(j,d?[a]:[]);return k}}),f.support=function(){var b,d,e,g,h,i,j,k,l,m,n,o,p=c.createElement("div"),q=c.documentElement;p.setAttribute("className","t"),p.innerHTML="   <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="<div "+n+"display:block;'><div style='"+t+"0;display:block;overflow:hidden;'></div></div>"+"<table "+n+"' cellpadding='0' cellspacing='0'>"+"<tr><td></td></tr></table>",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="<table><tr><td style='"+t+"0;display:none'></td><td>t</td></tr></table>",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="<div style='width:5px;'></div>",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e<g;e++)delete d[b[e]];if(!(c?m:f.isEmptyObject)(d))return}}if(!c){delete j[k].data;if(!m(j[k]))return}f.support.deleteExpando||!j.setInterval?delete j[k]:j[k]=null,i&&(f.support.deleteExpando?delete a[h]:a.removeAttribute?a.removeAttribute(h):a[h]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d,e,g,h,i,j=this[0],k=0,m=null;if(a===b){if(this.length){m=f.data(j);if(j.nodeType===1&&!f._data(j,"parsedAttrs")){g=j.attributes;for(i=g.length;k<i;k++)h=g[k].name,h.indexOf("data-")===0&&(h=f.camelCase(h.substring(5)),l(j,h,m[h]));f._data(j,"parsedAttrs",!0)}}return m}if(typeof a=="object")return this.each(function(){f.data(this,a)});d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!";return f.access(this,function(c){if(c===b){m=this.triggerHandler("getData"+e,[d[0]]),m===b&&j&&(m=f.data(j,a),m=l(j,a,m));return m===b&&d[1]?this.data(d[0]):m}d[1]=c,this.each(function(){var b=f(this);b.triggerHandler("setData"+e,d),f.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length<d)return f.queue(this[0],a);return c===b?this:this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f.Callbacks("once memory"),!0))h++,l.add(m);m();return d.promise(c)}});var o=/[\n\t\r]/g,p=/\s+/,q=/\r/g,r=/^(?:button|input)$/i,s=/^(?:button|input|object|select|textarea)$/i,t=/^a(?:rea)?$/i,u=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,v=f.support.getSetAttribute,w,x,y;f.fn.extend({attr:function(a,b){return f.access(this,f.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(p);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(o," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(p);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(o," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c<d;c++){e=i[c];if(e.selected&&(f.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!f.nodeName(e.parentNode,"optgroup"))){b=f(e).val();if(j)return b;h.push(b)}}if(j&&!h.length&&i.length)return f(i[g]).val();return h},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i<g;i++)e=d[i],e&&(c=f.propFix[e]||e,h=u.test(e),h||f.attr(a,e,""),a.removeAttribute(v?e:c),h&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(r.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(w&&f.nodeName(a,"button"))return w.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(w&&f.nodeName(a,"button"))return w.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,g,h,i=a.nodeType;if(!!a&&i!==3&&i!==8&&i!==2){h=i!==1||!f.isXMLDoc(a),h&&(c=f.propFix[c]||c,g=f.propHooks[c]);return d!==b?g&&"set"in g&&(e=g.set(a,d,c))!==b?e:a[c]=d:g&&"get"in g&&(e=g.get(a,c))!==null?e:a[c]}},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):s.test(a.nodeName)||t.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabindex=f.propHooks.tabIndex,x={get:function(a,c){var d,e=f.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},v||(y={name:!0,id:!0,coords:!0},w=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&(y[c]?d.nodeValue!=="":d.specified)?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.attrHooks.tabindex.set=w.set,f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})}),f.attrHooks.contenteditable={get:w.get,set:function(a,b,c){b===""&&(b="false"),w.set(a,b,c)}}),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.enctype||(f.propFix.enctype="encoding"),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(
+a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k<c.length;k++){l=A.exec(c[k])||[],m=l[1],n=(l[2]||"").split(".").sort(),s=f.event.special[m]||{},m=(g?s.delegateType:s.bindType)||m,s=f.event.special[m]||{},o=f.extend({type:m,origType:l[1],data:e,handler:d,guid:d.guid,selector:g,quick:g&&G(g),namespace:n.join(".")},p),r=j[m];if(!r){r=j[m]=[],r.delegateCount=0;if(!s.setup||s.setup.call(a,e,n,i)===!1)a.addEventListener?a.addEventListener(m,i,!1):a.attachEvent&&a.attachEvent("on"+m,i)}s.add&&(s.add.call(a,o),o.handler.guid||(o.handler.guid=d.guid)),g?r.splice(r.delegateCount++,0,o):r.push(o),f.event.global[m]=!0}a=null}},global:{},remove:function(a,b,c,d,e){var g=f.hasData(a)&&f._data(a),h,i,j,k,l,m,n,o,p,q,r,s;if(!!g&&!!(o=g.events)){b=f.trim(I(b||"")).split(" ");for(h=0;h<b.length;h++){i=A.exec(b[h])||[],j=k=i[1],l=i[2];if(!j){for(j in o)f.event.remove(a,j+b[h],c,d,!0);continue}p=f.event.special[j]||{},j=(d?p.delegateType:p.bindType)||j,r=o[j]||[],m=r.length,l=l?new RegExp("(^|\\.)"+l.split(".").sort().join("\\.(?:.*\\.)?")+"(\\.|$)"):null;for(n=0;n<r.length;n++)s=r[n],(e||k===s.origType)&&(!c||c.guid===s.guid)&&(!l||l.test(s.namespace))&&(!d||d===s.selector||d==="**"&&s.selector)&&(r.splice(n--,1),s.selector&&r.delegateCount--,p.remove&&p.remove.call(a,s));r.length===0&&m!==r.length&&((!p.teardown||p.teardown.call(a,l)===!1)&&f.removeEvent(a,j,g.handle),delete o[j])}f.isEmptyObject(o)&&(q=g.handle,q&&(q.elem=null),f.removeData(a,["events","handle"],!0))}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){if(!e||e.nodeType!==3&&e.nodeType!==8){var h=c.type||c,i=[],j,k,l,m,n,o,p,q,r,s;if(E.test(h+f.event.triggered))return;h.indexOf("!")>=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;l<r.length&&!c.isPropagationStopped();l++)m=r[l][0],c.type=r[l][1],q=(f._data(m,"events")||{})[c.type]&&f._data(m,"handle"),q&&q.apply(m,d),q=o&&m[o],q&&f.acceptData(m)&&q.apply(m,d)===!1&&c.preventDefault();c.type=h,!g&&!c.isDefaultPrevented()&&(!p._default||p._default.apply(e.ownerDocument,d)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)&&o&&e[h]&&(h!=="focus"&&h!=="blur"||c.target.offsetWidth!==0)&&!f.isWindow(e)&&(n=e[o],n&&(e[o]=null),f.event.triggered=h,e[h](),f.event.triggered=b,n&&(e[o]=n));return c.result}},dispatch:function(c){c=f.event.fix(c||a.event);var d=(f._data(this,"events")||{})[c.type]||[],e=d.delegateCount,g=[].slice.call(arguments,0),h=!c.exclusive&&!c.namespace,i=f.event.special[c.type]||{},j=[],k,l,m,n,o,p,q,r,s,t,u;g[0]=c,c.delegateTarget=this;if(!i.preDispatch||i.preDispatch.call(this,c)!==!1){if(e&&(!c.button||c.type!=="click")){n=f(this),n.context=this.ownerDocument||this;for(m=c.target;m!=this;m=m.parentNode||this)if(m.disabled!==!0){p={},r=[],n[0]=m;for(k=0;k<e;k++)s=d[k],t=s.selector,p[t]===b&&(p[t]=s.quick?H(m,s.quick):n.is(t)),p[t]&&r.push(s);r.length&&j.push({elem:m,matches:r})}}d.length>e&&j.push({elem:this,matches:d.slice(e)});for(k=0;k<j.length&&!c.isPropagationStopped();k++){q=j[k],c.currentTarget=q.elem;for(l=0;l<q.matches.length&&!c.isImmediatePropagationStopped();l++){s=q.matches[l];if(h||!c.namespace&&!s.namespace||c.namespace_re&&c.namespace_re.test(s.namespace))c.data=s.data,c.handleObj=s,o=((f.event.special[s.origType]||{}).handle||s.handler).apply(q.elem,g),o!==b&&(c.result=o,o===!1&&(c.preventDefault(),c.stopPropagation()))}}i.postDispatch&&i.postDispatch.call(this,c);return c.result}},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode);return a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,d){var e,f,g,h=d.button,i=d.fromElement;a.pageX==null&&d.clientX!=null&&(e=a.target.ownerDocument||c,f=e.documentElement,g=e.body,a.pageX=d.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=d.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?d.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0);return a}},fix:function(a){if(a[f.expando])return a;var d,e,g=a,h=f.event.fixHooks[a.type]||{},i=h.props?this.props.concat(h.props):this.props;a=f.Event(g);for(d=i.length;d;)e=i[--d],a[e]=g[e];a.target||(a.target=g.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey===b&&(a.metaKey=a.ctrlKey);return h.filter?h.filter(a,g):a},special:{ready:{setup:f.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=f.extend(new f.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?f.event.trigger(e,null,b):f.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},f.event.handle=f.event.dispatch,f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!(this instanceof f.Event))return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?K:J):this.type=a,b&&f.extend(this,b),this.timeStamp=a&&a.timeStamp||f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=K;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=K;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=K,this.stopPropagation()},isDefaultPrevented:J,isPropagationStopped:J,isImmediatePropagationStopped:J},f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c=this,d=a.relatedTarget,e=a.handleObj,g=e.selector,h;if(!d||d!==c&&!f.contains(c,d))a.type=e.origType,h=e.handler.apply(this,arguments),a.type=b;return h}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(){if(f.nodeName(this,"form"))return!1;f.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=f.nodeName(c,"input")||f.nodeName(c,"button")?c.form:b;d&&!d._submit_attached&&(f.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),d._submit_attached=!0)})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&f.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(f.nodeName(this,"form"))return!1;f.event.remove(this,"._submit")}}),f.support.changeBubbles||(f.event.special.change={setup:function(){if(z.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")f.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),f.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1,f.event.simulate("change",this,a,!0))});return!1}f.event.add(this,"beforeactivate._change",function(a){var b=a.target;z.test(b.nodeName)&&!b._change_attached&&(f.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&f.event.simulate("change",this.parentNode,a,!0)}),b._change_attached=!0)})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){f.event.remove(this,"._change");return z.test(this.nodeName)}}),f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){var d=0,e=function(a){f.event.simulate(b,a.target,f.event.fix(a),!0)};f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.fn.extend({on:function(a,c,d,e,g){var h,i;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(i in a)this.on(i,c,d,a[i],g);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=J;else if(!e)return this;g===1&&(h=e,e=function(a){f().off(a);return h.apply(this,arguments)},e.guid=h.guid||(h.guid=f.guid++));return this.each(function(){f.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){if(a&&a.preventDefault&&a.handleObj){var e=a.handleObj;f(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler);return this}if(typeof a=="object"){for(var g in a)this.off(g,c,a[g]);return this}if(c===!1||typeof c=="function")d=c,c=b;d===!1&&(d=J);return this.each(function(){f.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){f(this.context).on(a,this.selector,b,c);return this},die:function(a,b){f(this.context).off(a,this.selector||"**",b);return this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a,c)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f._data(this,"lastToggle"+a.guid)||0)%d;f._data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}if(j.nodeType===1){g||(j[d]=c,j.sizset=h);if(typeof b!="string"){if(j===b){k=!0;break}}else if(m.filter(b,[j]).length>0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h<i;h++){var j=e[h];if(j){var k=!1;j=j[a];while(j){if(j[d]===c){k=e[j.sizset];break}j.nodeType===1&&!g&&(j[d]=c,j.sizset=h);if(j.nodeName.toLowerCase()===b){k=j;break}j=j[a]}e[h]=k}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},m.matches=function(a,b){return m(a,null,null,b)},m.matchesSelector=function(a,b){return m(b,null,null,[a]).length>0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e<f;e++){h=o.order[e];if(g=o.leftMatch[h].exec(a)){i=g[1],g.splice(1,1);if(i.substr(i.length-1)!=="\\"){g[1]=(g[1]||"").replace(j,""),d=o.find[h](g,b,c);if(d!=null){a=a.replace(o.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},m.filter=function(a,c,d,e){var f,g,h,i,j,k,l,n,p,q=a,r=[],s=c,t=c&&c[0]&&m.isXML(c[0]);while(a&&c.length){for(h in o.filter)if((f=o.leftMatch[h].exec(a))!=null&&f[2]){k=o.filter[h],l=f[1],g=!1,f.splice(1,1);if(l.substr(l.length-1)==="\\")continue;s===r&&(r=[]);if(o.preFilter[h]){f=o.preFilter[h](f,s,d,r,e,t);if(!f)g=i=!0;else if(f===!0)continue}if(f)for(n=0;(j=s[n])!=null;n++)j&&(i=k(j,f,n,s),p=e^i,d&&i!=null?p?g=!0:s[n]=!1:p&&(r.push(j),g=!0));if(i!==b){d||(s=r),a=a.replace(o.match[h],"");if(!g)return[];break}}if(a===q)if(g==null)m.error(a);else break;q=a}return s},m.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)};var n=m.getText=function(a){var b,c,d=a.nodeType,e="";if(d){if(d===1||d===9||d===11){if(typeof a.textContent=="string")return a.textContent;if(typeof a.innerText=="string")return a.innerText.replace(k,"");for(a=a.firstChild;a;a=a.nextSibling)e+=n(a)}else if(d===3||d===4)return a.nodeValue}else for(b=0;c=a[b];b++)c.nodeType!==8&&(e+=n(c));return e},o=m.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!l.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&m.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&m.filter(b,a,!0)}},"":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("parentNode",b,f,a,d,c)},"~":function(a,b,c){var d,f=e++,g=x;typeof b=="string"&&!l.test(b)&&(b=b.toLowerCase(),d=b,g=w),g("previousSibling",b,f,a,d,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(j,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}m.error(e)},CHILD:function(a,b){var c,e,f,g,h,i,j,k=b[1],l=a;switch(k){case"only":case"first":while(l=l.previousSibling)if(l.nodeType===1)return!1;if(k==="first")return!0;l=a;case"last":while(l=l.nextSibling)if(l.nodeType===1)return!1;return!0;case"nth":c=b[2],e=b[3];if(c===1&&e===0)return!0;f=b[0],g=a.parentNode;if(g&&(g[d]!==f||!a.nodeIndex)){i=0;for(l=g.firstChild;l;l=l.nextSibling)l.nodeType===1&&(l.nodeIndex=++i);g[d]=f}j=a.nodeIndex-e;return c===0?j===0:j%c===0&&j/c>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c<e;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var u,v;c.documentElement.compareDocumentPosition?u=function(a,b){if(a===b){h=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(u=function(a,b){if(a===b){h=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],g=a.parentNode,i=b.parentNode,j=g;if(g===i)return v(a,b);if(!g)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return v(e[k],f[k]);return k===c?v(a,f[k],-1):v(e[k],b,1)},v=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h<i;h++)m(a,g[h],e,c);return m.filter(f,e)};m.attr=f.attr,m.selectors.attrMap={},f.find=m,f.expr=m.selectors,f.expr[":"]=f.expr.filters,f.unique=m.uniqueSort,f.text=m.getText,f.isXMLDoc=m.isXML,f.contains=m.contains}();var L=/Until$/,M=/^(?:parents|prevUntil|prevAll)/,N=/,/,O=/^.[^:#\[\.,]*$/,P=Array.prototype.slice,Q=f.expr.match.globalPOS,R={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(T(this,a,!1),"not",a)},filter:function(a){return this.pushStack(T(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?Q.test(a)?f(a,this.context).index(this[0])>=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d<a.length;d++)f(g).is(a[d])&&c.push({selector:a[d],elem:g,level:h});g=g.parentNode,h++}return c}var i=Q.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(i?i.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/<tbody/i,_=/<|&#?\w+;/,ba=/<(?:script|style)/i,bb=/<(?:script|object|embed|option|style)/i,bc=new RegExp("<(?:"+V+")[\\s/>]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*<!(?:\[CDATA\[|\-\-)/,bg={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f
+.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(f.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(g){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bd.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bi(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,function(a,b){b.src?f.ajax({type:"GET",global:!1,url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bf,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i,j=a[0];b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof j=="string"&&j.length<512&&i===c&&j.charAt(0)==="<"&&!bb.test(j)&&(f.support.checkClone||!bd.test(j))&&(f.support.html5Clone||!bc.test(j))&&(g=!0,h=f.fragments[j],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[j]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1></$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]==="<table>"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i<u;i++)bn(l[i]);else bn(l);l.nodeType?j.push(l):j=f.merge(j,l)}if(d){g=function(a){return!a.type||be.test(a.type)};for(k=0;j[k];k++){h=j[k];if(e&&f.nodeName(h,"script")&&(!h.type||be.test(h.type)))e.push(h.parentNode?h.parentNode.removeChild(h):h);else{if(h.nodeType===1){var v=f.grep(h.getElementsByTagName("script"),g);j.splice.apply(j,[k+1,0].concat(v))}d.appendChild(h)}}}return j},cleanData:function(a){var b,c,d=f.cache,e=f.event.special,g=f.support.deleteExpando;for(var h=0,i;(i=a[h])!=null;h++){if(i.nodeName&&f.noData[i.nodeName.toLowerCase()])continue;c=i[f.expando];if(c){b=d[c];if(b&&b.events){for(var j in b.events)e[j]?f.event.remove(i,j):f.removeEvent(i,j,b.handle);b.handle&&(b.handle.elem=null)}g?delete i[f.expando]:i.removeAttribute&&i.removeAttribute(f.expando),delete d[c]}}}});var bp=/alpha\([^)]*\)/i,bq=/opacity=([^)]*)/,br=/([A-Z]|^ms)/g,bs=/^[\-+]?(?:\d*\.)?\d+$/i,bt=/^-?(?:\d*\.)?\d+(?!px)[^\d\s]+$/i,bu=/^([\-+])=([\-+.\de]+)/,bv=/^margin/,bw={position:"absolute",visibility:"hidden",display:"block"},bx=["Top","Right","Bottom","Left"],by,bz,bA;f.fn.css=function(a,c){return f.access(this,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)},a,c,arguments.length>1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),(e===""&&f.css(d,"display")==="none"||!f.contains(d.ownerDocument.documentElement,d))&&f._data(d,"olddisplay",cu(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(ct("hide",3),a,b,c);var d,e,g=0,h=this.length;for(;g<h;g++)d=this[g],d.style&&(e=f.css(d,"display"),e!=="none"&&!f._data(d,"olddisplay")&&f._data(d,"olddisplay",e));for(g=0;g<h;g++)this[g].style&&(this[g].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(ct("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){function g(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o,p,q;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]);if((k=f.cssHooks[g])&&"expand"in k){l=k.expand(a[g]),delete a[g];for(i in l)i in a||(a[i]=l[i])}}for(g in a){h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(!f.support.inlineBlockNeedsLayout||cu(this.nodeName)==="inline"?this.style.display="inline-block":this.style.zoom=1))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)j=new f.fx(this,b,i),h=a[i],cm.test(h)?(q=f._data(this,"toggle"+i)||(h==="toggle"?d?"show":"hide":0),q?(f._data(this,"toggle"+i,q==="show"?"hide":"show"),j[q]()):j[h]()):(m=cn.exec(h),n=j.cur(),m?(o=parseFloat(m[2]),p=m[3]||(f.cssNumber[i]?"":"px"),p!=="px"&&(f.style(this,i,(o||1)+p),n=(o||1)/j.cur()*n,f.style(this,i,n+p)),m[1]&&(o=(m[1]==="-="?-1:1)*o+n),j.custom(n,o,p)):j.custom(n,h,""));return!0}var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return e.queue===!1?this.each(g):this.queue(e.queue,g)},stop:function(a,c,d){typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]);return this.each(function(){function h(a,b,c){var e=b[c];f.removeData(a,c,!0),e.stop(d)}var b,c=!1,e=f.timers,g=f._data(this);d||f._unmark(!0,this);if(a==null)for(b in g)g[b]&&g[b].stop&&b.indexOf(".run")===b.length-4&&h(this,g,b);else g[b=a+".run"]&&g[b].stop&&h(this,g,b);for(b=e.length;b--;)e[b].elem===this&&(a==null||e[b].queue===a)&&(d?e[b](!0):e[b].saveState(),c=!0,e.splice(b,1));(!d||!c)&&f.dequeue(this,a)})}}),f.each({slideDown:ct("show",1),slideUp:ct("hide",1),slideToggle:ct("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue?f.dequeue(this,d.queue):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a){return a},swing:function(a){return-Math.cos(a*Math.PI)/2+.5}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,c,d){function h(a){return e.step(a)}var e=this,g=f.fx;this.startTime=cq||cr(),this.end=c,this.now=this.start=a,this.pos=this.state=0,this.unit=d||this.unit||(f.cssNumber[this.prop]?"":"px"),h.queue=this.options.queue,h.elem=this.elem,h.saveState=function(){f._data(e.elem,"fxshow"+e.prop)===b&&(e.options.hide?f._data(e.elem,"fxshow"+e.prop,e.start):e.options.show&&f._data(e.elem,"fxshow"+e.prop,e.end))},h()&&f.timers.push(h)&&!co&&(co=setInterval(g.tick,g.interval))},show:function(){var a=f._data(this.elem,"fxshow"+this.prop);this.options.orig[this.prop]=a||f.style(this.elem,this.prop),this.options.show=!0,a!==b?this.custom(this.cur(),a):this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f._data(this.elem,"fxshow"+this.prop)||f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b,c,d,e=cq||cr(),g=!0,h=this.elem,i=this.options;if(a||e>=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||f.fx.stop()},interval:13,stop:function(){clearInterval(co),co=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=a.now+a.unit:a.elem[a.prop]=a.now}}}),f.each(cp.concat.apply([],cp),function(a,b){b.indexOf("margin")&&(f.fx.step[b]=function(a){f.style(a.elem,b,Math.max(0,a.now)+a.unit)})}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var cv,cw=/^t(?:able|d|h)$/i,cx=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?cv=function(a,b,c,d){try{d=a.getBoundingClientRect()}catch(e){}if(!d||!f.contains(c,a))return d?{top:d.top,left:d.left}:{top:0,left:0};var g=b.body,h=cy(b),i=c.clientTop||g.clientTop||0,j=c.clientLeft||g.clientLeft||0,k=h.pageYOffset||f.support.boxModel&&c.scrollTop||g.scrollTop,l=h.pageXOffset||f.support.boxModel&&c.scrollLeft||g.scrollLeft,m=d.top+k-i,n=d.left+l-j;return{top:m,left:n}}:cv=function(a,b,c){var d,e=a.offsetParent,g=a,h=b.body,i=b.defaultView,j=i?i.getComputedStyle(a,null):a.currentStyle,k=a.offsetTop,l=a.offsetLeft;while((a=a.parentNode)&&a!==h&&a!==c){if(f.support.fixedPosition&&j.position==="fixed")break;d=i?i.getComputedStyle(a,null):a.currentStyle,k-=a.scrollTop,l-=a.scrollLeft,a===e&&(k+=a.offsetTop,l+=a.offsetLeft,f.support.doesNotAddBorder&&(!f.support.doesAddBorderForTableAndCells||!cw.test(a.nodeName))&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),g=e,e=a.offsetParent),f.support.subtractsBorderForOverflowNotVisible&&d.overflow!=="visible"&&(k+=parseFloat(d.borderTopWidth)||0,l+=parseFloat(d.borderLeftWidth)||0),j=d}if(j.position==="relative"||j.position==="static")k+=h.offsetTop,l+=h.offsetLeft;f.support.fixedPosition&&j.position==="fixed"&&(k+=Math.max(c.scrollTop,h.scrollTop),l+=Math.max(c.scrollLeft,h.scrollLeft));return{top:k,left:l}},f.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){f.offset.setOffset(this,a,b)});var c=this[0],d=c&&c.ownerDocument;if(!d)return null;if(c===d.body)return f.offset.bodyOffset(c);return cv(c,d,d.documentElement)},f.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery.mobile.min.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2 @@
+/*! jQuery Mobile v1.1.0 db342b1f315c282692791aa870455901fdb46a55 jquerymobile.com | jquery.org/license */
+.ui-bar-a{border:1px solid #333;background:#111;color:#fff;font-weight:bold;text-shadow:0 -1px 1px #000;background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#111));background-image:-webkit-linear-gradient(#3c3c3c,#111);background-image:-moz-linear-gradient(#3c3c3c,#111);background-image:-ms-linear-gradient(#3c3c3c,#111);background-image:-o-linear-gradient(#3c3c3c,#111);background-image:linear-gradient(#3c3c3c,#111)}.ui-bar-a,.ui-bar-a input,.ui-bar-a select,.ui-bar-a textarea,.ui-bar-a button{font-family:Helvetica,Arial,sans-serif}.ui-bar-a .ui-link-inherit{color:#fff}.ui-bar-a .ui-link{color:#7cc4e7;font-weight:bold}.ui-bar-a .ui-link:hover{color:#2489ce}.ui-bar-a .ui-link:active{color:#2489ce}.ui-bar-a .ui-link:visited{color:#2489ce}.ui-body-a,.ui-overlay-a{border:1px solid #444;background:#222;color:#fff;text-shadow:0 1px 1px #111;font-weight:normal;background-image:-webkit-gradient(linear,left top,left bottom,from(#444),to(#222));background-image:-webkit-linear-gradient(#444,#222);background-image:-moz-linear-gradient(#444,#222);background-image:-ms-linear-gradient(#444,#222);background-image:-o-linear-gradient(#444,#222);background-image:linear-gradient(#444,#222)}.ui-overlay-a{background-image:none;border-width:0}.ui-body-a,.ui-body-a input,.ui-body-a select,.ui-body-a textarea,.ui-body-a button{font-family:Helvetica,Arial,sans-serif}.ui-body-a .ui-link-inherit{color:#fff}.ui-body-a .ui-link{color:#2489ce;font-weight:bold}.ui-body-a .ui-link:hover{color:#2489ce}.ui-body-a .ui-link:active{color:#2489ce}.ui-body-a .ui-link:visited{color:#2489ce}.ui-btn-up-a{border:1px solid #111;background:#333;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from(#444),to(#2d2d2d));background-image:-webkit-linear-gradient(#444,#2d2d2d);background-image:-moz-linear-gradient(#444,#2d2d2d);background-image:-ms-linear-gradient(#444,#2d2d2d);background-image:-o-linear-gradient(#444,#2d2d2d);background-image:linear-gradient(#444,#2d2d2d)}.ui-btn-up-a a.ui-link-inherit{color:#fff}.ui-btn-hover-a{border:1px solid #000;background:#444;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from(#555),to(#383838));background-image:-webkit-linear-gradient(#555,#383838);background-image:-moz-linear-gradient(#555,#383838);background-image:-ms-linear-gradient(#555,#383838);background-image:-o-linear-gradient(#555,#383838);background-image:linear-gradient(#555,#383838)}.ui-btn-hover-a a.ui-link-inherit{color:#fff}.ui-btn-down-a{border:1px solid #000;background:#222;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from(#202020),to(#2c2c2c));background-image:-webkit-linear-gradient(#202020,#2c2c2c);background-image:-moz-linear-gradient(#202020,#2c2c2c);background-image:-ms-linear-gradient(#202020,#2c2c2c);background-image:-o-linear-gradient(#202020,#2c2c2c);background-image:linear-gradient(#202020,#2c2c2c)}.ui-btn-down-a a.ui-link-inherit{color:#fff}.ui-btn-up-a,.ui-btn-hover-a,.ui-btn-down-a{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-b{border:1px solid #456f9a;background:#5e87b0;color:#fff;font-weight:bold;text-shadow:0 1px 1px #3e6790;background-image:-webkit-gradient(linear,left top,left bottom,from(#6facd5),to(#497bae));background-image:-webkit-linear-gradient(#6facd5,#497bae);background-image:-moz-linear-gradient(#6facd5,#497bae);background-image:-ms-linear-gradient(#6facd5,#497bae);background-image:-o-linear-gradient(#6facd5,#497bae);background-image:linear-gradient(#6facd5,#497bae)}.ui-bar-b,.ui-bar-b input,.ui-bar-b select,.ui-bar-b textarea,.ui-bar-b button{font-family:Helvetica,Arial,sans-serif}.ui-bar-b .ui-link-inherit{color:#fff}.ui-bar-b .ui-link{color:#ddf0f8;font-weight:bold}.ui-bar-b .ui-link:hover{color:#ddf0f8}.ui-bar-b .ui-link:active{color:#ddf0f8}.ui-bar-b .ui-link:visited{color:#ddf0f8}.ui-body-b,.ui-overlay-b{border:1px solid #999;background:#f3f3f3;color:#222;text-shadow:0 1px 0 #fff;font-weight:normal;background-image:-webkit-gradient(linear,left top,left bottom,from(#ddd),to(#ccc));background-image:-webkit-linear-gradient(#ddd,#ccc);background-image:-moz-linear-gradient(#ddd,#ccc);background-image:-ms-linear-gradient(#ddd,#ccc);background-image:-o-linear-gradient(#ddd,#ccc);background-image:linear-gradient(#ddd,#ccc)}.ui-overlay-b{background-image:none;border-width:0}.ui-body-b,.ui-body-b input,.ui-body-b select,.ui-body-b textarea,.ui-body-b button{font-family:Helvetica,Arial,sans-serif}.ui-body-b .ui-link-inherit{color:#333}.ui-body-b .ui-link{color:#2489ce;font-weight:bold}.ui-body-b .ui-link:hover{color:#2489ce}.ui-body-b .ui-link:active{color:#2489ce}.ui-body-b .ui-link:visited{color:#2489ce}.ui-btn-up-b{border:1px solid #044062;background:#396b9e;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from(#5f9cc5),to(#396b9e));background-image:-webkit-linear-gradient(#5f9cc5,#396b9e);background-image:-moz-linear-gradient(#5f9cc5,#396b9e);background-image:-ms-linear-gradient(#5f9cc5,#396b9e);background-image:-o-linear-gradient(#5f9cc5,#396b9e);background-image:linear-gradient(#5f9cc5,#396b9e)}.ui-btn-up-b a.ui-link-inherit{color:#fff}.ui-btn-hover-b{border:1px solid #00415e;background:#4b88b6;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from(#6facd5),to(#4272a4));background-image:-webkit-linear-gradient(#6facd5,#4272a4);background-image:-moz-linear-gradient(#6facd5,#4272a4);background-image:-ms-linear-gradient(#6facd5,#4272a4);background-image:-o-linear-gradient(#6facd5,#4272a4);background-image:linear-gradient(#6facd5,#4272a4)}.ui-btn-hover-b a.ui-link-inherit{color:#fff}.ui-btn-down-b{border:1px solid #225377;background:#4e89c5;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from(#295b8e),to(#3e79b5));background-image:-webkit-linear-gradient(#295b8e,#3e79b5);background-image:-moz-linear-gradient(#295b8e,#3e79b5);background-image:-ms-linear-gradient(#295b8e,#3e79b5);background-image:-o-linear-gradient(#295b8e,#3e79b5);background-image:linear-gradient(#295b8e,#3e79b5)}.ui-btn-down-b a.ui-link-inherit{color:#fff}.ui-btn-up-b,.ui-btn-hover-b,.ui-btn-down-b{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-c{border:1px solid #b3b3b3;background:#eee;color:#3e3e3e;font-weight:bold;text-shadow:0 1px 1px #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#f0f0f0),to(#ddd));background-image:-webkit-linear-gradient(#f0f0f0,#ddd);background-image:-moz-linear-gradient(#f0f0f0,#ddd);background-image:-ms-linear-gradient(#f0f0f0,#ddd);background-image:-o-linear-gradient(#f0f0f0,#ddd);background-image:linear-gradient(#f0f0f0,#ddd)}.ui-bar-c .ui-link-inherit{color:#3e3e3e}.ui-bar-c .ui-link{color:#7cc4e7;font-weight:bold}.ui-bar-c .ui-link:hover{color:#2489ce}.ui-bar-c .ui-link:active{color:#2489ce}.ui-bar-c .ui-link:visited{color:#2489ce}.ui-bar-c,.ui-bar-c input,.ui-bar-c select,.ui-bar-c textarea,.ui-bar-c button{font-family:Helvetica,Arial,sans-serif}.ui-body-c,.ui-overlay-c{border:1px solid #aaa;color:#333;text-shadow:0 1px 0 #fff;background:#f9f9f9;background-image:-webkit-gradient(linear,left top,left bottom,from(#f9f9f9),to(#eee));background-image:-webkit-linear-gradient(#f9f9f9,#eee);background-image:-moz-linear-gradient(#f9f9f9,#eee);background-image:-ms-linear-gradient(#f9f9f9,#eee);background-image:-o-linear-gradient(#f9f9f9,#eee);background-image:linear-gradient(#f9f9f9,#eee)}.ui-overlay-c{background-image:none;border-width:0}.ui-body-c,.ui-body-c input,.ui-body-c select,.ui-body-c textarea,.ui-body-c button{font-family:Helvetica,Arial,sans-serif}.ui-body-c .ui-link-inherit{color:#333}.ui-body-c .ui-link{color:#2489ce;font-weight:bold}.ui-body-c .ui-link:hover{color:#2489ce}.ui-body-c .ui-link:active{color:#2489ce}.ui-body-c .ui-link:visited{color:#2489ce}.ui-btn-up-c{border:1px solid #ccc;background:#eee;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f1f1f1));background-image:-webkit-linear-gradient(#fff,#f1f1f1);background-image:-moz-linear-gradient(#fff,#f1f1f1);background-image:-ms-linear-gradient(#fff,#f1f1f1);background-image:-o-linear-gradient(#fff,#f1f1f1);background-image:linear-gradient(#fff,#f1f1f1)}.ui-btn-up-c a.ui-link-inherit{color:#2f3e46}.ui-btn-hover-c{border:1px solid #bbb;background:#dfdfdf;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f6f6),to(#e0e0e0));background-image:-webkit-linear-gradient(#f9f9f9,#e0e0e0);background-image:-moz-linear-gradient(#f6f6f6,#e0e0e0);background-image:-ms-linear-gradient(#f6f6f6,#e0e0e0);background-image:-o-linear-gradient(#f6f6f6,#e0e0e0);background-image:linear-gradient(#f6f6f6,#e0e0e0)}.ui-btn-hover-c a.ui-link-inherit{color:#2f3e46}.ui-btn-down-c{border:1px solid #bbb;background:#d6d6d6;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#d0d0d0),to(#dfdfdf));background-image:-webkit-linear-gradient(#d0d0d0,#dfdfdf);background-image:-moz-linear-gradient(#d0d0d0,#dfdfdf);background-image:-ms-linear-gradient(#d0d0d0,#dfdfdf);background-image:-o-linear-gradient(#d0d0d0,#dfdfdf);background-image:linear-gradient(#d0d0d0,#dfdfdf)}.ui-btn-down-c a.ui-link-inherit{color:#2f3e46}.ui-btn-up-c,.ui-btn-hover-c,.ui-btn-down-c{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-d{border:1px solid #bbb;background:#bbb;color:#333;text-shadow:0 1px 0 #eee;background-image:-webkit-gradient(linear,left top,left bottom,from(#ddd),to(#bbb));background-image:-webkit-linear-gradient(#ddd,#bbb);background-image:-moz-linear-gradient(#ddd,#bbb);background-image:-ms-linear-gradient(#ddd,#bbb);background-image:-o-linear-gradient(#ddd,#bbb);background-image:linear-gradient(#ddd,#bbb)}.ui-bar-d,.ui-bar-d input,.ui-bar-d select,.ui-bar-d textarea,.ui-bar-d button{font-family:Helvetica,Arial,sans-serif}.ui-bar-d .ui-link-inherit{color:#333}.ui-bar-d .ui-link{color:#2489ce;font-weight:bold}.ui-bar-d .ui-link:hover{color:#2489ce}.ui-bar-d .ui-link:active{color:#2489ce}.ui-bar-d .ui-link:visited{color:#2489ce}.ui-body-d,.ui-overlay-d{border:1px solid #bbb;color:#333;text-shadow:0 1px 0 #fff;background:#fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#fff));background-image:-webkit-linear-gradient(#fff,#fff);background-image:-moz-linear-gradient(#fff,#fff);background-image:-ms-linear-gradient(#fff,#fff);background-image:-o-linear-gradient(#fff,#fff);background-image:linear-gradient(#fff,#fff)}.ui-overlay-d{background-image:none;border-width:0}.ui-body-d,.ui-body-d input,.ui-body-d select,.ui-body-d textarea,.ui-body-d button{font-family:Helvetica,Arial,sans-serif}.ui-body-d .ui-link-inherit{color:#333}.ui-body-d .ui-link{color:#2489ce;font-weight:bold}.ui-body-d .ui-link:hover{color:#2489ce}.ui-body-d .ui-link:active{color:#2489ce}.ui-body-d .ui-link:visited{color:#2489ce}.ui-btn-up-d{border:1px solid #bbb;background:#fff;font-weight:bold;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fafafa),to(#f6f6f6));background-image:-webkit-linear-gradient(#fafafa,#f6f6f6);background-image:-moz-linear-gradient(#fafafa,#f6f6f6);background-image:-ms-linear-gradient(#fafafa,#f6f6f6);background-image:-o-linear-gradient(#fafafa,#f6f6f6);background-image:linear-gradient(#fafafa,#f6f6f6)}.ui-btn-up-d a.ui-link-inherit{color:#333}.ui-btn-hover-d{border:1px solid #aaa;background:#eee;font-weight:bold;color:#333;cursor:pointer;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#eee),to(#fff));background-image:-webkit-linear-gradient(#eee,#fff);background-image:-moz-linear-gradient(#eee,#fff);background-image:-ms-linear-gradient(#eee,#fff);background-image:-o-linear-gradient(#eee,#fff);background-image:linear-gradient(#eee,#fff)}.ui-btn-hover-d a.ui-link-inherit{color:#333}.ui-btn-down-d{border:1px solid #aaa;background:#eee;font-weight:bold;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#e5e5e5),to(#f2f2f2));background-image:-webkit-linear-gradient(#e5e5e5,#f2f2f2);background-image:-moz-linear-gradient(#e5e5e5,#f2f2f2);background-image:-ms-linear-gradient(#e5e5e5,#f2f2f2);background-image:-o-linear-gradient(#e5e5e5,#f2f2f2);background-image:linear-gradient(#e5e5e5,#f2f2f2)}.ui-btn-down-d a.ui-link-inherit{color:#333}.ui-btn-up-d,.ui-btn-hover-d,.ui-btn-down-d{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-e{border:1px solid #f7c942;background:#fadb4e;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fceda7),to(#fbef7e));background-image:-webkit-linear-gradient(#fceda7,#fbef7e);background-image:-moz-linear-gradient(#fceda7,#fbef7e);background-image:-ms-linear-gradient(#fceda7,#fbef7e);background-image:-o-linear-gradient(#fceda7,#fbef7e);background-image:linear-gradient(#fceda7,#fbef7e)}.ui-bar-e,.ui-bar-e input,.ui-bar-e select,.ui-bar-e textarea,.ui-bar-e button{font-family:Helvetica,Arial,sans-serif}.ui-bar-e .ui-link-inherit{color:#333}.ui-bar-e .ui-link{color:#2489ce;font-weight:bold}.ui-bar-e .ui-link:hover{color:#2489ce}.ui-bar-e .ui-link:active{color:#2489ce}.ui-bar-e .ui-link:visited{color:#2489ce}.ui-body-e,.ui-overlay-e{border:1px solid #f7c942;color:#222;text-shadow:0 1px 0 #fff;background:#fff9df;background-image:-webkit-gradient(linear,left top,left bottom,from(#fffadf),to(#fff3a5));background-image:-webkit-linear-gradient(#fffadf,#fff3a5);background-image:-moz-linear-gradient(#fffadf,#fff3a5);background-image:-ms-linear-gradient(#fffadf,#fff3a5);background-image:-o-linear-gradient(#fffadf,#fff3a5);background-image:linear-gradient(#fffadf,#fff3a5)}.ui-overlay-e{background-image:none;border-width:0}.ui-body-e,.ui-body-e input,.ui-body-e select,.ui-body-e textarea,.ui-body-e button{font-family:Helvetica,Arial,sans-serif}.ui-body-e .ui-link-inherit{color:#333}.ui-body-e .ui-link{color:#2489ce;font-weight:bold}.ui-body-e .ui-link:hover{color:#2489ce}.ui-body-e .ui-link:active{color:#2489ce}.ui-body-e .ui-link:visited{color:#2489ce}.ui-btn-up-e{border:1px solid #f4c63f;background:#fadb4e;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#ffefaa),to(#ffe155));background-image:-webkit-linear-gradient(#ffefaa,#ffe155);background-image:-moz-linear-gradient(#ffefaa,#ffe155);background-image:-ms-linear-gradient(#ffefaa,#ffe155);background-image:-o-linear-gradient(#ffefaa,#ffe155);background-image:linear-gradient(#ffefaa,#ffe155)}.ui-btn-up-e a.ui-link-inherit{color:#222}.ui-btn-hover-e{border:1px solid #f2c43d;background:#fbe26f;font-weight:bold;color:#111;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#fff5ba),to(#fbdd52));background-image:-webkit-linear-gradient(#fff5ba,#fbdd52);background-image:-moz-linear-gradient(#fff5ba,#fbdd52);background-image:-ms-linear-gradient(#fff5ba,#fbdd52);background-image:-o-linear-gradient(#fff5ba,#fbdd52);background-image:linear-gradient(#fff5ba,#fbdd52)}.ui-btn-hover-e a.ui-link-inherit{color:#333}.ui-btn-down-e{border:1px solid #f2c43d;background:#fceda7;font-weight:bold;color:#111;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from(#f8d94c),to(#fadb4e));background-image:-webkit-linear-gradient(#f8d94c,#fadb4e);background-image:-moz-linear-gradient(#f8d94c,#fadb4e);background-image:-ms-linear-gradient(#f8d94c,#fadb4e);background-image:-o-linear-gradient(#f8d94c,#fadb4e);background-image:linear-gradient(#f8d94c,#fadb4e)}.ui-btn-down-e a.ui-link-inherit{color:#333}.ui-btn-up-e,.ui-btn-hover-e,.ui-btn-down-e{font-family:Helvetica,Arial,sans-serif;text-decoration:none}a.ui-link-inherit{text-decoration:none!important}.ui-btn-active{border:1px solid #2373a5;background:#5393c5;font-weight:bold;color:#fff;cursor:pointer;text-shadow:0 1px 1px #3373a5;text-decoration:none;background-image:-webkit-gradient(linear,left top,left bottom,from(#5393c5),to(#6facd5));background-image:-webkit-linear-gradient(#5393c5,#6facd5);background-image:-moz-linear-gradient(#5393c5,#6facd5);background-image:-ms-linear-gradient(#5393c5,#6facd5);background-image:-o-linear-gradient(#5393c5,#6facd5);background-image:linear-gradient(#5393c5,#6facd5);font-family:Helvetica,Arial,sans-serif}.ui-btn-active a.ui-link-inherit{color:#fff}.ui-btn-inner{border-top:1px solid #fff;border-color:rgba(255,255,255,.3)}.ui-corner-tl{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em}.ui-corner-tr{-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em}.ui-corner-bl{-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em}.ui-corner-br{-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-top{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em;-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em}.ui-corner-bottom{-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em;-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-right{-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em;-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-left{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em;-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em}.ui-corner-all{-moz-border-radius:.6em;-webkit-border-radius:.6em;border-radius:.6em}.ui-corner-none{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.ui-br{border-bottom:#828282;border-bottom:rgba(130,130,130,.3);border-bottom-width:1px;border-bottom-style:solid}.ui-disabled{opacity:.3}.ui-disabled,.ui-disabled a{cursor:default!important;pointer-events:none}.ui-disabled .ui-btn-text{-ms-filter:"alpha(opacity=30)";filter:alpha(opacity=30);zoom:1}.ui-icon,.ui-icon-searchfield:after{background:#666;background:rgba(0,0,0,.4);background-image:url(images/icons-18-white.png);background-repeat:no-repeat;-moz-border-radius:9px;-webkit-border-radius:9px;border-radius:9px}.ui-icon-alt{background:#fff;background:rgba(255,255,255,.3);background-image:url(images/icons-18-black.png);background-repeat:no-repeat}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-resolution:240dpi){.ui-icon-plus,.ui-icon-minus,.ui-icon-delete,.ui-icon-arrow-r,.ui-icon-arrow-l,.ui-icon-arrow-u,.ui-icon-arrow-d,.ui-icon-check,.ui-icon-gear,.ui-icon-refresh,.ui-icon-forward,.ui-icon-back,.ui-icon-grid,.ui-icon-star,.ui-icon-alert,.ui-icon-info,.ui-icon-home,.ui-icon-search,.ui-icon-searchfield:after,.ui-icon-checkbox-off,.ui-icon-checkbox-on,.ui-icon-radio-off,.ui-icon-radio-on{background-image:url(images/icons-36-white.png);-moz-background-size:776px 18px;-o-background-size:776px 18px;-webkit-background-size:776px 18px;background-size:776px 18px}.ui-icon-alt{background-image:url(images/icons-36-black.png)}}.ui-icon-plus{background-position:-0 50%}.ui-icon-minus{background-position:-36px 50%}.ui-icon-delete{background-position:-72px 50%}.ui-icon-arrow-r{background-position:-108px 50%}.ui-icon-arrow-l{background-position:-144px 50%}.ui-icon-arrow-u{background-position:-180px 50%}.ui-icon-arrow-d{background-position:-216px 50%}.ui-icon-check{background-position:-252px 50%}.ui-icon-gear{background-position:-288px 50%}.ui-icon-refresh{background-position:-324px 50%}.ui-icon-forward{background-position:-360px 50%}.ui-icon-back{background-position:-396px 50%}.ui-icon-grid{background-position:-432px 50%}.ui-icon-star{background-position:-468px 50%}.ui-icon-alert{background-position:-504px 50%}.ui-icon-info{background-position:-540px 50%}.ui-icon-home{background-position:-576px 50%}.ui-icon-search,.ui-icon-searchfield:after{background-position:-612px 50%}.ui-icon-checkbox-off{background-position:-684px 50%}.ui-icon-checkbox-on{background-position:-648px 50%}.ui-icon-radio-off{background-position:-756px 50%}.ui-icon-radio-on{background-position:-720px 50%}.ui-checkbox .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px}.ui-icon-checkbox-off,.ui-icon-radio-off{background-color:transparent}.ui-checkbox-on .ui-icon,.ui-radio-on .ui-icon{background-color:#4596ce}.ui-icon-loading{background:url(images/ajax-loader.gif);background-size:46px 46px}.ui-btn-corner-tl{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em}.ui-btn-corner-tr{-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em}.ui-btn-corner-bl{-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em}.ui-btn-corner-br{-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em}.ui-btn-corner-top{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em;-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em}.ui-btn-corner-bottom{-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em;-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em}.ui-btn-corner-right{-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em;-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em}.ui-btn-corner-left{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em;-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em}.ui-btn-corner-all{-moz-border-radius:1em;-webkit-border-radius:1em;border-radius:1em}.ui-corner-tl,.ui-corner-tr,.ui-corner-bl,.ui-corner-br,.ui-corner-top,.ui-corner-bottom,.ui-corner-right,.ui-corner-left,.ui-corner-all,.ui-btn-corner-tl,.ui-btn-corner-tr,.ui-btn-corner-bl,.ui-btn-corner-br,.ui-btn-corner-top,.ui-btn-corner-bottom,.ui-btn-corner-right,.ui-btn-corner-left,.ui-btn-corner-all{-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.ui-overlay{background:#666;opacity:.5;filter:Alpha(Opacity=50);position:absolute;width:100%;height:100%}.ui-overlay-shadow{-moz-box-shadow:0 0 12px rgba(0,0,0,.6);-webkit-box-shadow:0 0 12px rgba(0,0,0,.6);box-shadow:0 0 12px rgba(0,0,0,.6)}.ui-shadow{-moz-box-shadow:0 1px 4px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.3);box-shadow:0 1px 4px rgba(0,0,0,.3)}.ui-bar-a .ui-shadow,.ui-bar-b .ui-shadow,.ui-bar-c .ui-shadow{-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)}.ui-shadow-inset{-moz-box-shadow:inset 0 1px 4px rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 4px rgba(0,0,0,.2);box-shadow:inset 0 1px 4px rgba(0,0,0,.2)}.ui-icon-shadow{-moz-box-shadow:0 1px 0 rgba(255,255,255,.4);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.4);box-shadow:0 1px 0 rgba(255,255,255,.4)}.ui-btn:focus{outline:0}.ui-focus,.ui-btn:focus{-moz-box-shadow:0 0 12px #387bbe;-webkit-box-shadow:0 0 12px #387bbe;box-shadow:0 0 12px #387bbe}.ui-mobile-nosupport-boxshadow *{-moz-box-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}.ui-mobile-nosupport-boxshadow .ui-focus,.ui-mobile-nosupport-boxshadow .ui-btn:focus{outline-width:1px;outline-style:dotted}.ui-mobile,.ui-mobile body{height:99.9%}.ui-mobile fieldset,.ui-page{padding:0;margin:0}.ui-mobile a img,.ui-mobile fieldset{border-width:0}.ui-mobile-viewport{margin:0;overflow-x:visible;-webkit-text-size-adjust:none;-ms-text-size-adjust:none;-webkit-tap-highlight-color:rgba(0,0,0,0)}body.ui-mobile-viewport,div.ui-mobile-viewport{overflow-x:hidden}.ui-mobile [data-role=page],.ui-mobile [data-role=dialog],.ui-page{top:0;left:0;width:100%;min-height:100%;position:absolute;display:none;border:0}.ui-mobile .ui-page-active{display:block;overflow:visible}.ui-page{outline:0}@media screen and (orientation:portrait){.ui-mobile,.ui-mobile .ui-page{min-height:420px}}@media screen and (orientation:landscape){.ui-mobile,.ui-mobile .ui-page{min-height:300px}}.ui-loading .ui-loader{display:block}.ui-loader{display:none;z-index:9999999;position:fixed;top:50%;box-shadow:0 1px 1px -1px #fff;left:50%;border:0}.ui-loader-default{background:0;opacity:.18;width:46px;height:46px;margin-left:-23px;margin-top:-23px}.ui-loader-verbose{width:200px;opacity:.88;height:auto;margin-left:-110px;margin-top:-43px;padding:10px}.ui-loader-default h1{font-size:0;width:0;height:0;overflow:hidden}.ui-loader-verbose h1{font-size:16px;margin:0;text-align:center}.ui-loader .ui-icon{background-color:#000;display:block;margin:0;width:44px;height:44px;padding:1px;-webkit-border-radius:36px;-moz-border-radius:36px;border-radius:36px}.ui-loader-verbose .ui-icon{margin:0 auto 10px;opacity:.75}.ui-loader-textonly{padding:15px;margin-left:-115px}.ui-loader-textonly .ui-icon{display:none}.ui-loader-fakefix{position:absolute}.ui-mobile-rendering>*{visibility:hidden}.ui-bar,.ui-body{position:relative;padding:.4em 15px;overflow:hidden;display:block;clear:both}.ui-bar{font-size:16px;margin:0}.ui-bar h1,.ui-bar h2,.ui-bar h3,.ui-bar h4,.ui-bar h5,.ui-bar h6{margin:0;padding:0;font-size:16px;display:inline-block}.ui-header,.ui-footer{position:relative;border-left-width:0;border-right-width:0}.ui-header .ui-btn-left,.ui-header .ui-btn-right,.ui-footer .ui-btn-left,.ui-footer .ui-btn-right{position:absolute;top:3px}.ui-header .ui-btn-left,.ui-footer .ui-btn-left{left:5px}.ui-header .ui-btn-right,.ui-footer .ui-btn-right{right:5px}.ui-footer .ui-btn-icon-notext,.ui-header .ui-btn-icon-notext{top:6px}.ui-header .ui-title,.ui-footer .ui-title{min-height:1.1em;text-align:center;font-size:16px;display:block;margin:.6em 30% .8em;padding:0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;outline:0!important}.ui-footer .ui-title{margin:.6em 15px .8em}.ui-content{border-width:0;overflow:visible;overflow-x:hidden;padding:15px}.ui-icon{width:18px;height:18px}.ui-nojs{position:absolute;left:-9999px}.ui-hide-label label,.ui-hidden-accessible{position:absolute!important;left:-9999px;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px)}.ui-mobile-viewport-transitioning,.ui-mobile-viewport-transitioning .ui-page{width:100%;height:100%;overflow:hidden}.in{-webkit-animation-timing-function:ease-out;-webkit-animation-duration:350ms;-moz-animation-timing-function:ease-out;-moz-animation-duration:350ms}.out{-webkit-animation-timing-function:ease-in;-webkit-animation-duration:225ms;-moz-animation-timing-function:ease-in;-moz-animation-duration:225}@-webkit-keyframes fadein{from{opacity:0}to{opacity:1}}@-moz-keyframes fadein{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadeout{from{opacity:1}to{opacity:0}}@-moz-keyframes fadeout{from{opacity:1}to{opacity:0}}.fade.out{opacity:0;-webkit-animation-duration:125ms;-webkit-animation-name:fadeout;-moz-animation-duration:125ms;-moz-animation-name:fadeout}.fade.in{opacity:1;-webkit-animation-duration:225ms;-webkit-animation-name:fadein;-moz-animation-duration:225ms;-moz-animation-name:fadein}.pop{-webkit-transform-origin:50% 50%;-moz-transform-origin:50% 50%}.pop.in{-webkit-transform:scale(1);-moz-transform:scale(1);opacity:1;-webkit-animation-name:popin;-moz-animation-name:popin;-webkit-animation-duration:350ms;-moz-animation-duration:350ms}.pop.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;opacity:0;-webkit-animation-duration:100ms;-moz-animation-duration:100ms}.pop.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein}.pop.out.reverse{-webkit-transform:scale(.8);-moz-transform:scale(.8);-webkit-animation-name:popout;-moz-animation-name:popout}@-webkit-keyframes popin{from{-webkit-transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);opacity:1}}@-moz-keyframes popin{from{-moz-transform:scale(.8);opacity:0}to{-moz-transform:scale(1);opacity:1}}@-webkit-keyframes popout{from{-webkit-transform:scale(1);opacity:1}to{-webkit-transform:scale(.8);opacity:0}}@-moz-keyframes popout{from{-moz-transform:scale(1);opacity:1}to{-moz-transform:scale(.8);opacity:0}}@-webkit-keyframes slideinfromright{from{-webkit-transform:translateX(100%)}to{-webkit-transform:translateX(0)}}@-moz-keyframes slideinfromright{from{-moz-transform:translateX(100%)}to{-moz-transform:translateX(0)}}@-webkit-keyframes slideinfromleft{from{-webkit-transform:translateX(-100%)}to{-webkit-transform:translateX(0)}}@-moz-keyframes slideinfromleft{from{-moz-transform:translateX(-100%)}to{-moz-transform:translateX(0)}}@-webkit-keyframes slideouttoleft{from{-webkit-transform:translateX(0)}to{-webkit-transform:translateX(-100%)}}@-moz-keyframes slideouttoleft{from{-moz-transform:translateX(0)}to{-moz-transform:translateX(-100%)}}@-webkit-keyframes slideouttoright{from{-webkit-transform:translateX(0)}to{-webkit-transform:translateX(100%)}}@-moz-keyframes slideouttoright{from{-moz-transform:translateX(0)}to{-moz-transform:translateX(100%)}}.slide.out,.slide.in{-webkit-animation-timing-function:ease-out;-webkit-animation-duration:350ms;-moz-animation-timing-function:ease-out;-moz-animation-duration:350ms}.slide.out{-webkit-transform:translateX(-100%);-webkit-animation-name:slideouttoleft;-moz-transform:translateX(-100%);-moz-animation-name:slideouttoleft}.slide.in{-webkit-transform:translateX(0);-webkit-animation-name:slideinfromright;-moz-transform:translateX(0);-moz-animation-name:slideinfromright}.slide.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:slideouttoright;-moz-transform:translateX(100%);-moz-animation-name:slideouttoright}.slide.in.reverse{-webkit-transform:translateX(0);-webkit-animation-name:slideinfromleft;-moz-transform:translateX(0);-moz-animation-name:slideinfromleft}.slidefade.out{-webkit-transform:translateX(-100%);-webkit-animation-name:slideouttoleft;-moz-transform:translateX(-100%);-moz-animation-name:slideouttoleft;-webkit-animation-duration:225ms;-moz-animation-duration:225ms}.slidefade.in{-webkit-transform:translateX(0);-webkit-animation-name:fadein;-moz-transform:translateX(0);-moz-animation-name:fadein;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}.slidefade.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:slideouttoright;-moz-transform:translateX(100%);-moz-animation-name:slideouttoright;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}.slidefade.in.reverse{-webkit-transform:translateX(0);-webkit-animation-name:fadein;-moz-transform:translateX(0);-moz-animation-name:fadein;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}.slidedown.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;-webkit-animation-duration:100ms;-moz-animation-duration:100ms}.slidedown.in{-webkit-transform:translateY(0);-webkit-animation-name:slideinfromtop;-moz-transform:translateY(0);-moz-animation-name:slideinfromtop;-webkit-animation-duration:250ms;-moz-animation-duration:250ms}.slidedown.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein;-webkit-animation-duration:150ms;-moz-animation-duration:150ms}.slidedown.out.reverse{-webkit-transform:translateY(-100%);-moz-transform:translateY(-100%);-webkit-animation-name:slideouttotop;-moz-animation-name:slideouttotop;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}@-webkit-keyframes slideinfromtop{from{-webkit-transform:translateY(-100%)}to{-webkit-transform:translateY(0)}}@-moz-keyframes slideinfromtop{from{-moz-transform:translateY(-100%)}to{-moz-transform:translateY(0)}}@-webkit-keyframes slideouttotop{from{-webkit-transform:translateY(0)}to{-webkit-transform:translateY(-100%)}}@-moz-keyframes slideouttotop{from{-moz-transform:translateY(0)}to{-moz-transform:translateY(-100%)}}.slideup.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;-webkit-animation-duration:100ms;-moz-animation-duration:100ms}.slideup.in{-webkit-transform:translateY(0);-webkit-animation-name:slideinfrombottom;-moz-transform:translateY(0);-moz-animation-name:slideinfrombottom;-webkit-animation-duration:250ms;-moz-animation-duration:250ms}.slideup.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein;-webkit-animation-duration:150ms;-moz-animation-duration:150ms}.slideup.out.reverse{-webkit-transform:translateY(100%);-moz-transform:translateY(100%);-webkit-animation-name:slideouttobottom;-moz-animation-name:slideouttobottom;-webkit-animation-duration:200ms;-moz-animation-duration:200ms}@-webkit-keyframes slideinfrombottom{from{-webkit-transform:translateY(100%)}to{-webkit-transform:translateY(0)}}@-moz-keyframes slideinfrombottom{from{-moz-transform:translateY(100%)}to{-moz-transform:translateY(0)}}@-webkit-keyframes slideouttobottom{from{-webkit-transform:translateY(0)}to{-webkit-transform:translateY(100%)}}@-moz-keyframes slideouttobottom{from{-moz-transform:translateY(0)}to{-moz-transform:translateY(100%)}}.viewport-flip{-webkit-perspective:1000;-moz-perspective:1000;position:absolute}.flip{-webkit-backface-visibility:hidden;-webkit-transform:translateX(0);-moz-backface-visibility:hidden;-moz-transform:translateX(0)}.flip.out{-webkit-transform:rotateY(-90deg) scale(.9);-webkit-animation-name:flipouttoleft;-webkit-animation-duration:175ms;-moz-transform:rotateY(-90deg) scale(.9);-moz-animation-name:flipouttoleft;-moz-animation-duration:175ms}.flip.in{-webkit-animation-name:flipintoright;-webkit-animation-duration:225ms;-moz-animation-name:flipintoright;-moz-animation-duration:225ms}.flip.out.reverse{-webkit-transform:rotateY(90deg) scale(.9);-webkit-animation-name:flipouttoright;-moz-transform:rotateY(90deg) scale(.9);-moz-animation-name:flipouttoright}.flip.in.reverse{-webkit-animation-name:flipintoleft;-moz-animation-name:flipintoleft}@-webkit-keyframes flipouttoleft{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(-90deg) scale(.9)}}@-moz-keyframes flipouttoleft{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(-90deg) scale(.9)}}@-webkit-keyframes flipouttoright{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(90deg) scale(.9)}}@-moz-keyframes flipouttoright{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(90deg) scale(.9)}}@-webkit-keyframes flipintoleft{from{-webkit-transform:rotateY(-90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoleft{from{-moz-transform:rotateY(-90deg) scale(.9)}to{-moz-transform:rotateY(0)}}@-webkit-keyframes flipintoright{from{-webkit-transform:rotateY(90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoright{from{-moz-transform:rotateY(90deg) scale(.9)}to{-moz-transform:rotateY(0)}}.viewport-turn{-webkit-perspective:1000;-moz-perspective:1000;position:absolute}.turn{-webkit-backface-visibility:hidden;-webkit-transform:translateX(0);-webkit-transform-origin:0 0;-moz-backface-visibility:hidden;-moz-transform:translateX(0);-moz-transform-origin:0 0}.turn.out{-webkit-transform:rotateY(-90deg) scale(.9);-webkit-animation-name:flipouttoleft;-moz-transform:rotateY(-90deg) scale(.9);-moz-animation-name:flipouttoleft;-webkit-animation-duration:125ms;-moz-animation-duration:125ms}.turn.in{-webkit-animation-name:flipintoright;-moz-animation-name:flipintoright;-webkit-animation-duration:250ms;-moz-animation-duration:250ms}.turn.out.reverse{-webkit-transform:rotateY(90deg) scale(.9);-webkit-animation-name:flipouttoright;-moz-transform:rotateY(90deg) scale(.9);-moz-animation-name:flipouttoright}.turn.in.reverse{-webkit-animation-name:flipintoleft;-moz-animation-name:flipintoleft}@-webkit-keyframes flipouttoleft{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(-90deg) scale(.9)}}@-moz-keyframes flipouttoleft{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(-90deg) scale(.9)}}@-webkit-keyframes flipouttoright{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(90deg) scale(.9)}}@-moz-keyframes flipouttoright{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(90deg) scale(.9)}}@-webkit-keyframes flipintoleft{from{-webkit-transform:rotateY(-90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoleft{from{-moz-transform:rotateY(-90deg) scale(.9)}to{-moz-transform:rotateY(0)}}@-webkit-keyframes flipintoright{from{-webkit-transform:rotateY(90deg) scale(.9)}to{-webkit-transform:rotateY(0)}}@-moz-keyframes flipintoright{from{-moz-transform:rotateY(90deg) scale(.9)}to{-moz-transform:rotateY(0)}}.flow{-webkit-transform-origin:50% 30%;-moz-transform-origin:50% 30%;-webkit-box-shadow:0 0 20px rgba(0,0,0,.4);-moz-box-shadow:0 0 20px rgba(0,0,0,.4)}.ui-dialog.flow{-webkit-transform-origin:none;-moz-transform-origin:none;-webkit-box-shadow:none;-moz-box-shadow:none}.flow.out{-webkit-transform:translateX(-100%) scale(.7);-webkit-animation-name:flowouttoleft;-webkit-animation-timing-function:ease;-webkit-animation-duration:350ms;-moz-transform:translateX(-100%) scale(.7);-moz-animation-name:flowouttoleft;-moz-animation-timing-function:ease;-moz-animation-duration:350ms}.flow.in{-webkit-transform:translateX(0) scale(1);-webkit-animation-name:flowinfromright;-webkit-animation-timing-function:ease;-webkit-animation-duration:350ms;-moz-transform:translateX(0) scale(1);-moz-animation-name:flowinfromright;-moz-animation-timing-function:ease;-moz-animation-duration:350ms}.flow.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:flowouttoright;-moz-transform:translateX(100%);-moz-animation-name:flowouttoright}.flow.in.reverse{-webkit-animation-name:flowinfromleft;-moz-animation-name:flowinfromleft}@-webkit-keyframes flowouttoleft{0%{-webkit-transform:translateX(0) scale(1)}60%,70%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(-100%) scale(.7)}}@-moz-keyframes flowouttoleft{0%{-moz-transform:translateX(0) scale(1)}60%,70%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(-100%) scale(.7)}}@-webkit-keyframes flowouttoright{0%{-webkit-transform:translateX(0) scale(1)}60%,70%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(100%) scale(.7)}}@-moz-keyframes flowouttoright{0%{-moz-transform:translateX(0) scale(1)}60%,70%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(100%) scale(.7)}}@-webkit-keyframes flowinfromleft{0%{-webkit-transform:translateX(-100%) scale(.7)}30%,40%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(0) scale(1)}}@-moz-keyframes flowinfromleft{0%{-moz-transform:translateX(-100%) scale(.7)}30%,40%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(0) scale(1)}}@-webkit-keyframes flowinfromright{0%{-webkit-transform:translateX(100%) scale(.7)}30%,40%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(0) scale(1)}}@-moz-keyframes flowinfromright{0%{-moz-transform:translateX(100%) scale(.7)}30%,40%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(0) scale(1)}}.ui-grid-a,.ui-grid-b,.ui-grid-c,.ui-grid-d{overflow:hidden}.ui-block-a,.ui-block-b,.ui-block-c,.ui-block-d,.ui-block-e{margin:0;padding:0;border:0;float:left;min-height:1px}.ui-grid-solo .ui-block-a{width:100%;float:none}.ui-grid-a .ui-block-a,.ui-grid-a .ui-block-b{width:50%}.ui-grid-a .ui-block-a{clear:left}.ui-grid-b .ui-block-a,.ui-grid-b .ui-block-b,.ui-grid-b .ui-block-c{width:33.333%}.ui-grid-b .ui-block-a{clear:left}.ui-grid-c .ui-block-a,.ui-grid-c .ui-block-b,.ui-grid-c .ui-block-c,.ui-grid-c .ui-block-d{width:25%}.ui-grid-c .ui-block-a{clear:left}.ui-grid-d .ui-block-a,.ui-grid-d .ui-block-b,.ui-grid-d .ui-block-c,.ui-grid-d .ui-block-d,.ui-grid-d .ui-block-e{width:20%}.ui-grid-d .ui-block-a{clear:left}.ui-header-fixed,.ui-footer-fixed{left:0;right:0;width:100%;position:fixed;z-index:1000}.ui-header-fixed{top:0}.ui-footer-fixed{bottom:0}.ui-header-fullscreen,.ui-footer-fullscreen{opacity:.9}.ui-page-header-fixed{padding-top:2.5em}.ui-page-footer-fixed{padding-bottom:3em}.ui-page-header-fullscreen .ui-content,.ui-page-footer-fullscreen .ui-content{padding:0}.ui-fixed-hidden{position:absolute}.ui-page-header-fullscreen .ui-fixed-hidden,.ui-page-footer-fullscreen .ui-fixed-hidden{left:-99999em}.ui-header-fixed .ui-btn,.ui-footer-fixed .ui-btn{z-index:10}.ui-navbar{overflow:hidden}.ui-navbar ul,.ui-navbar-expanded ul{list-style:none;padding:0;margin:0;position:relative;display:block;border:0}.ui-navbar-collapsed ul{float:left;width:75%;margin-right:-2px}.ui-navbar-collapsed .ui-navbar-toggle{float:left;width:25%}.ui-navbar li.ui-navbar-truncate{position:absolute;left:-9999px;top:-9999px}.ui-navbar li .ui-btn,.ui-navbar .ui-navbar-toggle .ui-btn{display:block;font-size:12px;text-align:center;margin:0;border-right-width:0;max-width:100%}.ui-navbar li .ui-btn{margin-right:-1px}.ui-navbar li .ui-btn:last-child{margin-right:0}.ui-header .ui-navbar li .ui-btn,.ui-header .ui-navbar .ui-navbar-toggle .ui-btn,.ui-footer .ui-navbar li .ui-btn,.ui-footer .ui-navbar .ui-navbar-toggle .ui-btn{border-top-width:0;border-bottom-width:0}.ui-navbar .ui-btn-inner{padding-left:2px;padding-right:2px}.ui-navbar-noicons li .ui-btn .ui-btn-inner,.ui-navbar-noicons .ui-navbar-toggle .ui-btn-inner{padding-top:.8em;padding-bottom:.9em}.ui-navbar-expanded .ui-btn{margin:0;font-size:14px}.ui-navbar-expanded .ui-btn-inner{padding-left:5px;padding-right:5px}.ui-navbar-expanded .ui-btn-icon-top .ui-btn-inner{padding:45px 5px 15px;text-align:center}.ui-navbar-expanded .ui-btn-icon-top .ui-icon{top:15px}.ui-navbar-expanded .ui-btn-icon-bottom .ui-btn-inner{padding:15px 5px 45px;text-align:center}.ui-navbar-expanded .ui-btn-icon-bottom .ui-icon{bottom:15px}.ui-navbar-expanded li .ui-btn .ui-btn-inner{min-height:2.5em}.ui-navbar-expanded .ui-navbar-noicons .ui-btn .ui-btn-inner{padding-top:1.8em;padding-bottom:1.9em}.ui-btn{display:block;text-align:center;cursor:pointer;position:relative;margin:.5em 5px;padding:0}.ui-mini{margin:.25em 5px}.ui-btn-inner{padding:.6em 20px;min-width:.75em;display:block;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;position:relative;zoom:1}.ui-btn input,.ui-btn button{z-index:2}.ui-btn-left,.ui-btn-right,.ui-btn-inline{display:inline-block}.ui-btn-block{display:block}.ui-header .ui-btn,.ui-footer .ui-btn{display:inline-block;margin:0}.ui-header .ui-btn-inner,.ui-footer .ui-btn-inner,.ui-mini .ui-btn-inner{font-size:12.5px;padding:.55em 11px .5em}.ui-header .ui-fullsize .ui-btn-inner,.ui-footer .ui-fullsize .ui-btn-inner{font-size:16px;padding:.6em 25px}.ui-btn-icon-notext{width:24px;height:24px}.ui-btn-icon-notext .ui-btn-inner{padding:0;height:100%}.ui-btn-icon-notext .ui-btn-inner .ui-icon{margin:2px 1px 2px 3px}.ui-btn-text{position:relative;z-index:1;width:100%}.ui-btn-icon-notext .ui-btn-text{position:absolute;left:-9999px}.ui-btn-icon-left .ui-btn-inner{padding-left:40px}.ui-btn-icon-right .ui-btn-inner{padding-right:40px}.ui-btn-icon-top .ui-btn-inner{padding-top:40px}.ui-btn-icon-bottom .ui-btn-inner{padding-bottom:40px}.ui-header .ui-btn-icon-left .ui-btn-inner,.ui-footer .ui-btn-icon-left .ui-btn-inner,.ui-mini .ui-btn-icon-left .ui-btn-inner{padding-left:30px}.ui-header .ui-btn-icon-right .ui-btn-inner,.ui-footer .ui-btn-icon-right .ui-btn-inner,.ui-mini .ui-btn-icon-right .ui-btn-inner{padding-right:30px}.ui-header .ui-btn-icon-top .ui-btn-inner,.ui-footer .ui-btn-icon-top .ui-btn-inner,.ui-mini .ui-btn-icon-top .ui-btn-inner{padding:30px 3px .5em 3px}.ui-header .ui-btn-icon-bottom .ui-btn-inner,.ui-footer .ui-btn-icon-bottom .ui-btn-inner,.ui-mini .ui-btn-icon-bottom .ui-btn-inner{padding:.55em 3px 30px 3px}.ui-btn-icon-notext .ui-icon{display:block;z-index:0}.ui-btn-icon-left .ui-btn-inner .ui-icon,.ui-btn-icon-right .ui-btn-inner .ui-icon{position:absolute;top:50%;margin-top:-9px}.ui-btn-icon-top .ui-btn-inner .ui-icon,.ui-btn-icon-bottom .ui-btn-inner .ui-icon{position:absolute;left:50%;margin-left:-9px}.ui-btn-icon-left .ui-icon{left:10px}.ui-btn-icon-right .ui-icon{right:10px}.ui-btn-icon-top .ui-icon{top:10px}.ui-btn-icon-bottom .ui-icon{top:auto;bottom:10px}.ui-header .ui-btn-icon-left .ui-icon,.ui-footer .ui-btn-icon-left .ui-icon,.ui-mini.ui-btn-icon-left .ui-icon,.ui-mini .ui-btn-icon-left .ui-icon{left:5px}.ui-header .ui-btn-icon-right .ui-icon,.ui-footer .ui-btn-icon-right .ui-icon,.ui-mini.ui-btn-icon-right .ui-icon,.ui-mini .ui-btn-icon-right .ui-icon{right:5px}.ui-header .ui-btn-icon-top .ui-icon,.ui-footer .ui-btn-icon-top .ui-icon,.ui-mini.ui-btn-icon-top .ui-icon,.ui-mini .ui-btn-icon-top .ui-icon{top:5px}.ui-header .ui-btn-icon-bottom .ui-icon,.ui-footer .ui-btn-icon-bottom .ui-icon,.ui-mini.ui-btn-icon-bottom .ui-icon,.ui-mini .ui-btn-icon-bottom .ui-icon{bottom:5px}.ui-btn-hidden{position:absolute;top:0;left:0;width:100%;height:100%;-webkit-appearance:button;opacity:.1;cursor:pointer;background:#fff;background:rgba(255,255,255,0);filter:Alpha(Opacity=.0001);font-size:1px;border:0;text-indent:-9999px}.ui-collapsible{margin:.5em 0}.ui-collapsible-heading{font-size:16px;display:block;margin:0 -8px;padding:0;border-width:0 0 1px 0;position:relative}.ui-collapsible-heading a{text-align:left;margin:0}.ui-collapsible-heading .ui-btn-inner,.ui-collapsible-heading .ui-btn-icon-left .ui-btn-inner{padding-left:40px}.ui-collapsible-heading .ui-btn-icon-right .ui-btn-inner{padding-left:12px;padding-right:40px}.ui-collapsible-heading .ui-btn-icon-top .ui-btn-inner,.ui-collapsible-heading .ui-btn-icon-bottom .ui-btn-inner{padding-right:40px;text-align:center}.ui-collapsible-heading a span.ui-btn{position:absolute;left:6px;top:50%;margin:-12px 0 0 0;width:20px;height:20px;padding:1px 0 1px 2px;text-indent:-9999px}.ui-collapsible-heading a span.ui-btn .ui-btn-inner{padding:10px 0}.ui-collapsible-heading a span.ui-btn .ui-icon{left:0;margin-top:-10px}.ui-collapsible-heading-status{position:absolute;top:-9999px;left:0}.ui-collapsible-content{display:block;margin:0 -8px;padding:10px 16px;border-top:0;background-image:none;font-weight:normal}.ui-collapsible-content-collapsed{display:none}.ui-collapsible-set{margin:.5em 0}.ui-collapsible-set .ui-collapsible{margin:-1px 0 0}.ui-controlgroup,fieldset.ui-controlgroup{padding:0;margin:0 0 .5em;zoom:1}.ui-bar .ui-controlgroup{margin:0 .3em}.ui-controlgroup-label{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .4em}.ui-controlgroup-controls{display:block;width:100%}.ui-controlgroup li{list-style:none}.ui-controlgroup-vertical .ui-btn,.ui-controlgroup-vertical .ui-checkbox,.ui-controlgroup-vertical .ui-radio{margin:0;border-bottom-width:0}.ui-controlgroup-controls label.ui-select{position:absolute;left:-9999px}.ui-controlgroup-vertical .ui-controlgroup-last{border-bottom-width:1px}.ui-controlgroup-horizontal{padding:0}.ui-controlgroup-horizontal .ui-btn-inner{text-align:center}.ui-controlgroup-horizontal .ui-btn,.ui-controlgroup-horizontal .ui-select{display:inline-block;margin:0 -6px 0 0}.ui-controlgroup-horizontal .ui-checkbox,.ui-controlgroup-horizontal .ui-radio{float:left;clear:none;margin:0 -1px 0 0}.ui-controlgroup-horizontal .ui-checkbox .ui-btn,.ui-controlgroup-horizontal .ui-radio .ui-btn,.ui-controlgroup-horizontal .ui-checkbox:last-child,.ui-controlgroup-horizontal .ui-radio:last-child{margin-right:0}.ui-controlgroup-horizontal .ui-controlgroup-last{margin-right:0}.ui-controlgroup .ui-checkbox label,.ui-controlgroup .ui-radio label{font-size:16px}@media all and (min-width:450px){.ui-field-contain .ui-controlgroup-label{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-controlgroup-controls{width:60%;display:inline-block}.ui-field-contain .ui-controlgroup .ui-select{width:100%}.ui-field-contain .ui-controlgroup-horizontal .ui-select{width:auto}}.ui-dialog{background:none!important}.ui-dialog-contain{width:92.5%;max-width:500px;margin:10% auto 15px auto;padding:0}.ui-dialog .ui-header{margin-top:15%;border:0;overflow:hidden}.ui-dialog .ui-header,.ui-dialog .ui-content,.ui-dialog .ui-footer{display:block;position:relative;width:auto}.ui-dialog .ui-header,.ui-dialog .ui-footer{z-index:10;padding:0}.ui-dialog .ui-footer{padding:0 15px}.ui-dialog .ui-content{padding:15px}.ui-dialog{margin-top:-15px}.ui-checkbox,.ui-radio{position:relative;clear:both;margin:.2em 0 .5em;z-index:1}.ui-checkbox .ui-btn,.ui-radio .ui-btn{margin:0;text-align:left;z-index:2}.ui-checkbox .ui-btn-inner,.ui-radio .ui-btn-inner{white-space:normal}.ui-checkbox .ui-btn-icon-left .ui-btn-inner,.ui-radio .ui-btn-icon-left .ui-btn-inner{padding-left:45px}.ui-checkbox .ui-mini.ui-btn-icon-left .ui-btn-inner,.ui-radio .ui-mini.ui-btn-icon-left .ui-btn-inner{padding-left:36px}.ui-checkbox .ui-btn-icon-right .ui-btn-inner,.ui-radio .ui-btn-icon-right .ui-btn-inner{padding-right:45px}.ui-checkbox .ui-mini.ui-btn-icon-right .ui-btn-inner,.ui-radio .ui-mini.ui-btn-icon-right .ui-btn-inner{padding-right:36px}.ui-checkbox .ui-btn-icon-top .ui-btn-inner,.ui-radio .ui-btn-icon-top .ui-btn-inner{padding-right:0;padding-left:0;text-align:center}.ui-checkbox .ui-btn-icon-bottom .ui-btn-inner,.ui-radio .ui-btn-icon-bottom .ui-btn-inner{padding-right:0;padding-left:0;text-align:center}.ui-checkbox .ui-icon,.ui-radio .ui-icon{top:1.1em}.ui-checkbox .ui-btn-icon-left .ui-icon,.ui-radio .ui-btn-icon-left .ui-icon{left:15px}.ui-checkbox .ui-mini.ui-btn-icon-left .ui-icon,.ui-radio .ui-mini.ui-btn-icon-left .ui-icon{left:9px}.ui-checkbox .ui-btn-icon-right .ui-icon,.ui-radio .ui-btn-icon-right .ui-icon{right:15px}.ui-checkbox .ui-mini.ui-btn-icon-right .ui-icon,.ui-radio .ui-mini.ui-btn-icon-right .ui-icon{right:9px}.ui-checkbox .ui-btn-icon-top .ui-icon,.ui-radio .ui-btn-icon-top .ui-icon{top:10px}.ui-checkbox .ui-btn-icon-bottom .ui-icon,.ui-radio .ui-btn-icon-bottom .ui-icon{top:auto;bottom:10px}.ui-checkbox .ui-btn-icon-right .ui-icon,.ui-radio .ui-btn-icon-right .ui-icon{right:15px}.ui-checkbox .ui-mini.ui-btn-icon-right .ui-icon,.ui-radio .ui-mini.ui-btn-icon-right .ui-icon{right:9px}.ui-checkbox input,.ui-radio input{position:absolute;left:20px;top:50%;width:10px;height:10px;margin:-5px 0 0 0;outline:0!important;z-index:1}.ui-field-contain,fieldset.ui-field-contain{padding:.8em 0;margin:0;border-width:0 0 1px 0;overflow:visible}.ui-field-contain:first-child{border-top-width:0}.ui-header .ui-field-contain-left,.ui-header .ui-field-contain-right{position:absolute;top:0;width:25%}.ui-header .ui-field-contain-left{left:1em}.ui-header .ui-field-contain-right{right:1em}@media all and (min-width:450px){.ui-field-contain,.ui-mobile fieldset.ui-field-contain{border-width:0;padding:0;margin:1em 0}}.ui-select{display:block;position:relative}.ui-select select{position:absolute;left:-9999px;top:-9999px}.ui-select .ui-btn{overflow:hidden;opacity:1;margin:0}.ui-select .ui-btn select{cursor:pointer;-webkit-appearance:button;left:0;top:0;width:100%;min-height:1.5em;min-height:100%;height:3em;max-height:100%;opacity:0;-ms-filter:"alpha(opacity=0)";filter:alpha(opacity=0);z-index:2}.ui-select .ui-disabled{opacity:.3}@-moz-document url-prefix(){.ui-select .ui-btn select{opacity:.0001}}.ui-select .ui-btn select.ui-select-nativeonly{opacity:1;text-indent:0}.ui-select .ui-btn-icon-right .ui-btn-inner{padding-right:45px}.ui-select .ui-btn-icon-right .ui-icon{right:15px}.ui-select .ui-mini.ui-btn-icon-right .ui-icon{right:7px}label.ui-select{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block}.ui-select .ui-btn-text,.ui-selectmenu .ui-btn-text{display:block;min-height:1em;overflow:hidden!important}.ui-select .ui-btn-text{text-overflow:ellipsis}.ui-selectmenu{position:absolute;padding:0;z-index:1100!important;width:80%;max-width:350px;padding:6px}.ui-selectmenu .ui-listview{margin:0}.ui-selectmenu .ui-btn.ui-li-divider{cursor:default}.ui-selectmenu-hidden{top:-9999px;left:-9999px}.ui-selectmenu-screen{position:absolute;top:0;left:0;width:100%;height:100%;z-index:99}.ui-screen-hidden,.ui-selectmenu-list .ui-li .ui-icon{display:none}.ui-selectmenu-list .ui-li .ui-icon{display:block}.ui-li.ui-selectmenu-placeholder{display:none}.ui-selectmenu .ui-header .ui-title{margin:.6em 46px .8em}@media all and (min-width:450px){.ui-field-contain label.ui-select{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-select{width:60%;display:inline-block}}.ui-selectmenu .ui-header h1:after{content:'.';visibility:hidden}label.ui-input-text{font-size:16px;line-height:1.4;display:block;font-weight:normal;margin:0 0 .3em}input.ui-input-text,textarea.ui-input-text{background-image:none;padding:.4em;line-height:1.4;font-size:16px;display:block;width:97%;outline:0}.ui-header input.ui-input-text,.ui-footer input.ui-input-text{margin-left:1.25%;padding:.4em 1%;width:95.5%}input.ui-input-text{-webkit-appearance:none}textarea.ui-input-text{height:50px;-webkit-transition:height 200ms linear;-moz-transition:height 200ms linear;-o-transition:height 200ms linear;transition:height 200ms linear}.ui-input-search{padding:0 30px;background-image:none;position:relative}.ui-icon-searchfield:after{position:absolute;left:7px;top:50%;margin-top:-9px;content:"";width:18px;height:18px;opacity:.5}.ui-input-search input.ui-input-text{border:0;width:98%;padding:.4em 0;margin:0;display:block;background:transparent none;outline:0!important}.ui-input-search .ui-input-clear{position:absolute;right:0;top:50%;margin-top:-13px}.ui-mini .ui-input-clear{right:-3px}.ui-input-search .ui-input-clear-hidden{display:none}input.ui-mini,.ui-mini input,textarea.ui-mini{font-size:14px}textarea.ui-mini{height:45px}@media all and (min-width:450px){.ui-field-contain label.ui-input-text{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain input.ui-input-text,.ui-field-contain textarea.ui-input-text,.ui-field-contain .ui-input-search{width:60%;display:inline-block}.ui-field-contain .ui-input-search{width:50%}.ui-hide-label input.ui-input-text,.ui-hide-label textarea.ui-input-text,.ui-hide-label .ui-input-search{padding:.4em;width:97%}.ui-input-search input.ui-input-text{width:98%}}.ui-listview{margin:0;counter-reset:listnumbering}.ui-content .ui-listview{margin:-15px}.ui-content .ui-listview-inset{margin:1em 0}.ui-listview,.ui-li{list-style:none;padding:0}.ui-li,.ui-li.ui-field-contain{display:block;margin:0;position:relative;overflow:visible;text-align:left;border-width:0;border-top-width:1px}.ui-li .ui-btn-text a.ui-link-inherit{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ui-li-divider,.ui-li-static{padding:.5em 15px;font-size:14px;font-weight:bold}.ui-li-divider{counter-reset:listnumbering}ol.ui-listview .ui-link-inherit:before,ol.ui-listview .ui-li-static:before,.ui-li-dec{font-size:.8em;display:inline-block;padding-right:.3em;font-weight:normal;counter-increment:listnumbering;content:counter(listnumbering) ". "}ol.ui-listview .ui-li-jsnumbering:before{content:""!important}.ui-listview-inset .ui-li{border-right-width:1px;border-left-width:1px}.ui-li:last-child,.ui-li.ui-field-contain:last-child{border-bottom-width:1px}.ui-li>.ui-btn-inner{display:block;position:relative;padding:0}.ui-li .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li{padding:.7em 15px .7em 15px;display:block}.ui-li-has-thumb .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-thumb{min-height:60px;padding-left:100px}.ui-li-has-icon .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-icon{min-height:20px;padding-left:40px}.ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-count{padding-right:45px}.ui-li-has-arrow .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-arrow{padding-right:30px}.ui-li-has-arrow.ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-arrow.ui-li-has-count{padding-right:75px}.ui-li-has-count .ui-btn-text{padding-right:15px}.ui-li-heading{font-size:16px;font-weight:bold;display:block;margin:.6em 0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ui-li-desc{font-size:12px;font-weight:normal;display:block;margin:-.5em 0 .6em;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.ui-li-thumb,.ui-listview .ui-li-icon{position:absolute;left:1px;top:0;max-height:80px;max-width:80px}.ui-listview .ui-li-icon{max-height:40px;max-width:40px;left:10px;top:.9em}.ui-li-thumb,.ui-listview .ui-li-icon,.ui-li-content{float:left;margin-right:10px}.ui-li-aside{float:right;width:50%;text-align:right;margin:.3em 0}@media all and (min-width:480px){.ui-li-aside{width:45%}}.ui-li-divider{cursor:default}.ui-li-has-alt .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-alt{padding-right:95px}.ui-li-has-count .ui-li-count{position:absolute;font-size:11px;font-weight:bold;padding:.2em .5em;top:50%;margin-top:-.9em;right:48px}.ui-li-divider .ui-li-count,.ui-li-static .ui-li-count{right:10px}.ui-li-has-alt .ui-li-count{right:55px}.ui-li-link-alt{position:absolute;width:40px;height:100%;border-width:0;border-left-width:1px;top:0;right:0;margin:0;padding:0;z-index:2}.ui-li-link-alt .ui-btn{overflow:hidden;position:absolute;right:8px;top:50%;margin:-11px 0 0 0;border-bottom-width:1px;z-index:-1}.ui-li-link-alt .ui-btn-inner{padding:0;height:100%;position:absolute;width:100%;top:0;left:0}.ui-li-link-alt .ui-btn .ui-icon{right:50%;margin-right:-9px}.ui-listview * .ui-btn-inner>.ui-btn>.ui-btn-inner{border-top:0}.ui-listview-filter{border-width:0;overflow:hidden;margin:-15px -15px 15px -15px}.ui-listview-filter .ui-input-search{margin:5px;width:auto;display:block}.ui-listview-filter-inset{margin:-15px -5px -15px -5px;background:transparent}.ui-li.ui-screen-hidden{display:none}@media only screen and (min-device-width:768px) and (max-device-width:1024px){.ui-li .ui-btn-text{overflow:visible}}label.ui-slider{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block}input.ui-slider-input,.ui-field-contain input.ui-slider-input{display:inline-block;width:50px}select.ui-slider-switch{display:none}div.ui-slider{position:relative;display:inline-block;overflow:visible;height:15px;padding:0;margin:0 2% 0 20px;top:4px;width:65%}div.ui-slider-mini{height:12px;margin-left:10px}div.ui-slider-bg{border:0;height:100%;padding-right:8px}.ui-controlgroup a.ui-slider-handle,a.ui-slider-handle{position:absolute;z-index:1;top:50%;width:28px;height:28px;margin-top:-15px;margin-left:-15px;outline:0}a.ui-slider-handle .ui-btn-inner{padding:0;height:100%}div.ui-slider-mini a.ui-slider-handle{height:14px;width:14px;margin:-8px 0 0 -7px}div.ui-slider-mini a.ui-slider-handle .ui-btn-inner{height:30px;width:30px;padding:0;margin:-9px 0 0 -9px}@media all and (min-width:450px){.ui-field-contain label.ui-slider{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain div.ui-slider{width:43%}.ui-field-contain div.ui-slider-switch{width:5.5em}}div.ui-slider-switch{height:32px;margin-left:0;width:5.8em}a.ui-slider-handle-snapping{-webkit-transition:left 70ms linear;-moz-transition:left 70ms linear}div.ui-slider-switch .ui-slider-handle{margin-top:1px}.ui-slider-inneroffset{margin:0 16px;position:relative;z-index:1}div.ui-slider-switch.ui-slider-mini{width:5em;height:29px}div.ui-slider-switch.ui-slider-mini .ui-slider-inneroffset{margin:0 15px 0 14px}div.ui-slider-switch.ui-slider-mini .ui-slider-handle{width:25px;height:25px;margin:1px 0 0 -13px}div.ui-slider-switch.ui-slider-mini a.ui-slider-handle .ui-btn-inner{height:30px;width:30px;padding:0;margin:0}span.ui-slider-label{position:absolute;text-align:center;width:100%;overflow:hidden;font-size:16px;top:0;line-height:2;min-height:100%;border-width:0;white-space:nowrap}.ui-slider-mini span.ui-slider-label{font-size:14px}span.ui-slider-label-a{z-index:1;left:0;text-indent:-1.5em}span.ui-slider-label-b{z-index:0;right:0;text-indent:1.5em}.ui-slider-inline{width:120px;display:inline-block}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery.mobile.min.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,177 @@
+/*! jQuery Mobile v1.1.0 db342b1f315c282692791aa870455901fdb46a55 jquerymobile.com | jquery.org/license */
+(function(D,s,k){typeof define==="function"&&define.amd?define(["jquery"],function(a){k(a,D,s);return a.mobile}):k(D.jQuery,D,s)})(this,document,function(D,s,k){(function(a,c,b,e){function f(a){for(;a&&typeof a.originalEvent!=="undefined";)a=a.originalEvent;return a}function d(b){for(var d={},f,g;b;){f=a.data(b,t);for(g in f)if(f[g])d[g]=d.hasVirtualBinding=true;b=b.parentNode}return d}function g(){y&&(clearTimeout(y),y=0);y=setTimeout(function(){F=y=0;C.length=0;z=false;G=true},a.vmouse.resetTimerDuration)}
+function h(b,d,g){var c,h;if(!(h=g&&g[b])){if(g=!g)a:{for(g=d.target;g;){if((h=a.data(g,t))&&(!b||h[b]))break a;g=g.parentNode}g=null}h=g}if(h){c=d;var g=c.type,z,j;c=a.Event(c);c.type=b;h=c.originalEvent;z=a.event.props;g.search(/^(mouse|click)/)>-1&&(z=w);if(h)for(j=z.length;j;)b=z[--j],c[b]=h[b];if(g.search(/mouse(down|up)|click/)>-1&&!c.which)c.which=1;if(g.search(/^touch/)!==-1&&(b=f(h),g=b.touches,b=b.changedTouches,g=g&&g.length?g[0]:b&&b.length?b[0]:e))for(h=0,len=u.length;h<len;h++)b=u[h],
+c[b]=g[b];a(d.target).trigger(c)}return c}function j(b){var d=a.data(b.target,x);if(!z&&(!F||F!==d))if(d=h("v"+b.type,b))d.isDefaultPrevented()&&b.preventDefault(),d.isPropagationStopped()&&b.stopPropagation(),d.isImmediatePropagationStopped()&&b.stopImmediatePropagation()}function o(b){var g=f(b).touches,c;if(g&&g.length===1&&(c=b.target,g=d(c),g.hasVirtualBinding))F=L++,a.data(c,x,F),y&&(clearTimeout(y),y=0),A=G=false,c=f(b).touches[0],s=c.pageX,E=c.pageY,h("vmouseover",b,g),h("vmousedown",b,g)}
+function m(a){G||(A||h("vmousecancel",a,d(a.target)),A=true,g())}function p(b){if(!G){var c=f(b).touches[0],e=A,z=a.vmouse.moveDistanceThreshold;A=A||Math.abs(c.pageX-s)>z||Math.abs(c.pageY-E)>z;flags=d(b.target);A&&!e&&h("vmousecancel",b,flags);h("vmousemove",b,flags);g()}}function l(a){if(!G){G=true;var b=d(a.target),c;h("vmouseup",a,b);if(!A&&(c=h("vclick",a,b))&&c.isDefaultPrevented())c=f(a).changedTouches[0],C.push({touchID:F,x:c.clientX,y:c.clientY}),z=true;h("vmouseout",a,b);A=false;g()}}function r(b){var b=
+a.data(b,t),d;if(b)for(d in b)if(b[d])return true;return false}function n(){}function k(b){var d=b.substr(1);return{setup:function(){r(this)||a.data(this,t,{});a.data(this,t)[b]=true;v[b]=(v[b]||0)+1;v[b]===1&&H.bind(d,j);a(this).bind(d,n);if(K)v.touchstart=(v.touchstart||0)+1,v.touchstart===1&&H.bind("touchstart",o).bind("touchend",l).bind("touchmove",p).bind("scroll",m)},teardown:function(){--v[b];v[b]||H.unbind(d,j);K&&(--v.touchstart,v.touchstart||H.unbind("touchstart",o).unbind("touchmove",p).unbind("touchend",
+l).unbind("scroll",m));var f=a(this),g=a.data(this,t);g&&(g[b]=false);f.unbind(d,n);r(this)||f.removeData(t)}}}var t="virtualMouseBindings",x="virtualTouchID",c="vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(" "),u="clientX clientY pageX pageY screenX screenY".split(" "),w=a.event.props.concat(a.event.mouseHooks?a.event.mouseHooks.props:[]),v={},y=0,s=0,E=0,A=false,C=[],z=false,G=false,K="addEventListener"in b,H=a(b),L=1,F=0;a.vmouse={moveDistanceThreshold:10,clickDistanceThreshold:10,
+resetTimerDuration:1500};for(var I=0;I<c.length;I++)a.event.special[c[I]]=k(c[I]);K&&b.addEventListener("click",function(b){var d=C.length,f=b.target,g,c,e,h,z;if(d){g=b.clientX;c=b.clientY;threshold=a.vmouse.clickDistanceThreshold;for(e=f;e;){for(h=0;h<d;h++)if(z=C[h],e===f&&Math.abs(z.x-g)<threshold&&Math.abs(z.y-c)<threshold||a.data(e,x)===z.touchID){b.preventDefault();b.stopPropagation();return}e=e.parentNode}}},true)})(jQuery,s,k);(function(a,c,b){function e(a){a=a||location.href;return"#"+a.replace(/^[^#]*#?(.*)$/,
+"$1")}var f="hashchange",d=k,g,h=a.event.special,j=d.documentMode,o="on"+f in c&&(j===b||j>7);a.fn[f]=function(a){return a?this.bind(f,a):this.trigger(f)};a.fn[f].delay=50;h[f]=a.extend(h[f],{setup:function(){if(o)return false;a(g.start)},teardown:function(){if(o)return false;a(g.stop)}});g=function(){function g(){var b=e(),d=t(r);if(b!==r)k(r=b,d),a(c).trigger(f);else if(d!==r)location.href=location.href.replace(/#.*/,"")+d;j=setTimeout(g,a.fn[f].delay)}var h={},j,r=e(),n=function(a){return a},k=
+n,t=n;h.start=function(){j||g()};h.stop=function(){j&&clearTimeout(j);j=b};a.browser.msie&&!o&&function(){var b,c;h.start=function(){if(!b)c=(c=a.fn[f].src)&&c+e(),b=a('<iframe tabindex="-1" title="empty"/>').hide().one("load",function(){c||k(e());g()}).attr("src",c||"javascript:0").insertAfter("body")[0].contentWindow,d.onpropertychange=function(){try{if(event.propertyName==="title")b.document.title=d.title}catch(a){}}};h.stop=n;t=function(){return e(b.location.href)};k=function(g,c){var e=b.document,
+h=a.fn[f].domain;if(g!==c)e.title=d.title,e.open(),h&&e.write('<script>document.domain="'+h+'"<\/script>'),e.close(),b.location.hash=g}}();return h}()})(jQuery,this);(function(a,c){if(a.cleanData){var b=a.cleanData;a.cleanData=function(f){for(var d=0,g;(g=f[d])!=null;d++)a(g).triggerHandler("remove");b(f)}}else{var e=a.fn.remove;a.fn.remove=function(b,d){return this.each(function(){d||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){a(this).triggerHandler("remove")});return e.call(a(this),
+b,d)})}}a.widget=function(b,d,g){var c=b.split(".")[0],e,b=b.split(".")[1];e=c+"-"+b;if(!g)g=d,d=a.Widget;a.expr[":"][e]=function(d){return!!a.data(d,b)};a[c]=a[c]||{};a[c][b]=function(a,b){arguments.length&&this._createWidget(a,b)};d=new d;d.options=a.extend(true,{},d.options);a[c][b].prototype=a.extend(true,d,{namespace:c,widgetName:b,widgetEventPrefix:a[c][b].prototype.widgetEventPrefix||b,widgetBaseClass:e},g);a.widget.bridge(b,a[c][b])};a.widget.bridge=function(b,d){a.fn[b]=function(g){var e=
+typeof g==="string",j=Array.prototype.slice.call(arguments,1),o=this,g=!e&&j.length?a.extend.apply(null,[true,g].concat(j)):g;if(e&&g.charAt(0)==="_")return o;e?this.each(function(){var d=a.data(this,b);if(!d)throw"cannot call methods on "+b+" prior to initialization; attempted to call method '"+g+"'";if(!a.isFunction(d[g]))throw"no such method '"+g+"' for "+b+" widget instance";var e=d[g].apply(d,j);if(e!==d&&e!==c)return o=e,false}):this.each(function(){var c=a.data(this,b);c?c.option(g||{})._init():
+a.data(this,b,new d(g,this))});return o}};a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)};a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:false},_createWidget:function(b,d){a.data(d,this.widgetName,this);this.element=a(d);this.options=a.extend(true,{},this.options,this._getCreateOptions(),b);var g=this;this.element.bind("remove."+this.widgetName,function(){g.destroy()});this._create();this._trigger("create");this._init()},_getCreateOptions:function(){var b=
+{};a.metadata&&(b=a.metadata.get(element)[this.widgetName]);return b},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(b,d){var g=b;if(arguments.length===0)return a.extend({},this.options);if(typeof b==="string"){if(d===c)return this.options[b];
+g={};g[b]=d}this._setOptions(g);return this},_setOptions:function(b){var d=this;a.each(b,function(a,b){d._setOption(a,b)});return this},_setOption:function(a,b){this.options[a]=b;a==="disabled"&&this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",b);return this},enable:function(){return this._setOption("disabled",false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(b,d,g){var c=this.options[b],d=a.Event(d);
+d.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase();g=g||{};if(d.originalEvent)for(var b=a.event.props.length,e;b;)e=a.event.props[--b],d[e]=d.originalEvent[e];this.element.trigger(d,g);return!(a.isFunction(c)&&c.call(this.element[0],d,g)===false||d.isDefaultPrevented())}}})(jQuery);(function(a,c){a.widget("mobile.widget",{_createWidget:function(){a.Widget.prototype._createWidget.apply(this,arguments);this._trigger("init")},_getCreateOptions:function(){var b=this.element,
+e={};a.each(this.options,function(a){var d=b.jqmData(a.replace(/[A-Z]/g,function(a){return"-"+a.toLowerCase()}));d!==c&&(e[a]=d)});return e},enhanceWithin:function(b,c){this.enhance(a(this.options.initSelector,a(b)),c)},enhance:function(b,c){var f,d=a(b),d=a.mobile.enhanceable(d);c&&d.length&&(f=(f=a.mobile.closestPageData(d))&&f.keepNativeSelector()||"",d=d.not(f));d[this.widgetName]()},raise:function(a){throw"Widget ["+this.widgetName+"]: "+a;}})})(jQuery);(function(a,c){var b={};a.mobile=a.extend({},
+{version:"1.1.0",ns:"",subPageUrlKey:"ui-page",activePageClass:"ui-page-active",activeBtnClass:"ui-btn-active",focusClass:"ui-focus",ajaxEnabled:true,hashListeningEnabled:true,linkBindingEnabled:true,defaultPageTransition:"fade",maxTransitionWidth:false,minScrollBack:250,touchOverflowEnabled:false,defaultDialogTransition:"pop",loadingMessage:"loading",pageLoadErrorMessage:"Error Loading Page",loadingMessageTextVisible:false,loadingMessageTheme:"a",pageLoadErrorMessageTheme:"e",autoInitializePage:true,
+pushStateEnabled:true,ignoreContentEnabled:false,orientationChangeEnabled:true,buttonMarkup:{hoverDelay:200},keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91},silentScroll:function(b){if(a.type(b)!==
+"number")b=a.mobile.defaultHomeScroll;a.event.special.scrollstart.enabled=false;setTimeout(function(){c.scrollTo(0,b);a(k).trigger("silentscroll",{x:0,y:b})},20);setTimeout(function(){a.event.special.scrollstart.enabled=true},150)},nsNormalizeDict:b,nsNormalize:function(d){return!d?void 0:b[d]||(b[d]=a.camelCase(a.mobile.ns+d))},getInheritedTheme:function(a,b){for(var c=a[0],e="",f=/ui-(bar|body|overlay)-([a-z])\b/,m,p;c;){m=c.className||"";if((p=f.exec(m))&&(e=p[2]))break;c=c.parentNode}return e||
+b||"a"},closestPageData:function(a){return a.closest(':jqmData(role="page"), :jqmData(role="dialog")').data("page")},enhanceable:function(a){return this.haveParents(a,"enhance")},hijackable:function(a){return this.haveParents(a,"ajax")},haveParents:function(b,c){if(!a.mobile.ignoreContentEnabled)return b;for(var e=b.length,f=a(),o,m,p,l=0;l<e;l++){m=b.eq(l);p=false;for(o=b[l];o;){if((o.getAttribute?o.getAttribute("data-"+a.mobile.ns+c):"")==="false"){p=true;break}o=o.parentNode}p||(f=f.add(m))}return f}},
+a.mobile);a.fn.jqmData=function(b,c){var f;typeof b!="undefined"&&(b&&(b=a.mobile.nsNormalize(b)),f=this.data.apply(this,arguments.length<2?[b]:[b,c]));return f};a.jqmData=function(b,c,f){var e;typeof c!="undefined"&&(e=a.data(b,c?a.mobile.nsNormalize(c):c,f));return e};a.fn.jqmRemoveData=function(b){return this.removeData(a.mobile.nsNormalize(b))};a.jqmRemoveData=function(b,c){return a.removeData(b,a.mobile.nsNormalize(c))};a.fn.removeWithDependents=function(){a.removeWithDependents(this)};a.removeWithDependents=
+function(b){b=a(b);(b.jqmData("dependents")||a()).remove();b.remove()};a.fn.addDependents=function(b){a.addDependents(a(this),b)};a.addDependents=function(b,c){var f=a(b).jqmData("dependents")||a();a(b).jqmData("dependents",a.merge(f,c))};a.fn.getEncodedText=function(){return a("<div/>").text(a(this).text()).html()};a.fn.jqmEnhanceable=function(){return a.mobile.enhanceable(this)};a.fn.jqmHijackable=function(){return a.mobile.hijackable(this)};var e=a.find,f=/:jqmData\(([^)]*)\)/g;a.find=function(b,
+c,h,j){b=b.replace(f,"[data-"+(a.mobile.ns||"")+"$1]");return e.call(this,b,c,h,j)};a.extend(a.find,e);a.find.matches=function(b,c){return a.find(b,null,null,c)};a.find.matchesSelector=function(b,c){return a.find(c,null,null,[b]).length>0}})(jQuery,this);(function(a){a(s);var c=a("html");a.mobile.media=function(){var b={},e=a("<div id='jquery-mediatest'>"),f=a("<body>").append(e);return function(a){if(!(a in b)){var g=k.createElement("style"),h="@media "+a+" { #jquery-mediatest { position:absolute; } }";
+g.type="text/css";g.styleSheet?g.styleSheet.cssText=h:g.appendChild(k.createTextNode(h));c.prepend(f).prepend(g);b[a]=e.css("position")==="absolute";f.add(g).remove()}return b[a]}}()})(jQuery);(function(a,c){function b(a){var b=a.charAt(0).toUpperCase()+a.substr(1),a=(a+" "+g.join(b+" ")+b).split(" "),f;for(f in a)if(d[a[f]]!==c)return true}function e(a,b,c){var d=k.createElement("div"),c=c?[c]:g,f;for(i=0;i<c.length;i++){var e=c[i],h="-"+e.charAt(0).toLowerCase()+e.substr(1)+"-"+a+": "+b+";",e=e.charAt(0).toUpperCase()+
+e.substr(1)+(a.charAt(0).toUpperCase()+a.substr(1));d.setAttribute("style",h);d.style[e]&&(f=true)}return!!f}var f=a("<body>").prependTo("html"),d=f[0].style,g=["Webkit","Moz","O"],h="palmGetResource"in s,j=s.operamini&&{}.toString.call(s.operamini)==="[object OperaMini]",o=s.blackberry;a.extend(a.mobile,{browser:{}});a.mobile.browser.ie=function(){for(var a=3,b=k.createElement("div"),c=b.all||[];b.innerHTML="<\!--[if gt IE "+ ++a+"]><br><![endif]--\>",c[0];);return a>4?a:!a}();a.extend(a.support,
+{orientation:"orientation"in s&&"onorientationchange"in s,touch:"ontouchend"in k,cssTransitions:"WebKitTransitionEvent"in s||e("transition","height 100ms linear"),pushState:"pushState"in history&&"replaceState"in history,mediaquery:a.mobile.media("only all"),cssPseudoElement:!!b("content"),touchOverflow:!!b("overflowScrolling"),cssTransform3d:e("perspective","10px","moz")||a.mobile.media("(-"+g.join("-transform-3d),(-")+"-transform-3d),(transform-3d)"),boxShadow:!!b("boxShadow")&&!o,scrollTop:("pageXOffset"in
+s||"scrollTop"in k.documentElement||"scrollTop"in f[0])&&!h&&!j,dynamicBaseTag:function(){var b=location.protocol+"//"+location.host+location.pathname+"ui-dir/",c=a("head base"),d=null,e="",g;c.length?e=c.attr("href"):c=d=a("<base>",{href:b}).appendTo("head");g=a("<a href='testurl' />").prependTo(f)[0].href;c[0].href=e||location.pathname;d&&d.remove();return g.indexOf(b)===0}()});f.remove();h=function(){var a=s.navigator.userAgent;return a.indexOf("Nokia")>-1&&(a.indexOf("Symbian/3")>-1||a.indexOf("Series60/5")>
+-1)&&a.indexOf("AppleWebKit")>-1&&a.match(/(BrowserNG|NokiaBrowser)\/7\.[0-3]/)}();a.mobile.gradeA=function(){return a.support.mediaquery||a.mobile.browser.ie&&a.mobile.browser.ie>=7};a.mobile.ajaxBlacklist=s.blackberry&&!s.WebKitPoint||j||h;h&&a(function(){a("head link[rel='stylesheet']").attr("rel","alternate stylesheet").attr("rel","stylesheet")});a.support.boxShadow||a("html").addClass("ui-mobile-nosupport-boxshadow")})(jQuery);(function(a,c,b){function e(b,c,d){var f=d.type;d.type=c;a.event.handle.call(b,
+d);d.type=f}a.each("touchstart touchmove touchend orientationchange throttledresize tap taphold swipe swipeleft swiperight scrollstart scrollstop".split(" "),function(b,c){a.fn[c]=function(a){return a?this.bind(c,a):this.trigger(c)};a.attrFn[c]=true});var f=a.support.touch,d=f?"touchstart":"mousedown",g=f?"touchend":"mouseup",h=f?"touchmove":"mousemove";a.event.special.scrollstart={enabled:true,setup:function(){function b(a,f){d=f;e(c,d?"scrollstart":"scrollstop",a)}var c=this,d,f;a(c).bind("touchmove scroll",
+function(c){a.event.special.scrollstart.enabled&&(d||b(c,true),clearTimeout(f),f=setTimeout(function(){b(c,false)},50))})}};a.event.special.tap={setup:function(){var b=this,c=a(b);c.bind("vmousedown",function(d){function f(){clearTimeout(q)}function g(){f();c.unbind("vclick",h).unbind("vmouseup",f);a(k).unbind("vmousecancel",g)}function h(a){g();n==a.target&&e(b,"tap",a)}if(d.which&&d.which!==1)return false;var n=d.target,q;c.bind("vmouseup",f).bind("vclick",h);a(k).bind("vmousecancel",g);q=setTimeout(function(){e(b,
+"taphold",a.Event("taphold",{target:n}))},750)})}};a.event.special.swipe={scrollSupressionThreshold:10,durationThreshold:1E3,horizontalDistanceThreshold:30,verticalDistanceThreshold:75,setup:function(){var c=a(this);c.bind(d,function(d){function f(b){if(l){var c=b.originalEvent.touches?b.originalEvent.touches[0]:b;k={time:(new Date).getTime(),coords:[c.pageX,c.pageY]};Math.abs(l.coords[0]-k.coords[0])>a.event.special.swipe.scrollSupressionThreshold&&b.preventDefault()}}var e=d.originalEvent.touches?
+d.originalEvent.touches[0]:d,l={time:(new Date).getTime(),coords:[e.pageX,e.pageY],origin:a(d.target)},k;c.bind(h,f).one(g,function(){c.unbind(h,f);l&&k&&k.time-l.time<a.event.special.swipe.durationThreshold&&Math.abs(l.coords[0]-k.coords[0])>a.event.special.swipe.horizontalDistanceThreshold&&Math.abs(l.coords[1]-k.coords[1])<a.event.special.swipe.verticalDistanceThreshold&&l.origin.trigger("swipe").trigger(l.coords[0]>k.coords[0]?"swipeleft":"swiperight");l=k=b})})}};(function(a,b){function c(){var a=
+f();a!==e&&(e=a,d.trigger("orientationchange"))}var d=a(b),f,e,g,h,t={0:true,180:true};if(a.support.orientation&&(g=b.innerWidth||a(b).width(),h=b.innerHeight||a(b).height(),g=g>h&&g-h>50,h=t[b.orientation],g&&h||!g&&!h))t={"-90":true,90:true};a.event.special.orientationchange={setup:function(){if(a.support.orientation&&a.mobile.orientationChangeEnabled)return false;e=f();d.bind("throttledresize",c)},teardown:function(){if(a.support.orientation&&a.mobile.orientationChangeEnabled)return false;d.unbind("throttledresize",
+c)},add:function(a){var b=a.handler;a.handler=function(a){a.orientation=f();return b.apply(this,arguments)}}};a.event.special.orientationchange.orientation=f=function(){var c=true,c=k.documentElement;return(c=a.support.orientation?t[b.orientation]:c&&c.clientWidth/c.clientHeight<1.1)?"portrait":"landscape"}})(jQuery,c);(function(){a.event.special.throttledresize={setup:function(){a(this).bind("resize",b)},teardown:function(){a(this).unbind("resize",b)}};var b=function(){f=(new Date).getTime();g=f-
+c;g>=250?(c=f,a(this).trigger("throttledresize")):(d&&clearTimeout(d),d=setTimeout(b,250-g))},c=0,d,f,g})();a.each({scrollstop:"scrollstart",taphold:"tap",swipeleft:"swipe",swiperight:"swipe"},function(b,c){a.event.special[b]={setup:function(){a(this).bind(c,a.noop)}}})})(jQuery,this);(function(a){a.widget("mobile.page",a.mobile.widget,{options:{theme:"c",domCache:false,keepNativeDefault:":jqmData(role='none'), :jqmData(role='nojs')"},_create:function(){var a=this;if(a._trigger("beforecreate")===
+false)return false;a.element.attr("tabindex","0").addClass("ui-page ui-body-"+a.options.theme).bind("pagebeforehide",function(){a.removeContainerBackground()}).bind("pagebeforeshow",function(){a.setContainerBackground()})},removeContainerBackground:function(){a.mobile.pageContainer.removeClass("ui-overlay-"+a.mobile.getInheritedTheme(this.element.parent()))},setContainerBackground:function(c){this.options.theme&&a.mobile.pageContainer.addClass("ui-overlay-"+(c||this.options.theme))},keepNativeSelector:function(){var c=
+this.options;return c.keepNative&&a.trim(c.keepNative)&&c.keepNative!==c.keepNativeDefault?[c.keepNative,c.keepNativeDefault].join(", "):c.keepNativeDefault}})})(jQuery);(function(a,c,b){var e=function(d){d===b&&(d=true);return function(b,f,e,o){var k=new a.Deferred,p=f?" reverse":"",l=a.mobile.urlHistory.getActive().lastScroll||a.mobile.defaultHomeScroll,r=a.mobile.getScreenHeight(),n=a.mobile.maxTransitionWidth!==false&&a(c).width()>a.mobile.maxTransitionWidth,q=!a.support.cssTransitions||n||!b||
+b==="none",t=function(){a.mobile.pageContainer.toggleClass("ui-mobile-viewport-transitioning viewport-"+b)},x=function(){a.event.special.scrollstart.enabled=false;c.scrollTo(0,l);setTimeout(function(){a.event.special.scrollstart.enabled=true},150)},u=function(){o.removeClass(a.mobile.activePageClass+" out in reverse "+b).height("")},n=function(){o&&d&&u();e.addClass(a.mobile.activePageClass);a.mobile.focusPage(e);e.height(r+l);x();q||e.animationComplete(w);e.addClass(b+" in"+p);q&&w()},w=function(){d||
+o&&u();e.removeClass("out in reverse "+b).height("");t();a(c).scrollTop()!==l&&x();k.resolve(b,f,e,o,true)};t();o&&!q?(d?o.animationComplete(n):n(),o.height(r+a(c).scrollTop()).addClass(b+" out"+p)):n();return k.promise()}},f=e(),e=e(false);a.mobile.defaultTransitionHandler=f;a.mobile.transitionHandlers={"default":a.mobile.defaultTransitionHandler,sequential:f,simultaneous:e};a.mobile.transitionFallbacks={}})(jQuery,this);(function(a,c){function b(b){r&&(!r.closest(".ui-page-active").length||b)&&
+r.removeClass(a.mobile.activeBtnClass);r=null}function e(){t=false;q.length>0&&a.mobile.changePage.apply(null,q.pop())}function f(b,c,d,f){c&&c.data("page")._trigger("beforehide",null,{nextPage:b});b.data("page")._trigger("beforeshow",null,{prevPage:c||a("")});a.mobile.hidePageLoadingMsg();d&&!a.support.cssTransform3d&&a.mobile.transitionFallbacks[d]&&(d=a.mobile.transitionFallbacks[d]);d=(a.mobile.transitionHandlers[d||"default"]||a.mobile.defaultTransitionHandler)(d,f,b,c);d.done(function(){c&&
+c.data("page")._trigger("hide",null,{nextPage:b});b.data("page")._trigger("show",null,{prevPage:c||a("")})});return d}function d(){return s.innerHeight||a(s).height()}function g(){var b=a("."+a.mobile.activePageClass),c=parseFloat(b.css("padding-top")),f=parseFloat(b.css("padding-bottom"));b.css("min-height",d()-c-f)}function h(b,c){c&&b.attr("data-"+a.mobile.ns+"role",c);b.page()}function j(a){for(;a;){if(typeof a.nodeName==="string"&&a.nodeName.toLowerCase()=="a")break;a=a.parentNode}return a}function o(b){var b=
+a(b).closest(".ui-page").jqmData("url"),c=v.hrefNoHash;if(!b||!l.isPath(b))b=c;return l.makeUrlAbsolute(b,c)}var m=a(s);a("html");var p=a("head"),l={urlParseRE:/^(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/,parseUrl:function(b){if(a.type(b)==="object")return b;b=l.urlParseRE.exec(b||"")||[];return{href:b[0]||"",hrefNoHash:b[1]||"",hrefNoSearch:b[2]||"",domain:b[3]||"",
+protocol:b[4]||"",doubleSlash:b[5]||"",authority:b[6]||"",username:b[8]||"",password:b[9]||"",host:b[10]||"",hostname:b[11]||"",port:b[12]||"",pathname:b[13]||"",directory:b[14]||"",filename:b[15]||"",search:b[16]||"",hash:b[17]||""}},makePathAbsolute:function(a,b){if(a&&a.charAt(0)==="/")return a;for(var a=a||"",c=(b=b?b.replace(/^\/|(\/[^\/]*|[^\/]+)$/g,""):"")?b.split("/"):[],d=a.split("/"),f=0;f<d.length;f++){var e=d[f];switch(e){case ".":break;case "..":c.length&&c.pop();break;default:c.push(e)}}return"/"+
+c.join("/")},isSameDomain:function(a,b){return l.parseUrl(a).domain===l.parseUrl(b).domain},isRelativeUrl:function(a){return l.parseUrl(a).protocol===""},isAbsoluteUrl:function(a){return l.parseUrl(a).protocol!==""},makeUrlAbsolute:function(a,b){if(!l.isRelativeUrl(a))return a;var c=l.parseUrl(a),d=l.parseUrl(b),f=c.protocol||d.protocol,e=c.protocol?c.doubleSlash:c.doubleSlash||d.doubleSlash,g=c.authority||d.authority,h=c.pathname!=="",j=l.makePathAbsolute(c.pathname||d.filename,d.pathname);return f+
+e+g+j+(c.search||!h&&d.search||"")+c.hash},addSearchParams:function(b,c){var d=l.parseUrl(b),f=typeof c==="object"?a.param(c):c,e=d.search||"?";return d.hrefNoSearch+e+(e.charAt(e.length-1)!=="?"?"&":"")+f+(d.hash||"")},convertUrlToDataUrl:function(a){var b=l.parseUrl(a);if(l.isEmbeddedPage(b))return b.hash.split(x)[0].replace(/^#/,"");else if(l.isSameDomain(b,v))return b.hrefNoHash.replace(v.domain,"");return a},get:function(a){if(a===c)a=location.hash;return l.stripHash(a).replace(/[^\/]*\.[^\/*]+$/,
+"")},getFilePath:function(b){var c="&"+a.mobile.subPageUrlKey;return b&&b.split(c)[0].split(x)[0]},set:function(a){location.hash=a},isPath:function(a){return/\//.test(a)},clean:function(a){return a.replace(v.domain,"")},stripHash:function(a){return a.replace(/^#/,"")},cleanHash:function(a){return l.stripHash(a.replace(/\?.*$/,"").replace(x,""))},isExternal:function(a){a=l.parseUrl(a);return a.protocol&&a.domain!==w.domain?true:false},hasProtocol:function(a){return/^(:?\w+:)/.test(a)},isFirstPageUrl:function(b){var b=
+l.parseUrl(l.makeUrlAbsolute(b,v)),d=a.mobile.firstPage,d=d&&d[0]?d[0].id:c;return(b.hrefNoHash===w.hrefNoHash||y&&b.hrefNoHash===v.hrefNoHash)&&(!b.hash||b.hash==="#"||d&&b.hash.replace(/^#/,"")===d)},isEmbeddedPage:function(a){a=l.parseUrl(a);return a.protocol!==""?a.hash&&(a.hrefNoHash===w.hrefNoHash||y&&a.hrefNoHash===v.hrefNoHash):/^#/.test(a.href)}},r=null,n={stack:[],activeIndex:0,getActive:function(){return n.stack[n.activeIndex]},getPrev:function(){return n.stack[n.activeIndex-1]},getNext:function(){return n.stack[n.activeIndex+
+1]},addNew:function(a,b,c,d,f){n.getNext()&&n.clearForward();n.stack.push({url:a,transition:b,title:c,pageUrl:d,role:f});n.activeIndex=n.stack.length-1},clearForward:function(){n.stack=n.stack.slice(0,n.activeIndex+1)},directHashChange:function(b){var d,f,e;this.getActive();a.each(n.stack,function(a,c){b.currentUrl===c.url&&(d=a<n.activeIndex,f=!d,e=a)});this.activeIndex=e!==c?e:this.activeIndex;d?(b.either||b.isBack)(true):f&&(b.either||b.isForward)(false)},ignoreNextHashChange:false},q=[],t=false,
+x="&ui-state=dialog",u=p.children("base"),w=l.parseUrl(location.href),v=u.length?l.parseUrl(l.makeUrlAbsolute(u.attr("href"),w.href)):w,y=w.hrefNoHash!==v.hrefNoHash,B=a.support.dynamicBaseTag?{element:u.length?u:a("<base>",{href:v.hrefNoHash}).prependTo(p),set:function(a){B.element.attr("href",l.makeUrlAbsolute(a,v))},reset:function(){B.element.attr("href",v.hrefNoHash)}}:c;a.mobile.focusPage=function(a){var b=a.find("[autofocus]"),c=a.find(".ui-title:eq(0)");b.length?b.focus():c.length?c.focus():
+a.focus()};var E=true,A,C;A=function(){if(E){var b=a.mobile.urlHistory.getActive();if(b){var c=m.scrollTop();b.lastScroll=c<a.mobile.minScrollBack?a.mobile.defaultHomeScroll:c}}};C=function(){setTimeout(A,100)};m.bind(a.support.pushState?"popstate":"hashchange",function(){E=false});m.one(a.support.pushState?"popstate":"hashchange",function(){E=true});m.one("pagecontainercreate",function(){a.mobile.pageContainer.bind("pagechange",function(){E=true;m.unbind("scrollstop",C);m.bind("scrollstop",C)})});
+m.bind("scrollstop",C);a.mobile.getScreenHeight=d;a.fn.animationComplete=function(b){return a.support.cssTransitions?a(this).one("webkitAnimationEnd animationend",b):(setTimeout(b,0),a(this))};a.mobile.path=l;a.mobile.base=B;a.mobile.urlHistory=n;a.mobile.dialogHashKey=x;a.mobile.allowCrossDomainPages=false;a.mobile.getDocumentUrl=function(b){return b?a.extend({},w):w.href};a.mobile.getDocumentBase=function(b){return b?a.extend({},v):v.href};a.mobile._bindPageRemove=function(){var b=a(this);!b.data("page").options.domCache&&
+b.is(":jqmData(external-page='true')")&&b.bind("pagehide.remove",function(){var b=a(this),c=new a.Event("pageremove");b.trigger(c);c.isDefaultPrevented()||b.removeWithDependents()})};a.mobile.loadPage=function(b,d){var f=a.Deferred(),e=a.extend({},a.mobile.loadPage.defaults,d),g=null,j=null,k=l.makeUrlAbsolute(b,a.mobile.activePage&&o(a.mobile.activePage)||v.hrefNoHash);if(e.data&&e.type==="get")k=l.addSearchParams(k,e.data),e.data=c;if(e.data&&e.type==="post")e.reloadPage=true;var u=l.getFilePath(k),
+n=l.convertUrlToDataUrl(k);e.pageContainer=e.pageContainer||a.mobile.pageContainer;g=e.pageContainer.children(":jqmData(url='"+n+"')");g.length===0&&n&&!l.isPath(n)&&(g=e.pageContainer.children("#"+n).attr("data-"+a.mobile.ns+"url",n));if(g.length===0)if(a.mobile.firstPage&&l.isFirstPageUrl(u))a.mobile.firstPage.parent().length&&(g=a(a.mobile.firstPage));else if(l.isEmbeddedPage(u))return f.reject(k,d),f.promise();B&&B.reset();if(g.length){if(!e.reloadPage)return h(g,e.role),f.resolve(k,d,g),f.promise();
+j=g}var m=e.pageContainer,x=new a.Event("pagebeforeload"),p={url:b,absUrl:k,dataUrl:n,deferred:f,options:e};m.trigger(x,p);if(x.isDefaultPrevented())return f.promise();if(e.showLoadMsg)var r=setTimeout(function(){a.mobile.showPageLoadingMsg()},e.loadMsgDelay);!a.mobile.allowCrossDomainPages&&!l.isSameDomain(w,k)?f.reject(k,d):a.ajax({url:u,type:e.type,data:e.data,dataType:"html",success:function(c,o,m){var x=a("<div></div>"),v=c.match(/<title[^>]*>([^<]*)/)&&RegExp.$1,w=RegExp("\\bdata-"+a.mobile.ns+
+"url=[\"']?([^\"'>]*)[\"']?");RegExp("(<[^>]+\\bdata-"+a.mobile.ns+"role=[\"']?page[\"']?[^>]*>)").test(c)&&RegExp.$1&&w.test(RegExp.$1)&&RegExp.$1&&(b=u=l.getFilePath(RegExp.$1));B&&B.set(u);x.get(0).innerHTML=c;g=x.find(":jqmData(role='page'), :jqmData(role='dialog')").first();g.length||(g=a("<div data-"+a.mobile.ns+"role='page'>"+c.split(/<\/?body[^>]*>/gmi)[1]+"</div>"));v&&!g.jqmData("title")&&(~v.indexOf("&")&&(v=a("<div>"+v+"</div>").text()),g.jqmData("title",v));if(!a.support.dynamicBaseTag){var q=
+l.get(u);g.find("[src], link[href], a[rel='external'], :jqmData(ajax='false'), a[target]").each(function(){var b=a(this).is("[href]")?"href":a(this).is("[src]")?"src":"action",c=a(this).attr(b),c=c.replace(location.protocol+"//"+location.host+location.pathname,"");/^(\w+:|#|\/)/.test(c)||a(this).attr(b,q+c)})}g.attr("data-"+a.mobile.ns+"url",l.convertUrlToDataUrl(u)).attr("data-"+a.mobile.ns+"external-page",true).appendTo(e.pageContainer);g.one("pagecreate",a.mobile._bindPageRemove);h(g,e.role);k.indexOf("&"+
+a.mobile.subPageUrlKey)>-1&&(g=e.pageContainer.children(":jqmData(url='"+n+"')"));e.showLoadMsg&&(clearTimeout(r),a.mobile.hidePageLoadingMsg());p.xhr=m;p.textStatus=o;p.page=g;e.pageContainer.trigger("pageload",p);f.resolve(k,d,g,j)},error:function(b,c,g){B&&B.set(l.get());p.xhr=b;p.textStatus=c;p.errorThrown=g;b=new a.Event("pageloadfailed");e.pageContainer.trigger(b,p);b.isDefaultPrevented()||(e.showLoadMsg&&(clearTimeout(r),a.mobile.hidePageLoadingMsg(),a.mobile.showPageLoadingMsg(a.mobile.pageLoadErrorMessageTheme,
+a.mobile.pageLoadErrorMessage,true),setTimeout(a.mobile.hidePageLoadingMsg,1500)),f.reject(k,d))}});return f.promise()};a.mobile.loadPage.defaults={type:"get",data:c,reloadPage:false,role:c,showLoadMsg:false,pageContainer:c,loadMsgDelay:50};a.mobile.changePage=function(d,g){if(t)q.unshift(arguments);else{var j=a.extend({},a.mobile.changePage.defaults,g);j.pageContainer=j.pageContainer||a.mobile.pageContainer;j.fromPage=j.fromPage||a.mobile.activePage;var u=j.pageContainer,o=new a.Event("pagebeforechange"),
+m={toPage:d,options:j};u.trigger(o,m);if(!o.isDefaultPrevented())if(d=m.toPage,t=true,typeof d=="string")a.mobile.loadPage(d,j).done(function(b,c,d,e){t=false;c.duplicateCachedPage=e;a.mobile.changePage(d,c)}).fail(function(){t=false;b(true);e();j.pageContainer.trigger("pagechangefailed",m)});else{if(d[0]===a.mobile.firstPage[0]&&!j.dataUrl)j.dataUrl=w.hrefNoHash;var o=j.fromPage,p=j.dataUrl&&l.convertUrlToDataUrl(j.dataUrl)||d.jqmData("url"),v=p;l.getFilePath(p);var r=n.getActive(),s=n.activeIndex===
+0,y=0,B=k.title,A=j.role==="dialog"||d.jqmData("role")==="dialog";if(o&&o[0]===d[0]&&!j.allowSamePageTransition)t=false,u.trigger("pagechange",m);else{h(d,j.role);j.fromHashChange&&n.directHashChange({currentUrl:p,isBack:function(){y=-1},isForward:function(){y=1}});try{k.activeElement&&k.activeElement.nodeName.toLowerCase()!="body"?a(k.activeElement).blur():a("input:focus, textarea:focus, select:focus").blur()}catch(E){}A&&r&&(p=(r.url||"")+x);if(j.changeHash!==false&&p)n.ignoreNextHashChange=true,
+l.set(p);var C=!r?B:d.jqmData("title")||d.children(":jqmData(role='header')").find(".ui-title").getEncodedText();C&&B==k.title&&(B=C);d.jqmData("title")||d.jqmData("title",B);j.transition=j.transition||(y&&!s?r.transition:c)||(A?a.mobile.defaultDialogTransition:a.mobile.defaultPageTransition);y||n.addNew(p,j.transition,B,v,j.role);k.title=n.getActive().title;a.mobile.activePage=d;j.reverse=j.reverse||y<0;f(d,o,j.transition,j.reverse).done(function(c,f,g,h,l){b();j.duplicateCachedPage&&j.duplicateCachedPage.remove();
+l||a.mobile.focusPage(d);e();u.trigger("pagechange",m)})}}}};a.mobile.changePage.defaults={transition:c,reverse:false,changeHash:true,fromHashChange:false,role:c,duplicateCachedPage:c,pageContainer:c,showLoadMsg:true,dataUrl:c,fromPage:c,allowSamePageTransition:false};a.mobile._registerInternalEvents=function(){a(k).delegate("form","submit",function(b){var c=a(this);if(a.mobile.ajaxEnabled&&!c.is(":jqmData(ajax='false')")&&c.jqmHijackable().length){var d=c.attr("method"),e=c.attr("target"),f=c.attr("action");
+if(!f&&(f=o(c),f===v.hrefNoHash))f=w.hrefNoSearch;f=l.makeUrlAbsolute(f,o(c));!l.isExternal(f)&&!e&&(a.mobile.changePage(f,{type:d&&d.length&&d.toLowerCase()||"get",data:c.serialize(),transition:c.jqmData("transition"),direction:c.jqmData("direction"),reloadPage:true}),b.preventDefault())}});a(k).bind("vclick",function(c){if(!(c.which>1)&&a.mobile.linkBindingEnabled&&(c=j(c.target),a(c).jqmHijackable().length&&c&&l.parseUrl(c.getAttribute("href")||"#").hash!=="#"))b(true),r=a(c).closest(".ui-btn").not(".ui-disabled"),
+r.addClass(a.mobile.activeBtnClass),a("."+a.mobile.activePageClass+" .ui-btn").not(c).blur(),a(c).jqmData("href",a(c).attr("href")).attr("href","#")});a(k).bind("click",function(d){if(a.mobile.linkBindingEnabled){var f=j(d.target),e=a(f),g;if(f&&!(d.which>1)&&e.jqmHijackable().length){g=function(){s.setTimeout(function(){b(true)},200)};e.jqmData("href")&&e.attr("href",e.jqmData("href"));if(e.is(":jqmData(rel='back')"))return s.history.back(),false;var h=o(e),f=l.makeUrlAbsolute(e.attr("href")||"#",
+h);if(!a.mobile.ajaxEnabled&&!l.isEmbeddedPage(f))g();else{if(f.search("#")!=-1)if(f=f.replace(/[^#]*#/,""))f=l.isPath(f)?l.makeUrlAbsolute(f,h):l.makeUrlAbsolute("#"+f,w.hrefNoHash);else{d.preventDefault();return}var h=e.is("[rel='external']")||e.is(":jqmData(ajax='false')")||e.is("[target]"),k=a.mobile.allowCrossDomainPages&&w.protocol==="file:"&&f.search(/^https?:/)!=-1;h||l.isExternal(f)&&!k?g():(g=e.jqmData("transition"),h=(h=e.jqmData("direction"))&&h==="reverse"||e.jqmData("back"),e=e.attr("data-"+
+a.mobile.ns+"rel")||c,a.mobile.changePage(f,{transition:g,reverse:h,role:e}),d.preventDefault())}}}});a(k).delegate(".ui-page","pageshow.prefetch",function(){var b=[];a(this).find("a:jqmData(prefetch)").each(function(){var c=a(this),d=c.attr("href");d&&a.inArray(d,b)===-1&&(b.push(d),a.mobile.loadPage(d,{role:c.attr("data-"+a.mobile.ns+"rel")}))})});a.mobile._handleHashChange=function(b){var d=l.stripHash(b),f={transition:a.mobile.urlHistory.stack.length===0?"none":c,changeHash:false,fromHashChange:true};
+if(!a.mobile.hashListeningEnabled||n.ignoreNextHashChange)n.ignoreNextHashChange=false;else{if(n.stack.length>1&&d.indexOf(x)>-1)if(a.mobile.activePage.is(".ui-dialog"))n.directHashChange({currentUrl:d,either:function(b){var c=a.mobile.urlHistory.getActive();d=c.pageUrl;a.extend(f,{role:c.role,transition:c.transition,reverse:b})}});else{n.directHashChange({currentUrl:d,isBack:function(){s.history.back()},isForward:function(){s.history.forward()}});return}d?(d=typeof d==="string"&&!l.isPath(d)?l.makeUrlAbsolute("#"+
+d,v):d,a.mobile.changePage(d,f)):a.mobile.changePage(a.mobile.firstPage,f)}};m.bind("hashchange",function(){a.mobile._handleHashChange(location.hash)});a(k).bind("pageshow",g);a(s).bind("throttledresize",g)}})(jQuery);(function(a,c){var b={},e=a(c),f=a.mobile.path.parseUrl(location.href);a.extend(b,{initialFilePath:f.pathname+f.search,initialHref:f.hrefNoHash,state:function(){return{hash:location.hash||"#"+b.initialFilePath,title:k.title,initialHref:b.initialHref}},resetUIKeys:function(b){var c="&"+
+a.mobile.subPageUrlKey,f=b.indexOf(a.mobile.dialogHashKey);f>-1?b=b.slice(0,f)+"#"+b.slice(f):b.indexOf(c)>-1&&(b=b.split(c).join("#"+c));return b},hashValueAfterReset:function(c){c=b.resetUIKeys(c);return a.mobile.path.parseUrl(c).hash},nextHashChangePrevented:function(c){a.mobile.urlHistory.ignoreNextHashChange=c;b.onHashChangeDisabled=c},onHashChange:function(){if(!b.onHashChangeDisabled){var c,f;c=location.hash;var e=a.mobile.path.isPath(c),j=e?location.href:a.mobile.getDocumentUrl();c=e?c.replace("#",
+""):c;f=b.state();c=a.mobile.path.makeUrlAbsolute(c,j);e&&(c=b.resetUIKeys(c));history.replaceState(f,k.title,c)}},onPopState:function(c){var c=c.originalEvent.state,f,h;if(c){f=b.hashValueAfterReset(a.mobile.urlHistory.getActive().url);h=b.hashValueAfterReset(c.hash.replace("#",""));if(f=f!==h)e.one("hashchange.pushstate",function(){b.nextHashChangePrevented(false)});b.nextHashChangePrevented(false);a.mobile._handleHashChange(c.hash);f&&b.nextHashChangePrevented(true)}},init:function(){e.bind("hashchange",
+b.onHashChange);e.bind("popstate",b.onPopState);location.hash===""&&history.replaceState(b.state(),k.title,location.href)}});a(function(){a.mobile.pushStateEnabled&&a.support.pushState&&b.init()})})(jQuery,this);jQuery.mobile.transitionFallbacks.pop="fade";(function(a){a.mobile.transitionHandlers.slide=a.mobile.transitionHandlers.simultaneous;a.mobile.transitionFallbacks.slide="fade"})(jQuery,this);jQuery.mobile.transitionFallbacks.slidedown="fade";jQuery.mobile.transitionFallbacks.slideup="fade";
+jQuery.mobile.transitionFallbacks.flip="fade";jQuery.mobile.transitionFallbacks.flow="fade";jQuery.mobile.transitionFallbacks.turn="fade";(function(a){a.mobile.page.prototype.options.degradeInputs={color:false,date:false,datetime:false,"datetime-local":false,email:false,month:false,number:false,range:"number",search:"text",tel:false,time:false,url:false,week:false};a(k).bind("pagecreate create",function(c){var b=a.mobile.closestPageData(a(c.target)),e;if(b)e=b.options,a(c.target).find("input").not(b.keepNativeSelector()).each(function(){var b=
+a(this),c=this.getAttribute("type"),g=e.degradeInputs[c]||"text";if(e.degradeInputs[c]){var h=a("<div>").html(b.clone()).html(),j=h.indexOf(" type=")>-1;b.replaceWith(h.replace(j?/\s+type=["']?\w+['"]?/:/\/?>/,' type="'+g+'" data-'+a.mobile.ns+'type="'+c+'"'+(j?"":">")))}})})})(jQuery);(function(a,c){a.widget("mobile.dialog",a.mobile.widget,{options:{closeBtnText:"Close",overlayTheme:"a",initSelector:":jqmData(role='dialog')"},_create:function(){var b=this,c=this.element,f=a("<a href='#' data-"+a.mobile.ns+
+"icon='delete' data-"+a.mobile.ns+"iconpos='notext'>"+this.options.closeBtnText+"</a>"),d=a("<div/>",{role:"dialog","class":"ui-dialog-contain ui-corner-all ui-overlay-shadow"});c.addClass("ui-dialog ui-overlay-"+this.options.overlayTheme);c.wrapInner(d).children().find(":jqmData(role='header')").prepend(f).end().children(":first-child").addClass("ui-corner-top").end().children(":last-child").addClass("ui-corner-bottom");f.bind("click",function(){b.close()});c.bind("vclick submit",function(b){var b=
+a(b.target).closest(b.type==="vclick"?"a":"form"),c;b.length&&!b.jqmData("transition")&&(c=a.mobile.urlHistory.getActive()||{},b.attr("data-"+a.mobile.ns+"transition",c.transition||a.mobile.defaultDialogTransition).attr("data-"+a.mobile.ns+"direction","reverse"))}).bind("pagehide",function(){a(this).find("."+a.mobile.activeBtnClass).removeClass(a.mobile.activeBtnClass)}).bind("pagebeforeshow",function(){b.options.overlayTheme&&b.element.page("removeContainerBackground").page("setContainerBackground",
+b.options.overlayTheme)})},close:function(){c.history.back()}});a(k).delegate(a.mobile.dialog.prototype.options.initSelector,"pagecreate",function(){a.mobile.dialog.prototype.enhance(this)})})(jQuery,this);(function(a){a.fn.fieldcontain=function(){return this.addClass("ui-field-contain ui-body ui-br")};a(k).bind("pagecreate create",function(c){a(":jqmData(role='fieldcontain')",c.target).jqmEnhanceable().fieldcontain()})})(jQuery);(function(a){a.fn.grid=function(c){return this.each(function(){var b=
+a(this),e=a.extend({grid:null},c),f=b.children(),d={solo:1,a:2,b:3,c:4,d:5},e=e.grid;if(!e)if(f.length<=5)for(var g in d)d[g]===f.length&&(e=g);else e="a";d=d[e];b.addClass("ui-grid-"+e);f.filter(":nth-child("+d+"n+1)").addClass("ui-block-a");d>1&&f.filter(":nth-child("+d+"n+2)").addClass("ui-block-b");d>2&&f.filter(":nth-child(3n+3)").addClass("ui-block-c");d>3&&f.filter(":nth-child(4n+4)").addClass("ui-block-d");d>4&&f.filter(":nth-child(5n+5)").addClass("ui-block-e")})}})(jQuery);(function(a){a(k).bind("pagecreate create",
+function(c){a(":jqmData(role='nojs')",c.target).addClass("ui-nojs")})})(jQuery);(function(a,c){function b(a){for(var b;a;){if((b=typeof a.className==="string"&&a.className+" ")&&b.indexOf("ui-btn ")>-1&&b.indexOf("ui-disabled ")<0)break;a=a.parentNode}return a}a.fn.buttonMarkup=function(b){for(var b=b&&a.type(b)=="object"?b:{},d=0;d<this.length;d++){var g=this.eq(d),h=g[0],j=a.extend({},a.fn.buttonMarkup.defaults,{icon:b.icon!==c?b.icon:g.jqmData("icon"),iconpos:b.iconpos!==c?b.iconpos:g.jqmData("iconpos"),
+theme:b.theme!==c?b.theme:g.jqmData("theme")||a.mobile.getInheritedTheme(g,"c"),inline:b.inline!==c?b.inline:g.jqmData("inline"),shadow:b.shadow!==c?b.shadow:g.jqmData("shadow"),corners:b.corners!==c?b.corners:g.jqmData("corners"),iconshadow:b.iconshadow!==c?b.iconshadow:g.jqmData("iconshadow"),mini:b.mini!==c?b.mini:g.jqmData("mini")},b),o="ui-btn-inner",m,p,l,r,n,q;a.each(j,function(b,c){h.setAttribute("data-"+a.mobile.ns+b,c);g.jqmData(b,c)});(q=a.data(h.tagName==="INPUT"||h.tagName==="BUTTON"?
+h.parentNode:h,"buttonElements"))?(h=q.outer,g=a(h),l=q.inner,r=q.text,a(q.icon).remove(),q.icon=null):(l=k.createElement(j.wrapperEls),r=k.createElement(j.wrapperEls));n=j.icon?k.createElement("span"):null;e&&!q&&e();if(!j.theme)j.theme=a.mobile.getInheritedTheme(g,"c");m="ui-btn ui-btn-up-"+j.theme;m+=j.inline?" ui-btn-inline":"";m+=j.shadow?" ui-shadow":"";m+=j.corners?" ui-btn-corner-all":"";j.mini!==c&&(m+=j.mini?" ui-mini":" ui-fullsize");j.inline!==c&&(m+=j.inline===false?" ui-btn-block":" ui-btn-inline");
+if(j.icon)j.icon="ui-icon-"+j.icon,j.iconpos=j.iconpos||"left",p="ui-icon "+j.icon,j.iconshadow&&(p+=" ui-icon-shadow");j.iconpos&&(m+=" ui-btn-icon-"+j.iconpos,j.iconpos=="notext"&&!g.attr("title")&&g.attr("title",g.getEncodedText()));o+=j.corners?" ui-btn-corner-all":"";j.iconpos&&j.iconpos==="notext"&&!g.attr("title")&&g.attr("title",g.getEncodedText());q&&g.removeClass(q.bcls||"");g.removeClass("ui-link").addClass(m);l.className=o;r.className="ui-btn-text";q||l.appendChild(r);if(n&&(n.className=
+p,!q||!q.icon))n.appendChild(k.createTextNode("\u00a0")),l.appendChild(n);for(;h.firstChild&&!q;)r.appendChild(h.firstChild);q||h.appendChild(l);q={bcls:m,outer:h,inner:l,text:r,icon:n};a.data(h,"buttonElements",q);a.data(l,"buttonElements",q);a.data(r,"buttonElements",q);n&&a.data(n,"buttonElements",q)}return this};a.fn.buttonMarkup.defaults={corners:true,shadow:true,iconshadow:true,wrapperEls:"span"};var e=function(){var c=a.mobile.buttonMarkup.hoverDelay,d,g;a(k).bind({"vmousedown vmousecancel vmouseup vmouseover vmouseout focus blur scrollstart":function(e){var j,
+k=a(b(e.target)),e=e.type;if(k.length)if(j=k.attr("data-"+a.mobile.ns+"theme"),e==="vmousedown")a.support.touch?d=setTimeout(function(){k.removeClass("ui-btn-up-"+j).addClass("ui-btn-down-"+j)},c):k.removeClass("ui-btn-up-"+j).addClass("ui-btn-down-"+j);else if(e==="vmousecancel"||e==="vmouseup")k.removeClass("ui-btn-down-"+j).addClass("ui-btn-up-"+j);else if(e==="vmouseover"||e==="focus")a.support.touch?g=setTimeout(function(){k.removeClass("ui-btn-up-"+j).addClass("ui-btn-hover-"+j)},c):k.removeClass("ui-btn-up-"+
+j).addClass("ui-btn-hover-"+j);else if(e==="vmouseout"||e==="blur"||e==="scrollstart")k.removeClass("ui-btn-hover-"+j+" ui-btn-down-"+j).addClass("ui-btn-up-"+j),d&&clearTimeout(d),g&&clearTimeout(g)},"focusin focus":function(c){a(b(c.target)).addClass(a.mobile.focusClass)},"focusout blur":function(c){a(b(c.target)).removeClass(a.mobile.focusClass)}});e=null};a(k).bind("pagecreate create",function(b){a(":jqmData(role='button'), .ui-bar > a, .ui-header > a, .ui-footer > a, .ui-bar > :jqmData(role='controlgroup') > a",
+b.target).not(".ui-btn, :jqmData(role='none'), :jqmData(role='nojs')").buttonMarkup()})})(jQuery);(function(a){a.mobile.page.prototype.options.backBtnText="Back";a.mobile.page.prototype.options.addBackBtn=false;a.mobile.page.prototype.options.backBtnTheme=null;a.mobile.page.prototype.options.headerTheme="a";a.mobile.page.prototype.options.footerTheme="a";a.mobile.page.prototype.options.contentTheme=null;a(k).delegate(":jqmData(role='page'), :jqmData(role='dialog')","pagecreate",function(){var c=a(this),
+b=c.data("page").options,e=c.jqmData("role"),f=b.theme;a(":jqmData(role='header'), :jqmData(role='footer'), :jqmData(role='content')",this).jqmEnhanceable().each(function(){var d=a(this),g=d.jqmData("role"),h=d.jqmData("theme"),j=h||b.contentTheme||e==="dialog"&&f,k;d.addClass("ui-"+g);if(g==="header"||g==="footer"){var m=h||(g==="header"?b.headerTheme:b.footerTheme)||f;d.addClass("ui-bar-"+m).attr("role",g==="header"?"banner":"contentinfo");g==="header"&&(h=d.children("a"),k=h.hasClass("ui-btn-left"),
+j=h.hasClass("ui-btn-right"),k=k||h.eq(0).not(".ui-btn-right").addClass("ui-btn-left").length,j||h.eq(1).addClass("ui-btn-right"));b.addBackBtn&&g==="header"&&a(".ui-page").length>1&&c.jqmData("url")!==a.mobile.path.stripHash(location.hash)&&!k&&a("<a href='#' class='ui-btn-left' data-"+a.mobile.ns+"rel='back' data-"+a.mobile.ns+"icon='arrow-l'>"+b.backBtnText+"</a>").attr("data-"+a.mobile.ns+"theme",b.backBtnTheme||m).prependTo(d);d.children("h1, h2, h3, h4, h5, h6").addClass("ui-title").attr({role:"heading",
+"aria-level":"1"})}else g==="content"&&(j&&d.addClass("ui-body-"+j),d.attr("role","main"))})})})(jQuery);(function(a){a.widget("mobile.collapsible",a.mobile.widget,{options:{expandCueText:" click to expand contents",collapseCueText:" click to collapse contents",collapsed:true,heading:"h1,h2,h3,h4,h5,h6,legend",theme:null,contentTheme:null,iconTheme:"d",mini:false,initSelector:":jqmData(role='collapsible')"},_create:function(){var c=this.element,b=this.options,e=c.addClass("ui-collapsible"),f=c.children(b.heading).first(),
+d=e.wrapInner("<div class='ui-collapsible-content'></div>").find(".ui-collapsible-content"),g=c.closest(":jqmData(role='collapsible-set')").addClass("ui-collapsible-set");f.is("legend")&&(f=a("<div role='heading'>"+f.html()+"</div>").insertBefore(f),f.next().remove());if(g.length){if(!b.theme)b.theme=g.jqmData("theme")||a.mobile.getInheritedTheme(g,"c");if(!b.contentTheme)b.contentTheme=g.jqmData("content-theme");if(!b.iconPos)b.iconPos=g.jqmData("iconpos");if(!b.mini)b.mini=g.jqmData("mini")}d.addClass(b.contentTheme?
+"ui-body-"+b.contentTheme:"");f.insertBefore(d).addClass("ui-collapsible-heading").append("<span class='ui-collapsible-heading-status'></span>").wrapInner("<a href='#' class='ui-collapsible-heading-toggle'></a>").find("a").first().buttonMarkup({shadow:false,corners:false,iconpos:c.jqmData("iconpos")||b.iconPos||"left",icon:"plus",mini:b.mini,theme:b.theme}).add(".ui-btn-inner",c).addClass("ui-corner-top ui-corner-bottom");e.bind("expand collapse",function(c){if(!c.isDefaultPrevented()){c.preventDefault();
+var j=a(this),c=c.type==="collapse",k=b.contentTheme;f.toggleClass("ui-collapsible-heading-collapsed",c).find(".ui-collapsible-heading-status").text(c?b.expandCueText:b.collapseCueText).end().find(".ui-icon").toggleClass("ui-icon-minus",!c).toggleClass("ui-icon-plus",c);j.toggleClass("ui-collapsible-collapsed",c);d.toggleClass("ui-collapsible-content-collapsed",c).attr("aria-hidden",c);if(k&&(!g.length||e.jqmData("collapsible-last")))f.find("a").first().add(f.find(".ui-btn-inner")).toggleClass("ui-corner-bottom",
+c),d.toggleClass("ui-corner-bottom",!c);d.trigger("updatelayout")}}).trigger(b.collapsed?"collapse":"expand");f.bind("click",function(a){var b=f.is(".ui-collapsible-heading-collapsed")?"expand":"collapse";e.trigger(b);a.preventDefault()})}});a(k).bind("pagecreate create",function(c){a.mobile.collapsible.prototype.enhanceWithin(c.target)})})(jQuery);(function(a,c){a.widget("mobile.collapsibleset",a.mobile.widget,{options:{initSelector:":jqmData(role='collapsible-set')"},_create:function(){var b=this.element.addClass("ui-collapsible-set"),
+e=this.options;if(!e.theme)e.theme=a.mobile.getInheritedTheme(b,"c");if(!e.contentTheme)e.contentTheme=b.jqmData("content-theme");if(!e.corners)e.corners=b.jqmData("corners")===c?true:false;b.jqmData("collapsiblebound")||b.jqmData("collapsiblebound",true).bind("expand collapse",function(b){var c=b.type==="collapse",b=a(b.target).closest(".ui-collapsible"),e=b.data("collapsible");e.options.contentTheme&&b.jqmData("collapsible-last")&&(b.find(e.options.heading).first().find("a").first().add(".ui-btn-inner").toggleClass("ui-corner-bottom",
+c),b.find(".ui-collapsible-content").toggleClass("ui-corner-bottom",!c))}).bind("expand",function(b){a(b.target).closest(".ui-collapsible").siblings(".ui-collapsible").trigger("collapse")})},_init:function(){this.refresh()},refresh:function(){var b=this.options,c=this.element.children(":jqmData(role='collapsible')");a.mobile.collapsible.prototype.enhance(c.not(".ui-collapsible"));c.each(function(){a(this).find(a.mobile.collapsible.prototype.options.heading).find("a").first().add(".ui-btn-inner").removeClass("ui-corner-top ui-corner-bottom")});
+c.first().find("a").first().addClass(b.corners?"ui-corner-top":"").find(".ui-btn-inner").addClass("ui-corner-top");c.last().jqmData("collapsible-last",true).find("a").first().addClass(b.corners?"ui-corner-bottom":"").find(".ui-btn-inner").addClass("ui-corner-bottom")}});a(k).bind("pagecreate create",function(b){a.mobile.collapsibleset.prototype.enhanceWithin(b.target)})})(jQuery);(function(a,c){a.widget("mobile.navbar",a.mobile.widget,{options:{iconpos:"top",grid:null,initSelector:":jqmData(role='navbar')"},
+_create:function(){var b=this.element,e=b.find("a"),f=e.filter(":jqmData(icon)").length?this.options.iconpos:c;b.addClass("ui-navbar").attr("role","navigation").find("ul").jqmEnhanceable().grid({grid:this.options.grid});f||b.addClass("ui-navbar-noicons");e.buttonMarkup({corners:false,shadow:false,inline:true,iconpos:f});b.delegate("a","vclick",function(b){a(b.target).hasClass("ui-disabled")||(e.removeClass(a.mobile.activeBtnClass),a(this).addClass(a.mobile.activeBtnClass))});b.closest(".ui-page").bind("pagebeforeshow",
+function(){e.filter(".ui-state-persist").addClass(a.mobile.activeBtnClass)})}});a(k).bind("pagecreate create",function(b){a.mobile.navbar.prototype.enhanceWithin(b.target)})})(jQuery);(function(a){var c={};a.widget("mobile.listview",a.mobile.widget,{options:{theme:null,countTheme:"c",headerTheme:"b",dividerTheme:"b",splitIcon:"arrow-r",splitTheme:"b",mini:false,inset:false,initSelector:":jqmData(role='listview')"},_create:function(){var a="";a+=this.options.inset?" ui-listview-inset ui-corner-all ui-shadow ":
+"";a+=this.element.jqmData("mini")||this.options.mini===true?" ui-mini":"";this.element.addClass(function(c,f){return f+" ui-listview "+a});this.refresh(true)},_removeCorners:function(a,c){a=a.add(a.find(".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb"));c==="top"?a.removeClass("ui-corner-top ui-corner-tr ui-corner-tl"):c==="bottom"?a.removeClass("ui-corner-bottom ui-corner-br ui-corner-bl"):a.removeClass("ui-corner-top ui-corner-tr ui-corner-tl ui-corner-bottom ui-corner-br ui-corner-bl")},_refreshCorners:function(a){var c,
+f;this.options.inset&&(c=this.element.children("li"),f=a?c.not(".ui-screen-hidden"):c.filter(":visible"),this._removeCorners(c),c=f.first().addClass("ui-corner-top"),c.add(c.find(".ui-btn-inner").not(".ui-li-link-alt span:first-child")).addClass("ui-corner-top").end().find(".ui-li-link-alt, .ui-li-link-alt span:first-child").addClass("ui-corner-tr").end().find(".ui-li-thumb").not(".ui-li-icon").addClass("ui-corner-tl"),f=f.last().addClass("ui-corner-bottom"),f.add(f.find(".ui-btn-inner")).find(".ui-li-link-alt").addClass("ui-corner-br").end().find(".ui-li-thumb").not(".ui-li-icon").addClass("ui-corner-bl"));
+a||this.element.trigger("updatelayout")},_findFirstElementByTagName:function(a,c,f,d){var g={};for(g[f]=g[d]=true;a;){if(g[a.nodeName])return a;a=a[c]}return null},_getChildrenByTagName:function(b,c,f){var d=[],g={};g[c]=g[f]=true;for(b=b.firstChild;b;)g[b.nodeName]&&d.push(b),b=b.nextSibling;return a(d)},_addThumbClasses:function(b){var c,f,d=b.length;for(c=0;c<d;c++)f=a(this._findFirstElementByTagName(b[c].firstChild,"nextSibling","img","IMG")),f.length&&(f.addClass("ui-li-thumb"),a(this._findFirstElementByTagName(f[0].parentNode,
+"parentNode","li","LI")).addClass(f.is(".ui-li-icon")?"ui-li-has-icon":"ui-li-has-thumb"))},refresh:function(b){this.parentPage=this.element.closest(".ui-page");this._createSubPages();var c=this.options,f=this.element,d=f.jqmData("dividertheme")||c.dividerTheme,g=f.jqmData("splittheme"),h=f.jqmData("spliticon"),j=this._getChildrenByTagName(f[0],"li","LI"),o=a.support.cssPseudoElement||!a.nodeName(f[0],"ol")?0:1,m={},p,l,r,n,q,t,x;o&&f.find(".ui-li-dec").remove();if(!c.theme)c.theme=a.mobile.getInheritedTheme(this.element,
+"c");for(var u=0,w=j.length;u<w;u++){p=j.eq(u);l="ui-li";if(b||!p.hasClass("ui-li"))r=p.jqmData("theme")||c.theme,n=this._getChildrenByTagName(p[0],"a","A"),n.length?(t=p.jqmData("icon"),p.buttonMarkup({wrapperEls:"div",shadow:false,corners:false,iconpos:"right",icon:n.length>1||t===false?false:t||"arrow-r",theme:r}),t!=false&&n.length==1&&p.addClass("ui-li-has-arrow"),n.first().removeClass("ui-link").addClass("ui-link-inherit"),n.length>1&&(l+=" ui-li-has-alt",n=n.last(),q=g||n.jqmData("theme")||
+c.splitTheme,x=n.jqmData("icon"),n.appendTo(p).attr("title",n.getEncodedText()).addClass("ui-li-link-alt").empty().buttonMarkup({shadow:false,corners:false,theme:r,icon:false,iconpos:false}).find(".ui-btn-inner").append(a(k.createElement("span")).buttonMarkup({shadow:true,corners:true,theme:q,iconpos:"notext",icon:x||t||h||c.splitIcon})))):p.jqmData("role")==="list-divider"?(l+=" ui-li-divider ui-bar-"+d,p.attr("role","heading"),o&&(o=1)):l+=" ui-li-static ui-body-"+r;o&&l.indexOf("ui-li-divider")<
+0&&(r=p.is(".ui-li-static:first")?p:p.find(".ui-link-inherit"),r.addClass("ui-li-jsnumbering").prepend("<span class='ui-li-dec'>"+o++ +". </span>"));m[l]||(m[l]=[]);m[l].push(p[0])}for(l in m)a(m[l]).addClass(l).children(".ui-btn-inner").addClass(l);f.find("h1, h2, h3, h4, h5, h6").addClass("ui-li-heading").end().find("p, dl").addClass("ui-li-desc").end().find(".ui-li-aside").each(function(){var b=a(this);b.prependTo(b.parent())}).end().find(".ui-li-count").each(function(){a(this).closest("li").addClass("ui-li-has-count")}).addClass("ui-btn-up-"+
+(f.jqmData("counttheme")||this.options.countTheme)+" ui-btn-corner-all");this._addThumbClasses(j);this._addThumbClasses(f.find(".ui-link-inherit"));this._refreshCorners(b)},_idStringEscape:function(a){return a.replace(/[^a-zA-Z0-9]/g,"-")},_createSubPages:function(){var b=this.element,e=b.closest(".ui-page"),f=e.jqmData("url"),d=f||e[0][a.expando],g=b.attr("id"),h=this.options,j="data-"+a.mobile.ns,k=this,m=e.find(":jqmData(role='footer')").jqmData("id"),p;typeof c[d]==="undefined"&&(c[d]=-1);g=g||
+++c[d];a(b.find("li>ul, li>ol").toArray().reverse()).each(function(c){var d=a(this),e=d.attr("id")||g+"-"+c,c=d.parent(),k=a(d.prevAll().toArray().reverse()),k=k.length?k:a("<span>"+a.trim(c.contents()[0].nodeValue)+"</span>"),o=k.first().getEncodedText(),e=(f||"")+"&"+a.mobile.subPageUrlKey+"="+e,x=d.jqmData("theme")||h.theme,u=d.jqmData("counttheme")||b.jqmData("counttheme")||h.countTheme;p=true;d.detach().wrap("<div "+j+"role='page' "+j+"url='"+e+"' "+j+"theme='"+x+"' "+j+"count-theme='"+u+"'><div "+
+j+"role='content'></div></div>").parent().before("<div "+j+"role='header' "+j+"theme='"+h.headerTheme+"'><div class='ui-title'>"+o+"</div></div>").after(m?a("<div "+j+"role='footer' "+j+"id='"+m+"'>"):"").parent().appendTo(a.mobile.pageContainer).page();d=c.find("a:first");d.length||(d=a("<a/>").html(k||o).prependTo(c.empty()));d.attr("href","#"+e)}).listview();p&&e.is(":jqmData(external-page='true')")&&e.data("page").options.domCache===false&&e.unbind("pagehide.remove").bind("pagehide.remove",function(b,
+c){var d=c.nextPage;c.nextPage&&(d=d.jqmData("url"),d.indexOf(f+"&"+a.mobile.subPageUrlKey)!==0&&(k.childPages().remove(),e.remove()))})},childPages:function(){var b=this.parentPage.jqmData("url");return a(":jqmData(url^='"+b+"&"+a.mobile.subPageUrlKey+"')")}});a(k).bind("pagecreate create",function(b){a.mobile.listview.prototype.enhanceWithin(b.target)})})(jQuery);(function(a,c){a.widget("mobile.checkboxradio",a.mobile.widget,{options:{theme:null,initSelector:"input[type='checkbox'],input[type='radio']"},
+_create:function(){var b=this,e=this.element,f=a(e).closest("label"),d=f.length?f:a(e).closest("form,fieldset,:jqmData(role='page'),:jqmData(role='dialog')").find("label").filter("[for='"+e[0].id+"']"),g=e[0].type,f=e.jqmData("mini")||e.closest("form,fieldset").jqmData("mini"),h=g+"-on",j=g+"-off",o=e.parents(":jqmData(type='horizontal')").length?c:j,m=e.jqmData("iconpos")||e.closest("form,fieldset").jqmData("iconpos");if(!(g!=="checkbox"&&g!=="radio")){a.extend(this,{label:d,inputtype:g,checkedClass:"ui-"+
+h+(o?"":" "+a.mobile.activeBtnClass),uncheckedClass:"ui-"+j,checkedicon:"ui-icon-"+h,uncheckedicon:"ui-icon-"+j});if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.element,"c");d.buttonMarkup({theme:this.options.theme,icon:o,shadow:false,mini:f,iconpos:m});f=k.createElement("div");f.className="ui-"+g;e.add(d).wrapAll(f);d.bind({vmouseover:function(b){a(this).parent().is(".ui-disabled")&&b.stopPropagation()},vclick:function(a){if(e.is(":disabled"))a.preventDefault();else return b._cacheVals(),
+e.prop("checked",g==="radio"&&true||!e.prop("checked")),e.triggerHandler("click"),b._getInputSet().not(e).prop("checked",false),b._updateAll(),false}});e.bind({vmousedown:function(){b._cacheVals()},vclick:function(){var c=a(this);c.is(":checked")?(c.prop("checked",true),b._getInputSet().not(c).prop("checked",false)):c.prop("checked",false);b._updateAll()},focus:function(){d.addClass(a.mobile.focusClass)},blur:function(){d.removeClass(a.mobile.focusClass)}});this.refresh()}},_cacheVals:function(){this._getInputSet().each(function(){a(this).jqmData("cacheVal",
+this.checked)})},_getInputSet:function(){return this.inputtype==="checkbox"?this.element:this.element.closest("form,fieldset,:jqmData(role='page')").find("input[name='"+this.element[0].name+"'][type='"+this.inputtype+"']")},_updateAll:function(){var b=this;this._getInputSet().each(function(){var c=a(this);(this.checked||b.inputtype==="checkbox")&&c.trigger("change")}).checkboxradio("refresh")},refresh:function(){var a=this.element[0],c=this.label,f=c.find(".ui-icon");a.checked?(c.addClass(this.checkedClass).removeClass(this.uncheckedClass),
+f.addClass(this.checkedicon).removeClass(this.uncheckedicon)):(c.removeClass(this.checkedClass).addClass(this.uncheckedClass),f.removeClass(this.checkedicon).addClass(this.uncheckedicon));a.disabled?this.disable():this.enable()},disable:function(){this.element.prop("disabled",true).parent().addClass("ui-disabled")},enable:function(){this.element.prop("disabled",false).parent().removeClass("ui-disabled")}});a(k).bind("pagecreate create",function(b){a.mobile.checkboxradio.prototype.enhanceWithin(b.target,
+true)})})(jQuery);(function(a,c){a.widget("mobile.button",a.mobile.widget,{options:{theme:null,icon:null,iconpos:null,inline:false,corners:true,shadow:true,iconshadow:true,initSelector:"button, [type='button'], [type='submit'], [type='reset'], [type='image']",mini:false},_create:function(){var b=this.element,e,f=this.options,d;d="";var g;if(b[0].tagName==="A")!b.hasClass("ui-btn")&&b.buttonMarkup();else{if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.element,"c");~b[0].className.indexOf("ui-btn-left")&&
+(d="ui-btn-left");~b[0].className.indexOf("ui-btn-right")&&(d="ui-btn-right");e=this.button=a("<div></div>").text(b.text()||b.val()).insertBefore(b).buttonMarkup({theme:f.theme,icon:f.icon,iconpos:f.iconpos,inline:f.inline,corners:f.corners,shadow:f.shadow,iconshadow:f.iconshadow,mini:f.mini}).addClass(d).append(b.addClass("ui-btn-hidden"));f=b.attr("type");d=b.attr("name");f!=="button"&&f!=="reset"&&d&&b.bind("vclick",function(){g===c&&(g=a("<input>",{type:"hidden",name:b.attr("name"),value:b.attr("value")}).insertBefore(b),
+a(k).one("submit",function(){g.remove();g=c}))});b.bind({focus:function(){e.addClass(a.mobile.focusClass)},blur:function(){e.removeClass(a.mobile.focusClass)}});this.refresh()}},enable:function(){this.element.attr("disabled",false);this.button.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.button.addClass("ui-disabled").attr("aria-disabled",true);return this._setOption("disabled",true)},refresh:function(){var b=
+this.element;b.prop("disabled")?this.disable():this.enable();a(this.button.data("buttonElements").text).text(b.text()||b.val())}});a(k).bind("pagecreate create",function(b){a.mobile.button.prototype.enhanceWithin(b.target,true)})})(jQuery);(function(a){a.fn.controlgroup=function(c){function b(a,b){a.removeClass("ui-btn-corner-all ui-shadow").eq(0).addClass(b[0]).end().last().addClass(b[1]).addClass("ui-controlgroup-last")}return this.each(function(){var e=a(this),f=a.extend({direction:e.jqmData("type")||
+"vertical",shadow:false,excludeInvisible:true,mini:e.jqmData("mini")},c),d=e.children("legend"),g=f.direction=="horizontal"?["ui-corner-left","ui-corner-right"]:["ui-corner-top","ui-corner-bottom"];e.find("input").first().attr("type");d.length&&(e.wrapInner("<div class='ui-controlgroup-controls'></div>"),a("<div role='heading' class='ui-controlgroup-label'>"+d.html()+"</div>").insertBefore(e.children(0)),d.remove());e.addClass("ui-corner-all ui-controlgroup ui-controlgroup-"+f.direction);b(e.find(".ui-btn"+
+(f.excludeInvisible?":visible":"")).not(".ui-slider-handle"),g);b(e.find(".ui-btn-inner"),g);f.shadow&&e.addClass("ui-shadow");f.mini&&e.addClass("ui-mini")})}})(jQuery);(function(a){a(k).bind("pagecreate create",function(c){a(c.target).find("a").jqmEnhanceable().not(".ui-btn, .ui-link-inherit, :jqmData(role='none'), :jqmData(role='nojs')").addClass("ui-link")})})(jQuery);(function(a){var c=a("meta[name=viewport]"),b=c.attr("content"),e=b+",maximum-scale=1, user-scalable=no",f=b+",maximum-scale=10, user-scalable=yes",
+d=/(user-scalable[\s]*=[\s]*no)|(maximum-scale[\s]*=[\s]*1)[$,\s]/.test(b);a.mobile.zoom=a.extend({},{enabled:!d,locked:false,disable:function(b){if(!d&&!a.mobile.zoom.locked)c.attr("content",e),a.mobile.zoom.enabled=false,a.mobile.zoom.locked=b||false},enable:function(b){if(!d&&(!a.mobile.zoom.locked||b===true))c.attr("content",f),a.mobile.zoom.enabled=true,a.mobile.zoom.locked=false},restore:function(){if(!d)c.attr("content",b),a.mobile.zoom.enabled=true}})})(jQuery);(function(a){a.widget("mobile.textinput",
+a.mobile.widget,{options:{theme:null,preventFocusZoom:/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1,initSelector:"input[type='text'], input[type='search'], :jqmData(type='search'), input[type='number'], :jqmData(type='number'), input[type='password'], input[type='email'], input[type='url'], input[type='tel'], textarea, input[type='time'], input[type='date'], input[type='month'], input[type='week'], input[type='datetime'], input[type='datetime-local'], input[type='color'], input:not([type])",
+clearSearchButtonText:"clear text"},_create:function(){var c=this.element,b=this.options,e=b.theme||a.mobile.getInheritedTheme(this.element,"c"),f=" ui-body-"+e,d=c.jqmData("mini")==true,g=d?" ui-mini":"",h,j;a("label[for='"+c.attr("id")+"']").addClass("ui-input-text");h=c.addClass("ui-input-text ui-body-"+e);typeof c[0].autocorrect!=="undefined"&&!a.support.touchOverflow&&(c[0].setAttribute("autocorrect","off"),c[0].setAttribute("autocomplete","off"));c.is("[type='search'],:jqmData(type='search')")?
+(h=c.wrap("<div class='ui-input-search ui-shadow-inset ui-btn-corner-all ui-btn-shadow ui-icon-searchfield"+f+g+"'></div>").parent(),j=a("<a href='#' class='ui-input-clear' title='"+b.clearSearchButtonText+"'>"+b.clearSearchButtonText+"</a>").bind("click",function(a){c.val("").focus().trigger("change");j.addClass("ui-input-clear-hidden");a.preventDefault()}).appendTo(h).buttonMarkup({icon:"delete",iconpos:"notext",corners:true,shadow:true,mini:d}),e=function(){setTimeout(function(){j.toggleClass("ui-input-clear-hidden",
+!c.val())},0)},e(),c.bind("paste cut keyup focus change blur",e)):c.addClass("ui-corner-all ui-shadow-inset"+f+g);c.focus(function(){h.addClass(a.mobile.focusClass)}).blur(function(){h.removeClass(a.mobile.focusClass)}).bind("focus",function(){b.preventFocusZoom&&a.mobile.zoom.disable(true)}).bind("blur",function(){b.preventFocusZoom&&a.mobile.zoom.enable(true)});if(c.is("textarea")){var o=function(){var a=c[0].scrollHeight;c[0].clientHeight<a&&c.height(a+15)},m;c.keyup(function(){clearTimeout(m);
+m=setTimeout(o,100)});a(k).one("pagechange",o);a.trim(c.val())&&a(s).load(o)}},disable:function(){(this.element.attr("disabled",true).is("[type='search'],:jqmData(type='search')")?this.element.parent():this.element).addClass("ui-disabled")},enable:function(){(this.element.attr("disabled",false).is("[type='search'],:jqmData(type='search')")?this.element.parent():this.element).removeClass("ui-disabled")}});a(k).bind("pagecreate create",function(c){a.mobile.textinput.prototype.enhanceWithin(c.target,
+true)})})(jQuery);(function(a){a.mobile.listview.prototype.options.filter=false;a.mobile.listview.prototype.options.filterPlaceholder="Filter items...";a.mobile.listview.prototype.options.filterTheme="c";a.mobile.listview.prototype.options.filterCallback=function(a,b){return a.toLowerCase().indexOf(b)===-1};a(k).delegate(":jqmData(role='listview')","listviewcreate",function(){var c=a(this),b=c.data("listview");if(b.options.filter){var e=a("<form>",{"class":"ui-listview-filter ui-bar-"+b.options.filterTheme,
+role:"search"});a("<input>",{placeholder:b.options.filterPlaceholder}).attr("data-"+a.mobile.ns+"type","search").jqmData("lastval","").bind("keyup change",function(){var e=a(this),d=this.value.toLowerCase(),g=null,g=e.jqmData("lastval")+"",h=false,j="";e.jqmData("lastval",d);g=d.length<g.length||d.indexOf(g)!==0?c.children():c.children(":not(.ui-screen-hidden)");if(d){for(var k=g.length-1;k>=0;k--)e=a(g[k]),j=e.jqmData("filtertext")||e.text(),e.is("li:jqmData(role=list-divider)")?(e.toggleClass("ui-filter-hidequeue",
+!h),h=false):b.options.filterCallback(j,d)?e.toggleClass("ui-filter-hidequeue",true):h=true;g.filter(":not(.ui-filter-hidequeue)").toggleClass("ui-screen-hidden",false);g.filter(".ui-filter-hidequeue").toggleClass("ui-screen-hidden",true).toggleClass("ui-filter-hidequeue",false)}else g.toggleClass("ui-screen-hidden",false);b._refreshCorners()}).appendTo(e).textinput();b.options.inset&&e.addClass("ui-listview-filter-inset");e.bind("submit",function(){return false}).insertBefore(c)}})})(jQuery);(function(a,
+c){a.widget("mobile.slider",a.mobile.widget,{options:{theme:null,trackTheme:null,disabled:false,initSelector:"input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",mini:false},_create:function(){var b=this,e=this.element,f=a.mobile.getInheritedTheme(e,"c"),d=this.options.theme||f,f=this.options.trackTheme||f,g=e[0].nodeName.toLowerCase(),h=g=="select"?"ui-slider-switch":"",j=e.attr("id"),o=j+"-label",j=a("[for='"+j+"']").attr("id",o),m=function(){return g=="input"?parseFloat(e.val()):
+e[0].selectedIndex},p=g=="input"?parseFloat(e.attr("min")):0,l=g=="input"?parseFloat(e.attr("max")):e.find("option").length-1,r=s.parseFloat(e.attr("step")||1),n=this.options.inline||e.jqmData("inline")==true?" ui-slider-inline":"",q=this.options.mini||e.jqmData("mini")?" ui-slider-mini":"",t=k.createElement("a"),x=a(t),u=k.createElement("div"),w=a(u),v=e.jqmData("highlight")&&g!="select"?function(){var b=k.createElement("div");b.className="ui-slider-bg ui-btn-active ui-btn-corner-all";return a(b).prependTo(w)}():
+false;t.setAttribute("href","#");u.setAttribute("role","application");u.className=["ui-slider ",h," ui-btn-down-",f," ui-btn-corner-all",n,q].join("");t.className="ui-slider-handle";u.appendChild(t);x.buttonMarkup({corners:true,theme:d,shadow:true}).attr({role:"slider","aria-valuemin":p,"aria-valuemax":l,"aria-valuenow":m(),"aria-valuetext":m(),title:m(),"aria-labelledby":o});a.extend(this,{slider:w,handle:x,valuebg:v,dragging:false,beforeStart:null,userModified:false,mouseMoved:false});if(g=="select"){d=
+k.createElement("div");d.className="ui-slider-inneroffset";h=0;for(o=u.childNodes.length;h<o;h++)d.appendChild(u.childNodes[h]);u.appendChild(d);x.addClass("ui-slider-handle-snapping");u=e.find("option");d=0;for(h=u.length;d<h;d++)o=!d?"b":"a",n=!d?" ui-btn-down-"+f:" "+a.mobile.activeBtnClass,k.createElement("div"),q=k.createElement("span"),q.className=["ui-slider-label ui-slider-label-",o,n," ui-btn-corner-all"].join(""),q.setAttribute("role","img"),q.appendChild(k.createTextNode(u[d].innerHTML)),
+a(q).prependTo(w);b._labels=a(".ui-slider-label",w)}j.addClass("ui-slider");e.addClass(g==="input"?"ui-slider-input":"ui-slider-switch").change(function(){b.mouseMoved||b.refresh(m(),true)}).keyup(function(){b.refresh(m(),true,true)}).blur(function(){b.refresh(m(),true)});a(k).bind("vmousemove",function(a){if(b.dragging)return b.mouseMoved=true,g==="select"&&x.removeClass("ui-slider-handle-snapping"),b.refresh(a),b.userModified=b.beforeStart!==e[0].selectedIndex,false});w.bind("vmousedown",function(a){b.dragging=
+true;b.userModified=false;b.mouseMoved=false;if(g==="select")b.beforeStart=e[0].selectedIndex;b.refresh(a);return false}).bind("vclick",false);w.add(k).bind("vmouseup",function(){if(b.dragging)return b.dragging=false,g==="select"&&(x.addClass("ui-slider-handle-snapping"),b.mouseMoved?b.userModified?b.refresh(b.beforeStart==0?1:0):b.refresh(b.beforeStart):b.refresh(b.beforeStart==0?1:0)),b.mouseMoved=false});w.insertAfter(e);g=="select"&&this.handle.bind({focus:function(){w.addClass(a.mobile.focusClass)},
+blur:function(){w.removeClass(a.mobile.focusClass)}});this.handle.bind({vmousedown:function(){a(this).focus()},vclick:false,keydown:function(c){var d=m();if(!b.options.disabled){switch(c.keyCode){case a.mobile.keyCode.HOME:case a.mobile.keyCode.END:case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:if(c.preventDefault(),!b._keySliding)b._keySliding=true,a(this).addClass("ui-state-active")}switch(c.keyCode){case a.mobile.keyCode.HOME:b.refresh(p);
+break;case a.mobile.keyCode.END:b.refresh(l);break;case a.mobile.keyCode.PAGE_UP:case a.mobile.keyCode.UP:case a.mobile.keyCode.RIGHT:b.refresh(d+r);break;case a.mobile.keyCode.PAGE_DOWN:case a.mobile.keyCode.DOWN:case a.mobile.keyCode.LEFT:b.refresh(d-r)}}},keyup:function(){if(b._keySliding)b._keySliding=false,a(this).removeClass("ui-state-active")}});this.refresh(c,c,true)},refresh:function(b,c,f){(this.options.disabled||this.element.attr("disabled"))&&this.disable();var d=this.element,g=d[0].nodeName.toLowerCase(),
+h=g==="input"?parseFloat(d.attr("min")):0,j=g==="input"?parseFloat(d.attr("max")):d.find("option").length-1,k=g==="input"&&parseFloat(d.attr("step"))>0?parseFloat(d.attr("step")):1;if(typeof b==="object"){if(!this.dragging||b.pageX<this.slider.offset().left-8||b.pageX>this.slider.offset().left+this.slider.width()+8)return;b=Math.round((b.pageX-this.slider.offset().left)/this.slider.width()*100)}else b==null&&(b=g==="input"?parseFloat(d.val()||0):d[0].selectedIndex),b=(parseFloat(b)-h)/(j-h)*100;if(!isNaN(b)){b<
+0&&(b=0);b>100&&(b=100);var m=b/100*(j-h)+h,p=(m-h)%k;m-=p;Math.abs(p)*2>=k&&(m+=p>0?k:-k);m=parseFloat(m.toFixed(5));m<h&&(m=h);m>j&&(m=j);this.handle.css("left",b+"%");this.handle.attr({"aria-valuenow":g==="input"?m:d.find("option").eq(m).attr("value"),"aria-valuetext":g==="input"?m:d.find("option").eq(m).getEncodedText(),title:g==="input"?m:d.find("option").eq(m).getEncodedText()});this.valuebg&&this.valuebg.css("width",b+"%");if(this._labels){var h=this.handle.width()/this.slider.width()*100,
+l=b&&h+(100-h)*b/100,r=b===100?0:Math.min(h+100-l,100);this._labels.each(function(){var b=a(this).is(".ui-slider-label-a");a(this).width((b?l:r)+"%")})}if(!f)f=false,g==="input"?(f=d.val()!==m,d.val(m)):(f=d[0].selectedIndex!==m,d[0].selectedIndex=m),!c&&f&&d.trigger("change")}},enable:function(){this.element.attr("disabled",false);this.slider.removeClass("ui-disabled").attr("aria-disabled",false);return this._setOption("disabled",false)},disable:function(){this.element.attr("disabled",true);this.slider.addClass("ui-disabled").attr("aria-disabled",
+true);return this._setOption("disabled",true)}});a(k).bind("pagecreate create",function(b){a.mobile.slider.prototype.enhanceWithin(b.target,true)})})(jQuery);(function(a){a.widget("mobile.selectmenu",a.mobile.widget,{options:{theme:null,disabled:false,icon:"arrow-d",iconpos:"right",inline:false,corners:true,shadow:true,iconshadow:true,overlayTheme:"a",hidePlaceholderMenuItems:true,closeText:"Close",nativeMenu:true,preventFocusZoom:/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>
+-1,initSelector:"select:not(:jqmData(role='slider'))",mini:false},_button:function(){return a("<div/>")},_setDisabled:function(a){this.element.attr("disabled",a);this.button.attr("aria-disabled",a);return this._setOption("disabled",a)},_focusButton:function(){var a=this;setTimeout(function(){a.button.focus()},40)},_selectOptions:function(){return this.select.find("option")},_preExtension:function(){var c="";~this.element[0].className.indexOf("ui-btn-left")&&(c=" ui-btn-left");~this.element[0].className.indexOf("ui-btn-right")&&
+(c=" ui-btn-right");this.select=this.element.wrap("<div class='ui-select"+c+"'>");this.selectID=this.select.attr("id");this.label=a("label[for='"+this.selectID+"']").addClass("ui-select");this.isMultiple=this.select[0].multiple;if(!this.options.theme)this.options.theme=a.mobile.getInheritedTheme(this.select,"c")},_create:function(){this._preExtension();this._trigger("beforeCreate");this.button=this._button();var c=this,b=this.options,e=this.button.text(a(this.select[0].options.item(this.select[0].selectedIndex==
+-1?0:this.select[0].selectedIndex)).text()).insertBefore(this.select).buttonMarkup({theme:b.theme,icon:b.icon,iconpos:b.iconpos,inline:b.inline,corners:b.corners,shadow:b.shadow,iconshadow:b.iconshadow,mini:b.mini});b.nativeMenu&&s.opera&&s.opera.version&&this.select.addClass("ui-select-nativeonly");if(this.isMultiple)this.buttonCount=a("<span>").addClass("ui-li-count ui-btn-up-c ui-btn-corner-all").hide().appendTo(e.addClass("ui-li-has-count"));(b.disabled||this.element.attr("disabled"))&&this.disable();
+this.select.change(function(){c.refresh()});this.build()},build:function(){var c=this;this.select.appendTo(c.button).bind("vmousedown",function(){c.button.addClass(a.mobile.activeBtnClass)}).bind("focus",function(){c.button.addClass(a.mobile.focusClass)}).bind("blur",function(){c.button.removeClass(a.mobile.focusClass)}).bind("focus vmouseover",function(){c.button.trigger("vmouseover")}).bind("vmousemove",function(){c.button.removeClass(a.mobile.activeBtnClass)}).bind("change blur vmouseout",function(){c.button.trigger("vmouseout").removeClass(a.mobile.activeBtnClass)}).bind("change blur",
+function(){c.button.removeClass("ui-btn-down-"+c.options.theme)});c.button.bind("vmousedown",function(){c.options.preventFocusZoom&&a.mobile.zoom.disable(true)}).bind("mouseup",function(){c.options.preventFocusZoom&&a.mobile.zoom.enable(true)})},selected:function(){return this._selectOptions().filter(":selected")},selectedIndices:function(){var a=this;return this.selected().map(function(){return a._selectOptions().index(this)}).get()},setButtonText:function(){var c=this,b=this.selected();this.button.find(".ui-btn-text").text(function(){return!c.isMultiple?
+b.text():b.length?b.map(function(){return a(this).text()}).get().join(", "):c.placeholder})},setButtonCount:function(){var a=this.selected();this.isMultiple&&this.buttonCount[a.length>1?"show":"hide"]().text(a.length)},refresh:function(){this.setButtonText();this.setButtonCount()},open:a.noop,close:a.noop,disable:function(){this._setDisabled(true);this.button.addClass("ui-disabled")},enable:function(){this._setDisabled(false);this.button.removeClass("ui-disabled")}});a(k).bind("pagecreate create",
+function(c){a.mobile.selectmenu.prototype.enhanceWithin(c.target,true)})})(jQuery);(function(a){var c=function(b){var c=b.selectID,f=b.label,d=b.select.closest(".ui-page"),g=a("<div>",{"class":"ui-selectmenu-screen ui-screen-hidden"}).appendTo(d),h=b._selectOptions(),j=b.isMultiple=b.select[0].multiple,o=c+"-button",m=c+"-menu",p=a("<div data-"+a.mobile.ns+"role='dialog' data-"+a.mobile.ns+"theme='"+b.options.theme+"' data-"+a.mobile.ns+"overlay-theme='"+b.options.overlayTheme+"'><div data-"+a.mobile.ns+
+"role='header'><div class='ui-title'>"+f.getEncodedText()+"</div></div><div data-"+a.mobile.ns+"role='content'></div></div>"),l=a("<div>",{"class":"ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-"+b.options.overlayTheme+" "+a.mobile.defaultDialogTransition}).insertAfter(g),r=a("<ul>",{"class":"ui-selectmenu-list",id:m,role:"listbox","aria-labelledby":o}).attr("data-"+a.mobile.ns+"theme",b.options.theme).appendTo(l),n=a("<div>",{"class":"ui-header ui-bar-"+b.options.theme}).prependTo(l),
+q=a("<h1>",{"class":"ui-title"}).appendTo(n),t;b.isMultiple&&(t=a("<a>",{text:b.options.closeText,href:"#","class":"ui-btn-left"}).attr("data-"+a.mobile.ns+"iconpos","notext").attr("data-"+a.mobile.ns+"icon","delete").appendTo(n).buttonMarkup());a.extend(b,{select:b.select,selectID:c,buttonId:o,menuId:m,thisPage:d,menuPage:p,label:f,screen:g,selectOptions:h,isMultiple:j,theme:b.options.theme,listbox:l,list:r,header:n,headerTitle:q,headerClose:t,menuPageContent:void 0,menuPageClose:void 0,placeholder:"",
+build:function(){var c=this;c.refresh();c.select.attr("tabindex","-1").focus(function(){a(this).blur();c.button.focus()});c.button.bind("vclick keydown",function(b){if(b.type=="vclick"||b.keyCode&&(b.keyCode===a.mobile.keyCode.ENTER||b.keyCode===a.mobile.keyCode.SPACE))c.open(),b.preventDefault()});c.list.attr("role","listbox").bind("focusin",function(b){a(b.target).attr("tabindex","0").trigger("vmouseover")}).bind("focusout",function(b){a(b.target).attr("tabindex","-1").trigger("vmouseout")}).delegate("li:not(.ui-disabled, .ui-li-divider)",
+"click",function(b){var d=c.select[0].selectedIndex,e=c.list.find("li:not(.ui-li-divider)").index(this),f=c._selectOptions().eq(e)[0];f.selected=c.isMultiple?!f.selected:true;c.isMultiple&&a(this).find(".ui-icon").toggleClass("ui-icon-checkbox-on",f.selected).toggleClass("ui-icon-checkbox-off",!f.selected);(c.isMultiple||d!==e)&&c.select.trigger("change");c.isMultiple||c.close();b.preventDefault()}).keydown(function(c){var d=a(c.target),e=d.closest("li");switch(c.keyCode){case 38:return c=e.prev().not(".ui-selectmenu-placeholder"),
+c.is(".ui-li-divider")&&(c=c.prev()),c.length&&(d.blur().attr("tabindex","-1"),c.addClass("ui-btn-down-"+b.options.theme).find("a").first().focus()),false;case 40:return c=e.next(),c.is(".ui-li-divider")&&(c=c.next()),c.length&&(d.blur().attr("tabindex","-1"),c.addClass("ui-btn-down-"+b.options.theme).find("a").first().focus()),false;case 13:case 32:return d.trigger("click"),false}});c.menuPage.bind("pagehide",function(){c.list.appendTo(c.listbox);c._focusButton();a.mobile._bindPageRemove.call(c.thisPage)});
+c.screen.bind("vclick",function(){c.close()});c.isMultiple&&c.headerClose.click(function(){if(c.menuType=="overlay")return c.close(),false});c.thisPage.addDependents(this.menuPage)},_isRebuildRequired:function(){var a=this.list.find("li");return this._selectOptions().text()!==a.text()},refresh:function(b){var c=this;this._selectOptions();this.selected();var d=this.selectedIndices();(b||this._isRebuildRequired())&&c._buildList();c.setButtonText();c.setButtonCount();c.list.find("li:not(.ui-li-divider)").removeClass(a.mobile.activeBtnClass).attr("aria-selected",
+false).each(function(b){a.inArray(b,d)>-1&&(b=a(this),b.attr("aria-selected",true),c.isMultiple?b.find(".ui-icon").removeClass("ui-icon-checkbox-off").addClass("ui-icon-checkbox-on"):b.is(".ui-selectmenu-placeholder")?b.next().addClass(a.mobile.activeBtnClass):b.addClass(a.mobile.activeBtnClass))})},close:function(){if(!this.options.disabled&&this.isOpen)this.menuType=="page"?s.history.back():(this.screen.addClass("ui-screen-hidden"),this.listbox.addClass("ui-selectmenu-hidden").removeAttr("style").removeClass("in"),
+this.list.appendTo(this.listbox),this._focusButton()),this.isOpen=false},open:function(){function b(){c.list.find("."+a.mobile.activeBtnClass+" a").focus()}if(!this.options.disabled){var c=this,d=a(s),e=c.list.parent(),f=e.outerHeight(),e=e.outerWidth();a(".ui-page-active");var g=d.scrollTop(),j=c.button.offset().top,h=d.height(),d=d.width();c.button.addClass(a.mobile.activeBtnClass);setTimeout(function(){c.button.removeClass(a.mobile.activeBtnClass)},300);if(f>h-80||!a.support.scrollTop){c.menuPage.appendTo(a.mobile.pageContainer).page();
+c.menuPageContent=p.find(".ui-content");c.menuPageClose=p.find(".ui-header a");c.thisPage.unbind("pagehide.remove");if(g==0&&j>h)c.thisPage.one("pagehide",function(){a(this).jqmData("lastScroll",j)});c.menuPage.one("pageshow",function(){b();c.isOpen=true});c.menuType="page";c.menuPageContent.append(c.list);c.menuPage.find("div .ui-title").text(c.label.text());a.mobile.changePage(c.menuPage,{transition:a.mobile.defaultDialogTransition})}else{c.menuType="overlay";c.screen.height(a(k).height()).removeClass("ui-screen-hidden");
+var l=j-g,m=g+h-j,n=f/2,o=parseFloat(c.list.parent().css("max-width")),f=l>f/2&&m>f/2?j+c.button.outerHeight()/2-n:l>m?g+h-f-30:g+30;e<o?g=(d-e)/2:(g=c.button.offset().left+c.button.outerWidth()/2-e/2,g<30?g=30:g+e>d&&(g=d-e-30));c.listbox.append(c.list).removeClass("ui-selectmenu-hidden").css({top:f,left:g}).addClass("in");b();c.isOpen=true}}},_buildList:function(){var b=this.options,c=this.placeholder,d=true,e=this.isMultiple?"checkbox-off":"false";this.list.empty().filter(".ui-listview").listview("destroy");
+var f=this.select.find("option"),g=f.length,j=this.select[0],h="data-"+a.mobile.ns,l=h+"option-index",m=h+"icon";h+="role";for(var n=k.createDocumentFragment(),o,p=0;p<g;p++){var r=f[p],q=a(r),s=r.parentNode,t=q.text(),D=k.createElement("a"),J=[];D.setAttribute("href","#");D.appendChild(k.createTextNode(t));s!==j&&s.nodeName.toLowerCase()==="optgroup"&&(s=s.getAttribute("label"),s!=o&&(o=k.createElement("li"),o.setAttribute(h,"list-divider"),o.setAttribute("role","option"),o.setAttribute("tabindex",
+"-1"),o.appendChild(k.createTextNode(s)),n.appendChild(o),o=s));if(d&&(!r.getAttribute("value")||t.length==0||q.jqmData("placeholder")))if(d=false,b.hidePlaceholderMenuItems&&J.push("ui-selectmenu-placeholder"),!c)c=this.placeholder=t;q=k.createElement("li");r.disabled&&(J.push("ui-disabled"),q.setAttribute("aria-disabled",true));q.setAttribute(l,p);q.setAttribute(m,e);q.className=J.join(" ");q.setAttribute("role","option");D.setAttribute("tabindex","-1");q.appendChild(D);n.appendChild(q)}this.list[0].appendChild(n);
+!this.isMultiple&&!c.length?this.header.hide():this.headerTitle.text(this.placeholder);this.list.listview()},_button:function(){return a("<a>",{href:"#",role:"button",id:this.buttonId,"aria-haspopup":"true","aria-owns":this.menuId})}})};a(k).bind("selectmenubeforecreate",function(b){b=a(b.target).data("selectmenu");b.options.nativeMenu||c(b)})})(jQuery);(function(a){a.widget("mobile.fixedtoolbar",a.mobile.widget,{options:{visibleOnPageShow:true,disablePageZoom:true,transition:"slide",fullscreen:false,
+tapToggle:true,tapToggleBlacklist:"a, input, select, textarea, .ui-header-fixed, .ui-footer-fixed",hideDuringFocus:"input, textarea, select",updatePagePadding:true,trackPersistentToolbars:true,supportBlacklist:function(){var a=s,b=navigator.userAgent,e=navigator.platform,f=b.match(/AppleWebKit\/([0-9]+)/),f=!!f&&f[1],d=b.match(/Fennec\/([0-9]+)/),d=!!d&&d[1],g=b.match(/Opera Mobi\/([0-9]+)/),h=!!g&&g[1];return(e.indexOf("iPhone")>-1||e.indexOf("iPad")>-1||e.indexOf("iPod")>-1)&&f&&f<534||a.operamini&&
+{}.toString.call(a.operamini)==="[object OperaMini]"||g&&h<7458||b.indexOf("Android")>-1&&f&&f<533||d&&d<6||"palmGetResource"in s&&f&&f<534||b.indexOf("MeeGo")>-1&&b.indexOf("NokiaBrowser/8.5.0")>-1?true:false},initSelector:":jqmData(position='fixed')"},_create:function(){var a=this.options,b=this.element,e=b.is(":jqmData(role='header')")?"header":"footer",f=b.closest(".ui-page");a.supportBlacklist()?this.destroy():(b.addClass("ui-"+e+"-fixed"),a.fullscreen?(b.addClass("ui-"+e+"-fullscreen"),f.addClass("ui-page-"+
+e+"-fullscreen")):f.addClass("ui-page-"+e+"-fixed"),this._addTransitionClass(),this._bindPageEvents(),this._bindToggleHandlers())},_addTransitionClass:function(){var a=this.options.transition;a&&a!=="none"&&(a==="slide"&&(a=this.element.is(".ui-header")?"slidedown":"slideup"),this.element.addClass(a))},_bindPageEvents:function(){var c=this,b=c.options;c.element.closest(".ui-page").bind("pagebeforeshow",function(){b.disablePageZoom&&a.mobile.zoom.disable(true);b.visibleOnPageShow||c.hide(true)}).bind("webkitAnimationStart animationstart updatelayout",
+function(){b.updatePagePadding&&c.updatePagePadding()}).bind("pageshow",function(){c.updatePagePadding();b.updatePagePadding&&a(s).bind("throttledresize."+c.widgetName,function(){c.updatePagePadding()})}).bind("pagebeforehide",function(e,f){b.disablePageZoom&&a.mobile.zoom.enable(true);b.updatePagePadding&&a(s).unbind("throttledresize."+c.widgetName);if(b.trackPersistentToolbars){var d=a(".ui-footer-fixed:jqmData(id)",this),g=a(".ui-header-fixed:jqmData(id)",this),h=d.length&&f.nextPage&&a(".ui-footer-fixed:jqmData(id='"+
+d.jqmData("id")+"')",f.nextPage),j=g.length&&f.nextPage&&a(".ui-header-fixed:jqmData(id='"+g.jqmData("id")+"')",f.nextPage),h=h||a();if(h.length||j.length)h.add(j).appendTo(a.mobile.pageContainer),f.nextPage.one("pageshow",function(){h.add(j).appendTo(this)})}})},_visible:true,updatePagePadding:function(){var a=this.element,b=a.is(".ui-header");this.options.fullscreen||a.closest(".ui-page").css("padding-"+(b?"top":"bottom"),a.outerHeight())},_useTransition:function(c){var b=this.element,e=a(s).scrollTop(),
+f=b.height(),d=b.closest(".ui-page").height(),g=a.mobile.getScreenHeight(),b=b.is(":jqmData(role='header')")?"header":"footer";return!c&&(this.options.transition&&this.options.transition!=="none"&&(b==="header"&&!this.options.fullscreen&&e>f||b==="footer"&&!this.options.fullscreen&&e+g<d-f)||this.options.fullscreen)},show:function(a){var b=this.element;this._useTransition(a)?b.removeClass("out ui-fixed-hidden").addClass("in"):b.removeClass("ui-fixed-hidden");this._visible=true},hide:function(a){var b=
+this.element,e="out"+(this.options.transition==="slide"?" reverse":"");this._useTransition(a)?b.addClass(e).removeClass("in").animationComplete(function(){b.addClass("ui-fixed-hidden").removeClass(e)}):b.addClass("ui-fixed-hidden").removeClass(e);this._visible=false},toggle:function(){this[this._visible?"hide":"show"]()},_bindToggleHandlers:function(){var c=this,b=c.options;c.element.closest(".ui-page").bind("vclick",function(e){b.tapToggle&&!a(e.target).closest(b.tapToggleBlacklist).length&&c.toggle()}).bind("focusin focusout",
+function(e){if(screen.width<500&&a(e.target).is(b.hideDuringFocus)&&!a(e.target).closest(".ui-header-fixed, .ui-footer-fixed").length)c[e.type==="focusin"&&c._visible?"hide":"show"]()})},destroy:function(){this.element.removeClass("ui-header-fixed ui-footer-fixed ui-header-fullscreen ui-footer-fullscreen in out fade slidedown slideup ui-fixed-hidden");this.element.closest(".ui-page").removeClass("ui-page-header-fixed ui-page-footer-fixed ui-page-header-fullscreen ui-page-footer-fullscreen")}});a(k).bind("pagecreate create",
+function(c){a(c.target).jqmData("fullscreen")&&a(a.mobile.fixedtoolbar.prototype.options.initSelector,c.target).not(":jqmData(fullscreen)").jqmData("fullscreen",true);a.mobile.fixedtoolbar.prototype.enhanceWithin(c.target)})})(jQuery);(function(a,c){if(/iPhone|iPad|iPod/.test(navigator.platform)&&navigator.userAgent.indexOf("AppleWebKit")>-1){var b=a.mobile.zoom,e,f,d,g,h;a(c).bind("orientationchange.iosorientationfix",b.enable).bind("devicemotion.iosorientationfix",function(a){e=a.originalEvent;
+h=e.accelerationIncludingGravity;f=Math.abs(h.x);d=Math.abs(h.y);g=Math.abs(h.z);!c.orientation&&(f>7||(g>6&&d<8||g<8&&d>6)&&f>5)?b.enabled&&b.disable():b.enabled||b.enable()})}})(jQuery,this);(function(a,c){function b(){var b=a("."+a.mobile.activeBtnClass).first();h.css({top:a.support.scrollTop&&g.scrollTop()+g.height()/2||b.length&&b.offset().top||100})}function e(){var c=h.offset(),d=g.scrollTop(),f=a.mobile.getScreenHeight();if(c.top<d||c.top-d>f)h.addClass("ui-loader-fakefix"),b(),g.unbind("scroll",
+e).bind("scroll",b)}function f(){d.removeClass("ui-mobile-rendering")}var d=a("html");a("head");var g=a(c);a(c.document).trigger("mobileinit");if(a.mobile.gradeA()){if(a.mobile.ajaxBlacklist)a.mobile.ajaxEnabled=false;d.addClass("ui-mobile ui-mobile-rendering");setTimeout(f,5E3);var h=a("<div class='ui-loader'><span class='ui-icon ui-icon-loading'></span><h1></h1></div>");a.extend(a.mobile,{showPageLoadingMsg:function(b,c,f){d.addClass("ui-loading");if(a.mobile.loadingMessage){var k=f||a.mobile.loadingMessageTextVisible;
+b=b||a.mobile.loadingMessageTheme;h.attr("class","ui-loader ui-corner-all ui-body-"+(b||"a")+" ui-loader-"+(k?"verbose":"default")+(f?" ui-loader-textonly":"")).find("h1").text(c||a.mobile.loadingMessage).end().appendTo(a.mobile.pageContainer);e();g.bind("scroll",e)}},hidePageLoadingMsg:function(){d.removeClass("ui-loading");a.mobile.loadingMessage&&h.removeClass("ui-loader-fakefix");a(c).unbind("scroll",b);a(c).unbind("scroll",e)},initializePage:function(){var b=a(":jqmData(role='page'), :jqmData(role='dialog')");
+b.length||(b=a("body").wrapInner("<div data-"+a.mobile.ns+"role='page'></div>").children(0));b.each(function(){var b=a(this);b.jqmData("url")||b.attr("data-"+a.mobile.ns+"url",b.attr("id")||location.pathname+location.search)});a.mobile.firstPage=b.first();a.mobile.pageContainer=b.first().parent().addClass("ui-mobile-viewport");g.trigger("pagecontainercreate");a.mobile.showPageLoadingMsg();f();!a.mobile.hashListeningEnabled||!a.mobile.path.stripHash(location.hash)?a.mobile.changePage(a.mobile.firstPage,
+{transition:"none",reverse:true,changeHash:false,fromHashChange:true}):g.trigger("hashchange",[true])}});a.mobile._registerInternalEvents();a(function(){c.scrollTo(0,1);a.mobile.defaultHomeScroll=!a.support.scrollTop||a(c).scrollTop()===1?0:1;a.fn.controlgroup&&a(k).bind("pagecreate create",function(b){a(":jqmData(role='controlgroup')",b.target).jqmEnhanceable().controlgroup({excludeInvisible:false})});a.mobile.autoInitializePage&&a.mobile.initializePage();g.load(a.mobile.silentScroll)})}})(jQuery,
+this)});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery.mobile.simpledialog.min.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,8 @@
+/*
+ * jQuery Mobile Framework : plugin to provide a simple Dialog widget.
+ * Copyright (c) JTSage
+ * CC 3.0 Attribution.  May be relicensed without permission/notifcation.
+ * https://github.com/jtsage/jquery-mobile-simpledialog
+ */
+
+.ui-simpledialog-header h4{margin-top:5px;margin-bottom:5px;text-align:center}.ui-simpledialog-container{border:5px solid #111!important;width:85%;max-width:500px}.ui-simpledialog-screen{position:absolute;top:0;left:0;width:100%;height:100%}.ui-simpledialog-hidden{display:none}.ui-simpledialog-input{width:85%!important;display:block!important;margin-left:auto;margin-right:auto}.ui-simpledialog-screen-modal{background-color:black;-moz-opacity:.8;opacity:.80;filter:alpha(opacity=80)}.ui-simpledialog-subtitle{text-align:center}.ui-simpledialog-controls .buttons-separator{min-height:.6em}.ui-simpledialog-controls .button-hidden{display:none}.ui-dialog .ui-simpledialog-container{border:none!important}.ui-dialog-simpledialog .ui-content{padding:5px!important}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/jquery.mobile.simpledialog2.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,380 @@
+/*
+ * jQuery Mobile Framework : plugin to provide a dialogs Widget. ver2
+ * Copyright (c) JTSage
+ * CC 3.0 Attribution.  May be relicensed without permission/notifcation.
+ * https://github.com/jtsage/jquery-mobile-simpledialog
+ */
+
+(function($, undefined ) {
+  $.widget( "mobile.simpledialog2", $.mobile.widget, {
+    options: {
+      version: '1.0.1-2012061300', // jQueryMobile-YrMoDaySerial
+      mode: 'blank', // or 'button'
+      themeDialog: 'b',
+      themeInput: false,
+      themeButtonDefault: false,
+      themeHeader: 'a',
+      
+      fullScreen: false,
+      fullScreenForce: false,
+      dialogAllow: false,
+      dialogForce: false,
+      
+      headerText: false,
+      headerClose: false,
+      buttonPrompt: false,
+      buttonInput: false,
+      buttonInputDefault: false,
+      buttonPassword: false,
+      blankContent: false,
+      blankContentAdopt: false,
+      
+      resizeListener: true,
+      safeNuke: true,
+      forceInput: true,
+      showModal: true,
+      animate: true,
+      transition: 'pop',
+      clickEvent: 'click',
+      zindex: '500',
+      width: '280px',
+      left: false,
+      top: false,
+      
+      callbackOpen: false,
+      callbackOpenArgs: [],
+      callbackClose: false,
+      callbackCloseArgs: []
+    },
+    _eventHandler: function(e,p) {
+      // Handle the triggers
+      var self = e.data.widget,
+      o = e.data.widget.options;
+      
+      if ( ! e.isPropagationStopped() ) {
+	switch (p.method) {
+	case 'close':
+	  self.close();
+	  break;
+	case 'html':
+	  self.updateBlank(p.source);
+	  break;
+	}
+      }
+    },
+    _create: function () {
+      var self = this,
+      o = $.extend(this.options, this.element.jqmData('options')),
+      initDate = new Date(),
+      content = $("<div class='ui-simpledialog-container ui-overlay-shadow ui-corner-all ui-simpledialog-hidden " + 
+		  ((o.animate === true) ? o.transition : '') + " ui-body-" + o.themeDialog + "'></div>");
+      
+      if ( o.themeButtonDefault === false ) { o.themeButtonDefault = o.themeDialog; }
+      if ( o.themeInput === false ) { o.themeInput = o.themeDialog; }
+      $.mobile.sdCurrentDialog = self;
+      if ( typeof $.mobile.sdLastInput !== 'undefined' ) { delete $.mobile.sdLastInput; }
+      self.internalID = initDate.getTime();
+      self.displayAnchor = $.mobile.activePage.children('.ui-content').first();
+      if ( self.displayAnchor.length === 0 ) { self.displayAnchor = $.mobile.activePage; }
+      
+      self.dialogPage = $("<div data-role='dialog' data-theme='" + o.themeDialog + "'><div data-role='header'></div><div data-role='content'></div></div>");
+      self.sdAllContent = self.dialogPage.find('[data-role=content]');
+      
+      content.appendTo(self.sdAllContent);
+      
+      self.sdIntContent = self.sdAllContent.find('.ui-simpledialog-container');
+      self.sdIntContent.css('width', o.width);
+      
+      if ( o.headerText !== false || o.headerClose !== false ) {
+	self.sdHeader = $('<div style="margin-bottom: 4px;" class="ui-header ui-bar-'+o.themeHeader+'"></div>');
+	if ( o.headerClose === true ) {
+	  $("<a class='ui-btn-left' rel='close' href='#'>Close</a>").appendTo(self.sdHeader).buttonMarkup({ theme  : o.themeHeader, icon   : 'delete', iconpos: 'notext', corners: true, shadow : true });
+	}
+	$('<h1 class="ui-title">'+((o.headerText !== false)?o.headerText:'')+'</h1>').appendTo(self.sdHeader);
+	self.sdHeader.appendTo(self.sdIntContent);
+      }
+      
+      if ( o.mode === 'blank' ) {
+	if ( o.blankContent === true ) {
+	  if ( o.blankContentAdopt === true ) {
+	    o.blankContent = self.element.children();
+	  } else {
+	    o.blankContent = self.element.html();
+	  }
+	}
+	$(o.blankContent).appendTo(self.sdIntContent);
+      } else if ( o.mode === 'button' ) {
+	self._makeButtons().appendTo(self.sdIntContent);
+      }
+      
+      self.sdIntContent.appendTo(self.displayAnchor.parent());
+      
+      self.dialogPage.appendTo( $.mobile.pageContainer )
+	.page().css('minHeight', '0px').css('zIndex', o.zindex);
+      
+      if ( o.animate === true ) { self.dialogPage.addClass(o.transition); }
+      
+      self.screen = $("<div>", {'class':'ui-simpledialog-screen ui-simpledialog-hidden'})
+	.css('z-index', (o.zindex-1))
+	.appendTo(self.displayAnchor.parent())
+	.bind(o.clickEvent, function(event){
+	  if ( !o.forceInput ) {
+	    self.close();
+	  }
+	  event.preventDefault();
+	});
+
+      if ( o.showModal ) { self.screen.addClass('ui-simpledialog-screen-modal'); }
+      
+      $(document).bind('simpledialog.'+self.internalID, {widget:self}, function(e,p) { self._eventHandler(e,p); });
+    },
+    _makeButtons: function () {
+      var self = this,
+      o = self.options,
+      buttonHTML = $('<div></div>'),
+      pickerInput = $("<div class='ui-simpledialog-controls'><input class='ui-simpledialog-input ui-input-text ui-shadow-inset ui-corner-all ui-body-"+o.themeInput+"' type='"+((o.buttonPassword===true)?"password":"text")+"' value='"+((o.buttonInputDefault!==false)?o.buttonInputDefault.replace( '"', "&#34;" ).replace( "'", "&#39;" ):"")+"' name='pickin' /></div>"),
+      pickerChoice = $("<div>", { "class":'ui-simpledialog-controls' });
+      
+      
+      if ( o.buttonPrompt !== false ) {
+	self.buttonPromptText = $("<p class='ui-simpledialog-subtitle'>"+o.buttonPrompt+"</p>").appendTo(buttonHTML);
+      }
+      
+      if ( o.buttonInput !== false ) {
+	$.mobile.sdLastInput = "";
+	pickerInput.appendTo(buttonHTML);
+	pickerInput.find('input').bind('change', function () {
+	  $.mobile.sdLastInput = pickerInput.find('input').first().val();
+	  self.thisInput = pickerInput.find('input').first().val();
+	});
+      }
+      
+      pickerChoice.appendTo(buttonHTML);
+      
+      self.butObj = [];
+      
+      $.each(o.buttons, function(name, props) {
+	props = $.isFunction( props ) ? { click: props } : props;
+	props = $.extend({
+	  text   : name,
+	  id     : name + self.internalID,
+	  theme  : o.themeButtonDefault,
+	  icon   : 'check',
+	  iconpos: 'left',
+	  corners: 'true',
+	  shadow : 'true',
+	  args   : [],
+	  close  : true
+	}, props);
+	
+	self.butObj.push($("<a href='#'>"+name+"</a>")
+			 .appendTo(pickerChoice)
+			 .attr('id', props.id)
+			 .buttonMarkup({
+			   theme  : props.theme,
+			   icon   : props.icon,
+			   iconpos: props.iconpos,
+			   corners: props.corners,
+			   shadow : props.shadow
+			 }).unbind("vclick click")
+			 .bind(o.clickEvent, function() {
+			   if ( o.buttonInput ) { self.sdIntContent.find('input [name=pickin]').trigger('change'); }
+			   var returnValue = props.click.apply(self, $.merge(arguments, props.args));
+			   if ( returnValue !== false && props.close === true ) {
+			     self.close();
+			   }
+			 })
+			);
+      });
+      
+      return buttonHTML;
+    },
+    _getCoords: function(widget) {
+      var self = widget,
+      docWinWidth   = $.mobile.activePage.width(),
+      docWinHighOff = $(window).scrollTop(),
+      docWinHigh    = $(window).height(),
+      diaWinWidth   = widget.sdIntContent.innerWidth(),
+      diaWinHigh    = widget.sdIntContent.outerHeight(),
+      
+      coords        = {
+	'high'    : $(window).height(),
+	'width'   : $.mobile.activePage.width(),
+	'fullTop' : $(window).scrollTop(),
+	'fullLeft': $(window).scrollLeft(),
+	'winTop'  : docWinHighOff + ((widget.options.top !== false) ? widget.options.top : (( docWinHigh / 2 ) - ( diaWinHigh / 2 ) )),
+	'winLeft' : ((widget.options.left !== false) ? widget.options.left : (( docWinWidth / 2 ) - ( diaWinWidth / 2 ) ))
+      };
+      
+      if ( coords.winTop < 45 ) { coords.winTop = 45; }
+      
+      return coords;
+    },
+    _orientChange: function(e) {
+      var self = e.data.widget,
+      o = e.data.widget.options,
+      coords = e.data.widget._getCoords(e.data.widget);
+      
+      e.stopPropagation();
+      
+      if ( self.isDialog === true ) {
+	return true;
+      } else {
+	if ( o.fullScreen === true && ( coords.width < 400 || o.fullScreenForce === true ) ) {
+	  self.sdIntContent.css({'border': 'none', 'position': 'absolute', 'top': coords.fullTop, 'left': coords.fullLeft, 'height': coords.high, 'width': coords.width, 'maxWidth': coords.width }).removeClass('ui-simpledialog-hidden');
+	} else {
+	  self.sdIntContent.css({'position': 'absolute', 'top': coords.winTop, 'left': coords.winLeft}).removeClass('ui-simpledialog-hidden');
+	}
+      }
+    },
+    repos: function() {
+      var bsEvent = { data: {widget:this}, stopPropagation: function () { return true; }};
+      this._orientChange(bsEvent);
+    },
+    open: function() {
+      var self = this,
+      o = this.options,
+      coords = this._getCoords(this);
+      
+      self.sdAllContent.find('.ui-btn-active').removeClass('ui-btn-active');
+      self.sdIntContent.delegate('[rel=close]', o.clickEvent, function (e) { e.preventDefault(); self.close(); });
+      
+      if ( ( o.dialogAllow === true && coords.width < 400 ) || o.dialogForce ) {
+	self.isDialog = true;
+	
+	if ( o.mode === 'blank' ) { // Custom selects do not play well with dialog mode - so, we turn them off.
+	  self.sdIntContent.find('select').each(function () {
+	    $(this).jqmData('nativeMenu', true);
+	  });
+	}
+	
+	self.displayAnchor.parent().unbind("pagehide.remove");
+	self.sdAllContent.append(self.sdIntContent);
+	self.sdAllContent.trigger('create');
+	if ( o.headerText !== false ) {
+	  self.sdHeader.find('h1').appendTo(self.dialogPage.find('[data-role=header]'));
+	  self.sdIntContent.find('.ui-header').empty().removeClass();
+	}
+	if ( o.headerClose === true ) {
+	  self.dialogPage.find('.ui-header a').bind('click', function () {
+	    setTimeout("$.mobile.sdCurrentDialog.destroy();", 1000);
+	  });
+	} else {
+	  self.dialogPage.find('.ui-header a').remove();
+	}
+	
+	self.sdIntContent.removeClass().css({'top': 'auto', 'width': 'auto', 'left': 'auto', 'marginLeft': 'auto', 'marginRight': 'auto', 'zIndex': o.zindex});
+	$.mobile.changePage(self.dialogPage, {'transition': (o.animate === true) ? o.transition : 'none'});
+      } else {
+	self.isDialog = false;
+	self.selects = [];
+	
+	if ( o.fullScreen === false ) {
+	  if ( o.showModal === true && o.animate === true ) { self.screen.fadeIn('slow'); }
+	  else { self.screen.removeClass('ui-simpledialog-hidden'); }
+	}
+	
+	self.sdIntContent.addClass('ui-overlay-shadow in').css('zIndex', o.zindex).trigger('create');
+	
+	if ( o.fullScreen === true && ( coords.width < 400 || o.fullScreenForce === true ) ) {
+	  self.sdIntContent.removeClass('ui-simpledialog-container').css({'border': 'none', 'position': 'absolute', 'top': coords.fullTop, 'left': coords.fullLeft, 'height': coords.high, 'width': coords.width, 'maxWidth': coords.width }).removeClass('ui-simpledialog-hidden');
+	} else {
+	  self.sdIntContent.css({'position': 'absolute', 'top': coords.winTop, 'left': coords.winLeft}).removeClass('ui-simpledialog-hidden');
+	}
+	
+	$(document).bind('orientationchange.simpledialog', {widget:self}, function(e) { self._orientChange(e); });
+	if ( o.resizeListener === true ) {
+	  $(window).bind('resize.simpledialog', {widget:self}, function (e) { self._orientChange(e); });
+	}
+      }
+      if ( $.isFunction(o.callbackOpen) ) {
+	o.callbackOpen.apply(self, o.callbackOpenArgs);
+      }
+    },
+    close: function() {
+      var self = this, o = this.options, retty;
+      
+      if ( $.isFunction(self.options.callbackClose) ) {
+	retty = self.options.callbackClose.apply(self, self.options.callbackCloseArgs);
+	if ( retty === false ) { return false; }
+      }
+      
+      if ( self.isDialog ) {
+	$(self.dialogPage).dialog('close');
+	self.sdIntContent.addClass('ui-simpledialog-hidden');
+	self.sdIntContent.appendTo(self.displayAnchor.parent());
+	if ( $.mobile.activePage.jqmData("page").options.domCache != true && $.mobile.activePage.is(":jqmData(external-page='true')") ) {
+	  $.mobile.activePage.bind("pagehide.remove", function () {
+	    $(this).remove();
+	  });
+	}
+      } else {
+	if ( self.options.showModal === true && self.options.animate === true ) {
+	  self.screen.fadeOut('slow');
+	} else {
+	  self.screen.addClass('ui-simpledialog-hidden');
+	}
+	self.sdIntContent.addClass('ui-simpledialog-hidden').removeClass('in');
+	$(document).unbind('orientationchange.simpledialog');
+	if ( self.options.resizeListener === true ) { $(window).unbind('resize.simpledialog'); }
+      }
+      
+      if ( o.mode === 'blank' && o.blankContent !== false && o.blankContentAdopt === true ) {
+	self.element.append(o.blankContent);
+	o.blankContent = true;
+      }
+      
+      if ( self.isDialog === true || self.options.animate === true ) {
+	setTimeout(function(that) { return function () { that.destroy(); };}(self), 1000);
+      } else {
+	self.destroy();
+      }
+    },
+    destroy: function() {
+      var self = this,
+      ele = self.element;
+      
+      if ( self.options.mode === 'blank' ) {
+	$.mobile.sdCurrentDialog.sdIntContent.find('select').each(function() {
+	  if ( $(this).data('nativeMenu') == false ) {
+	    $(this).data('selectmenu').menuPage.remove();
+	    $(this).data('selectmenu').screen.remove();
+	    $(this).data('selectmenu').listbox.remove();
+	  }
+	});
+      }
+      
+      $(self.sdIntContent).remove();
+      $(self.dialogPage).remove();
+      $(self.screen).remove();
+      $(document).unbind('simpledialog.'+self.internalID);
+      delete $.mobile.sdCurrentDialog;
+      $.Widget.prototype.destroy.call(self);
+      if ( self.options.safeNuke === true && $(ele).parents().length === 0 && $(ele).contents().length === 0 ) {
+	ele.remove();
+      }
+    },
+    updateBlank: function (newHTML) {
+      var self = this,
+      o = this.options;
+      
+      self.sdIntContent.empty();
+      
+      if ( o.headerText !== false || o.headerClose !== false ) {
+	self.sdHeader = $('<div class="ui-header ui-bar-'+o.themeHeader+'"></div>');
+	if ( o.headerClose === true ) {
+	  $("<a class='ui-btn-left' rel='close' href='#'>Close</a>").appendTo(self.sdHeader).buttonMarkup({ theme  : o.themeHeader, icon   : 'delete', iconpos: 'notext', corners: true, shadow : true });
+	}
+	$('<h1 class="ui-title">'+((o.headerText !== false)?o.headerText:'')+'</h1>').appendTo(self.sdHeader);
+	self.sdHeader.appendTo(self.sdIntContent);
+      }
+      
+      $(newHTML).appendTo(self.sdIntContent);
+      self.sdIntContent.trigger('create');
+      $(document).trigger('orientationchange.simpledialog');
+    },
+    _init: function() {
+      this.open();
+    }
+  });
+})( jQuery );
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/slimbox2.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,244 @@
+/*!
+  Slimbox v2.04 - The ultimate lightweight Lightbox clone for jQuery
+  (c) 2007-2010 Christophe Beyls <http://www.digitalia.be>
+  MIT-style license.
+*/
+
+(function($) {
+
+  // Global variables, accessible to Slimbox only
+  var win = $(window), options, images, activeImage = -1, activeURL, prevImage, nextImage, compatibleOverlay, middle, centerWidth, centerHeight,
+  ie6 = !window.XMLHttpRequest, hiddenElements = [], documentElement = document.documentElement,
+
+  // Preload images
+  preload = {}, preloadPrev = new Image(), preloadNext = new Image(),
+
+  // DOM elements
+  overlay, center, image, sizer, prevLink, nextLink, bottomContainer, bottom, caption, number;
+
+  /*
+    Initialization
+  */
+
+  $(function() {
+    // Append the Slimbox HTML code at the bottom of the document
+    $("body").append(
+      $([
+	overlay = $('<div id="lbOverlay" />')[0],
+	center = $('<div id="lbCenter" />')[0],
+	bottomContainer = $('<div id="lbBottomContainer" />')[0]
+      ]).css("display", "none")
+    );
+
+    image = $('<div id="lbImage" />').appendTo(center).append(
+      sizer = $('<div style="position: relative;" />').append([
+	prevLink = $('<a id="lbPrevLink" href="#" />').click(previous)[0],
+	nextLink = $('<a id="lbNextLink" href="#" />').click(next)[0]
+      ])[0]
+    )[0];
+
+    bottom = $('<div id="lbBottom" />').appendTo(bottomContainer).append([
+      $('<a id="lbCloseLink" href="#" />').add(overlay).click(close)[0],
+      caption = $('<div id="lbCaption" />')[0],
+      number = $('<div id="lbNumber" />')[0],
+      $('<div style="clear: both;" />')[0]
+    ])[0];
+  });
+
+
+  /*
+    API
+  */
+
+  // Open Slimbox with the specified parameters
+  $.slimbox = function(_images, startImage, _options) {
+    options = $.extend({
+      loop: false,				// Allows one to navigate between first and last images
+      overlayOpacity: 0.8,			// 1 is opaque, 0 is completely transparent (change the color in the CSS file)
+      overlayFadeDuration: 400,		// Duration of the overlay fade-in and fade-out animations (in milliseconds)
+      resizeDuration: 400,			// Duration of each of the box resize animations (in milliseconds)
+      resizeEasing: "swing",			// "swing" is jQuery's default easing
+      initialWidth: 250,			// Initial width of the box (in pixels)
+      initialHeight: 250,			// Initial height of the box (in pixels)
+      imageFadeDuration: 400,			// Duration of the image fade-in animation (in milliseconds)
+      captionAnimationDuration: 400,		// Duration of the caption animation (in milliseconds)
+      counterText: "Image {x} of {y}",	// Translate or change as you wish, or set it to false to disable counter text for image groups
+      closeKeys: [27, 88, 67],		// Array of keycodes to close Slimbox, default: Esc (27), 'x' (88), 'c' (67)
+      previousKeys: [37, 80],			// Array of keycodes to navigate to the previous image, default: Left arrow (37), 'p' (80)
+      nextKeys: [39, 78]			// Array of keycodes to navigate to the next image, default: Right arrow (39), 'n' (78)
+    }, _options);
+
+    // The function is called for a single image, with URL and Title as first two arguments
+    if (typeof _images == "string") {
+      _images = [[_images, startImage]];
+      startImage = 0;
+    }
+
+    middle = win.scrollTop() + (win.height() / 2);
+    centerWidth = options.initialWidth;
+    centerHeight = options.initialHeight;
+    $(center).css({top: Math.max(0, middle - (centerHeight / 2)), width: centerWidth, height: centerHeight, marginLeft: -centerWidth/2}).show();
+    compatibleOverlay = ie6 || (overlay.currentStyle && (overlay.currentStyle.position != "fixed"));
+    if (compatibleOverlay) overlay.style.position = "absolute";
+    $(overlay).css("opacity", options.overlayOpacity).fadeIn(options.overlayFadeDuration);
+    position();
+    setup(1);
+
+    images = _images;
+    options.loop = options.loop && (images.length > 1);
+    return changeImage(startImage);
+  };
+
+  /*
+    options:	Optional options object, see jQuery.slimbox()
+    linkMapper:	Optional function taking a link DOM element and an index as arguments and returning an array containing 2 elements:
+    the image URL and the image caption (may contain HTML)
+    linksFilter:	Optional function taking a link DOM element and an index as arguments and returning true if the element is part of
+    the image collection that will be shown on click, false if not. "this" refers to the element that was clicked.
+    This function must always return true when the DOM element argument is "this".
+  */
+  $.fn.slimbox = function(_options, linkMapper, linksFilter) {
+    linkMapper = linkMapper || function(el) {
+      return [el.href, el.title];
+    };
+
+    linksFilter = linksFilter || function() {
+      return true;
+    };
+
+    var links = this;
+
+    return links.unbind("click").click(function() {
+      // Build the list of images that will be displayed
+      var link = this, startIndex = 0, filteredLinks, i = 0, length;
+      filteredLinks = $.grep(links, function(el, i) {
+	return linksFilter.call(link, el, i);
+      });
+
+      // We cannot use jQuery.map() because it flattens the returned array
+      for (length = filteredLinks.length; i < length; ++i) {
+	if (filteredLinks[i] == link) startIndex = i;
+	filteredLinks[i] = linkMapper(filteredLinks[i], i);
+      }
+
+      return $.slimbox(filteredLinks, startIndex, _options);
+    });
+  };
+
+
+  /*
+    Internal functions
+  */
+
+  function position() {
+    var l = win.scrollLeft(), w = win.width();
+    $([center, bottomContainer]).css("left", l + (w / 2));
+    if (compatibleOverlay) $(overlay).css({left: l, top: win.scrollTop(), width: w, height: win.height()});
+  }
+
+  function setup(open) {
+    if (open) {
+      $("object").add(ie6 ? "select" : "embed").each(function(index, el) {
+	hiddenElements[index] = [el, el.style.visibility];
+	el.style.visibility = "hidden";
+      });
+    } else {
+      $.each(hiddenElements, function(index, el) {
+	el[0].style.visibility = el[1];
+      });
+      hiddenElements = [];
+    }
+    var fn = open ? "bind" : "unbind";
+    win[fn]("scroll resize", position);
+    $(document)[fn]("keydown", keyDown);
+  }
+
+  function keyDown(event) {
+    var code = event.keyCode, fn = $.inArray;
+    // Prevent default keyboard action (like navigating inside the page)
+    return (fn(code, options.closeKeys) >= 0) ? close()
+      : (fn(code, options.nextKeys) >= 0) ? next()
+      : (fn(code, options.previousKeys) >= 0) ? previous()
+      : false;
+  }
+
+  function previous() {
+    return changeImage(prevImage);
+  }
+
+  function next() {
+    return changeImage(nextImage);
+  }
+
+  function changeImage(imageIndex) {
+    if (imageIndex >= 0) {
+      activeImage = imageIndex;
+      activeURL = images[activeImage][0];
+      prevImage = (activeImage || (options.loop ? images.length : 0)) - 1;
+      nextImage = ((activeImage + 1) % images.length) || (options.loop ? 0 : -1);
+
+      stop();
+      center.className = "lbLoading";
+
+      preload = new Image();
+      preload.onload = animateBox;
+      preload.src = activeURL;
+    }
+
+    return false;
+  }
+
+  function animateBox() {
+    center.className = "";
+    $(image).css({backgroundImage: "url(" + activeURL + ")", visibility: "hidden", display: "" });
+    $(sizer).width(preload.width);
+    $([sizer, prevLink, nextLink]).height(preload.height);
+
+    $(caption).html(images[activeImage][1] || "");
+    $(number).html((((images.length > 1) && options.counterText) || "").replace(/{x}/, activeImage + 1).replace(/{y}/, images.length));
+
+    if (prevImage >= 0) preloadPrev.src = images[prevImage][0];
+    if (nextImage >= 0) preloadNext.src = images[nextImage][0];
+
+    centerWidth = image.offsetWidth;
+    centerHeight = image.offsetHeight;
+    var top = Math.max(0, middle - (centerHeight / 2));
+    if (center.offsetHeight != centerHeight) {
+      $(center).animate({height: centerHeight, top: top}, options.resizeDuration, options.resizeEasing);
+    }
+    if (center.offsetWidth != centerWidth) {
+      $(center).animate({width: centerWidth, marginLeft: -centerWidth/2}, options.resizeDuration, options.resizeEasing);
+    }
+    $(center).queue(function() {
+      $(bottomContainer).css({width: centerWidth, top: top + centerHeight, marginLeft: -centerWidth/2, visibility: "hidden", display: ""});
+      animateCaption();
+      $(image).css({display: "none", visibility: "", opacity: ""}).fadeIn(options.imageFadeDuration, animateCaption);
+    });
+  }
+
+  function animateCaption() {
+    if (prevImage >= 0) $(prevLink).show();
+    if (nextImage >= 0) $(nextLink).show();
+    $(bottom).css("marginTop", -bottom.offsetHeight).animate({marginTop: 0}, options.captionAnimationDuration);
+    bottomContainer.style.visibility = "";
+  }
+
+  function stop() {
+    preload.onload = null;
+    preload.src = preloadPrev.src = preloadNext.src = activeURL;
+    $([center, image, bottom]).stop(true);
+    $([prevLink, nextLink, image, bottomContainer]).hide();
+  }
+
+  function close() {
+    if (activeImage >= 0) {
+      stop();
+      activeImage = prevImage = nextImage = -1;
+      $(center).hide();
+      $(overlay).stop().fadeOut(options.overlayFadeDuration, setup);
+    }
+
+    return false;
+  }
+
+})(jQuery);
Binary file OrthancServer/OrthancExplorer/libs/slimbox2/closelabel.gif has changed
Binary file OrthancServer/OrthancExplorer/libs/slimbox2/loading.gif has changed
Binary file OrthancServer/OrthancExplorer/libs/slimbox2/nextlabel.gif has changed
Binary file OrthancServer/OrthancExplorer/libs/slimbox2/prevlabel.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/slimbox2/slimbox2-rtl.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,84 @@
+/* SLIMBOX */
+
+#lbOverlay {
+	position: fixed;
+	z-index: 9999;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	background-color: #000;
+	cursor: pointer;
+}
+
+#lbCenter, #lbBottomContainer {
+	position: absolute;
+	z-index: 9999;
+	overflow: hidden;
+	background-color: #fff;
+}
+
+.lbLoading {
+	background: #fff url(loading.gif) no-repeat center;
+}
+
+#lbImage {
+	position: absolute;
+	left: 0;
+	top: 0;
+	border: 10px solid #fff;
+	background-repeat: no-repeat;
+}
+
+#lbPrevLink, #lbNextLink {
+	display: block;
+	position: absolute;
+	top: 0;
+	width: 50%;
+	outline: none;
+}
+
+#lbPrevLink {
+	right: 0;
+}
+
+#lbPrevLink:hover {
+	background: transparent url(prevlabel.gif) no-repeat 100% 15%;
+}
+
+#lbNextLink {
+	left: 0;
+}
+
+#lbNextLink:hover {
+	background: transparent url(nextlabel.gif) no-repeat 0 15%;
+}
+
+#lbBottom {
+	font-family: Verdana, Arial, Geneva, Helvetica, sans-serif;
+	font-size: 10px;
+	color: #666;
+	line-height: 1.4em;
+	text-align: right;
+	border: 10px solid #fff;
+	border-top-style: none;
+	direction: rtl;
+}
+
+#lbCloseLink {
+	display: block;
+	float: left;
+	width: 66px;
+	height: 22px;
+	background: transparent url(closelabel.gif) no-repeat center;
+	margin: 5px 0;
+	outline: none;
+}
+
+#lbCaption, #lbNumber {
+	margin-left: 71px;
+}
+
+#lbCaption {
+	font-weight: bold;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/slimbox2/slimbox2.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,83 @@
+/* SLIMBOX */
+
+#lbOverlay {
+	position: fixed;
+	z-index: 9999;
+	left: 0;
+	top: 0;
+	width: 100%;
+	height: 100%;
+	background-color: #000;
+	cursor: pointer;
+}
+
+#lbCenter, #lbBottomContainer {
+	position: absolute;
+	z-index: 9999;
+	overflow: hidden;
+	background-color: #fff;
+}
+
+.lbLoading {
+	background: #fff url(loading.gif) no-repeat center;
+}
+
+#lbImage {
+	position: absolute;
+	left: 0;
+	top: 0;
+	border: 10px solid #fff;
+	background-repeat: no-repeat;
+}
+
+#lbPrevLink, #lbNextLink {
+	display: block;
+	position: absolute;
+	top: 0;
+	width: 50%;
+	outline: none;
+}
+
+#lbPrevLink {
+	left: 0;
+}
+
+#lbPrevLink:hover {
+	background: transparent url(prevlabel.gif) no-repeat 0 15%;
+}
+
+#lbNextLink {
+	right: 0;
+}
+
+#lbNextLink:hover {
+	background: transparent url(nextlabel.gif) no-repeat 100% 15%;
+}
+
+#lbBottom {
+	font-family: Verdana, Arial, Geneva, Helvetica, sans-serif;
+	font-size: 10px;
+	color: #666;
+	line-height: 1.4em;
+	text-align: left;
+	border: 10px solid #fff;
+	border-top-style: none;
+}
+
+#lbCloseLink {
+	display: block;
+	float: right;
+	width: 66px;
+	height: 22px;
+	background: transparent url(closelabel.gif) no-repeat center;
+	margin: 5px 0;
+	outline: none;
+}
+
+#lbCaption, #lbNumber {
+	margin-right: 71px;
+}
+
+#lbCaption {
+	font-weight: bold;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/libs/tree.jquery.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1837 @@
+// Generated by CoffeeScript 1.3.3
+
+/*
+Copyright 2012 Marco Braak
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+(function() {
+  var $, BorderDropHint, DragAndDropHandler, DragElement, FolderElement, GhostDropHint, JqTreeWidget, Json, MouseWidget, Node, NodeElement, Position, SaveStateHandler, SelectNodeHandler, SimpleWidget, Tree, html_escape, indexOf, toJson,
+    __slice = [].slice,
+    __hasProp = {}.hasOwnProperty,
+    __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+  $ = this.jQuery;
+
+  SimpleWidget = (function() {
+
+    SimpleWidget.prototype.defaults = {};
+
+    function SimpleWidget(el, options) {
+      this.$el = $(el);
+      this.options = $.extend({}, this.defaults, options);
+      this._init();
+    }
+
+    SimpleWidget.prototype.destroy = function() {
+      return this._deinit();
+    };
+
+    SimpleWidget.prototype._init = function() {
+      return null;
+    };
+
+    SimpleWidget.prototype._deinit = function() {
+      return null;
+    };
+
+    SimpleWidget.register = function(widget_class, widget_name) {
+      var callFunction, createWidget, destroyWidget, getDataKey;
+      getDataKey = function() {
+        return "simple_widget_" + widget_name;
+      };
+      createWidget = function($el, options) {
+        var data_key;
+        data_key = getDataKey();
+        $el.each(function() {
+          var widget;
+          widget = new widget_class(this, options);
+          if (!$.data(this, data_key)) {
+            return $.data(this, data_key, widget);
+          }
+        });
+        return $el;
+      };
+      destroyWidget = function($el) {
+        var data_key;
+        data_key = getDataKey();
+        return $el.each(function() {
+          var widget;
+          widget = $.data(this, data_key);
+          if (widget && (widget instanceof SimpleWidget)) {
+            widget.destroy();
+          }
+          return $.removeData(this, data_key);
+        });
+      };
+      callFunction = function($el, function_name, args) {
+        var result;
+        result = null;
+        $el.each(function() {
+          var widget, widget_function;
+          widget = $.data(this, getDataKey());
+          if (widget && (widget instanceof SimpleWidget)) {
+            widget_function = widget[function_name];
+            if (widget_function && (typeof widget_function === 'function')) {
+              return result = widget_function.apply(widget, args);
+            }
+          }
+        });
+        return result;
+      };
+      return $.fn[widget_name] = function() {
+        var $el, args, argument1, function_name, options;
+        argument1 = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+        $el = this;
+        if (argument1 === void 0 || typeof argument1 === 'object') {
+          options = argument1;
+          return createWidget($el, options);
+        } else if (typeof argument1 === 'string' && argument1[0] !== '_') {
+          function_name = argument1;
+          if (function_name === 'destroy') {
+            return destroyWidget($el);
+          } else {
+            return callFunction($el, function_name, args);
+          }
+        }
+      };
+    };
+
+    return SimpleWidget;
+
+  })();
+
+  this.SimpleWidget = SimpleWidget;
+
+  /*
+  This widget does the same a the mouse widget in jqueryui.
+  */
+
+
+  MouseWidget = (function(_super) {
+
+    __extends(MouseWidget, _super);
+
+    function MouseWidget() {
+      return MouseWidget.__super__.constructor.apply(this, arguments);
+    }
+
+    MouseWidget.is_mouse_handled = false;
+
+    MouseWidget.prototype._init = function() {
+      this.$el.bind('mousedown.mousewidget', $.proxy(this._mouseDown, this));
+      return this.is_mouse_started = false;
+    };
+
+    MouseWidget.prototype._deinit = function() {
+      var $document;
+      this.$el.unbind('mousedown.mousewidget');
+      $document = $(document);
+      $document.unbind('mousemove.mousewidget');
+      return $document.unbind('mouseup.mousewidget');
+    };
+
+    MouseWidget.prototype._mouseDown = function(e) {
+      var $document;
+      if (MouseWidget.is_mouse_handled) {
+        return;
+      }
+      if (!this.is_mouse_started) {
+        this._mouseUp(e);
+      }
+      if (e.which !== 1) {
+        return;
+      }
+      if (!this._mouseCapture(e)) {
+        return;
+      }
+      this.mouse_down_event = e;
+      $document = $(document);
+      $document.bind('mousemove.mousewidget', $.proxy(this._mouseMove, this));
+      $document.bind('mouseup.mousewidget', $.proxy(this._mouseUp, this));
+      e.preventDefault();
+      this.is_mouse_handled = true;
+      return true;
+    };
+
+    MouseWidget.prototype._mouseMove = function(e) {
+      if (this.is_mouse_started) {
+        this._mouseDrag(e);
+        return e.preventDefault();
+      }
+      this.is_mouse_started = this._mouseStart(this.mouse_down_event) !== false;
+      if (this.is_mouse_started) {
+        this._mouseDrag(e);
+      } else {
+        this._mouseUp(e);
+      }
+      return !this.is_mouse_started;
+    };
+
+    MouseWidget.prototype._mouseUp = function(e) {
+      var $document;
+      $document = $(document);
+      $document.unbind('mousemove.mousewidget');
+      $document.unbind('mouseup.mousewidget');
+      if (this.is_mouse_started) {
+        this.is_mouse_started = false;
+        this._mouseStop(e);
+      }
+      return false;
+    };
+
+    MouseWidget.prototype._mouseCapture = function(e) {
+      return true;
+    };
+
+    MouseWidget.prototype._mouseStart = function(e) {
+      return null;
+    };
+
+    MouseWidget.prototype._mouseDrag = function(e) {
+      return null;
+    };
+
+    MouseWidget.prototype._mouseStop = function(e) {
+      return null;
+    };
+
+    return MouseWidget;
+
+  })(SimpleWidget);
+
+  /*
+  Copyright 2012 Marco Braak
+  
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+  
+      http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  */
+
+
+  this.Tree = {};
+
+  $ = this.jQuery;
+
+  indexOf = function(array, item) {
+    var i, value, _i, _len;
+    if (array.indexOf) {
+      return array.indexOf(item);
+    } else {
+      for (i = _i = 0, _len = array.length; _i < _len; i = ++_i) {
+        value = array[i];
+        if (value === item) {
+          return i;
+        }
+      }
+      return -1;
+    }
+  };
+
+  this.Tree.indexOf = indexOf;
+
+  Json = {};
+
+  Json.escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
+
+  Json.meta = {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '"': '\\"',
+    '\\': '\\\\'
+  };
+
+  Json.quote = function(string) {
+    Json.escapable.lastIndex = 0;
+    if (Json.escapable.test(string)) {
+      return '"' + string.replace(Json.escapable, function(a) {
+        var c;
+        c = Json.meta[a];
+        return (typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4));
+      }) + '"';
+    } else {
+      return '"' + string + '"';
+    }
+  };
+
+  Json.str = function(key, holder) {
+    var i, k, partial, v, value, _i, _len;
+    value = holder[key];
+    switch (typeof value) {
+      case 'string':
+        return Json.quote(value);
+      case 'number':
+        if (isFinite(value)) {
+          return String(value);
+        } else {
+          return 'null';
+        }
+      case 'boolean':
+      case 'null':
+        return String(value);
+      case 'object':
+        if (!value) {
+          return 'null';
+        }
+        partial = [];
+        if (Object.prototype.toString.apply(value) === '[object Array]') {
+          for (i = _i = 0, _len = value.length; _i < _len; i = ++_i) {
+            v = value[i];
+            partial[i] = Json.str(i, value) || 'null';
+          }
+          return (partial.length === 0 ? '[]' : '[' + partial.join(',') + ']');
+        }
+        for (k in value) {
+          if (Object.prototype.hasOwnProperty.call(value, k)) {
+            v = Json.str(k, value);
+            if (v) {
+              partial.push(Json.quote(k) + ':' + v);
+            }
+          }
+        }
+        return (partial.length === 0 ? '{}' : '{' + partial.join(',') + '}');
+    }
+  };
+
+  toJson = function(value) {
+    return Json.str('', {
+      '': value
+    });
+  };
+
+  this.Tree.toJson = toJson;
+
+  html_escape = function(string) {
+    return ('' + string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;');
+  };
+
+  Position = {
+    getName: function(position) {
+      if (position === Position.BEFORE) {
+        return 'before';
+      } else if (position === Position.AFTER) {
+        return 'after';
+      } else if (position === Position.INSIDE) {
+        return 'inside';
+      } else {
+        return 'none';
+      }
+    }
+  };
+
+  Position.BEFORE = 1;
+
+  Position.AFTER = 2;
+
+  Position.INSIDE = 3;
+
+  Position.NONE = 4;
+
+  this.Tree.Position = Position;
+
+  Node = (function() {
+
+    function Node(o) {
+      this.setData(o);
+    }
+
+    Node.prototype.setData = function(o) {
+      var key, value;
+      if (typeof o !== 'object') {
+        this.name = o;
+      } else {
+        for (key in o) {
+          value = o[key];
+          if (key === 'label') {
+            this.name = value;
+          } else {
+            this[key] = value;
+          }
+        }
+      }
+      this.children = [];
+      return this.parent = null;
+    };
+
+    Node.prototype.initFromData = function(data) {
+      var addChildren, addNode,
+        _this = this;
+      addNode = function(node_data) {
+        _this.setData(node_data);
+        if (node_data.children) {
+          return addChildren(node_data.children);
+        }
+      };
+      addChildren = function(children_data) {
+        var child, node, _i, _len;
+        for (_i = 0, _len = children_data.length; _i < _len; _i++) {
+          child = children_data[_i];
+          node = new Node('');
+          node.initFromData(child);
+          _this.addChild(node);
+        }
+        return null;
+      };
+      addNode(data);
+      return null;
+    };
+
+    /*
+        Create tree from data.
+    
+        Structure of data is:
+        [
+            {
+                label: 'node1',
+                children: [
+                    { label: 'child1' },
+                    { label: 'child2' }
+                ]
+            },
+            {
+                label: 'node2'
+            }
+        ]
+    */
+
+
+    Node.prototype.loadFromData = function(data) {
+      var node, o, _i, _len;
+      this.children = [];
+      for (_i = 0, _len = data.length; _i < _len; _i++) {
+        o = data[_i];
+        node = new Node(o);
+        this.addChild(node);
+        if (typeof o === 'object' && o.children) {
+          node.loadFromData(o.children);
+        }
+      }
+      return null;
+    };
+
+    /*
+        Add child.
+    
+        tree.addChild(
+            new Node('child1')
+        );
+    */
+
+
+    Node.prototype.addChild = function(node) {
+      this.children.push(node);
+      return node._setParent(this);
+    };
+
+    /*
+        Add child at position. Index starts at 0.
+    
+        tree.addChildAtPosition(
+            new Node('abc'),
+            1
+        );
+    */
+
+
+    Node.prototype.addChildAtPosition = function(node, index) {
+      this.children.splice(index, 0, node);
+      return node._setParent(this);
+    };
+
+    Node.prototype._setParent = function(parent) {
+      this.parent = parent;
+      this.tree = parent.tree;
+      return this.tree.addNodeToIndex(this);
+    };
+
+    /*
+        Remove child.
+    
+        tree.removeChild(tree.children[0]);
+    */
+
+
+    Node.prototype.removeChild = function(node) {
+      this.children.splice(this.getChildIndex(node), 1);
+      return this.tree.removeNodeFromIndex(node);
+    };
+
+    /*
+        Get child index.
+    
+        var index = getChildIndex(node);
+    */
+
+
+    Node.prototype.getChildIndex = function(node) {
+      return $.inArray(node, this.children);
+    };
+
+    /*
+        Does the tree have children?
+    
+        if (tree.hasChildren()) {
+            //
+        }
+    */
+
+
+    Node.prototype.hasChildren = function() {
+      return this.children.length !== 0;
+    };
+
+    /*
+        Iterate over all the nodes in the tree.
+    
+        Calls callback with (node, level).
+    
+        The callback must return true to continue the iteration on current node.
+    
+        tree.iterate(
+            function(node, level) {
+               console.log(node.name);
+    
+               // stop iteration after level 2
+               return (level <= 2);
+            }
+        );
+    */
+
+
+    Node.prototype.iterate = function(callback) {
+      var _iterate,
+        _this = this;
+      _iterate = function(node, level) {
+        var child, result, _i, _len, _ref;
+        if (node.children) {
+          _ref = node.children;
+          for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+            child = _ref[_i];
+            result = callback(child, level);
+            if (_this.hasChildren() && result) {
+              _iterate(child, level + 1);
+            }
+          }
+          return null;
+        }
+      };
+      _iterate(this, 0);
+      return null;
+    };
+
+    /*
+        Move node relative to another node.
+    
+        Argument position: Position.BEFORE, Position.AFTER or Position.Inside
+    
+        // move node1 after node2
+        tree.moveNode(node1, node2, Position.AFTER);
+    */
+
+
+    Node.prototype.moveNode = function(moved_node, target_node, position) {
+      moved_node.parent.removeChild(moved_node);
+      if (position === Position.AFTER) {
+        return target_node.parent.addChildAtPosition(moved_node, target_node.parent.getChildIndex(target_node) + 1);
+      } else if (position === Position.BEFORE) {
+        return target_node.parent.addChildAtPosition(moved_node, target_node.parent.getChildIndex(target_node));
+      } else if (position === Position.INSIDE) {
+        return target_node.addChildAtPosition(moved_node, 0);
+      }
+    };
+
+    /*
+        Get the tree as data.
+    */
+
+
+    Node.prototype.getData = function() {
+      var getDataFromNodes,
+        _this = this;
+      getDataFromNodes = function(nodes) {
+        var data, k, node, tmp_node, v, _i, _len;
+        data = [];
+        for (_i = 0, _len = nodes.length; _i < _len; _i++) {
+          node = nodes[_i];
+          tmp_node = {};
+          for (k in node) {
+            v = node[k];
+            if ((k !== 'parent' && k !== 'children' && k !== 'element' && k !== 'tree') && Object.prototype.hasOwnProperty.call(node, k)) {
+              tmp_node[k] = v;
+            }
+          }
+          if (node.hasChildren()) {
+            tmp_node.children = getDataFromNodes(node.children);
+          }
+          data.push(tmp_node);
+        }
+        return data;
+      };
+      return getDataFromNodes(this.children);
+    };
+
+    Node.prototype.getNodeByName = function(name) {
+      var result;
+      result = null;
+      this.iterate(function(node) {
+        if (node.name === name) {
+          result = node;
+          return false;
+        } else {
+          return true;
+        }
+      });
+      return result;
+    };
+
+    Node.prototype.addAfter = function(node_info) {
+      var child_index, node;
+      if (!this.parent) {
+        return null;
+      } else {
+        node = new Node(node_info);
+        child_index = this.parent.getChildIndex(this);
+        this.parent.addChildAtPosition(node, child_index + 1);
+        return node;
+      }
+    };
+
+    Node.prototype.addBefore = function(node_info) {
+      var child_index, node;
+      if (!this.parent) {
+        return null;
+      } else {
+        node = new Node(node_info);
+        child_index = this.parent.getChildIndex(this);
+        return this.parent.addChildAtPosition(node, child_index);
+      }
+    };
+
+    Node.prototype.addParent = function(node_info) {
+      var child, new_parent, original_parent, _i, _len, _ref;
+      if (!this.parent) {
+        return null;
+      } else {
+        new_parent = new Node(node_info);
+        new_parent._setParent(this.tree);
+        original_parent = this.parent;
+        _ref = original_parent.children;
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          child = _ref[_i];
+          new_parent.addChild(child);
+        }
+        original_parent.children = [];
+        original_parent.addChild(new_parent);
+        return new_parent;
+      }
+    };
+
+    Node.prototype.remove = function() {
+      if (this.parent) {
+        this.parent.removeChild(this);
+        return this.parent = null;
+      }
+    };
+
+    Node.prototype.append = function(node_info) {
+      var node;
+      node = new Node(node_info);
+      this.addChild(node);
+      return node;
+    };
+
+    Node.prototype.prepend = function(node_info) {
+      var node;
+      node = new Node(node_info);
+      this.addChildAtPosition(node, 0);
+      return node;
+    };
+
+    return Node;
+
+  })();
+
+  Tree = (function(_super) {
+
+    __extends(Tree, _super);
+
+    function Tree(o) {
+      Tree.__super__.constructor.call(this, o, null, true);
+      this.id_mapping = {};
+      this.tree = this;
+    }
+
+    Tree.prototype.getNodeById = function(node_id) {
+      return this.id_mapping[node_id];
+    };
+
+    Tree.prototype.addNodeToIndex = function(node) {
+      if (node.id) {
+        return this.id_mapping[node.id] = node;
+      }
+    };
+
+    Tree.prototype.removeNodeFromIndex = function(node) {
+      if (node.id) {
+        return delete this.id_mapping[node.id];
+      }
+    };
+
+    return Tree;
+
+  })(Node);
+
+  this.Tree.Tree = Tree;
+
+  JqTreeWidget = (function(_super) {
+
+    __extends(JqTreeWidget, _super);
+
+    function JqTreeWidget() {
+      return JqTreeWidget.__super__.constructor.apply(this, arguments);
+    }
+
+    JqTreeWidget.prototype.defaults = {
+      autoOpen: false,
+      saveState: false,
+      dragAndDrop: false,
+      selectable: false,
+      onCanSelectNode: null,
+      onSetStateFromStorage: null,
+      onGetStateFromStorage: null,
+      onCreateLi: null,
+      onIsMoveHandle: null,
+      onCanMove: null,
+      onCanMoveTo: null,
+      autoEscape: true,
+      dataUrl: null
+    };
+
+    JqTreeWidget.prototype.toggle = function(node) {
+      if (node.hasChildren()) {
+        new FolderElement(node, this.element).toggle();
+      }
+      return this._saveState();
+    };
+
+    JqTreeWidget.prototype.getTree = function() {
+      return this.tree;
+    };
+
+    JqTreeWidget.prototype.selectNode = function(node, must_open_parents) {
+      return this.select_node_handler.selectNode(node, must_open_parents);
+    };
+
+    JqTreeWidget.prototype.getSelectedNode = function() {
+      return this.selected_node || false;
+    };
+
+    JqTreeWidget.prototype.toJson = function() {
+      return toJson(this.tree.getData());
+    };
+
+    JqTreeWidget.prototype.loadData = function(data, parent_node) {
+      var child, subtree, _i, _len, _ref;
+      if (!parent_node) {
+        this._initTree(data);
+      } else {
+        subtree = new Node('');
+        subtree._setParent(parent_node.tree);
+        subtree.loadFromData(data);
+        _ref = subtree.children;
+        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+          child = _ref[_i];
+          parent_node.addChild(child);
+        }
+        this._refreshElements(parent_node.parent);
+      }
+      if (this.is_dragging) {
+        return this.dnd_handler.refreshHitAreas();
+      }
+    };
+
+    JqTreeWidget.prototype.getNodeById = function(node_id) {
+      return this.tree.getNodeById(node_id);
+    };
+
+    JqTreeWidget.prototype.getNodeByName = function(name) {
+      return this.tree.getNodeByName(name);
+    };
+
+    JqTreeWidget.prototype.openNode = function(node, skip_slide) {
+      if (node.hasChildren()) {
+        new FolderElement(node, this.element).open(null, skip_slide);
+        return this._saveState();
+      }
+    };
+
+    JqTreeWidget.prototype.closeNode = function(node, skip_slide) {
+      if (node.hasChildren()) {
+        new FolderElement(node, this.element).close(skip_slide);
+        return this._saveState();
+      }
+    };
+
+    JqTreeWidget.prototype.isDragging = function() {
+      return this.is_dragging;
+    };
+
+    JqTreeWidget.prototype.refreshHitAreas = function() {
+      return this.dnd_handler.refreshHitAreas();
+    };
+
+    JqTreeWidget.prototype.addNodeAfter = function(new_node_info, existing_node) {
+      var new_node;
+      new_node = existing_node.addAfter(new_node_info);
+      this._refreshElements(existing_node.parent);
+      return new_node;
+    };
+
+    JqTreeWidget.prototype.addNodeBefore = function(new_node_info, existing_node) {
+      var new_node;
+      new_node = existing_node.addBefore(new_node_info);
+      this._refreshElements(existing_node.parent);
+      return new_node;
+    };
+
+    JqTreeWidget.prototype.addParentNode = function(new_node_info, existing_node) {
+      var new_node;
+      new_node = existing_node.addParent(new_node_info);
+      this._refreshElements(new_node.parent);
+      return new_node;
+    };
+
+    JqTreeWidget.prototype.removeNode = function(node) {
+      var parent;
+      parent = node.parent;
+      if (parent) {
+        node.remove();
+        return this._refreshElements(parent.parent);
+      }
+    };
+
+    JqTreeWidget.prototype.appendNode = function(new_node_info, parent_node) {
+      var is_already_root_node, node;
+      if (!parent_node) {
+        parent_node = this.tree;
+      }
+      is_already_root_node = parent_node.hasChildren();
+      node = parent_node.append(new_node_info);
+      if (is_already_root_node) {
+        this._refreshElements(parent_node);
+      } else {
+        this._refreshElements(parent_node.parent);
+      }
+      return node;
+    };
+
+    JqTreeWidget.prototype.prependNode = function(new_node_info, parent_node) {
+      var node;
+      if (!parent_node) {
+        parent_node = this.tree;
+      }
+      node = parent_node.prepend(new_node_info);
+      this._refreshElements(parent_node);
+      return node;
+    };
+
+    JqTreeWidget.prototype._init = function() {
+      JqTreeWidget.__super__._init.call(this);
+      this.element = this.$el;
+      this._initData();
+      this.element.click($.proxy(this._click, this));
+      return this.element.bind('contextmenu', $.proxy(this._contextmenu, this));
+    };
+
+    JqTreeWidget.prototype._deinit = function() {
+      this.element.empty();
+      this.element.unbind();
+      this.tree = null;
+      return JqTreeWidget.__super__._deinit.call(this);
+    };
+
+    JqTreeWidget.prototype._initData = function() {
+      var data_url,
+        _this = this;
+      if (this.options.data) {
+        return this._initTree(this.options.data);
+      } else {
+        data_url = this.options.dataUrl || this.element.data('url');
+        if (data_url) {
+          return $.ajax({
+            url: data_url,
+            cache: false,
+            success: function(response) {
+              var data;
+              if ($.isArray(response) || typeof response === 'object') {
+                data = response;
+              } else {
+                data = $.parseJSON(response);
+              }
+              return _this._initTree(data);
+            }
+          });
+        }
+      }
+    };
+
+    JqTreeWidget.prototype._initTree = function(data) {
+      var event;
+      this.tree = new Tree();
+      this.tree.loadFromData(data);
+      this.selected_node = null;
+      this.save_state_handler = new SaveStateHandler(this);
+      this.select_node_handler = new SelectNodeHandler(this);
+      this.dnd_handler = new DragAndDropHandler(this);
+      this._openNodes();
+      this._refreshElements();
+      this.select_node_handler.selectCurrentNode();
+      event = $.Event('tree.init');
+      return this.element.trigger(event);
+    };
+
+    JqTreeWidget.prototype._openNodes = function() {
+      var max_level;
+      if (this.options.saveState) {
+        if (this.save_state_handler.restoreState()) {
+          return;
+        }
+      }
+      if (this.options.autoOpen === false) {
+        return;
+      } else if (this.options.autoOpen === true) {
+        max_level = -1;
+      } else {
+        max_level = parseInt(this.options.autoOpen);
+      }
+      return this.tree.iterate(function(node, level) {
+        node.is_open = true;
+        return level !== max_level;
+      });
+    };
+
+    JqTreeWidget.prototype._refreshElements = function(from_node) {
+      var $element, createFolderLi, createLi, createNodeLi, createUl, doCreateDomElements, escapeIfNecessary, is_root_node, node_element,
+        _this = this;
+      if (from_node == null) {
+        from_node = null;
+      }
+      escapeIfNecessary = function(value) {
+        if (_this.options.autoEscape) {
+          return html_escape(value);
+        } else {
+          return value;
+        }
+      };
+      createUl = function(is_root_node) {
+        var class_string;
+        if (is_root_node) {
+          class_string = ' class="tree"';
+        } else {
+          class_string = '';
+        }
+        return $("<ul" + class_string + "></ul>");
+      };
+      createLi = function(node) {
+        var $li;
+        if (node.hasChildren()) {
+          $li = createFolderLi(node);
+        } else {
+          $li = createNodeLi(node);
+        }
+        if (_this.options.onCreateLi) {
+          _this.options.onCreateLi(node, $li);
+        }
+        return $li;
+      };
+      createNodeLi = function(node) {
+        var escaped_name;
+        escaped_name = escapeIfNecessary(node.name);
+        return $("<li><div><span class=\"title\">" + escaped_name + "</span></div></li>");
+      };
+      createFolderLi = function(node) {
+        var button_class, escaped_name, folder_class, getButtonClass, getFolderClass;
+        getButtonClass = function() {
+          var classes;
+          classes = ['toggler'];
+          if (!node.is_open) {
+            classes.push('closed');
+          }
+          return classes.join(' ');
+        };
+        getFolderClass = function() {
+          var classes;
+          classes = ['folder'];
+          if (!node.is_open) {
+            classes.push('closed');
+          }
+          return classes.join(' ');
+        };
+        button_class = getButtonClass();
+        folder_class = getFolderClass();
+        escaped_name = escapeIfNecessary(node.name);
+        return $("<li class=\"" + folder_class + "\"><div><a class=\"" + button_class + "\">&raquo;</a><span class=\"title\">" + escaped_name + "</span></div></li>");
+      };
+      doCreateDomElements = function($element, children, is_root_node, is_open) {
+        var $li, $ul, child, _i, _len;
+        $ul = createUl(is_root_node);
+        $element.append($ul);
+        for (_i = 0, _len = children.length; _i < _len; _i++) {
+          child = children[_i];
+          $li = createLi(child);
+          $ul.append($li);
+          child.element = $li[0];
+          $li.data('node', child);
+          if (child.hasChildren()) {
+            doCreateDomElements($li, child.children, false, child.is_open);
+          }
+        }
+        return null;
+      };
+      if (from_node && from_node.parent) {
+        is_root_node = false;
+        node_element = this._getNodeElementForNode(from_node);
+        node_element.getUl().remove();
+        $element = node_element.$element;
+      } else {
+        from_node = this.tree;
+        $element = this.element;
+        $element.empty();
+        is_root_node = true;
+      }
+      return doCreateDomElements($element, from_node.children, is_root_node, is_root_node);
+    };
+
+    JqTreeWidget.prototype._click = function(e) {
+      var $target, event, node, node_element;
+      if (e.ctrlKey) {
+        return;
+      }
+      $target = $(e.target);
+      if ($target.is('.toggler')) {
+        node_element = this._getNodeElement($target);
+        if (node_element && node_element.node.hasChildren()) {
+          node_element.toggle();
+          this._saveState();
+          e.preventDefault();
+          return e.stopPropagation();
+        }
+      } else if ($target.is('div') || $target.is('span')) {
+        node = this._getNode($target);
+        if (node) {
+          if ((!this.options.onCanSelectNode) || this.options.onCanSelectNode(node)) {
+            this.selectNode(node);
+            event = $.Event('tree.click');
+            event.node = node;
+            return this.element.trigger(event);
+          }
+        }
+      }
+    };
+
+    JqTreeWidget.prototype._getNode = function($element) {
+      var $li;
+      $li = $element.closest('li');
+      if ($li.length === 0) {
+        return null;
+      } else {
+        return $li.data('node');
+      }
+    };
+
+    JqTreeWidget.prototype._getNodeElementForNode = function(node) {
+      if (node.hasChildren()) {
+        return new FolderElement(node, this.element);
+      } else {
+        return new NodeElement(node, this.element);
+      }
+    };
+
+    JqTreeWidget.prototype._getNodeElement = function($element) {
+      var node;
+      node = this._getNode($element);
+      if (node) {
+        return this._getNodeElementForNode(node);
+      } else {
+        return null;
+      }
+    };
+
+    JqTreeWidget.prototype._contextmenu = function(e) {
+      var $div, event, node;
+      $div = $(e.target).closest('ul.tree div');
+      if ($div.length) {
+        node = this._getNode($div);
+        if (node) {
+          e.preventDefault();
+          e.stopPropagation();
+          event = $.Event('tree.contextmenu');
+          event.node = node;
+          event.click_event = e;
+          this.element.trigger(event);
+          return false;
+        }
+      }
+    };
+
+    JqTreeWidget.prototype._saveState = function() {
+      if (this.options.saveState) {
+        return this.save_state_handler.saveState();
+      }
+    };
+
+    JqTreeWidget.prototype._mouseCapture = function(event) {
+      if (this.options.dragAndDrop) {
+        return this.dnd_handler.mouseCapture(event);
+      } else {
+        return false;
+      }
+    };
+
+    JqTreeWidget.prototype._mouseStart = function(event) {
+      if (this.options.dragAndDrop) {
+        return this.dnd_handler.mouseStart(event);
+      } else {
+        return false;
+      }
+    };
+
+    JqTreeWidget.prototype._mouseDrag = function(event) {
+      if (this.options.dragAndDrop) {
+        return this.dnd_handler.mouseDrag(event);
+      } else {
+        return false;
+      }
+    };
+
+    JqTreeWidget.prototype._mouseStop = function() {
+      if (this.options.dragAndDrop) {
+        return this.dnd_handler.mouseStop();
+      } else {
+        return false;
+      }
+    };
+
+    JqTreeWidget.prototype.testGenerateHitAreas = function(moving_node) {
+      this.dnd_handler.current_item = this._getNodeElementForNode(moving_node);
+      this.dnd_handler.generateHitAreas();
+      return this.dnd_handler.hit_areas;
+    };
+
+    return JqTreeWidget;
+
+  })(MouseWidget);
+
+  SimpleWidget.register(JqTreeWidget, 'tree');
+
+  GhostDropHint = (function() {
+
+    function GhostDropHint(node, $element, position) {
+      this.$element = $element;
+      this.node = node;
+      this.$ghost = $('<li class="ghost"><span class="circle"></span><span class="line"></span></li>');
+      if (position === Position.AFTER) {
+        this.moveAfter();
+      } else if (position === Position.BEFORE) {
+        this.moveBefore();
+      } else if (position === Position.INSIDE) {
+        if (node.hasChildren() && node.is_open) {
+          this.moveInsideOpenFolder();
+        } else {
+          this.moveInside();
+        }
+      }
+    }
+
+    GhostDropHint.prototype.remove = function() {
+      return this.$ghost.remove();
+    };
+
+    GhostDropHint.prototype.moveAfter = function() {
+      return this.$element.after(this.$ghost);
+    };
+
+    GhostDropHint.prototype.moveBefore = function() {
+      return this.$element.before(this.$ghost);
+    };
+
+    GhostDropHint.prototype.moveInsideOpenFolder = function() {
+      return $(this.node.children[0].element).before(this.$ghost);
+    };
+
+    GhostDropHint.prototype.moveInside = function() {
+      this.$element.after(this.$ghost);
+      return this.$ghost.addClass('inside');
+    };
+
+    return GhostDropHint;
+
+  })();
+
+  BorderDropHint = (function() {
+
+    function BorderDropHint($element) {
+      var $div, width;
+      $div = $element.children('div');
+      width = $element.width() - 4;
+      this.$hint = $('<span class="border"></span>');
+      $div.append(this.$hint);
+      this.$hint.css({
+        width: width,
+        height: $div.height() - 4
+      });
+    }
+
+    BorderDropHint.prototype.remove = function() {
+      return this.$hint.remove();
+    };
+
+    return BorderDropHint;
+
+  })();
+
+  NodeElement = (function() {
+
+    function NodeElement(node, tree_element) {
+      this.init(node, tree_element);
+    }
+
+    NodeElement.prototype.init = function(node, tree_element) {
+      this.node = node;
+      this.tree_element = tree_element;
+      return this.$element = $(node.element);
+    };
+
+    NodeElement.prototype.getUl = function() {
+      return this.$element.children('ul:first');
+    };
+
+    NodeElement.prototype.getSpan = function() {
+      return this.$element.children('div').find('span.title');
+    };
+
+    NodeElement.prototype.getLi = function() {
+      return this.$element;
+    };
+
+    NodeElement.prototype.addDropHint = function(position) {
+      if (position === Position.INSIDE) {
+        return new BorderDropHint(this.$element);
+      } else {
+        return new GhostDropHint(this.node, this.$element, position);
+      }
+    };
+
+    NodeElement.prototype.select = function() {
+      return this.getLi().addClass('selected');
+    };
+
+    NodeElement.prototype.deselect = function() {
+      return this.getLi().removeClass('selected');
+    };
+
+    return NodeElement;
+
+  })();
+
+  FolderElement = (function(_super) {
+
+    __extends(FolderElement, _super);
+
+    function FolderElement() {
+      return FolderElement.__super__.constructor.apply(this, arguments);
+    }
+
+    FolderElement.prototype.toggle = function() {
+      if (this.node.is_open) {
+        return this.close();
+      } else {
+        return this.open();
+      }
+    };
+
+    FolderElement.prototype.open = function(on_finished, skip_slide) {
+      var doOpen,
+        _this = this;
+      if (!this.node.is_open) {
+        this.node.is_open = true;
+        this.getButton().removeClass('closed');
+        doOpen = function() {
+          var event;
+          _this.getLi().removeClass('closed');
+          if (on_finished) {
+            on_finished();
+          }
+          event = $.Event('tree.open');
+          event.node = _this.node;
+          return _this.tree_element.trigger(event);
+        };
+        if (skip_slide) {
+          return doOpen();
+        } else {
+          return this.getUl().slideDown('fast', doOpen);
+        }
+      }
+    };
+
+    FolderElement.prototype.close = function(skip_slide) {
+      var doClose,
+        _this = this;
+      if (this.node.is_open) {
+        this.node.is_open = false;
+        this.getButton().addClass('closed');
+        doClose = function() {
+          var event;
+          _this.getLi().addClass('closed');
+          event = $.Event('tree.close');
+          event.node = _this.node;
+          return _this.tree_element.trigger(event);
+        };
+        if (skip_slide) {
+          return doClose();
+        } else {
+          return this.getUl().slideUp('fast', doClose);
+        }
+      }
+    };
+
+    FolderElement.prototype.getButton = function() {
+      return this.$element.children('div').find('a.toggler');
+    };
+
+    FolderElement.prototype.addDropHint = function(position) {
+      if (!this.node.is_open && position === Position.INSIDE) {
+        return new BorderDropHint(this.$element);
+      } else {
+        return new GhostDropHint(this.node, this.$element, position);
+      }
+    };
+
+    return FolderElement;
+
+  })(NodeElement);
+
+  DragElement = (function() {
+
+    function DragElement(node, offset_x, offset_y, $tree) {
+      this.offset_x = offset_x;
+      this.offset_y = offset_y;
+      this.$element = $("<span class=\"title tree-dragging\">" + node.name + "</span>");
+      this.$element.css("position", "absolute");
+      $tree.append(this.$element);
+    }
+
+    DragElement.prototype.move = function(page_x, page_y) {
+      return this.$element.offset({
+        left: page_x - this.offset_x,
+        top: page_y - this.offset_y
+      });
+    };
+
+    DragElement.prototype.remove = function() {
+      return this.$element.remove();
+    };
+
+    return DragElement;
+
+  })();
+
+  SaveStateHandler = (function() {
+
+    function SaveStateHandler(tree_widget) {
+      this.tree_widget = tree_widget;
+    }
+
+    SaveStateHandler.prototype.saveState = function() {
+      if (this.tree_widget.options.onSetStateFromStorage) {
+        return this.tree_widget.options.onSetStateFromStorage(this.getState());
+      } else if (typeof localStorage !== "undefined" && localStorage !== null) {
+        return localStorage.setItem(this.getCookieName(), this.getState());
+      } else if ($.cookie) {
+        return $.cookie(this.getCookieName(), this.getState(), {
+          path: '/'
+        });
+      }
+    };
+
+    SaveStateHandler.prototype.restoreState = function() {
+      var state;
+      if (this.tree_widget.options.onGetStateFromStorage) {
+        state = this.tree_widget.options.onGetStateFromStorage();
+      } else if (typeof localStorage !== "undefined" && localStorage !== null) {
+        state = localStorage.getItem(this.getCookieName());
+      } else if ($.cookie) {
+        state = $.cookie(this.getCookieName(), {
+          path: '/'
+        });
+      } else {
+        state = null;
+      }
+      if (!state) {
+        return false;
+      } else {
+        this.setState(state);
+        return true;
+      }
+    };
+
+    SaveStateHandler.prototype.getState = function() {
+      var open_nodes, selected_node,
+        _this = this;
+      open_nodes = [];
+      this.tree_widget.tree.iterate(function(node) {
+        if (node.is_open && node.id && node.hasChildren()) {
+          open_nodes.push(node.id);
+        }
+        return true;
+      });
+      selected_node = '';
+      if (this.tree_widget.selected_node) {
+        selected_node = this.tree_widget.selected_node.id;
+      }
+      return toJson({
+        open_nodes: open_nodes,
+        selected_node: selected_node
+      });
+    };
+
+    SaveStateHandler.prototype.setState = function(state) {
+      var data, open_nodes, selected_node_id,
+        _this = this;
+      data = $.parseJSON(state);
+      if (data) {
+        open_nodes = data.open_nodes;
+        selected_node_id = data.selected_node;
+        return this.tree_widget.tree.iterate(function(node) {
+          if (node.id && node.hasChildren() && (indexOf(open_nodes, node.id) >= 0)) {
+            node.is_open = true;
+          }
+          if (selected_node_id && (node.id === selected_node_id)) {
+            _this.tree_widget.selected_node = node;
+          }
+          return true;
+        });
+      }
+    };
+
+    SaveStateHandler.prototype.getCookieName = function() {
+      if (typeof this.tree_widget.options.saveState === 'string') {
+        return this.tree_widget.options.saveState;
+      } else {
+        return 'tree';
+      }
+    };
+
+    return SaveStateHandler;
+
+  })();
+
+  SelectNodeHandler = (function() {
+
+    function SelectNodeHandler(tree_widget) {
+      this.tree_widget = tree_widget;
+    }
+
+    SelectNodeHandler.prototype.selectNode = function(node, must_open_parents) {
+      var parent;
+      if (this.tree_widget.options.selectable) {
+        if (this.tree_widget.selected_node) {
+          this.tree_widget._getNodeElementForNode(this.tree_widget.selected_node).deselect();
+          this.tree_widget.selected_node = null;
+        }
+        if (node) {
+          this.tree_widget._getNodeElementForNode(node).select();
+          this.tree_widget.selected_node = node;
+          if (must_open_parents) {
+            parent = this.tree_widget.selected_node.parent;
+            while (parent) {
+              if (!parent.is_open) {
+                this.tree_widget.openNode(parent, true);
+              }
+              parent = parent.parent;
+            }
+          }
+        }
+        if (this.tree_widget.options.saveState) {
+          return this.tree_widget.save_state_handler.saveState();
+        }
+      }
+    };
+
+    SelectNodeHandler.prototype.selectCurrentNode = function() {
+      var node_element;
+      if (this.tree_widget.selected_node) {
+        node_element = this.tree_widget._getNodeElementForNode(this.tree_widget.selected_node);
+        if (node_element) {
+          return node_element.select();
+        }
+      }
+    };
+
+    return SelectNodeHandler;
+
+  })();
+
+  DragAndDropHandler = (function() {
+
+    function DragAndDropHandler(tree_widget) {
+      this.tree_widget = tree_widget;
+      this.hovered_area = null;
+      this.$ghost = null;
+      this.hit_areas = [];
+      this.is_dragging = false;
+    }
+
+    DragAndDropHandler.prototype.mouseCapture = function(event) {
+      var $element, node_element;
+      $element = $(event.target);
+      if (this.tree_widget.options.onIsMoveHandle && !this.tree_widget.options.onIsMoveHandle($element)) {
+        return null;
+      }
+      node_element = this.tree_widget._getNodeElement($element);
+      if (node_element && this.tree_widget.options.onCanMove) {
+        if (!this.tree_widget.options.onCanMove(node_element.node)) {
+          node_element = null;
+        }
+      }
+      this.current_item = node_element;
+      return this.current_item !== null;
+    };
+
+    DragAndDropHandler.prototype.mouseStart = function(event) {
+      var offsetX, offsetY, _ref;
+      this.refreshHitAreas();
+      _ref = this.getOffsetFromEvent(event), offsetX = _ref[0], offsetY = _ref[1];
+      this.drag_element = new DragElement(this.current_item.node, offsetX, offsetY, this.tree_widget.element);
+      this.is_dragging = true;
+      this.current_item.$element.addClass('moving');
+      return true;
+    };
+
+    DragAndDropHandler.prototype.mouseDrag = function(event) {
+      var area, position_name;
+      this.drag_element.move(event.pageX, event.pageY);
+      area = this.findHoveredArea(event.pageX, event.pageY);
+      if (area && this.tree_widget.options.onCanMoveTo) {
+        position_name = Position.getName(area.position);
+        if (!this.tree_widget.options.onCanMoveTo(this.current_item.node, area.node, position_name)) {
+          area = null;
+        }
+      }
+      if (!area) {
+        this.removeDropHint();
+        this.removeHover();
+        this.stopOpenFolderTimer();
+      } else {
+        if (this.hovered_area !== area) {
+          this.hovered_area = area;
+          this.updateDropHint();
+        }
+      }
+      return true;
+    };
+
+    DragAndDropHandler.prototype.mouseStop = function() {
+      this.moveItem();
+      this.clear();
+      this.removeHover();
+      this.removeDropHint();
+      this.removeHitAreas();
+      this.current_item.$element.removeClass('moving');
+      this.is_dragging = false;
+      return false;
+    };
+
+    DragAndDropHandler.prototype.getOffsetFromEvent = function(event) {
+      var element_offset;
+      element_offset = $(event.target).offset();
+      return [event.pageX - element_offset.left, event.pageY - element_offset.top];
+    };
+
+    DragAndDropHandler.prototype.refreshHitAreas = function() {
+      this.removeHitAreas();
+      return this.generateHitAreas();
+    };
+
+    DragAndDropHandler.prototype.removeHitAreas = function() {
+      return this.hit_areas = [];
+    };
+
+    DragAndDropHandler.prototype.clear = function() {
+      this.drag_element.remove();
+      return this.drag_element = null;
+    };
+
+    DragAndDropHandler.prototype.removeDropHint = function() {
+      if (this.previous_ghost) {
+        return this.previous_ghost.remove();
+      }
+    };
+
+    DragAndDropHandler.prototype.removeHover = function() {
+      return this.hovered_area = null;
+    };
+
+    DragAndDropHandler.prototype.generateHitAreas = function() {
+      var addPosition, getTop, groupPositions, handleAfterOpenFolder, handleClosedFolder, handleFirstNode, handleNode, handleOpenFolder, hit_areas, last_top, positions,
+        _this = this;
+      positions = [];
+      last_top = 0;
+      getTop = function($element) {
+        return $element.offset().top;
+      };
+      addPosition = function(node, position, top) {
+        positions.push({
+          top: top,
+          node: node,
+          position: position
+        });
+        return last_top = top;
+      };
+      groupPositions = function(handle_group) {
+        var group, position, previous_top, _i, _len;
+        previous_top = -1;
+        group = [];
+        for (_i = 0, _len = positions.length; _i < _len; _i++) {
+          position = positions[_i];
+          if (position.top !== previous_top) {
+            if (group.length) {
+              handle_group(group, previous_top, position.top);
+            }
+            previous_top = position.top;
+            group = [];
+          }
+          group.push(position);
+        }
+        return handle_group(group, previous_top, _this.tree_widget.element.offset().top + _this.tree_widget.element.height());
+      };
+      handleNode = function(node, next_node, $element) {
+        var top;
+        top = getTop($element);
+        if (node === _this.current_item.node) {
+          addPosition(node, Position.NONE, top);
+        } else {
+          addPosition(node, Position.INSIDE, top);
+        }
+        if (next_node === _this.current_item.node || node === _this.current_item.node) {
+          return addPosition(node, Position.NONE, top);
+        } else {
+          return addPosition(node, Position.AFTER, top);
+        }
+      };
+      handleOpenFolder = function(node, $element) {
+        if (node === _this.current_item.node) {
+          return false;
+        }
+        if (node.children[0] !== _this.current_item.node) {
+          addPosition(node, Position.INSIDE, getTop($element));
+        }
+        return true;
+      };
+      handleAfterOpenFolder = function(node, next_node, $element) {
+        if (node === _this.current_item.node || next_node === _this.current_item.node) {
+          return addPosition(node, Position.NONE, last_top);
+        } else {
+          return addPosition(node, Position.AFTER, last_top);
+        }
+      };
+      handleClosedFolder = function(node, next_node, $element) {
+        var top;
+        top = getTop($element);
+        if (node === _this.current_item.node) {
+          return addPosition(node, Position.NONE, top);
+        } else {
+          addPosition(node, Position.INSIDE, top);
+          if (next_node !== _this.current_item.node) {
+            return addPosition(node, Position.AFTER, top);
+          }
+        }
+      };
+      handleFirstNode = function(node, $element) {
+        if (node !== _this.current_item.node) {
+          return addPosition(node, Position.BEFORE, getTop($(node.element)));
+        }
+      };
+      this.iterateVisibleNodes(handleNode, handleOpenFolder, handleClosedFolder, handleAfterOpenFolder, handleFirstNode);
+      hit_areas = [];
+      groupPositions(function(positions_in_group, top, bottom) {
+        var area_height, area_top, position, _i, _len;
+        area_height = (bottom - top) / positions_in_group.length;
+        area_top = top;
+        for (_i = 0, _len = positions_in_group.length; _i < _len; _i++) {
+          position = positions_in_group[_i];
+          hit_areas.push({
+            top: area_top,
+            bottom: area_top + area_height,
+            node: position.node,
+            position: position.position
+          });
+          area_top += area_height;
+        }
+        return null;
+      });
+      return this.hit_areas = hit_areas;
+    };
+
+    DragAndDropHandler.prototype.iterateVisibleNodes = function(handle_node, handle_open_folder, handle_closed_folder, handle_after_open_folder, handle_first_node) {
+      var is_first_node, iterate,
+        _this = this;
+      is_first_node = true;
+      iterate = function(node, next_node) {
+        var $element, child, children_length, i, must_iterate_inside, _i, _len, _ref;
+        must_iterate_inside = (node.is_open || !node.element) && node.hasChildren();
+        if (node.element) {
+          $element = $(node.element);
+          if (!$element.is(':visible')) {
+            return;
+          }
+          if (is_first_node) {
+            handle_first_node(node, $element);
+            is_first_node = false;
+          }
+          if (!node.hasChildren()) {
+            handle_node(node, next_node, $element);
+          } else if (node.is_open) {
+            if (!handle_open_folder(node, $element)) {
+              must_iterate_inside = false;
+            }
+          } else {
+            handle_closed_folder(node, next_node, $element);
+          }
+        }
+        if (must_iterate_inside) {
+          children_length = node.children.length;
+          _ref = node.children;
+          for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
+            child = _ref[i];
+            if (i === (children_length - 1)) {
+              iterate(node.children[i], null);
+            } else {
+              iterate(node.children[i], node.children[i + 1]);
+            }
+          }
+          if (node.is_open) {
+            return handle_after_open_folder(node, next_node, $element);
+          }
+        }
+      };
+      return iterate(this.tree_widget.tree);
+    };
+
+    DragAndDropHandler.prototype.findHoveredArea = function(x, y) {
+      var area, high, low, mid, tree_offset;
+      tree_offset = this.tree_widget.element.offset();
+      if (x < tree_offset.left || y < tree_offset.top || x > (tree_offset.left + this.tree_widget.element.width()) || y > (tree_offset.top + this.tree_widget.element.height())) {
+        return null;
+      }
+      low = 0;
+      high = this.hit_areas.length;
+      while (low < high) {
+        mid = (low + high) >> 1;
+        area = this.hit_areas[mid];
+        if (y < area.top) {
+          high = mid;
+        } else if (y > area.bottom) {
+          low = mid + 1;
+        } else {
+          return area;
+        }
+      }
+      return null;
+    };
+
+    DragAndDropHandler.prototype.updateDropHint = function() {
+      var node, node_element;
+      this.stopOpenFolderTimer();
+      if (!this.hovered_area) {
+        return;
+      }
+      node = this.hovered_area.node;
+      if (node.hasChildren() && !node.is_open && this.hovered_area.position === Position.INSIDE) {
+        this.startOpenFolderTimer(node);
+      }
+      this.removeDropHint();
+      node_element = this.tree_widget._getNodeElementForNode(this.hovered_area.node);
+      return this.previous_ghost = node_element.addDropHint(this.hovered_area.position);
+    };
+
+    DragAndDropHandler.prototype.startOpenFolderTimer = function(folder) {
+      var openFolder,
+        _this = this;
+      openFolder = function() {
+        return _this.tree_widget._getNodeElementForNode(folder).open(function() {
+          _this.refreshHitAreas();
+          return _this.updateDropHint();
+        });
+      };
+      return this.open_folder_timer = setTimeout(openFolder, 500);
+    };
+
+    DragAndDropHandler.prototype.stopOpenFolderTimer = function() {
+      if (this.open_folder_timer) {
+        clearTimeout(this.open_folder_timer);
+        return this.open_folder_timer = null;
+      }
+    };
+
+    DragAndDropHandler.prototype.moveItem = function() {
+      var doMove, event, moved_node, position, previous_parent, target_node,
+        _this = this;
+      if (this.hovered_area && this.hovered_area.position !== Position.NONE) {
+        moved_node = this.current_item.node;
+        target_node = this.hovered_area.node;
+        position = this.hovered_area.position;
+        previous_parent = moved_node.parent;
+        if (position === Position.INSIDE) {
+          this.hovered_area.node.is_open = true;
+        }
+        doMove = function() {
+          _this.tree_widget.tree.moveNode(moved_node, target_node, position);
+          _this.tree_widget.element.empty();
+          return _this.tree_widget._refreshElements();
+        };
+        event = $.Event('tree.move');
+        event.move_info = {
+          moved_node: moved_node,
+          target_node: target_node,
+          position: Position.getName(position),
+          previous_parent: previous_parent,
+          do_move: doMove
+        };
+        this.tree_widget.element.trigger(event);
+        if (!event.isDefaultPrevented()) {
+          return doMove();
+        }
+      }
+    };
+
+    return DragAndDropHandler;
+
+  })();
+
+  this.Tree.Node = Node;
+
+}).call(this);
Binary file OrthancServer/OrthancExplorer/orthanc-logo.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/OrthancExplorer/query-retrieve.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,337 @@
+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() {
+  var targetDate;
+
+  $.ajax({
+    url: '../modalities',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(modalities) {
+      var targetServer = $('#qr-server');
+      var option;
+
+      $('option', targetServer).remove();
+
+      for (var i = 0; i < modalities.length; i++) {
+        option = $('<option>').text(modalities[i]);
+        targetServer.append(option);
+      }
+
+      targetServer.selectmenu('refresh');
+    }
+  });
+
+  targetDate = $('#qr-date');
+  $('option', targetDate).remove();
+  targetDate.append($('<option>').attr('value', '').text('Any date'));
+  targetDate.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
+  targetDate.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
+  targetDate.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
+  targetDate.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
+  targetDate.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
+  targetDate.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
+  targetDate.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, server, modalities, field;
+
+  query = {
+    'Level' : 'Study',
+    'Query' : {
+      'AccessionNumber' : '',
+      'PatientBirthDate' : '',
+      'PatientID' : '',
+      'PatientName' : '',
+      'PatientSex' : '',
+      'StudyDate' : $('#qr-date').val(),
+      'StudyDescription' : ''
+    }
+  };
+
+  modalities = '';
+
+  field = $('#qr-fields input:checked').val();
+  query['Query'][field] = $('#qr-value').val().toUpperCase();
+
+  $('#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;
+  }
+
+
+  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() {
+  var pageData, uri;
+  
+  if ($.mobile.pageData) {
+    pageData = DeepCopy($.mobile.pageData);
+
+    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() {
+  var pageData, query;
+
+  if ($.mobile.pageData) {
+    pageData = DeepCopy($.mobile.pageData);
+
+    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() {
+  var pageData, uri;
+  
+  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) {
+          var aet;
+
+          event.preventDefault();
+
+          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/OrthancFindRequestHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,733 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancFindRequestHandler.h"
-
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/Logging.h"
-#include "../Core/Lua/LuaFunctionCall.h"
-#include "../Core/MetricsRegistry.h"
-#include "OrthancConfiguration.h"
-#include "Search/DatabaseLookup.h"
-#include "ServerContext.h"
-#include "ServerToolbox.h"
-
-#include <boost/regex.hpp> 
-
-
-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 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;
-
-      for (std::list<std::string>::const_iterator
-             it = series.begin(); it != series.end(); ++it)
-      {
-        DicomMap tags;
-        if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
-        {
-          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-
-          if (value != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary())
-          {
-            values.insert(value->GetContent());
-          }
-        }
-      }
-
-      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;
-
-      for (std::list<std::string>::const_iterator
-             it = instances.begin(); it != instances.end(); ++it)
-      {
-        std::string value;
-        if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid))
-        {
-          values.insert(value);
-        }
-      }
-
-      StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
-    }
-  }
-
-
-  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::unique_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 DicomMap& mainDicomTags,
-                        const Json::Value* dicomAsJson,
-                        const DicomArray& query,
-                        const std::list<DicomTag>& sequencesToReturn,
-                        const DicomMap* counters,
-                        const std::string& defaultPrivateCreator,
-                        const std::map<uint16_t, std::string>& privateCreators)
-  {
-    DicomMap match;
-
-    if (dicomAsJson != NULL)
-    {
-      match.FromDicomAsJson(*dicomAsJson);
-    }
-    else
-    {
-      match.Assign(mainDicomTags);
-    }
-    
-    DicomMap result;
-
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      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
-      {
-        const DicomTag& tag = query.GetElement(i).GetTag();
-        const DicomValue* value = match.TestAndGetValue(tag);
-
-        if (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          result.SetValue(tag, value->GetContent(), false);
-        }
-        else
-        {
-          result.SetValue(tag, "", false);
-        }
-      }
-    }
-
-    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 if (dicomAsJson == NULL)
-    {
-      LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled";
-      answers.Add(result);
-    }
-    else
-    {
-      ParsedDicomFile dicom(result, GetDefaultDicomEncoding(),
-                            true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators);
-
-      for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
-           tag != sequencesToReturn.end(); ++tag)
-      {
-        assert(dicomAsJson != NULL);
-        const Json::Value& source = (*dicomAsJson) [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);
-          }
-
-          if (tag->IsPrivate())
-          {
-            std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup());
-            
-            if (found != privateCreators.end())
-            {
-              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
-            }
-            else
-            {
-              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
-            }
-          }
-          else
-          {
-            dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
-          }
-        }
-      }
-
-      answers.Add(dicom);
-    }
-  }
-
-
-
-  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)
-  {
-  }
-
-
-  class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor
-  {
-  private:
-    DicomFindAnswers&           answers_;
-    ServerContext&              context_;
-    ResourceType                level_;
-    const DicomMap&             query_;
-    DicomArray                  queryAsArray_;
-    const std::list<DicomTag>&  sequencesToReturn_;
-    std::string                 defaultPrivateCreator_;       // the private creator to use if the group is not defined in the query itself
-    const std::map<uint16_t, std::string>& privateCreators_;  // the private creators defined in the query itself
-
-  public:
-    LookupVisitor(DicomFindAnswers&  answers,
-                  ServerContext& context,
-                  ResourceType level,
-                  const DicomMap& query,
-                  const std::list<DicomTag>& sequencesToReturn,
-                  const std::map<uint16_t, std::string>& privateCreators) :
-      answers_(answers),
-      context_(context),
-      level_(level),
-      query_(query),
-      queryAsArray_(query),
-      sequencesToReturn_(sequencesToReturn),
-      privateCreators_(privateCreators)
-    {
-      answers_.SetComplete(false);
-
-      {
-        OrthancConfiguration::ReaderLock lock;
-        defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator();
-      }
-    }
-
-    virtual bool IsDicomAsJsonNeeded() const
-    {
-      // Ask the "DICOM-as-JSON" attachment only if sequences are to
-      // be returned OR if "query_" contains non-main DICOM tags!
-
-      DicomMap withoutSpecialTags;
-      withoutSpecialTags.Assign(query_);
-
-      // Check out "ComputeCounters()"
-      withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
-      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
-      withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY);
-
-      // Check out "AddAnswer()"
-      withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
-      withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-      
-      return (!sequencesToReturn_.empty() ||
-              !withoutSpecialTags.HasOnlyMainDicomTags());
-    }
-      
-    virtual void MarkAsComplete()
-    {
-      answers_.SetComplete(true);
-    }
-
-    virtual void Visit(const std::string& publicId,
-                       const std::string& instanceId,
-                       const DicomMap& mainDicomTags,
-                       const Json::Value* dicomAsJson) 
-    {
-      std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));
-
-      AddAnswer(answers_, mainDicomTags, dicomAsJson,
-                queryAsArray_, sequencesToReturn_, counters.get(), defaultPrivateCreator_, privateCreators_);
-    }
-  };
-
-
-  void OrthancFindRequestHandler::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)
-  {
-    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms");
-
-    /**
-     * Possibly apply the user-supplied Lua filter.
-     **/
-
-    DicomMap lua;
-    const DicomMap* filteredInput = &input;
-
-    if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet, manufacturer))
-    {
-      filteredInput = &lua;
-    }
-
-
-    /**
-     * Retrieve the query level.
-     **/
-
-    assert(filteredInput != NULL);
-    const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    if (levelTmp == NULL ||
-        levelTmp->IsNull() ||
-        levelTmp->IsBinary())
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)");
-    }
-
-    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());
-
-    if (level != ResourceType_Patient &&
-        level != ResourceType_Study &&
-        level != ResourceType_Series &&
-        level != ResourceType_Instance)
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-
-    DicomArray query(*filteredInput);
-    LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level);
-
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      if (!query.GetElement(i).GetValue().IsNull())
-      {
-        LOG(INFO) << "  " << 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";
-    }
-
-    // collect the private creators from the query itself
-    std::map<uint16_t, std::string> privateCreators;
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      const DicomElement& element = query.GetElement(i);
-      if (element.GetTag().IsPrivate() && element.GetTag().GetElement() == 0x10)
-      {
-        privateCreators[element.GetTag().GetGroup()] = element.GetValue().GetContent();
-      }
-    }
-
-    /**
-     * Build up the query object.
-     **/
-
-    DatabaseLookup lookup;
-
-    bool caseSensitivePN;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
-    }
-
-    for (size_t i = 0; i < query.GetSize(); i++)
-    {
-      const DicomElement& element = query.GetElement(i);
-      const DicomTag tag = element.GetTag();
-
-      if (element.GetValue().IsNull() ||
-          tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
-          tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
-      {
-        continue;
-      }
-
-      std::string value = element.GetValue().GetContent();
-      if (value.size() == 0)
-      {
-        // An empty string corresponds to an universal constraint, so we ignore it
-        continue;
-      }
-
-      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_PersonName)
-        {
-          sensitive = caseSensitivePN;
-        }
-
-        lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */);
-      }
-      else
-      {
-        LOG(INFO) << "Because of a patch for the manufacturer of the remote modality, " 
-                  << "ignoring constraint on tag (" << tag.Format() << ") " << FromDcmtkBridge::GetTagName(element);
-      }
-    }
-
-
-    /**
-     * Run the query.
-     **/
-
-    size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
-
-
-    LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators);
-    context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
-  }
-
-
-  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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomNetworking/IFindRequestHandler.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class OrthancFindRequestHandler : public IFindRequestHandler
-  {
-  private:
-    class LookupVisitor;
-
-    ServerContext& context_;
-    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);
-
-    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);
-
-    unsigned int GetMaxResults() const
-    {
-      return maxResults_;
-    }
-
-    void SetMaxResults(unsigned int results)
-    {
-      maxResults_ = results;
-    }
-
-    unsigned int GetMaxInstances() const
-    {
-      return maxInstances_;
-    }
-
-    void SetMaxInstances(unsigned int instances)
-    {
-      maxInstances_ = instances;
-    }
-
-    static void FormatOrigin(Json::Value& origin,
-                             const std::string& remoteIp,
-                             const std::string& remoteAet,
-                             const std::string& calledAet,
-                             ModalityManufacturer manufacturer);
-  };
-}
--- a/OrthancServer/OrthancGetRequestHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,583 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancGetRequestHandler.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/Logging.h"
-#include "../Core/MetricsRegistry.h"
-#include "OrthancConfiguration.h"
-#include "ServerContext.h"
-#include "ServerJobs/DicomModalityStoreJob.h"
-
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcistrmb.h>
-#include <dcmtk/dcmnet/assoc.h>
-#include <dcmtk/dcmnet/dimse.h>
-#include <dcmtk/dcmnet/diutil.h>
-#include <dcmtk/ofstd/ofstring.h>
-
-#include <sstream>  // For std::stringstream
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-    
-    static void GetSubOpProgressCallback(
-      void * /* callbackData == pointer to the "OrthancGetRequestHandler" object */,
-      T_DIMSE_StoreProgress *progress,
-      T_DIMSE_C_StoreRQ * /*req*/)
-    {
-      // SBL - no logging to be done here.
-    }
-  }
-
-  OrthancGetRequestHandler::Status
-  OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc)
-  {
-    if (position_ >= instances_.size())
-    {
-      return Status_Failure;
-    }
-    
-    const std::string& id = instances_[position_++];
-
-    std::string dicom;
-    context_.ReadDicom(dicom, id);
-    
-    if (dicom.size() <= 0)
-    {
-      return Status_Failure;
-    }
-
-    std::unique_ptr<DcmFileFormat> parsed(
-      FromDcmtkBridge::LoadFromMemoryBuffer(dicom.c_str(), dicom.size()));
-
-    if (parsed.get() == NULL ||
-        parsed->getDataset() == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    DcmDataset& dataset = *parsed->getDataset();
-    
-    OFString a, b;
-    if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() ||
-        !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good())
-    {
-      throw OrthancException(ErrorCode_NoSopClassOrInstance,
-                             "Unable to determine the SOP class/instance for C-STORE with AET " +
-                             originatorAet_);
-    }
-
-    std::string sopClassUid(a.c_str());
-    std::string sopInstanceUid(b.c_str());
-    
-    OFCondition cond = PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release());
-    
-    if (getCancelled_)
-    {
-      LOG(INFO) << "C-GET SCP: Received C-Cancel RQ";
-    }
-    
-    if (cond.bad() || getCancelled_)
-    {
-      return Status_Failure;
-    }
-    
-    return Status_Success;
-  }
-
-  
-  void OrthancGetRequestHandler::AddFailedUIDInstance(const std::string& sopInstance)
-  {
-    if (failedUIDs_.empty())
-    {
-      failedUIDs_ = sopInstance;
-    }
-    else
-    {
-      failedUIDs_ += "\\" + sopInstance;
-    }
-  }
-
-
-  static bool SelectPresentationContext(T_ASC_PresentationContextID& selectedPresentationId,
-                                        DicomTransferSyntax& selectedSyntax,
-                                        T_ASC_Association* assoc,
-                                        const std::string& sopClassUid,
-                                        DicomTransferSyntax sourceSyntax,
-                                        bool allowTranscoding)
-  {
-    typedef std::map<DicomTransferSyntax, T_ASC_PresentationContextID> Accepted;
-
-    Accepted accepted;
-
-    /**
-     * 1. Inspect and index all the accepted transfer syntaxes. This
-     * is similar to the code from "DicomAssociation::Open()".
-     **/
-
-    LST_HEAD **l = &assoc->params->DULparams.acceptedPresentationContext;
-    if (*l != NULL)
-    {
-      DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
-      LST_Position(l, (LST_NODE*)pc);
-      while (pc)
-      {
-        DicomTransferSyntax transferSyntax;
-        if (pc->result == ASC_P_ACCEPTANCE &&
-            LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
-        {
-          VLOG(0) << "C-GET SCP accepted: SOP class " << sopClassUid
-                  << " with transfer syntax " << GetTransferSyntaxUid(transferSyntax);
-          if (std::string(pc->abstractSyntax) == sopClassUid)
-          {
-            accepted[transferSyntax] = pc->presentationContextID;
-          }
-        }
-        else
-        {
-          LOG(WARNING) << "C-GET: Unknown transfer syntax received: "
-                       << pc->acceptedTransferSyntax;
-        }
-            
-        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
-      }
-    }
-
-    
-    /**
-     * 2. Select the preferred transfer syntaxes, which corresponds to
-     * the source transfer syntax, plus all the uncompressed transfer
-     * syntaxes if transcoding is enabled.
-     **/
-    
-    std::list<DicomTransferSyntax> preferred;
-    preferred.push_back(sourceSyntax);
-
-    if (allowTranscoding)
-    {
-      if (sourceSyntax != DicomTransferSyntax_LittleEndianImplicit)
-      {
-        // Default Transfer Syntax for DICOM
-        preferred.push_back(DicomTransferSyntax_LittleEndianImplicit);
-      }
-
-      if (sourceSyntax != DicomTransferSyntax_LittleEndianExplicit)
-      {
-        preferred.push_back(DicomTransferSyntax_LittleEndianExplicit);
-      }
-
-      if (sourceSyntax != DicomTransferSyntax_BigEndianExplicit)
-      {
-        // Retired
-        preferred.push_back(DicomTransferSyntax_BigEndianExplicit);
-      }
-    }
-
-
-    /**
-     * 3. Lookup whether one of the preferred transfer syntaxes was
-     * accepted.
-     **/
-    
-    for (std::list<DicomTransferSyntax>::const_iterator
-           it = preferred.begin(); it != preferred.end(); ++it)
-    {
-      Accepted::const_iterator found = accepted.find(*it);
-      if (found != accepted.end())
-      {
-        selectedPresentationId = found->second;
-        selectedSyntax = *it;
-        return true;
-      }
-    }
-
-    // No preferred syntax was accepted
-    return false;
-  }                                                           
-
-
-  OFCondition OrthancGetRequestHandler::PerformGetSubOp(T_ASC_Association* assoc,
-                                                        const std::string& sopClassUid,
-                                                        const std::string& sopInstanceUid,
-                                                        DcmFileFormat* dicomRaw)
-  {
-    assert(dicomRaw != NULL);
-    std::unique_ptr<DcmFileFormat> dicom(dicomRaw);
-    
-    DicomTransferSyntax sourceSyntax;
-    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *dicom))
-    {
-      nFailed_++;
-      AddFailedUIDInstance(sopInstanceUid);
-      LOG(ERROR) << "C-GET SCP: Unknown transfer syntax: ("
-                 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
-      return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
-    }
-
-    bool allowTranscoding = (context_.IsTranscodeDicomProtocol() &&
-                             remote_.IsTranscodingAllowed());
-    
-    T_ASC_PresentationContextID presId = 0;  // Unnecessary initialization, makes code clearer
-    DicomTransferSyntax selectedSyntax;
-    if (!SelectPresentationContext(presId, selectedSyntax, assoc, sopClassUid,
-                                   sourceSyntax, allowTranscoding) ||
-        presId == 0)
-    {
-      nFailed_++;
-      AddFailedUIDInstance(sopInstanceUid);
-      LOG(ERROR) << "C-GET SCP: storeSCU: No presentation context for: ("
-                 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
-      return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
-    }
-    else
-    {
-      LOG(INFO) << "C-GET SCP selected transfer syntax " << GetTransferSyntaxUid(selectedSyntax)
-                << ", for source instance with SOP class " << sopClassUid
-                << " and transfer syntax " << GetTransferSyntaxUid(sourceSyntax);
-
-      // make sure that we can send images in this presentation context
-      T_ASC_PresentationContext pc;
-      ASC_findAcceptedPresentationContext(assoc->params, presId, &pc);
-      // the acceptedRole is the association requestor role
-
-      if (pc.acceptedRole != ASC_SC_ROLE_DEFAULT &&  // "DEFAULT" is necessary for GinkgoCADx
-          pc.acceptedRole != ASC_SC_ROLE_SCP &&
-          pc.acceptedRole != ASC_SC_ROLE_SCUSCP)
-      {
-        // the role is not appropriate
-        nFailed_++;
-        AddFailedUIDInstance(sopInstanceUid);
-        LOG(ERROR) << "C-GET SCP: storeSCU: [No presentation context with requestor SCP role for: ("
-                   << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
-        return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
-      }
-    }
-
-    const DIC_US msgId = assoc->nextMsgID++;
-    
-    T_DIMSE_C_StoreRQ req;
-    memset(&req, 0, sizeof(req));
-    req.MessageID = msgId;
-    strncpy(req.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
-    strncpy(req.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
-    req.DataSetType = DIMSE_DATASET_PRESENT;
-    req.Priority = DIMSE_PRIORITY_MEDIUM;
-    req.opts = 0;
-    
-    T_DIMSE_C_StoreRSP rsp;
-    memset(&rsp, 0, sizeof(rsp));
-
-    LOG(INFO) << "Store SCU RQ: MsgID " << msgId << ", ("
-              << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ")";
-    
-    T_DIMSE_DetectedCancelParameters cancelParameters;
-    memset(&cancelParameters, 0, sizeof(cancelParameters));
-
-    std::unique_ptr<DcmDataset> stDetail;
-
-    OFCondition cond;
-
-    if (sourceSyntax == selectedSyntax)
-    {
-      // No transcoding is required
-      DcmDataset *stDetailTmp = NULL;
-      cond = DIMSE_storeUser(
-        assoc, presId, &req, NULL /* imageFileName */, dicom->getDataset(),
-        GetSubOpProgressCallback, this /* callbackData */,
-        (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
-        &rsp, &stDetailTmp, &cancelParameters);
-      stDetail.reset(stDetailTmp);
-    }
-    else
-    {
-      // Transcoding to the selected uncompressed transfer syntax
-      IDicomTranscoder::DicomImage source, transcoded;
-      source.AcquireParsed(dicom.release());
-
-      std::set<DicomTransferSyntax> ts;
-      ts.insert(selectedSyntax);
-      
-      if (context_.Transcode(transcoded, source, ts, true))
-      {
-        // Transcoding has succeeded
-        DcmDataset *stDetailTmp = NULL;
-        cond = DIMSE_storeUser(
-          assoc, presId, &req, NULL /* imageFileName */,
-          transcoded.GetParsed().getDataset(),
-          GetSubOpProgressCallback, this /* callbackData */,
-          (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
-          &rsp, &stDetailTmp, &cancelParameters);
-        stDetail.reset(stDetailTmp);
-      }
-      else
-      {
-        // Cannot transcode
-        nFailed_++;
-        AddFailedUIDInstance(sopInstanceUid);
-        LOG(ERROR) << "C-GET SCP: Cannot transcode " << sopClassUid
-                   << " from transfer syntax " << GetTransferSyntaxUid(sourceSyntax)
-                   << " to " << GetTransferSyntaxUid(selectedSyntax);
-        return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
-      }      
-    }
-    
-    if (cond.good())
-    {
-      if (cancelParameters.cancelEncountered)
-      {
-        if (origPresId_ == cancelParameters.presId &&
-            origMsgId_ == cancelParameters.req.MessageIDBeingRespondedTo)
-        {
-          getCancelled_ = OFTrue;
-        }
-        else
-        {
-          LOG(ERROR) << "C-GET SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId
-                     << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo;
-        }
-      }
-      
-      if (rsp.DimseStatus == STATUS_Success)
-      {
-        // everything ok
-        nCompleted_++;
-      }
-      else if ((rsp.DimseStatus & 0xf000) == 0xb000)
-      {
-        // a warning status message
-        warningCount_++;
-        LOG(ERROR) << "C-GET SCP: Store Warning: Response Status: "
-                   << DU_cstoreStatusString(rsp.DimseStatus);
-      }
-      else
-      {
-        nFailed_++;
-        AddFailedUIDInstance(sopInstanceUid);
-        // print a status message
-        LOG(ERROR) << "C-GET SCP: Store Failed: Response Status: "
-                   << DU_cstoreStatusString(rsp.DimseStatus);
-      }
-    }
-    else
-    {
-      nFailed_++;
-      AddFailedUIDInstance(sopInstanceUid);
-      OFString temp_str;
-      LOG(ERROR) << "C-GET SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond);
-    }
-    
-    if (stDetail.get() != NULL)
-    {
-      // It is impossible to directly use the "<<" stream construct
-      // with "DcmObject::PrintHelper" using MSVC2008
-      std::stringstream s;
-      DcmObject::PrintHelper obj(*stDetail);
-      obj.dcmobj_.print(s);
-
-      LOG(INFO) << "  Status Detail: " << s.str();
-    }
-    
-    return cond;
-  }
-
-  bool OrthancGetRequestHandler::LookupIdentifiers(std::list<std::string>& publicIds,
-                                                   ResourceType level,
-                                                   const DicomMap& input) const
-  {
-    DicomTag tag(0, 0);   // Dummy initialization
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        tag = DICOM_TAG_PATIENT_ID;
-        break;
-
-      case ResourceType_Study:
-        tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ?
-               DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
-        break;
-        
-      case ResourceType_Series:
-        tag = DICOM_TAG_SERIES_INSTANCE_UID;
-        break;
-        
-      case ResourceType_Instance:
-        tag = DICOM_TAG_SOP_INSTANCE_UID;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    if (!input.HasTag(tag))
-    {
-      return false;
-    }
-
-    const DicomValue& value = input.GetValue(tag);
-    if (value.IsNull() ||
-        value.IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      std::vector<std::string> tokens;
-      Toolbox::TokenizeString(tokens, value.GetContent(), '\\');
-
-      for (size_t i = 0; i < tokens.size(); i++)
-      {
-        std::vector<std::string> tmp;
-        context_.GetIndex().LookupIdentifierExact(tmp, level, tag, tokens[i]);
-
-        if (tmp.empty())
-        {
-          LOG(ERROR) << "C-GET: Cannot locate resource \"" << tokens[i]
-                     << "\" at the " << EnumerationToString(level) << " level";
-          return false;
-        }
-        else
-        {
-          for (size_t i = 0; i < tmp.size(); i++)
-          {
-            publicIds.push_back(tmp[i]);
-          }
-        }
-      }
-
-      return true;      
-    }
-  }
-
-
-    OrthancGetRequestHandler::OrthancGetRequestHandler(ServerContext& context) :
-      context_(context)
-    {
-      position_ = 0;
-      nRemaining_ = 0;
-      nCompleted_  = 0;
-      warningCount_ = 0;
-      nFailed_ = 0;
-      timeout_ = 0;
-    }
-
-
-  bool OrthancGetRequestHandler::Handle(const DicomMap& input,
-                                        const std::string& originatorIp,
-                                        const std::string& originatorAet,
-                                        const std::string& calledAet,
-                                        uint32_t timeout)
-  {
-    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms");
-
-    LOG(WARNING) << "C-GET-SCU request received from AET \"" << originatorAet << "\"";
-
-    {
-      DicomArray query(input);
-      for (size_t i = 0; i < query.GetSize(); i++)
-      {
-        if (!query.GetElement(i).GetValue().IsNull())
-        {
-          LOG(INFO) << "  " << query.GetElement(i).GetTag()
-                    << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
-                    << " = " << query.GetElement(i).GetValue().GetContent();
-        }
-      }
-    }
-
-    /**
-     * Retrieve the query level.
-     **/
-
-    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-
-    assert(levelTmp != NULL);
-    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());      
-
-
-    /**
-     * Lookup for the resource to be sent.
-     **/
-
-    std::list<std::string> publicIds;
-
-    if (!LookupIdentifiers(publicIds, level, input))
-    {
-      LOG(ERROR) << "Cannot determine what resources are requested by C-GET";
-      return false; 
-    }
-
-    localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
-    position_ = 0;
-    originatorAet_ = originatorAet;
-    
-    {
-      OrthancConfiguration::ReaderLock lock;
-      remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet);
-    }
-
-    for (std::list<std::string>::const_iterator
-           resource = publicIds.begin(); resource != publicIds.end(); ++resource)
-    {
-      LOG(INFO) << "C-GET: Sending resource " << *resource
-                << " to modality \"" << originatorAet << "\"";
-      
-      std::list<std::string> tmp;
-      context_.GetIndex().GetChildInstances(tmp, *resource);
-      
-      instances_.reserve(tmp.size());
-      for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
-      {
-        instances_.push_back(*it);
-      }
-    }
-
-    failedUIDs_.clear();
-    getCancelled_ = OFFalse;
-
-    nRemaining_ = GetSubOperationCount();
-    nCompleted_ = 0;
-    nFailed_ = 0;
-    warningCount_ = 0;
-    timeout_ = timeout;
-
-    return true;
-  }
-};
--- a/OrthancServer/OrthancGetRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomNetworking/IGetRequestHandler.h"
-#include "../Core/DicomNetworking/RemoteModalityParameters.h"
-
-#include <dcmtk/dcmnet/dimse.h>
-
-#include <list>
-
-class DcmFileFormat;
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class OrthancGetRequestHandler : public IGetRequestHandler
-  {
-  private:
-    ServerContext& context_;
-    std::string localAet_;
-    std::vector<std::string> instances_;
-    size_t position_;
-    RemoteModalityParameters remote_;
-    std::string originatorAet_;
-    
-    unsigned int nRemaining_;
-    unsigned int nCompleted_;
-    unsigned int warningCount_;
-    unsigned int nFailed_;
-    std::string failedUIDs_;
-    
-    DIC_US origMsgId_;
-    T_ASC_PresentationContextID origPresId_;
-    
-    bool getCancelled_;
-    uint32_t timeout_;
-
-    bool LookupIdentifiers(std::list<std::string>& publicIds,
-                           ResourceType level,
-                           const DicomMap& input) const;
-    
-    OFCondition PerformGetSubOp(T_ASC_Association *assoc,
-                                const std::string& sopClassUid,
-                                const std::string& sopInstanceUid,
-                                DcmFileFormat* datasetRaw);
-    
-    void AddFailedUIDInstance(const std::string& sopInstance);
-
-  public:
-    OrthancGetRequestHandler(ServerContext& context);
-    
-    virtual bool Handle(const DicomMap& input,
-                        const std::string& originatorIp,
-                        const std::string& originatorAet,
-                        const std::string& calledAet,
-                        uint32_t timeout) ORTHANC_OVERRIDE;
-    
-    virtual Status DoNext(T_ASC_Association *assoc) ORTHANC_OVERRIDE;
-    
-    virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE
-    {
-      return static_cast<unsigned int>(instances_.size());
-    }
-    
-    virtual unsigned int GetRemainingCount() const ORTHANC_OVERRIDE
-    {
-      return nRemaining_;
-    }
-    
-    virtual unsigned int GetCompletedCount() const ORTHANC_OVERRIDE
-    {
-      return nCompleted_;
-    }
-    
-    virtual unsigned int GetWarningCount() const ORTHANC_OVERRIDE
-    {
-      return warningCount_;
-    }
-    
-    virtual unsigned int GetFailedCount() const ORTHANC_OVERRIDE
-    {
-      return nFailed_;
-    }
-    
-    virtual const std::string& GetFailedUids() const ORTHANC_OVERRIDE
-    {
-      return failedUIDs_;
-    }    
-  };
-}
--- a/OrthancServer/OrthancHttpHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,127 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancHttpHandler.h"
-
-#include "../Core/OrthancException.h"
-
-
-namespace Orthanc
-{
-  bool OrthancHttpHandler::CreateChunkedRequestReader(
-    std::unique_ptr<IHttpHandler::IChunkedRequestReader>& target,
-    RequestOrigin origin,
-    const char* remoteIp,
-    const char* username,
-    HttpMethod method,
-    const UriComponents& uri,
-    const Arguments& headers)
-  {
-    if (method != HttpMethod_Post &&
-        method != HttpMethod_Put)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) 
-    {
-      if ((*it)->CreateChunkedRequestReader
-          (target, origin, remoteIp, username, method, uri, headers))
-      {
-        if (target.get() == NULL)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  bool OrthancHttpHandler::Handle(HttpOutput& output,
-                                  RequestOrigin origin,
-                                  const char* remoteIp,
-                                  const char* username,
-                                  HttpMethod method,
-                                  const UriComponents& uri,
-                                  const Arguments& headers,
-                                  const GetArguments& getArguments,
-                                  const void* bodyData,
-                                  size_t bodySize)
-  {
-    for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) 
-    {
-      if ((*it)->Handle(output, origin, remoteIp, username, method, uri, 
-                        headers, getArguments, bodyData, bodySize))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  void OrthancHttpHandler::Register(IHttpHandler& handler,
-                                    bool isOrthancRestApi)
-  {
-    handlers_.push_back(&handler);
-
-    if (isOrthancRestApi)
-    {
-      orthancRestApi_ = &handler;
-    }
-  }
-
-
-  IHttpHandler& OrthancHttpHandler::RestrictToOrthancRestApi(bool restrict)
-  {
-    if (restrict)
-    {
-      if (orthancRestApi_ == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      return *orthancRestApi_;
-    }
-    else
-    {
-      return *this;
-    }
-  }
-}
--- a/OrthancServer/OrthancHttpHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/HttpServer/IHttpHandler.h"
-
-namespace Orthanc
-{
-  class OrthancHttpHandler : public IHttpHandler
-  {
-  private:
-    typedef std::list<IHttpHandler*> Handlers;
-
-    Handlers      handlers_;
-    IHttpHandler *orthancRestApi_;
-
-  public:
-    OrthancHttpHandler() : orthancRestApi_(NULL)
-    {
-    }
-
-    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                            RequestOrigin origin,
-                                            const char* remoteIp,
-                                            const char* username,
-                                            HttpMethod method,
-                                            const UriComponents& uri,
-                                            const Arguments& headers);
-
-    virtual bool Handle(HttpOutput& output,
-                        RequestOrigin origin,
-                        const char* remoteIp,
-                        const char* username,
-                        HttpMethod method,
-                        const UriComponents& uri,
-                        const Arguments& headers,
-                        const GetArguments& getArguments,
-                        const void* bodyData,
-                        size_t bodySize);
-
-    void Register(IHttpHandler& handler,
-                  bool isOrthancRestApi);
-
-    bool HasOrthancRestApi() const
-    {
-      return orthancRestApi_ != NULL;
-    }
-
-    IHttpHandler& RestrictToOrthancRestApi(bool restrict);
-  };
-}
--- a/OrthancServer/OrthancInitialization.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,391 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#if defined(_WIN32)
-// "Please include winsock2.h before windows.h"
-#  include <winsock2.h>
-#endif
-
-#include "OrthancInitialization.h"
-
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/FileStorage/FilesystemStorage.h"
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-
-#include "Database/SQLiteDatabaseWrapper.h"
-#include "OrthancConfiguration.h"
-
-#include <OrthancServerResources.h>
-
-#include <dcmtk/dcmnet/dul.h>   // For dcmDisableGethostbyaddr()
-
-
-
-namespace Orthanc
-{
-  static void RegisterUserMetadata(const Json::Value& config)
-  {
-    if (config.isMember("UserMetadata"))
-    {
-      const Json::Value& parameter = config["UserMetadata"];
-
-      Json::Value::Members members = parameter.getMemberNames();
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        const std::string& name = members[i];
-
-        if (!parameter[name].isInt())
-        {
-          throw OrthancException(ErrorCode_BadParameterType,
-                                 "Not a number in this user-defined metadata: " + name);
-        }
-
-        int metadata = parameter[name].asInt();        
-
-        LOG(INFO) << "Registering user-defined metadata: " << name << " (index " 
-                  << metadata << ")";
-
-        try
-        {
-          RegisterUserMetadata(metadata, name);
-        }
-        catch (OrthancException&)
-        {
-          LOG(ERROR) << "Cannot register this user-defined metadata: " << name;
-          throw;
-        }
-      }
-    }
-  }
-
-
-  static void RegisterUserContentType(const Json::Value& config)
-  {
-    if (config.isMember("UserContentType"))
-    {
-      const Json::Value& parameter = config["UserContentType"];
-
-      Json::Value::Members members = parameter.getMemberNames();
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        const std::string& name = members[i];
-        std::string mime = MIME_BINARY;
-
-        const Json::Value& value = parameter[name];
-        int contentType;
-
-        if (value.isArray() &&
-            value.size() == 2 &&
-            value[0].isInt() &&
-            value[1].isString())
-        {
-          contentType = value[0].asInt();
-          mime = value[1].asString();
-        }
-        else if (value.isInt())
-        {
-          contentType = value.asInt();
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_BadParameterType,
-                                 "Not a number in this user-defined attachment type: " + name);
-        }
-
-        LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " 
-                  << contentType << ") with MIME type \"" << mime << "\"";
-
-        try
-        {
-          RegisterUserContentType(contentType, name, mime);
-        }
-        catch (OrthancException&)
-        {
-          throw;
-        }
-      }
-    }
-  }
-
-
-  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, "The definition of the '" + tags[i] + "' dictionary entry is invalid.");
-      }
-
-      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)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "No path to the PKCS#11 module (DLL or .so) is provided "
-                             "for HTTPS client authentication");
-    }
-
-    std::string pin;
-    if (config.isMember("Pin"))
-    {
-      if (config["Pin"].type() == Json::stringValue)
-      {
-        pin = config["Pin"].asString();
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "The PIN number in the PKCS#11 configuration must be a string");
-      }
-    }
-
-    bool verbose = false;
-    if (config.isMember("Verbose"))
-    {
-      if (config["Verbose"].type() == Json::booleanValue)
-      {
-        verbose = config["Verbose"].asBool();
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "The Verbose option in the PKCS#11 configuration must be a Boolean");
-      }
-    }
-
-    HttpClient::InitializePkcs11(config["Module"].asString(), pin, verbose);
-  }
-
-
-
-  void OrthancInitialize(const char* configurationFile)
-  {
-    OrthancConfiguration::WriterLock lock;
-
-    InitializeServerEnumerations();
-
-    // Read the user-provided configuration
-    lock.GetConfiguration().Read(configurationFile);
-
-    {
-      std::string locale;
-      
-      if (lock.GetJson().isMember("Locale"))
-      {
-        locale = lock.GetConfiguration().GetStringParameter("Locale", "");
-      }
-      
-      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter("LoadPrivateDictionary", true);
-      Orthanc::InitializeFramework(locale, loadPrivate);
-    }
-
-    // The Orthanc framework is now initialized
-
-    if (lock.GetJson().isMember("DefaultEncoding"))
-    {
-      std::string encoding = lock.GetConfiguration().GetStringParameter("DefaultEncoding", "");
-      SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
-    }
-    else
-    {
-      SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
-    }
-
-    if (lock.GetJson().isMember("Pkcs11"))
-    {
-      ConfigurePkcs11(lock.GetJson()["Pkcs11"]);
-    }
-
-    RegisterUserMetadata(lock.GetJson());
-    RegisterUserContentType(lock.GetJson());
-
-    LoadCustomDictionary(lock.GetJson());
-
-    lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16);
-  }
-
-
-
-  void OrthancFinalize()
-  {
-    OrthancConfiguration::WriterLock lock;
-    Orthanc::FinalizeFramework();
-  }
-
-
-  static IDatabaseWrapper* CreateSQLiteWrapper()
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    std::string storageDirectoryStr = 
-      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
-
-    // Open the database
-    boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
-      lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr));
-
-    LOG(WARNING) << "SQLite index directory: " << indexDirectory;
-
-    try
-    {
-      boost::filesystem::create_directories(indexDirectory);
-    }
-    catch (boost::filesystem::filesystem_error&)
-    {
-    }
-
-    return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index");
-  }
-
-
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-
-    class FilesystemStorageWithoutDicom : public IStorageArea
-    {
-    private:
-      FilesystemStorage storage_;
-
-    public:
-      FilesystemStorageWithoutDicom(const std::string& path) : storage_(path)
-      {
-      }
-
-      virtual void Create(const std::string& uuid,
-                          const void* content, 
-                          size_t size,
-                          FileContentType type)
-      {
-        if (type != FileContentType_Dicom)
-        {
-          storage_.Create(uuid, content, size, type);
-        }
-      }
-
-      virtual void Read(std::string& content,
-                        const std::string& uuid,
-                        FileContentType type)
-      {
-        if (type != FileContentType_Dicom)
-        {
-          storage_.Read(content, uuid, type);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-      }
-
-      virtual void Remove(const std::string& uuid,
-                          FileContentType type) 
-      {
-        if (type != FileContentType_Dicom)
-        {
-          storage_.Remove(uuid, type);
-        }
-      }
-    };
-  }
-
-
-  static IStorageArea* CreateFilesystemStorage()
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    std::string storageDirectoryStr = 
-      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
-
-    boost::filesystem::path storageDirectory = 
-      lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);
-
-    LOG(WARNING) << "Storage directory: " << storageDirectory;
-
-    if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true))
-    {
-      return new FilesystemStorage(storageDirectory.string());
-    }
-    else
-    {
-      LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode";
-      return new FilesystemStorageWithoutDicom(storageDirectory.string());
-    }
-  }
-
-
-  IDatabaseWrapper* CreateDatabaseWrapper()
-  {
-    return CreateSQLiteWrapper();
-  }
-
-
-  IStorageArea* CreateStorageArea()
-  {
-    return CreateFilesystemStorage();
-  }  
-}
--- a/OrthancServer/OrthancInitialization.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/FileStorage/IStorageArea.h"
-#include "Database/IDatabaseWrapper.h"
-
-namespace Orthanc
-{
-  void OrthancInitialize(const char* configurationFile = NULL);
-
-  void OrthancFinalize();
-
-  IDatabaseWrapper* CreateDatabaseWrapper();
-
-  IStorageArea* CreateStorageArea();
-}
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,378 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancMoveRequestHandler.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/Logging.h"
-#include "../Core/MetricsRegistry.h"
-#include "OrthancConfiguration.h"
-#include "ServerContext.h"
-#include "ServerJobs/DicomModalityStoreJob.h"
-
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-
-    class SynchronousMove : public IMoveRequestIterator
-    {
-    private:
-      ServerContext& context_;
-      const std::string& localAet_;
-      std::vector<std::string> instances_;
-      size_t position_;
-      RemoteModalityParameters remote_;
-      std::string originatorAet_;
-      uint16_t originatorId_;
-      std::unique_ptr<DicomStoreUserConnection> connection_;
-
-    public:
-      SynchronousMove(ServerContext& context,
-                      const std::string& targetAet,
-                      const std::vector<std::string>& publicIds,
-                      const std::string& originatorAet,
-                      uint16_t originatorId) :
-        context_(context),
-        localAet_(context.GetDefaultLocalApplicationEntityTitle()),
-        position_(0),
-        originatorAet_(originatorAet),
-        originatorId_(originatorId)
-      {
-        {
-          OrthancConfiguration::ReaderLock lock;
-          remote_ = lock.GetConfiguration().GetModalityUsingAet(targetAet);
-        }
-
-        for (size_t i = 0; i < publicIds.size(); i++)
-        {
-          LOG(INFO) << "Sending resource " << publicIds[i] << " to modality \""
-                    << targetAet << "\" in synchronous mode";
-
-          std::list<std::string> tmp;
-          context_.GetIndex().GetChildInstances(tmp, publicIds[i]);
-
-          instances_.reserve(tmp.size());
-          for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
-          {
-            instances_.push_back(*it);
-          }
-        }
-      }
-
-      virtual unsigned int GetSubOperationCount() const
-      {
-        return instances_.size();
-      }
-
-      virtual Status DoNext()
-      {
-        if (position_ >= instances_.size())
-        {
-          return Status_Failure;
-        }
-
-        const std::string& id = instances_[position_++];
-
-        std::string dicom;
-        context_.ReadDicom(dicom, id);
-
-        if (connection_.get() == NULL)
-        {
-          DicomAssociationParameters params(localAet_, remote_);
-          connection_.reset(new DicomStoreUserConnection(params));
-        }
-
-        std::string sopClassUid, sopInstanceUid;  // Unused
-
-        const void* data = dicom.empty() ? NULL : dicom.c_str();
-        connection_->Store(sopClassUid, sopInstanceUid, data, dicom.size(),
-                           true, originatorAet_, originatorId_);
-
-        return Status_Success;
-      }
-    };
-
-
-    class AsynchronousMove : public IMoveRequestIterator
-    {
-    private:
-      ServerContext&                          context_;
-      std::unique_ptr<DicomModalityStoreJob>  job_;
-      size_t                                  position_;
-      size_t                                  countInstances_;
-      
-    public:
-      AsynchronousMove(ServerContext& context,
-                       const std::string& targetAet,
-                       const std::vector<std::string>& publicIds,
-                       const std::string& originatorAet,
-                       uint16_t originatorId) :
-        context_(context),
-        job_(new DicomModalityStoreJob(context)),
-        position_(0)
-      {
-        job_->SetDescription("C-MOVE");
-        //job_->SetPermissive(true);  // This was the behavior of Orthanc < 1.6.0
-        job_->SetPermissive(false);
-        job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle());
-
-        {
-          OrthancConfiguration::ReaderLock lock;
-          job_->SetRemoteModality(lock.GetConfiguration().GetModalityUsingAet(targetAet));
-        }
-
-        if (originatorId != 0)
-        {
-          job_->SetMoveOriginator(originatorAet, originatorId);
-        }
-
-        for (size_t i = 0; i < publicIds.size(); i++)
-        {
-          LOG(INFO) << "Sending resource " << publicIds[i] << " to modality \""
-                    << targetAet << "\" in asynchronous mode";
-
-          std::list<std::string> tmp;
-          context_.GetIndex().GetChildInstances(tmp, publicIds[i]);
-
-          countInstances_ = tmp.size();
-
-          job_->Reserve(job_->GetCommandsCount() + tmp.size());
-
-          for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
-          {
-            job_->AddInstance(*it);
-          }
-        }
-      }
-
-      virtual unsigned int GetSubOperationCount() const
-      {
-        return countInstances_;
-      }
-
-      virtual Status DoNext()
-      {
-        if (position_ >= countInstances_)
-        {
-          return Status_Failure;
-        }
-        
-        if (position_ == 0)
-        {
-          context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */);
-        }
-        
-        position_ ++;
-        return Status_Success;
-      }
-    };
-  }
-
-
-  bool OrthancMoveRequestHandler::LookupIdentifiers(std::vector<std::string>& publicIds,
-                                                    ResourceType level,
-                                                    const DicomMap& input)
-  {
-    DicomTag tag(0, 0);   // Dummy initialization
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        tag = DICOM_TAG_PATIENT_ID;
-        break;
-
-      case ResourceType_Study:
-        tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ? 
-               DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
-        break;
-        
-      case ResourceType_Series:
-        tag = DICOM_TAG_SERIES_INSTANCE_UID;
-        break;
-        
-      case ResourceType_Instance:
-        tag = DICOM_TAG_SOP_INSTANCE_UID;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    if (!input.HasTag(tag))
-    {
-      return false;
-    }
-
-    const DicomValue& value = input.GetValue(tag);
-    if (value.IsNull() ||
-        value.IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      const std::string& content = value.GetContent();
-
-      /**
-       * This tokenization fixes issue 154 ("Matching against list of
-       * UID-s by C-MOVE").
-       * https://bitbucket.org/sjodogne/orthanc/issues/154/
-       **/
-
-      std::vector<std::string> tokens;
-      Toolbox::TokenizeString(tokens, content, '\\');
-      for (size_t i = 0; i < tokens.size(); i++)
-      {
-        std::vector<std::string> matches;
-        context_.GetIndex().LookupIdentifierExact(matches, level, tag, tokens[i]);
-
-        // Concatenate "publicIds" with "matches"
-        publicIds.insert(publicIds.end(), matches.begin(), matches.end());
-      }
-
-      return true;
-    }
-  }
-
-
-  static IMoveRequestIterator* CreateIterator(ServerContext& context,
-                                              const std::string& targetAet,
-                                              const std::vector<std::string>& publicIds,
-                                              const std::string& originatorAet,
-                                              uint16_t originatorId)
-  {
-    if (publicIds.empty())
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "C-MOVE request matching no resource stored in Orthanc");
-    }
-    
-    bool synchronous;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      synchronous = lock.GetConfiguration().GetBooleanParameter("SynchronousCMove", true);
-    }
-
-    if (synchronous)
-    {
-      return new SynchronousMove(context, targetAet, publicIds, originatorAet, originatorId);
-    }
-    else
-    {
-      return new AsynchronousMove(context, targetAet, publicIds, originatorAet, originatorId);
-    }
-  }
-
-
-  IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet,
-                                                          const DicomMap& input,
-                                                          const std::string& originatorIp,
-                                                          const std::string& originatorAet,
-                                                          const std::string& calledAet,
-                                                          uint16_t originatorId)
-  {
-    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_move_scp_duration_ms");
-
-    LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\"";
-
-    {
-      DicomArray query(input);
-      for (size_t i = 0; i < query.GetSize(); i++)
-      {
-        if (!query.GetElement(i).GetValue().IsNull())
-        {
-          LOG(INFO) << "  " << query.GetElement(i).GetTag()
-                    << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
-                    << " = " << query.GetElement(i).GetValue().GetContent();
-        }
-      }
-    }
-
-    /**
-     * Retrieve the query level.
-     **/
-
-    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-
-    if (levelTmp == NULL ||
-        levelTmp->IsNull() ||
-        levelTmp->IsBinary())
-    {
-      // The query level is not present in the C-Move request, which
-      // does not follow the DICOM standard. This is for instance the
-      // behavior of Tudor DICOM. Try and automatically deduce the
-      // query level: Start from the instance level, going up to the
-      // patient level until a valid DICOM identifier is found.
-
-      std::vector<std::string> publicIds;
-
-      if (LookupIdentifiers(publicIds, ResourceType_Instance, input) ||
-          LookupIdentifiers(publicIds, ResourceType_Series, input) ||
-          LookupIdentifiers(publicIds, ResourceType_Study, input) ||
-          LookupIdentifiers(publicIds, ResourceType_Patient, input))
-      {
-        return CreateIterator(context_, targetAet, publicIds, originatorAet, originatorId);
-      }
-      else
-      {
-        // No identifier is present in the request.
-        throw OrthancException(ErrorCode_BadRequest, "Invalid fields in a C-MOVE request");
-      }
-    }
-
-    assert(levelTmp != NULL);
-    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());      
-
-
-    /**
-     * Lookup for the resource to be sent.
-     **/
-
-    std::vector<std::string> publicIds;
-
-    if (LookupIdentifiers(publicIds, level, input))
-    {
-      return CreateIterator(context_, targetAet, publicIds, originatorAet, originatorId);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadRequest, "Invalid fields in a C-MOVE request");
-    }
-  }
-}
--- a/OrthancServer/OrthancMoveRequestHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomNetworking/IMoveRequestHandler.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class OrthancMoveRequestHandler : public IMoveRequestHandler
-  {
-  private:
-    ServerContext& context_;
-
-    bool LookupIdentifiers(std::vector<std::string>& publicIds,
-                           ResourceType level,
-                           const DicomMap& input);
-
-  public:
-    OrthancMoveRequestHandler(ServerContext& context) :
-    context_(context)
-    {
-    }
-
-    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);
-  };
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,855 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/Logging.h"
-#include "../../Core/SerializationToolbox.h"
-#include "../OrthancConfiguration.h"
-#include "../ServerContext.h"
-#include "../ServerJobs/MergeStudyJob.h"
-#include "../ServerJobs/ResourceModificationJob.h"
-#include "../ServerJobs/SplitStudyJob.h"
-
-#include <boost/lexical_cast.hpp>
-#include <boost/algorithm/string/predicate.hpp>
-
-namespace Orthanc
-{
-  // Modification of DICOM instances ------------------------------------------
-
-  
-  static std::string GeneratePatientName(ServerContext& context)
-  {
-    uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence);
-    return "Anonymized" + boost::lexical_cast<std::string>(seq);
-  }
-
-
-  static void ParseModifyRequest(Json::Value& request,
-                                 DicomModification& target,
-                                 const RestApiPostCall& call)
-  {
-    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}'
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator());
-    }
-    
-    if (call.ParseJsonRequest(request))
-    {
-      target.ParseModifyRequest(request);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  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": true,"Remove":["Modality"]}' > Anonymized.dcm
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator());
-    }
-    
-    if (call.ParseJsonRequest(request) &&
-        request.isObject())
-    {
-      bool patientNameReplaced;
-      target.ParseAnonymizationRequest(patientNameReplaced, request);
-
-      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);
-      }
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  static void AnonymizeOrModifyInstance(DicomModification& modification,
-                                        RestApiPostCall& call,
-                                        bool transcode,
-                                        DicomTransferSyntax targetSyntax)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    std::string id = call.GetUriComponent("id", "");
-
-    std::unique_ptr<ParsedDicomFile> modified;
-
-    {
-      ServerContext::DicomCacheLocker locker(context, id);
-      modified.reset(locker.GetDicom().Clone(true));
-    }
-    
-    modification.Apply(*modified);
-
-    if (transcode)
-    {
-      IDicomTranscoder::DicomImage source;
-      source.AcquireParsed(*modified);  // "modified" is invalid below this point
-      
-      IDicomTranscoder::DicomImage transcoded;
-
-      std::set<DicomTransferSyntax> s;
-      s.insert(targetSyntax);
-      
-      if (context.Transcode(transcoded, source, s, true))
-      {      
-        call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
-                                      transcoded.GetBufferSize(), MimeType_Dicom);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot transcode to transfer syntax: " +
-                               std::string(GetTransferSyntaxUid(targetSyntax)));
-      }
-    }
-    else
-    {
-      modified->Answer(call.GetOutput());
-    }
-  }
-
-
-  static void ModifyInstance(RestApiPostCall& call)
-  {
-    DicomModification modification;
-    modification.SetAllowManualIdentifiers(true);
-
-    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))
-    {
-      modification.SetLevel(ResourceType_Study);
-    }
-    else if (modification.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      modification.SetLevel(ResourceType_Series);
-    }
-    else
-    {
-      modification.SetLevel(ResourceType_Instance);
-    }
-
-    static const char* TRANSCODE = "Transcode";
-    if (request.isMember(TRANSCODE))
-    {
-      std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
-      
-      DicomTransferSyntax syntax;
-      if (LookupTransferSyntax(syntax, s))
-      {
-        AnonymizeOrModifyInstance(modification, call, true, syntax);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s);
-      }
-    }
-    else
-    {
-      AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
-                                DicomTransferSyntax_LittleEndianImplicit /* unused */);
-    }
-  }
-
-
-  static void AnonymizeInstance(RestApiPostCall& call)
-  {
-    DicomModification modification;
-    modification.SetAllowManualIdentifiers(true);
-
-    Json::Value request;
-    ParseAnonymizationRequest(request, modification, call);
-
-    AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
-                              DicomTransferSyntax_LittleEndianImplicit /* unused */);
-  }
-
-
-  static void SetKeepSource(CleaningInstancesJob& job,
-                            const Json::Value& body)
-  {
-    static const char* KEEP_SOURCE = "KeepSource";
-    if (body.isMember(KEEP_SOURCE))
-    {
-      job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE));
-    }
-  }
-
-
-  static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification,
-                                    bool isAnonymization,
-                                    RestApiPostCall& call,
-                                    const Json::Value& body,
-                                    ResourceType level)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
-
-    job->SetModification(modification.release(), level, isAnonymization);
-    job->SetOrigin(call);
-    SetKeepSource(*job, body);
-
-    static const char* TRANSCODE = "Transcode";
-    if (body.isMember(TRANSCODE))
-    {
-      job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE));
-    }
-    
-    context.AddChildInstances(*job, call.GetUriComponent("id", ""));
-    job->AddTrailingStep();
-
-    OrthancRestApi::GetApi(call).SubmitCommandsJob
-      (call, job.release(), true /* synchronous by default */, body);
-  }
-
-
-  template <enum ResourceType resourceType>
-  static void ModifyResource(RestApiPostCall& call)
-  {
-    std::unique_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)
-  {
-    std::unique_ptr<DicomModification> modification(new DicomModification);
-
-    Json::Value body;
-    ParseAnonymizationRequest(body, *modification, call);
-
-    SubmitModificationJob(modification, true /* anonymization */,
-                          call, body, resourceType);
-  }
-
-
-  static void StoreCreatedInstance(std::string& id /* out */,
-                                   RestApiPostCall& call,
-                                   ParsedDicomFile& dicom,
-                                   bool sendAnswer)
-  {
-    DicomInstanceToStore toStore;
-    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
-    toStore.SetParsedDicomFile(dicom);
-
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    StoreStatus status = context.Store(id, toStore, StoreInstanceMode_Default);
-
-    if (status == StoreStatus_Failure)
-    {
-      throw OrthancException(ErrorCode_CannotStoreInstance);
-    }
-
-    if (sendAnswer)
-    {
-      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, id);
-    }
-  }
-
-
-  static void CreateDicomV1(ParsedDicomFile& dicom,
-                            RestApiPostCall& call,
-                            const Json::Value& request)
-  {
-    // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}'
-    // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}'
-
-    assert(request.isObject());
-    LOG(WARNING) << "Using a deprecated call to /tools/create-dicom";
-
-    Json::Value::Members members = request.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const std::string& name = members[i];
-      if (request[name].type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_CreateDicomNotString);
-      }
-
-      std::string value = request[name].asString();
-
-      DicomTag tag = FromDcmtkBridge::ParseTag(name);
-      if (tag == DICOM_TAG_PIXEL_DATA)
-      {
-        dicom.EmbedContent(value);
-      }
-      else
-      {
-        // This is V1, don't try and decode data URI scheme
-        dicom.ReplacePlainString(tag, value);
-      }
-    }
-  }
-
-
-  static void InjectTags(ParsedDicomFile& dicom,
-                         const Json::Value& tags,
-                         bool decodeBinaryTags,
-                         const std::string& privateCreator)
-  {
-    if (tags.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, "Tags field is not an array");
-    }
-
-    // Inject the user-specified tags
-    Json::Value::Members members = tags.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const std::string& name = members[i];
-      DicomTag tag = FromDcmtkBridge::ParseTag(name);
-
-      if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
-      {
-        if (tag != DICOM_TAG_PATIENT_ID &&
-            tag != DICOM_TAG_ACQUISITION_DATE &&
-            tag != DICOM_TAG_ACQUISITION_TIME &&
-            tag != DICOM_TAG_CONTENT_DATE &&
-            tag != DICOM_TAG_CONTENT_TIME &&
-            tag != DICOM_TAG_INSTANCE_CREATION_DATE &&
-            tag != DICOM_TAG_INSTANCE_CREATION_TIME &&
-            tag != DICOM_TAG_SERIES_DATE &&
-            tag != DICOM_TAG_SERIES_TIME &&
-            tag != DICOM_TAG_STUDY_DATE &&
-            tag != DICOM_TAG_STUDY_TIME &&
-            dicom.HasTag(tag))
-        {
-          throw OrthancException(ErrorCode_CreateDicomOverrideTag, name);
-        }
-
-        if (tag == DICOM_TAG_PIXEL_DATA)
-        {
-          throw OrthancException(ErrorCode_CreateDicomUseContent);
-        }
-        else
-        {
-          dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent, privateCreator);
-        }
-      }
-    }
-  }
-
-
-  static void CreateSeries(RestApiPostCall& call,
-                           ParsedDicomFile& base /* in */,
-                           const Json::Value& content,
-                           bool decodeBinaryTags,
-                           const std::string& privateCreator)
-  {
-    assert(content.isArray());
-    assert(content.size() > 0);
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    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;
-
-    try
-    {
-      for (Json::ArrayIndex i = 0; i < content.size(); i++)
-      {
-        std::unique_ptr<ParsedDicomFile> dicom(base.Clone(false));
-        const Json::Value* payload = NULL;
-
-        if (content[i].type() == Json::stringValue)
-        {
-          payload = &content[i];
-        }
-        else if (content[i].type() == Json::objectValue)
-        {
-          if (!content[i].isMember("Content"))
-          {
-            throw OrthancException(ErrorCode_CreateDicomNoPayload);
-          }
-
-          payload = &content[i]["Content"];
-
-          if (content[i].isMember("Tags"))
-          {
-            InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags, privateCreator);
-          }
-        }
-
-        if (payload == NULL ||
-            payload->type() != Json::stringValue)
-        {
-          throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme);
-        }
-
-        dicom->EmbedContent(payload->asString());
-        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, false);
-      }
-    }
-    catch (OrthancException&)
-    {
-      // Error: Remove the newly-created series
-      
-      std::string series;
-      if (context.GetIndex().LookupParent(series, someInstance))
-      {
-        Json::Value dummy;
-        context.GetIndex().DeleteResource(dummy, series, ResourceType_Series);
-      }
-
-      throw;
-    }
-
-    std::string series;
-    if (context.GetIndex().LookupParent(series, someInstance))
-    {
-      OrthancRestApi::GetApi(call).AnswerStoredResource(call, series, ResourceType_Series, StoreStatus_Success);
-    }
-  }
-
-
-  static void CreateDicomV2(RestApiPostCall& call,
-                            const Json::Value& request)
-  {
-    assert(request.isObject());
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    if (!request.isMember("Tags") ||
-        request["Tags"].type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    ParsedDicomFile dicom(true);
-
-    {
-      Encoding encoding;
-
-      if (request["Tags"].isMember("SpecificCharacterSet"))
-      {
-        const char* tmp = request["Tags"]["SpecificCharacterSet"].asCString();
-        if (!GetDicomEncoding(encoding, tmp))
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Unknown specific character set: " + std::string(tmp));
-        }
-      }
-      else
-      {
-        encoding = GetDefaultDicomEncoding();
-      }
-
-      dicom.SetEncoding(encoding);
-    }
-
-    ResourceType parentType = ResourceType_Instance;
-
-    if (request.isMember("Parent"))
-    {
-      // Locate the parent tags
-      std::string parent = request["Parent"].asString();
-      if (!context.GetIndex().LookupResourceType(parentType, parent))
-      {
-        throw OrthancException(ErrorCode_CreateDicomBadParent);
-      }
-
-      if (parentType == ResourceType_Instance)
-      {
-        throw OrthancException(ErrorCode_CreateDicomParentIsInstance);
-      }
-
-      // Select one existing child instance of the parent resource, to
-      // retrieve all its tags
-      Json::Value siblingTags;
-      std::string siblingInstanceId;
-
-      {
-        // Retrieve all the instances of the parent resource
-        std::list<std::string>  siblingInstances;
-        context.GetIndex().GetChildInstances(siblingInstances, parent);
-
-        if (siblingInstances.empty())
-	{
-	  // Error: No instance (should never happen)
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        siblingInstanceId = siblingInstances.front();
-        context.ReadDicomAsJson(siblingTags, siblingInstanceId);
-      }
-
-
-      // Choose the same encoding as the parent resource
-      {
-        static const char* SPECIFIC_CHARACTER_SET = "0008,0005";
-
-        if (siblingTags.isMember(SPECIFIC_CHARACTER_SET))
-        {
-          Encoding encoding;
-
-          if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember("Value") ||
-              siblingTags[SPECIFIC_CHARACTER_SET]["Value"].type() != Json::stringValue ||
-              !GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET]["Value"].asCString()))
-          {
-            LOG(WARNING) << "Instance with an incorrect Specific Character Set, "
-                         << "using the default Orthanc encoding: " << siblingInstanceId;
-            encoding = GetDefaultDicomEncoding();
-          }
-
-          dicom.SetEncoding(encoding);
-        }
-      }
-
-
-      // Retrieve the tags for all the parent modules
-      typedef std::set<DicomTag> ModuleTags;
-      ModuleTags moduleTags;
-
-      ResourceType type = parentType;
-      for (;;)
-      {
-        DicomTag::AddTagsForModule(moduleTags, GetModule(type));
-      
-        if (type == ResourceType_Patient)
-        {
-          break;   // We're done
-        }
-
-        // Go up
-        std::string tmp;
-        if (!context.GetIndex().LookupParent(tmp, parent))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        parent = tmp;
-        type = GetParentResourceType(type);
-      }
-
-      for (ModuleTags::const_iterator it = moduleTags.begin();
-           it != moduleTags.end(); ++it)
-      {
-        std::string t = it->Format();
-        if (siblingTags.isMember(t))
-        {
-          const Json::Value& tag = siblingTags[t];
-          if (tag["Type"] == "Null")
-          {
-            dicom.ReplacePlainString(*it, "");
-          }
-          else if (tag["Type"] == "String")
-          {
-            std::string value = tag["Value"].asString();  // This is an UTF-8 value (as it comes from JSON)
-            dicom.ReplacePlainString(*it, value);
-          }
-        }
-      }
-    }
-
-
-    bool decodeBinaryTags = true;
-    if (request.isMember("InterpretBinaryTags"))
-    {
-      const Json::Value& v = request["InterpretBinaryTags"];
-      if (v.type() != Json::booleanValue)
-      {
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-
-      decodeBinaryTags = v.asBool();
-    }
-
-
-    // New argument in Orthanc 1.6.0
-    std::string privateCreator;
-    if (request.isMember("PrivateCreator"))
-    {
-      const Json::Value& v = request["PrivateCreator"];
-      if (v.type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_BadRequest);
-      }
-
-      privateCreator = v.asString();
-    }
-    else
-    {
-      OrthancConfiguration::ReaderLock lock;
-      privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
-    }
-
-    
-    // Inject time-related information
-    std::string date, 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.ReplacePlainString(DICOM_TAG_SERIES_DATE, date);
-      dicom.ReplacePlainString(DICOM_TAG_SERIES_TIME, time);
-    }
-
-    if (parentType == ResourceType_Patient ||
-        parentType == ResourceType_Instance /* no parent */)
-    {
-      dicom.ReplacePlainString(DICOM_TAG_STUDY_DATE, date);
-      dicom.ReplacePlainString(DICOM_TAG_STUDY_TIME, time);
-    }
-
-
-    InjectTags(dicom, request["Tags"], decodeBinaryTags, privateCreator);
-
-
-    // Inject the content (either an image, or a PDF file)
-    if (request.isMember("Content"))
-    {
-      const Json::Value& content = request["Content"];
-
-      if (content.type() == Json::stringValue)
-      {
-        dicom.EmbedContent(request["Content"].asString());
-
-      }
-      else if (content.type() == Json::arrayValue)
-      {
-        if (content.size() > 0)
-        {
-          // Let's create a series instead of a single instance
-          CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator);
-          return;
-        }
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme);
-      }
-    }
-
-    std::string id;
-    StoreCreatedInstance(id, call, dicom, true);
-  }
-
-
-  static void CreateDicom(RestApiPostCall& call)
-  {
-    Json::Value request;
-    if (!call.ParseJsonRequest(request) ||
-        !request.isObject())
-    {
-      throw OrthancException(ErrorCode_BadRequest);
-    }
-
-    if (request.isMember("Tags"))
-    {
-      CreateDicomV2(call, request);
-    }
-    else
-    {
-      // Compatibility with Orthanc <= 0.9.3
-      ParsedDicomFile dicom(true);
-      CreateDicomV1(dicom, call, request);
-
-      std::string id;
-      StoreCreatedInstance(id, call, dicom, true);
-    }
-  }
-
-
-  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::unique_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();
-
-    SetKeepSource(*job, request);
-
-    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::unique_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();
-
-    SetKeepSource(*job, request);
-
-    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<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<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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,333 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../../Core/Compression/GzipCompressor.h"
-#include "../../Core/Logging.h"
-#include "../../Core/MetricsRegistry.h"
-#include "../../Core/SerializationToolbox.h"
-#include "../ServerContext.h"
-
-#include <boost/algorithm/string/predicate.hpp>
-
-namespace Orthanc
-{
-  static void SetupResourceAnswer(Json::Value& result,
-                                  const std::string& publicId,
-                                  ResourceType resourceType,
-                                  StoreStatus status)
-  {
-    result = Json::objectValue;
-
-    if (status != StoreStatus_Failure)
-    {
-      result["ID"] = publicId;
-      result["Path"] = GetBasePath(resourceType, publicId);
-    }
-    
-    result["Status"] = EnumerationToString(status);
-  }
-
-
-  void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call,
-                                            DicomInstanceToStore& instance,
-                                            StoreStatus status,
-                                            const std::string& instanceId) const
-  {
-    Json::Value result;
-    SetupResourceAnswer(result, instanceId, ResourceType_Instance, status);
-
-    result["ParentPatient"] = instance.GetHasher().HashPatient();
-    result["ParentStudy"] = instance.GetHasher().HashStudy();
-    result["ParentSeries"] = instance.GetHasher().HashSeries();
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  void OrthancRestApi::AnswerStoredResource(RestApiPostCall& call,
-                                            const std::string& publicId,
-                                            ResourceType resourceType,
-                                            StoreStatus status) const
-  {
-    Json::Value result;
-    SetupResourceAnswer(result, publicId, resourceType, status);
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  void OrthancRestApi::ResetOrthanc(RestApiPostCall& call)
-  {
-    OrthancRestApi::GetApi(call).leaveBarrier_ = true;
-    OrthancRestApi::GetApi(call).resetRequestReceived_ = true;
-    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-  }
-
-
-  void OrthancRestApi::ShutdownOrthanc(RestApiPostCall& call)
-  {
-    OrthancRestApi::GetApi(call).leaveBarrier_ = true;
-    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-    LOG(WARNING) << "Shutdown request received";
-  }
-
-
-
-
-
-  // Upload of DICOM files through HTTP ---------------------------------------
-
-  static void UploadDicomFile(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    LOG(INFO) << "Receiving a DICOM file of " << call.GetBodySize() << " bytes through HTTP";
-
-    if (call.GetBodySize() == 0)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Received an empty DICOM file");
-    }
-
-    // The lifetime of "dicom" must be longer than "toStore", as the
-    // latter can possibly store a reference to the former (*)
-    std::string dicom;
-
-    DicomInstanceToStore toStore;
-    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
-
-    if (boost::iequals(call.GetHttpHeader("content-encoding", ""), "gzip"))
-    {
-      GzipCompressor compressor;
-      compressor.Uncompress(dicom, call.GetBodyData(), call.GetBodySize());
-      toStore.SetBuffer(dicom.c_str(), dicom.size());  // (*)
-    }
-    else
-    {
-      toStore.SetBuffer(call.GetBodyData(), call.GetBodySize());
-    }    
-
-    std::string publicId;
-    StoreStatus status = context.Store(publicId, toStore, StoreInstanceMode_Default);
-
-    OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, publicId);
-  }
-
-
-
-  // Registration of the various REST handlers --------------------------------
-
-  OrthancRestApi::OrthancRestApi(ServerContext& context) : 
-    context_(context),
-    leaveBarrier_(false),
-    resetRequestReceived_(false),
-    activeRequests_(context.GetMetricsRegistry(), 
-                    "orthanc_rest_api_active_requests", 
-                    MetricsType_MaxOver10Seconds)
-  {
-    RegisterSystem();
-
-    RegisterChanges();
-    RegisterResources();
-    RegisterModalities();
-    RegisterAnonymizeModify();
-    RegisterArchive();
-
-    Register("/instances", UploadDicomFile);
-
-    // Auto-generated directories
-    Register("/tools", RestApi::AutoListChildren);
-    Register("/tools/reset", ResetOrthanc);
-    Register("/tools/shutdown", ShutdownOrthanc);
-    Register("/instances/{id}/frames/{frame}", RestApi::AutoListChildren);
-  }
-
-
-  bool OrthancRestApi::Handle(HttpOutput& output,
-                              RequestOrigin origin,
-                              const char* remoteIp,
-                              const char* username,
-                              HttpMethod method,
-                              const UriComponents& uri,
-                              const Arguments& headers,
-                              const GetArguments& getArguments,
-                              const void* bodyData,
-                              size_t bodySize)
-  {
-    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_rest_api_duration_ms");
-    MetricsRegistry::ActiveCounter counter(activeRequests_);
-
-    return RestApi::Handle(output, origin, remoteIp, username, method,
-                           uri, headers, getArguments, bodyData, bodySize);
-  }
-
-
-  ServerContext& OrthancRestApi::GetContext(RestApiCall& call)
-  {
-    return GetApi(call).context_;
-  }
-
-
-  ServerIndex& OrthancRestApi::GetIndex(RestApiCall& call)
-  {
-    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";
-
-  
-  bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous,
-                                               const Json::Value& body)
-  {
-    if (body.type() != Json::objectValue)
-    {
-      return isDefaultSynchronous;
-    }
-    else if (body.isMember(KEY_SYNCHRONOUS))
-    {
-      return SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS);
-    }
-    else if (body.isMember(KEY_ASYNCHRONOUS))
-    {
-      return !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS);
-    }
-    else
-    {
-      return isDefaultSynchronous;
-    }
-  }
-
-  
-  unsigned int OrthancRestApi::GetJobRequestPriority(const Json::Value& body)
-  {
-    if (body.type() != Json::objectValue ||
-        !body.isMember(KEY_PRIORITY))
-    {
-      return 0;   // Default priority
-    }
-    else 
-    {
-      return SerializationToolbox::ReadInteger(body, KEY_PRIORITY);
-    }
-  }
-  
-
-  void OrthancRestApi::SubmitGenericJob(RestApiOutput& output,
-                                        ServerContext& context,
-                                        IJob* job,
-                                        bool synchronous,
-                                        int priority)
-  {
-    std::unique_ptr<IJob> raii(job);
-    
-    if (job == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    if (synchronous)
-    {
-      Json::Value successContent;
-      context.GetJobsEngine().GetRegistry().SubmitAndWait
-        (successContent, raii.release(), priority);
-
-      // Success in synchronous execution
-      output.AnswerJson(successContent);
-    }
-    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;
-      output.AnswerJson(v);
-    }
-  }
-
-  
-  void OrthancRestApi::SubmitGenericJob(RestApiPostCall& call,
-                                        IJob* job,
-                                        bool isDefaultSynchronous,
-                                        const Json::Value& body) const
-  {
-    std::unique_ptr<IJob> raii(job);
-
-    if (body.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    bool synchronous = IsSynchronousJobRequest(isDefaultSynchronous, body);
-    int priority = GetJobRequestPriority(body);
-
-    SubmitGenericJob(call.GetOutput(), context_, raii.release(), synchronous, priority);
-  }
-
-  
-  void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call,
-                                         SetOfCommandsJob* job,
-                                         bool isDefaultSynchronous,
-                                         const Json::Value& body) const
-  {
-    std::unique_ptr<SetOfCommandsJob> raii(job);
-    
-    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);
-    }
-
-    SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
-  }
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,143 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/SetOfCommandsJob.h"
-#include "../../Core/MetricsRegistry.h"
-#include "../../Core/RestApi/RestApi.h"
-#include "../ServerEnumerations.h"
-
-#include <set>
-
-namespace Orthanc
-{
-  class ServerContext;
-  class ServerIndex;
-  class DicomInstanceToStore;
-
-  class OrthancRestApi : public RestApi
-  {
-  public:
-    typedef std::set<std::string> SetOfStrings;
-
-  private:
-    ServerContext&                  context_;
-    bool                            leaveBarrier_;
-    bool                            resetRequestReceived_;
-    MetricsRegistry::SharedMetrics  activeRequests_;
-
-    void RegisterSystem();
-
-    void RegisterChanges();
-
-    void RegisterResources();
-
-    void RegisterModalities();
-
-    void RegisterAnonymizeModify();
-
-    void RegisterArchive();
-
-    static void ResetOrthanc(RestApiPostCall& call);
-
-    static void ShutdownOrthanc(RestApiPostCall& call);
-
-  public:
-    OrthancRestApi(ServerContext& context);
-
-    virtual bool Handle(HttpOutput& output,
-                        RequestOrigin origin,
-                        const char* remoteIp,
-                        const char* username,
-                        HttpMethod method,
-                        const UriComponents& uri,
-                        const Arguments& headers,
-                        const GetArguments& getArguments,
-                        const void* bodyData,
-                        size_t bodySize) ORTHANC_OVERRIDE;
-
-    const bool& LeaveBarrierFlag() const
-    {
-      return leaveBarrier_;
-    }
-
-    bool IsResetRequestReceived() const
-    {
-      return resetRequestReceived_;
-    }
-
-    static OrthancRestApi& GetApi(RestApiCall& call)
-    {
-      return dynamic_cast<OrthancRestApi&>(call.GetContext());
-    }
-
-    static ServerContext& GetContext(RestApiCall& call);
-
-    static ServerIndex& GetIndex(RestApiCall& call);
-
-    // WARNING: "instanceId" can be different from
-    // "instance.GetHasher().HashInstance()" if transcoding is enabled
-    void AnswerStoredInstance(RestApiPostCall& call,
-                              DicomInstanceToStore& instance,
-                              StoreStatus status,
-                              const std::string& instanceId) const;
-
-    void AnswerStoredResource(RestApiPostCall& call,
-                              const std::string& publicId,
-                              ResourceType resourceType,
-                              StoreStatus status) const;
-
-    static bool IsSynchronousJobRequest(bool isDefaultSynchronous,
-                                        const Json::Value& body);
-    
-    static unsigned int GetJobRequestPriority(const Json::Value& body);
-    
-    static void SubmitGenericJob(RestApiOutput& output,
-                                 ServerContext& context,
-                                 IJob* job,
-                                 bool synchronous,
-                                 int priority);
-    
-    void SubmitGenericJob(RestApiPostCall& call,
-                          IJob* job,
-                          bool isDefaultSynchronous,
-                          const Json::Value& body) const;
-
-    void SubmitCommandsJob(RestApiPostCall& call,
-                           SetOfCommandsJob* job,
-                           bool isDefaultSynchronous,
-                           const Json::Value& body) const;
-  };
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,334 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../../Core/HttpServer/FilesystemHttpSender.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/SerializationToolbox.h"
-#include "../OrthancConfiguration.h"
-#include "../ServerContext.h"
-#include "../ServerJobs/ArchiveJob.h"
-
-
-namespace Orthanc
-{
-  static const char* const KEY_RESOURCES = "Resources";
-  static const char* const KEY_EXTENDED = "Extended";
-  static const char* const KEY_TRANSCODE = "Transcode";
-  
-  static void AddResourcesOfInterestFromArray(ArchiveJob& job,
-                                              const Json::Value& resources)
-  {
-    if (resources.type() != Json::arrayValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Expected a list of strings (Orthanc identifiers)");
-    }
-    
-    for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
-    {
-      if (resources[i].type() != Json::stringValue)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Expected a list of strings (Orthanc identifiers)");
-      }
-      else
-      {
-        job.AddResource(resources[i].asString());
-      }
-    }
-  }
-
-  
-  static void AddResourcesOfInterest(ArchiveJob& job         /* inout */,
-                                     const Json::Value& body /* in */)
-  {
-    if (body.type() == Json::arrayValue)
-    {
-      AddResourcesOfInterestFromArray(job, body);
-    }
-    else if (body.type() == Json::objectValue)
-    {
-      if (body.isMember(KEY_RESOURCES))
-      {
-        AddResourcesOfInterestFromArray(job, body[KEY_RESOURCES]);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing field " + std::string(KEY_RESOURCES) +
-                               " in the JSON body");
-      }
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-
-  static DicomTransferSyntax GetTransferSyntax(const std::string& value)
-  {
-    DicomTransferSyntax syntax;
-    if (LookupTransferSyntax(syntax, value))
-    {
-      return syntax;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Unknown transfer syntax: " + value);
-    }
-  }
-  
-  
-  static void GetJobParameters(bool& synchronous,            /* out */
-                               bool& extended,               /* out */
-                               bool& transcode,              /* out */
-                               DicomTransferSyntax& syntax,  /* out */
-                               int& priority,                /* out */
-                               const Json::Value& body,      /* in */
-                               const bool defaultExtended    /* in */)
-  {
-    synchronous = OrthancRestApi::IsSynchronousJobRequest
-      (true /* synchronous by default */, body);
-
-    priority = OrthancRestApi::GetJobRequestPriority(body);
-
-    if (body.type() == Json::objectValue &&
-        body.isMember(KEY_EXTENDED))
-    {
-      extended = SerializationToolbox::ReadBoolean(body, KEY_EXTENDED);
-    }
-    else
-    {
-      extended = defaultExtended;
-    }
-
-    if (body.type() == Json::objectValue &&
-        body.isMember(KEY_TRANSCODE))
-    {
-      transcode = true;
-      syntax = GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
-    }
-    else
-    {
-      transcode = false;
-    }
-  }
-
-
-  static void SubmitJob(RestApiOutput& output,
-                        ServerContext& context,
-                        std::unique_ptr<ArchiveJob>& job,
-                        int priority,
-                        bool synchronous,
-                        const std::string& filename)
-  {
-    if (job.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    job->SetDescription("REST API");
-
-    if (synchronous)
-    {
-      boost::shared_ptr<TemporaryFile> tmp;
-
-      {
-        OrthancConfiguration::ReaderLock lock;
-        tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
-      }
-
-      job->SetSynchronousTarget(tmp);
-    
-      Json::Value publicContent;
-      context.GetJobsEngine().GetRegistry().SubmitAndWait
-        (publicContent, job.release(), priority);
-      
-      {
-        // The archive is now created: Prepare the sending of the ZIP file
-        FilesystemHttpSender sender(tmp->GetPath(), MimeType_Zip);
-        sender.SetContentFilename(filename);
-
-        // Send the ZIP
-        output.AnswerStream(sender);
-      }
-    }
-    else
-    {
-      OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority);
-    }
-  }
-
-  
-  template <bool IS_MEDIA,
-            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
-  static void CreateBatch(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value body;
-    if (call.ParseJsonRequest(body))
-    {
-      bool synchronous, extended, transcode;
-      DicomTransferSyntax transferSyntax;
-      int priority;
-      GetJobParameters(synchronous, extended, transcode, transferSyntax,
-                       priority, body, DEFAULT_IS_EXTENDED);
-      
-      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
-      AddResourcesOfInterest(*job, body);
-
-      if (transcode)
-      {
-        job->SetTranscode(transferSyntax);
-      }
-      
-      SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip");
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Expected a list of resources to archive in the body");
-    }
-  }
-  
-
-  template <bool IS_MEDIA,
-            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
-  static void CreateSingleGet(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string id = call.GetUriComponent("id", "");
-
-    bool extended;
-    if (IS_MEDIA)
-    {
-      extended = call.HasArgument("extended");
-    }
-    else
-    {
-      extended = false;
-    }
-
-    std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
-    job->AddResource(id);
-
-    static const char* const TRANSCODE = "transcode";
-    if (call.HasArgument(TRANSCODE))
-    {
-      job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, "")));
-    }
-
-    SubmitJob(call.GetOutput(), context, job, 0 /* priority */,
-              true /* synchronous */, id + ".zip");
-  }
-
-
-  template <bool IS_MEDIA,
-            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
-  static void CreateSinglePost(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string id = call.GetUriComponent("id", "");
-
-    Json::Value body;
-    if (call.ParseJsonRequest(body))
-    {
-      bool synchronous, extended, transcode;
-      DicomTransferSyntax transferSyntax;
-      int priority;
-      GetJobParameters(synchronous, extended, transcode, transferSyntax,
-                       priority, body, DEFAULT_IS_EXTENDED);
-      
-      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
-      job->AddResource(id);
-
-      if (transcode)
-      {
-        job->SetTranscode(transferSyntax);
-      }
-
-      SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip");
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
-
-    
-  void OrthancRestApi::RegisterArchive()
-  {
-    Register("/patients/{id}/archive",
-             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
-    Register("/studies/{id}/archive",
-             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
-    Register("/series/{id}/archive",
-             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
-
-    Register("/patients/{id}/archive",
-             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
-    Register("/studies/{id}/archive",
-             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
-    Register("/series/{id}/archive",
-             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
-
-    Register("/patients/{id}/media",
-             CreateSingleGet<true /* media */, false /* not extended by default */>);
-    Register("/studies/{id}/media",
-             CreateSingleGet<true /* media */, false /* not extended by default */>);
-    Register("/series/{id}/media",
-             CreateSingleGet<true /* media */, false /* not extended by default */>);
-
-    Register("/patients/{id}/media",
-             CreateSinglePost<true /* media */, false /* not extended by default */>);
-    Register("/studies/{id}/media",
-             CreateSinglePost<true /* media */, false /* not extended by default */>);
-    Register("/series/{id}/media",
-             CreateSinglePost<true /* media */, false /* not extended by default */>);
-
-    Register("/tools/create-archive",
-             CreateBatch<false /* ZIP */,  false /* extended makes no sense in ZIP */>);
-    Register("/tools/create-media",
-             CreateBatch<true /* media */, false /* not extended by default */>);
-    Register("/tools/create-media-extended",
-             CreateBatch<true /* media */, true /* extended by default */>);
-  }
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestChanges.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,141 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  // Changes API --------------------------------------------------------------
- 
-  static void GetSinceAndLimit(int64_t& since,
-                               unsigned int& limit,
-                               bool& last,
-                               const RestApiGetCall& call)
-  {
-    static const unsigned int DEFAULT_LIMIT = 100;
-    
-    if (call.HasArgument("last"))
-    {
-      last = true;
-      return;
-    }
-
-    last = false;
-
-    try
-    {
-      since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0"));
-      limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", boost::lexical_cast<std::string>(DEFAULT_LIMIT)));
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      since = 0;
-      limit = DEFAULT_LIMIT;
-      return;
-    }
-  }
-
-  static void GetChanges(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    //std::string filter = GetArgument(getArguments, "filter", "");
-    int64_t since;
-    unsigned int limit;
-    bool last;
-    GetSinceAndLimit(since, limit, last, call);
-
-    Json::Value result;
-    if (last)
-    {
-      context.GetIndex().GetLastChange(result);
-    }
-    else
-    {
-      context.GetIndex().GetChanges(result, since, limit);
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  static void DeleteChanges(RestApiDeleteCall& call)
-  {
-    OrthancRestApi::GetIndex(call).DeleteChanges();
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  // Exports API --------------------------------------------------------------
- 
-  static void GetExports(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    int64_t since;
-    unsigned int limit;
-    bool last;
-    GetSinceAndLimit(since, limit, last, call);
-
-    Json::Value result;
-    if (last)
-    {
-      context.GetIndex().GetLastExportedResource(result);
-    }
-    else
-    {
-      context.GetIndex().GetExportedResources(result, since, limit);
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  static void DeleteExports(RestApiDeleteCall& call)
-  {
-    OrthancRestApi::GetIndex(call).DeleteExportedResources();
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-  
-
-  void OrthancRestApi::RegisterChanges()
-  {
-    Register("/changes", GetChanges);
-    Register("/changes", DeleteChanges);
-    Register("/exports", GetExports);
-    Register("/exports", DeleteExports);
-  }
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1647 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../../Core/Cache/SharedArchive.h"
-#include "../../Core/DicomNetworking/DicomAssociation.h"
-#include "../../Core/DicomNetworking/DicomControlUserConnection.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/Logging.h"
-#include "../../Core/SerializationToolbox.h"
-
-#include "../OrthancConfiguration.h"
-#include "../QueryRetrieveHandler.h"
-#include "../ServerContext.h"
-#include "../ServerJobs/DicomModalityStoreJob.h"
-#include "../ServerJobs/DicomMoveScuJob.h"
-#include "../ServerJobs/OrthancPeerStoreJob.h"
-#include "../ServerToolbox.h"
-#include "../StorageCommitmentReports.h"
-
-
-namespace Orthanc
-{
-  static const char* const KEY_LEVEL = "Level";
-  static const char* const KEY_LOCAL_AET = "LocalAet";
-  static const char* const KEY_NORMALIZE = "Normalize";
-  static const char* const KEY_QUERY = "Query";
-  static const char* const KEY_RESOURCES = "Resources";
-  static const char* const KEY_TARGET_AET = "TargetAet";
-  static const char* const KEY_TIMEOUT = "Timeout";
-  static const char* const SOP_CLASS_UID = "SOPClassUID";
-  static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
-  
-  static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
-  {
-    OrthancConfiguration::ReaderLock lock;
-    return lock.GetConfiguration().GetModalityUsingSymbolicName(name);
-  }
-
-
-  static void InjectAssociationTimeout(DicomAssociationParameters& params,
-                                       const Json::Value& body)
-  {
-    if (body.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
-    }
-    else if (body.isMember(KEY_TIMEOUT))
-    {
-      // New in Orthanc 1.7.0
-      params.SetTimeout(SerializationToolbox::ReadUnsignedInteger(body, KEY_TIMEOUT));
-    }
-  }
-
-  static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call,
-                                                             const Json::Value& body)
-  {   
-    const std::string& localAet =
-      OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle();
-    const RemoteModalityParameters remote =
-      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-
-    DicomAssociationParameters params(localAet, remote);
-    InjectAssociationTimeout(params, body);
-    
-    return params;
-  }
-
-
-  static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call)
-  {
-    Json::Value body;
-    call.ParseJsonRequest(body);
-    return GetAssociationParameters(call, body);
-  }
-  
-
-  /***************************************************************************
-   * DICOM C-Echo SCU
-   ***************************************************************************/
-
-  static void DicomEcho(RestApiPostCall& call)
-  {
-    DicomControlUserConnection connection(GetAssociationParameters(call));
-
-    if (connection.Echo())
-    {
-      // Echo has succeeded
-      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-      return;
-    }
-    else
-    {
-      // Echo has failed
-      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
-    }
-  }
-
-
-
-  /***************************************************************************
-   * DICOM C-Find SCU => DEPRECATED!
-   ***************************************************************************/
-
-  static bool MergeQueryAndTemplate(DicomMap& result,
-                                    const RestApiCall& call)
-  {
-    Json::Value query;
-
-    if (!call.ParseJsonRequest(query) ||
-        query.type() != Json::objectValue)
-    {
-      return false;
-    }
-
-    Json::Value::Members members = query.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
-      result.SetValue(t, query[members[i]].asString(), false);
-    }
-
-    return true;
-  }
-
-
-  static void FindPatient(DicomFindAnswers& result,
-                          DicomControlUserConnection& connection,
-                          const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the patient
-    DicomMap s;
-    fields.ExtractPatientInformation(s);
-    connection.Find(result, ResourceType_Patient, s, true /* normalize */);
-  }
-
-
-  static void FindStudy(DicomFindAnswers& result,
-                        DicomControlUserConnection& connection,
-                        const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the study
-    DicomMap s;
-    fields.ExtractStudyInformation(s);
-
-    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
-    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
-    s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY);
-
-    connection.Find(result, ResourceType_Study, s, true /* normalize */);
-  }
-
-  static void FindSeries(DicomFindAnswers& result,
-                         DicomControlUserConnection& connection,
-                         const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the series
-    DicomMap s;
-    fields.ExtractSeriesInformation(s);
-
-    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
-    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
-    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
-
-    connection.Find(result, ResourceType_Series, s, true /* normalize */);
-  }
-
-  static void FindInstance(DicomFindAnswers& result,
-                           DicomControlUserConnection& connection,
-                           const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the instance
-    DicomMap s;
-    fields.ExtractInstanceInformation(s);
-
-    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
-    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
-    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
-    s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID);
-
-    connection.Find(result, ResourceType_Instance, s, true /* normalize */);
-  }
-
-
-  static void DicomFindPatient(RestApiPostCall& call)
-  {
-    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
-
-    DicomMap fields;
-    DicomMap::SetupFindPatientTemplate(fields);
-    if (!MergeQueryAndTemplate(fields, call))
-    {
-      return;
-    }
-
-    DicomFindAnswers answers(false);
-
-    {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
-      FindPatient(answers, connection, fields);
-    }
-
-    Json::Value result;
-    answers.ToJson(result, true);
-    call.GetOutput().AnswerJson(result);
-  }
-
-  static void DicomFindStudy(RestApiPostCall& call)
-  {
-    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
-
-    DicomMap fields;
-    DicomMap::SetupFindStudyTemplate(fields);
-    if (!MergeQueryAndTemplate(fields, call))
-    {
-      return;
-    }
-
-    if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
-        fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2)
-    {
-      return;
-    }        
-      
-    DicomFindAnswers answers(false);
-
-    {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
-      FindStudy(answers, connection, fields);
-    }
-
-    Json::Value result;
-    answers.ToJson(result, true);
-    call.GetOutput().AnswerJson(result);
-  }
-
-  static void DicomFindSeries(RestApiPostCall& call)
-  {
-    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
-
-    DicomMap fields;
-    DicomMap::SetupFindSeriesTemplate(fields);
-    if (!MergeQueryAndTemplate(fields, call))
-    {
-      return;
-    }
-
-    if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
-         fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
-        fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2)
-    {
-      return;
-    }        
-         
-    DicomFindAnswers answers(false);
-
-    {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
-      FindSeries(answers, connection, fields);
-    }
-
-    Json::Value result;
-    answers.ToJson(result, true);
-    call.GetOutput().AnswerJson(result);
-  }
-
-  static void DicomFindInstance(RestApiPostCall& call)
-  {
-    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
-
-    DicomMap fields;
-    DicomMap::SetupFindInstanceTemplate(fields);
-    if (!MergeQueryAndTemplate(fields, call))
-    {
-      return;
-    }
-
-    if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
-         fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
-        fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2 ||
-        fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent().size() <= 2)
-    {
-      return;
-    }        
-         
-    DicomFindAnswers answers(false);
-
-    {
-      DicomControlUserConnection connection(GetAssociationParameters(call));
-      FindInstance(answers, connection, fields);
-    }
-
-    Json::Value result;
-    answers.ToJson(result, true);
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  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();
-
-    DicomMap m;
-    DicomMap::SetupFindPatientTemplate(m);
-    if (!MergeQueryAndTemplate(m, call))
-    {
-      return;
-    }
- 
-    DicomControlUserConnection connection(GetAssociationParameters(call));
-    
-    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;
-      patients.ToJson(patient, i, true);
-
-      DicomMap::SetupFindStudyTemplate(m);
-      if (!MergeQueryAndTemplate(m, call))
-      {
-        return;
-      }
-
-      CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
-
-      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;
-        studies.ToJson(study, j, true);
-
-        DicomMap::SetupFindSeriesTemplate(m);
-        if (!MergeQueryAndTemplate(m, call))
-        {
-          return;
-        }
-
-        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;
-          series.ToJson(series2, k, true);
-          study["Series"].append(series2);
-        }
-
-        patient["Studies"].append(study);
-      }
-
-      result.append(patient);
-    }
-    
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-
-  /***************************************************************************
-   * DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0
-   ***************************************************************************/
-
-  static void AnswerQueryHandler(RestApiPostCall& call,
-                                 std::unique_ptr<QueryRetrieveHandler>& handler)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    if (handler.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    handler->Run();
-    
-    std::string s = context.GetQueryRetrieveArchive().Add(handler.release());
-    Json::Value result = Json::objectValue;
-    result["ID"] = s;
-    result["Path"] = "/queries/" + s;
-    
-    call.GetOutput().AnswerJson(result);
-  }
-
-  
-  static void DicomQuery(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    Json::Value request;
-
-    if (!call.ParseJsonRequest(request) ||
-        request.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
-    }
-    else if (!request.isMember(KEY_LEVEL) ||
-             request[KEY_LEVEL].type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "The JSON body must contain field " + std::string(KEY_LEVEL));
-    }
-    else if (request.isMember(KEY_NORMALIZE) &&
-             request[KEY_NORMALIZE].type() != Json::booleanValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "The field " + std::string(KEY_NORMALIZE) + " must contain a Boolean");
-    }
-    else if (request.isMember(KEY_QUERY) &&
-             request[KEY_QUERY].type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "The field " + std::string(KEY_QUERY) + " must contain a JSON object");
-    }
-    else
-    {
-      std::unique_ptr<QueryRetrieveHandler>  handler(new QueryRetrieveHandler(context));
-      
-      handler->SetModality(call.GetUriComponent("id", ""));
-      handler->SetLevel(StringToResourceType(request[KEY_LEVEL].asCString()));
-
-      if (request.isMember(KEY_QUERY))
-      {
-        std::map<DicomTag, std::string> query;
-        SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
-
-        for (std::map<DicomTag, std::string>::const_iterator
-               it = query.begin(); it != query.end(); ++it)
-        {
-          handler->SetQuery(it->first, it->second);
-        }
-      }
-
-      if (request.isMember(KEY_NORMALIZE))
-      {
-        handler->SetFindNormalized(request[KEY_NORMALIZE].asBool());
-      }
-
-      AnswerQueryHandler(call, handler);
-    }
-  }
-
-
-  static void ListQueries(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::list<std::string> queries;
-    context.GetQueryRetrieveArchive().List(queries);
-
-    Json::Value result = Json::arrayValue;
-    for (std::list<std::string>::const_iterator
-           it = queries.begin(); it != queries.end(); ++it)
-    {
-      result.append(*it);
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  namespace
-  {
-    class QueryAccessor
-    {
-    private:
-      ServerContext&            context_;
-      SharedArchive::Accessor   accessor_;
-      QueryRetrieveHandler*     handler_;
-
-    public:
-      QueryAccessor(RestApiCall& call) :
-        context_(OrthancRestApi::GetContext(call)),
-        accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")),
-        handler_(NULL)
-      {
-        if (accessor_.IsValid())
-        {
-          handler_ = &dynamic_cast<QueryRetrieveHandler&>(accessor_.GetItem());
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-      }                     
-
-      QueryRetrieveHandler& GetHandler() const
-      {
-        assert(handler_ != NULL);
-        return *handler_;
-      }
-    };
-
-    static void AnswerDicomMap(RestApiCall& call,
-                               const DicomMap& value,
-                               bool simplify)
-    {
-      Json::Value full = Json::objectValue;
-      FromDcmtkBridge::ToJson(full, value, simplify);
-      call.GetOutput().AnswerJson(full);
-    }
-  }
-
-
-  static void ListQueryAnswers(RestApiGetCall& call)
-  {
-    const bool expand = call.HasArgument("expand");
-    const bool simplify = call.HasArgument("simplify");
-    
-    QueryAccessor query(call);
-    size_t count = query.GetHandler().GetAnswersCount();
-
-    Json::Value result = Json::arrayValue;
-    for (size_t i = 0; i < count; i++)
-    {
-      if (expand)
-      {
-        // New in Orthanc 1.5.0
-        DicomMap value;
-        query.GetHandler().GetAnswer(value, i);
-        
-        Json::Value json = Json::objectValue;
-        FromDcmtkBridge::ToJson(json, value, simplify);
-
-        result.append(json);
-      }
-      else
-      {
-        result.append(boost::lexical_cast<std::string>(i));
-      }
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  static void GetQueryOneAnswer(RestApiGetCall& call)
-  {
-    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
-
-    QueryAccessor query(call);
-
-    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;
-    int timeout = -1;
-    
-    Json::Value body;
-    if (call.ParseJsonRequest(body))
-    {
-      targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
-      timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
-    }
-    else
-    {
-      body = Json::objectValue;
-      if (call.GetBodySize() > 0)
-      {
-        call.BodyToString(targetAet);
-      }
-      else
-      {
-        targetAet = context.GetDefaultLocalApplicationEntityTitle();
-      }
-    }
-    
-    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
-    
-    {
-      QueryAccessor query(call);
-      job->SetTargetAet(targetAet);
-      job->SetLocalAet(query.GetHandler().GetLocalAet());
-      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
-
-      if (timeout >= 0)
-      {
-        // New in Orthanc 1.7.0
-        job->SetTimeout(static_cast<uint32_t>(timeout));
-      }
-
-      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", ""));
-    SubmitRetrieveJob(call, false, index);
-  }
-
-
-  static void RetrieveAllAnswers(RestApiPostCall& call)
-  {
-    SubmitRetrieveJob(call, true, 0);
-  }
-
-
-  static void GetQueryArguments(RestApiGetCall& call)
-  {
-    QueryAccessor query(call);
-    AnswerDicomMap(call, query.GetHandler().GetQuery(), call.HasArgument("simplify"));
-  }
-
-
-  static void GetQueryLevel(RestApiGetCall& call)
-  {
-    QueryAccessor query(call);
-    call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText);
-  }
-
-
-  static void GetQueryModality(RestApiGetCall& call)
-  {
-    QueryAccessor query(call);
-    call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText);
-  }
-
-
-  static void DeleteQuery(RestApiDeleteCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", ""));
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  static void ListQueryOperations(RestApiGetCall& call)
-  {
-    // Ensure that the query of interest does exist
-    QueryAccessor query(call);  
-
-    RestApi::AutoListChildren(call);
-  }
-
-
-  static void ListQueryAnswerOperations(RestApiGetCall& call)
-  {
-    // Ensure that the query of interest does exist
-    QueryAccessor query(call);
-
-    // Ensure that the answer of interest does exist
-    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
-
-    DicomMap map;
-    query.GetHandler().GetAnswer(map, index);
-
-    Json::Value answer = Json::arrayValue;
-    answer.append("content");
-    answer.append("retrieve");
-
-    switch (query.GetHandler().GetLevel())
-    {
-      case ResourceType_Patient:
-        answer.append("query-study");
-
-      case ResourceType_Study:
-        answer.append("query-series");
-
-      case ResourceType_Series:
-        answer.append("query-instances");
-        break;
-
-      default:
-        break;
-    }
-    
-    call.GetOutput().AnswerJson(answer);
-  }
-
-
-  template <ResourceType CHILDREN_LEVEL>
-  static void QueryAnswerChildren(RestApiPostCall& call)
-  {
-    // New in Orthanc 1.5.0
-    assert(CHILDREN_LEVEL == ResourceType_Study ||
-           CHILDREN_LEVEL == ResourceType_Series ||
-           CHILDREN_LEVEL == ResourceType_Instance);
-    
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::unique_ptr<QueryRetrieveHandler>  handler(new QueryRetrieveHandler(context));
-      
-    {
-      const QueryAccessor parent(call);
-      const ResourceType level = parent.GetHandler().GetLevel();
-    
-      const size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
-
-      Json::Value request;
-
-      if (index >= parent.GetHandler().GetAnswersCount())
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else if (CHILDREN_LEVEL == ResourceType_Study &&
-               level != ResourceType_Patient)
-      {
-        throw OrthancException(ErrorCode_UnknownResource);
-      }
-      else if (CHILDREN_LEVEL == ResourceType_Series &&
-               level != ResourceType_Patient &&
-               level != ResourceType_Study)
-      {
-        throw OrthancException(ErrorCode_UnknownResource);
-      }      
-      else if (CHILDREN_LEVEL == ResourceType_Instance &&
-               level != ResourceType_Patient &&
-               level != ResourceType_Study &&
-               level != ResourceType_Series)
-      {
-        throw OrthancException(ErrorCode_UnknownResource);
-      }
-      else if (!call.ParseJsonRequest(request))
-      {
-        throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
-      }
-      else
-      {
-        handler->SetFindNormalized(parent.GetHandler().IsFindNormalized());
-        handler->SetModality(parent.GetHandler().GetModalitySymbolicName());
-        handler->SetLevel(CHILDREN_LEVEL);
-
-        if (request.isMember(KEY_QUERY))
-        {
-          std::map<DicomTag, std::string> query;
-          SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
-
-          for (std::map<DicomTag, std::string>::const_iterator
-                 it = query.begin(); it != query.end(); ++it)
-          {
-            handler->SetQuery(it->first, it->second);
-          }
-        }
-
-        DicomMap answer;
-        parent.GetHandler().GetAnswer(answer, index);
-
-        // This switch-case mimics "DicomControlUserConnection::Move()"
-        switch (parent.GetHandler().GetLevel())
-        {
-          case ResourceType_Patient:
-            handler->CopyStringTag(answer, DICOM_TAG_PATIENT_ID);
-            break;
-
-          case ResourceType_Study:
-            handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
-            break;
-
-          case ResourceType_Series:
-            handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
-            handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
-            break;
-
-          case ResourceType_Instance:
-            handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
-            handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
-            handler->CopyStringTag(answer, DICOM_TAG_SOP_INSTANCE_UID);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-    }
-      
-    AnswerQueryHandler(call, handler);
-  }
-  
-
-
-  /***************************************************************************
-   * DICOM C-Store SCU
-   ***************************************************************************/
-
-  static void GetInstancesToExport(Json::Value& otherArguments,
-                                   SetOfInstancesJob& job,
-                                   const std::string& remote,
-                                   RestApiPostCall& call)
-  {
-    otherArguments = Json::objectValue;
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value request;
-    if (Toolbox::IsSHA1(call.GetBodyData(), call.GetBodySize()))
-    {
-      std::string s;
-      call.BodyToString(s);
-
-      // This is for compatibility with Orthanc <= 0.5.1.
-      request = Json::arrayValue;
-      request.append(Toolbox::StripSpaces(s));
-    }
-    else if (!call.ParseJsonRequest(request))
-    {
-      // Bad JSON request
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON value");
-    }
-
-    if (request.isString())
-    {
-      std::string item = request.asString();
-      request = Json::arrayValue;
-      request.append(item);
-    }
-    else if (!request.isArray() &&
-             !request.isObject())
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object, or a JSON array of strings");
-    }
-
-    const Json::Value* resources;
-    if (request.isArray())
-    {
-      resources = &request;
-    }
-    else
-    {
-      if (request.type() != Json::objectValue ||
-          !request.isMember(KEY_RESOURCES))
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing field in JSON: \"" + std::string(KEY_RESOURCES) + "\"");
-      }
-
-      resources = &request[KEY_RESOURCES];
-      if (!resources->isArray())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "JSON field \"" + std::string(KEY_RESOURCES) + "\" must contain an array");
-      }
-
-      // Copy the remaining arguments
-      Json::Value::Members members = request.getMemberNames();
-      for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
-      {
-        otherArguments[members[i]] = request[members[i]];
-      }
-    }
-
-    bool logExportedResources;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources", false);
-    }
-
-    for (Json::Value::ArrayIndex i = 0; i < resources->size(); i++)
-    {
-      if (!(*resources) [i].isString())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Resources to be exported must be specified as a JSON array of strings");
-      }
-
-      std::string stripped = Toolbox::StripSpaces((*resources) [i].asString());
-      if (!Toolbox::IsSHA1(stripped))
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "This string is not a valid Orthanc identifier: " + stripped);
-      }
-
-      job.AddParentResource(stripped);  // New in Orthanc 1.5.7
-      
-      context.AddChildInstances(job, stripped);
-
-      if (logExportedResources)
-      {
-        context.GetIndex().LogExportedResource(stripped, remote);
-      }
-    }
-  }
-
-
-  static void DicomStore(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string remote = call.GetUriComponent("id", "");
-
-    Json::Value request;
-    std::unique_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
-
-    GetInstancesToExport(request, *job, remote, call);
-
-    std::string localAet = Toolbox::GetJsonStringField
-      (request, KEY_LOCAL_AET, 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(MyGetModalityUsingSymbolicName(remote));
-
-    if (moveOriginatorID != 0)
-    {
-      job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
-    }
-
-    // New in Orthanc 1.6.0
-    if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false))
-    {
-      job->EnableStorageCommitment(true);
-    }
-
-    // New in Orthanc 1.7.0
-    if (request.isMember(KEY_TIMEOUT))
-    {
-      job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
-    }
-
-    OrthancRestApi::GetApi(call).SubmitCommandsJob
-      (call, job.release(), true /* synchronous by default */, request);
-  }
-
-
-  static void DicomStoreStraight(RestApiPostCall& call)
-  {
-    Json::Value body = Json::objectValue;  // No body
-    DicomStoreUserConnection connection(GetAssociationParameters(call, body));
-
-    std::string sopClassUid, sopInstanceUid;
-    connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(),
-                     call.GetBodySize(), false /* Not a C-MOVE */, "", 0);
-
-    Json::Value answer = Json::objectValue;
-    answer[SOP_CLASS_UID] = sopClassUid;
-    answer[SOP_INSTANCE_UID] = sopInstanceUid;
-    
-    call.GetOutput().AnswerJson(answer);
-  }
-
-
-  /***************************************************************************
-   * DICOM C-Move SCU
-   ***************************************************************************/
-  
-  static void DicomMove(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value request;
-
-    if (!call.ParseJsonRequest(request) ||
-        request.type() != Json::objectValue ||
-        !request.isMember(KEY_RESOURCES) ||
-        !request.isMember(KEY_LEVEL) ||
-        request[KEY_RESOURCES].type() != Json::arrayValue ||
-        request[KEY_LEVEL].type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
-                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
-    }
-
-    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
-    
-    std::string localAet = Toolbox::GetJsonStringField
-      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
-    std::string targetAet = Toolbox::GetJsonStringField
-      (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
-
-    const RemoteModalityParameters source =
-      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-
-    DicomAssociationParameters params(localAet, source);
-    InjectAssociationTimeout(params, request);
-
-    DicomControlUserConnection connection(params);
-
-    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
-    {
-      DicomMap resource;
-      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i]);
-      
-      connection.Move(targetAet, level, resource);
-    }
-
-    // Move has succeeded
-    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-  }
-
-
-
-  /***************************************************************************
-   * Orthanc Peers => Store client
-   ***************************************************************************/
-
-  static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers,
-                             const std::string& id)
-  {
-    return peers.find(id) != peers.end();
-  }
-
-  static void ListPeers(RestApiGetCall& call)
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    OrthancRestApi::SetOfStrings peers;
-    lock.GetConfiguration().GetListOfOrthancPeers(peers);
-
-    if (call.HasArgument("expand"))
-    {
-      Json::Value result = Json::objectValue;
-      for (OrthancRestApi::SetOfStrings::const_iterator
-             it = peers.begin(); it != peers.end(); ++it)
-      {
-        WebServiceParameters peer;
-        
-        if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
-        {
-          Json::Value info;
-          peer.FormatPublic(info);
-          result[*it] = info;
-        }
-      }
-      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);
-    }
-  }
-
-  static void ListPeerOperations(RestApiGetCall& call)
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    OrthancRestApi::SetOfStrings peers;
-    lock.GetConfiguration().GetListOfOrthancPeers(peers);
-
-    std::string id = call.GetUriComponent("id", "");
-    if (IsExistingPeer(peers, id))
-    {
-      RestApi::AutoListChildren(call);
-    }
-  }
-
-  static void PeerStore(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string remote = call.GetUriComponent("id", "");
-
-    Json::Value request;
-    std::unique_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
-
-    GetInstancesToExport(request, *job, remote, call);
-
-    static const char* TRANSCODE = "Transcode";
-    if (request.type() == Json::objectValue &&
-        request.isMember(TRANSCODE))
-    {
-      job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE));
-    }
-    
-    {
-      OrthancConfiguration::ReaderLock lock;
-      
-      WebServiceParameters peer;
-      if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
-      {
-        job->SetPeer(peer);    
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_UnknownResource,
-                               "No peer with symbolic name: " + remote);
-      }
-    }
-
-    OrthancRestApi::GetApi(call).SubmitCommandsJob
-      (call, job.release(), true /* synchronous by default */, request);
-  }
-
-  static void PeerSystem(RestApiGetCall& call)
-  {
-    std::string remote = call.GetUriComponent("id", "");
-
-    OrthancConfiguration::ReaderLock lock;
-
-    WebServiceParameters peer;
-    if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
-    {
-      HttpClient client(peer, "system");
-      std::string answer;
-
-      client.SetMethod(HttpMethod_Get);
-
-      if (!client.Apply(answer))
-      {
-        LOG(ERROR) << "Unable to get the system info from remote Orthanc peer: " << peer.GetUrl();
-        call.GetOutput().SignalError(client.GetLastStatus());
-        return;
-      }
-
-      call.GetOutput().AnswerBuffer(answer, MimeType_Json);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "No peer with symbolic name: " + remote);
-    }
-  }
-
-  // DICOM bridge -------------------------------------------------------------
-
-  static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities,
-                                 const std::string& id)
-  {
-    return modalities.find(id) != modalities.end();
-  }
-
-  static void ListModalities(RestApiGetCall& call)
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    OrthancRestApi::SetOfStrings modalities;
-    lock.GetConfiguration().GetListOfDicomModalities(modalities);
-
-    if (call.HasArgument("expand"))
-    {
-      Json::Value result = Json::objectValue;
-      for (OrthancRestApi::SetOfStrings::const_iterator
-             it = modalities.begin(); it != modalities.end(); ++it)
-      {
-        const RemoteModalityParameters& remote = lock.GetConfiguration().GetModalityUsingSymbolicName(*it);
-        
-        Json::Value info;
-        remote.Serialize(info, true /* force advanced format */);
-        result[*it] = info;
-      }
-      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);
-    }
-  }
-
-
-  static void ListModalityOperations(RestApiGetCall& call)
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    OrthancRestApi::SetOfStrings modalities;
-    lock.GetConfiguration().GetListOfDicomModalities(modalities);
-
-    std::string id = call.GetUriComponent("id", "");
-    if (IsExistingModality(modalities, id))
-    {
-      RestApi::AutoListChildren(call);
-    }
-  }
-
-
-  static void UpdateModality(RestApiPutCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value json;
-    if (call.ParseJsonRequest(json))
-    {
-      RemoteModalityParameters modality;
-      modality.Unserialize(json);
-
-      {
-        OrthancConfiguration::WriterLock lock;
-        lock.GetConfiguration().UpdateModality(call.GetUriComponent("id", ""), modality);
-      }
-
-      context.SignalUpdatedModalities();
-
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-    }
-  }
-
-
-  static void DeleteModality(RestApiDeleteCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    {
-      OrthancConfiguration::WriterLock lock;
-      lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", ""));
-    }
-
-    context.SignalUpdatedModalities();
-
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  static void UpdatePeer(RestApiPutCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value json;
-    if (call.ParseJsonRequest(json))
-    {
-      WebServiceParameters peer;
-      peer.Unserialize(json);
-
-      {
-        OrthancConfiguration::WriterLock lock;
-        lock.GetConfiguration().UpdatePeer(call.GetUriComponent("id", ""), peer);
-      }
-
-      context.SignalUpdatedPeers();
-
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-    }
-  }
-
-
-  static void DeletePeer(RestApiDeleteCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    {
-      OrthancConfiguration::WriterLock lock;
-      lock.GetConfiguration().RemovePeer(call.GetUriComponent("id", ""));
-    }
-
-    context.SignalUpdatedPeers();
-
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  static void DicomFindWorklist(RestApiPostCall& call)
-  {
-    Json::Value json;
-    if (call.ParseJsonRequest(json))
-    {
-      std::unique_ptr<ParsedDicomFile> query
-        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
-                                         "" /* no private creator */));
-
-      DicomFindAnswers answers(true);
-
-      {
-        DicomControlUserConnection connection(GetAssociationParameters(call, json));
-        connection.FindWorklist(answers, *query);
-      }
-
-      Json::Value result;
-      answers.ToJson(result, true);
-      call.GetOutput().AnswerJson(result);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
-    }
-  }
-
-
-  // Storage commitment SCU ---------------------------------------------------
-
-  static void StorageCommitmentScu(RestApiPostCall& call)
-  {
-    static const char* const ORTHANC_RESOURCES = "Resources";
-    static const char* const DICOM_INSTANCES = "DicomInstances";
-
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value json;
-    if (!call.ParseJsonRequest(json) ||
-        json.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Must provide a JSON object with a list of resources");
-    }
-    else if (!json.isMember(ORTHANC_RESOURCES) &&
-             !json.isMember(DICOM_INSTANCES))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Empty storage commitment request, one of these fields is mandatory: \"" +
-                             std::string(ORTHANC_RESOURCES) + "\" or \"" + std::string(DICOM_INSTANCES) + "\"");
-    }
-    else
-    {
-      std::list<std::string> sopClassUids, sopInstanceUids;
-
-      if (json.isMember(ORTHANC_RESOURCES))
-      {
-        const Json::Value& resources = json[ORTHANC_RESOURCES];
-          
-        if (resources.type() != Json::arrayValue)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "The \"" + std::string(ORTHANC_RESOURCES) +
-                                 "\" field must provide an array of Orthanc resources");
-        }
-        else
-        {
-          for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
-          {
-            if (resources[i].type() != Json::stringValue)
-            {
-              throw OrthancException(ErrorCode_BadFileFormat,
-                                     "The \"" + std::string(ORTHANC_RESOURCES) +
-                                     "\" field must provide an array of strings, found: " + resources[i].toStyledString());
-            }
-
-            std::list<std::string> instances;
-            context.GetIndex().GetChildInstances(instances, resources[i].asString());
-            
-            for (std::list<std::string>::const_iterator
-                   it = instances.begin(); it != instances.end(); ++it)
-            {
-              std::string sopClassUid, sopInstanceUid;
-              DicomMap tags;
-              if (context.LookupOrReconstructMetadata(sopClassUid, *it, MetadataType_Instance_SopClassUid) &&
-                  context.GetIndex().GetAllMainDicomTags(tags, *it) &&
-                  tags.LookupStringValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID, false))
-              {
-                sopClassUids.push_back(sopClassUid);
-                sopInstanceUids.push_back(sopInstanceUid);
-              }
-              else
-              {
-                throw OrthancException(ErrorCode_InternalError,
-                                       "Cannot retrieve SOP Class/Instance UID of Orthanc instance: " + *it);
-              }
-            }
-          }
-        }
-      }
-
-      if (json.isMember(DICOM_INSTANCES))
-      {
-        const Json::Value& instances = json[DICOM_INSTANCES];
-          
-        if (instances.type() != Json::arrayValue)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "The \"" + std::string(DICOM_INSTANCES) +
-                                 "\" field must provide an array of DICOM instances");
-        }
-        else
-        {
-          for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
-          {
-            if (instances[i].type() == Json::arrayValue)
-            {
-              if (instances[i].size() != 2 ||
-                  instances[i][0].type() != Json::stringValue ||
-                  instances[i][1].type() != Json::stringValue)
-              {
-                throw OrthancException(ErrorCode_BadFileFormat,
-                                       "An instance entry must provide an array with 2 strings: "
-                                       "SOP Class UID and SOP Instance UID");
-              }
-              else
-              {
-                sopClassUids.push_back(instances[i][0].asString());
-                sopInstanceUids.push_back(instances[i][1].asString());
-              }
-            }
-            else if (instances[i].type() == Json::objectValue)
-            {
-              if (!instances[i].isMember(SOP_CLASS_UID) ||
-                  !instances[i].isMember(SOP_INSTANCE_UID) ||
-                  instances[i][SOP_CLASS_UID].type() != Json::stringValue ||
-                  instances[i][SOP_INSTANCE_UID].type() != Json::stringValue)
-              {
-                throw OrthancException(ErrorCode_BadFileFormat,
-                                       "An instance entry must provide an object with 2 string fiels: "
-                                       "\"" + std::string(SOP_CLASS_UID) + "\" and \"" +
-                                       std::string(SOP_INSTANCE_UID));
-              }
-              else
-              {
-                sopClassUids.push_back(instances[i][SOP_CLASS_UID].asString());
-                sopInstanceUids.push_back(instances[i][SOP_INSTANCE_UID].asString());
-              }
-            }
-            else
-            {
-              throw OrthancException(ErrorCode_BadFileFormat,
-                                     "JSON array or object is expected to specify one "
-                                     "instance to be queried, found: " + instances[i].toStyledString());
-            }
-          }
-        }
-      }
-
-      if (sopClassUids.size() != sopInstanceUids.size())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      const std::string transactionUid = Toolbox::GenerateDicomPrivateUniqueIdentifier();
-
-      if (sopClassUids.empty())
-      {
-        LOG(WARNING) << "Issuing an outgoing storage commitment request that is empty: " << transactionUid;
-      }
-
-      {
-        const RemoteModalityParameters remote =
-          MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-
-        const std::string& remoteAet = remote.GetApplicationEntityTitle();
-        const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-        
-        // Create a "pending" storage commitment report BEFORE the
-        // actual SCU call in order to avoid race conditions
-        context.GetStorageCommitmentReports().Store(
-          transactionUid, new StorageCommitmentReports::Report(remoteAet));
-
-        DicomAssociationParameters parameters(localAet, remote);
-        
-        std::vector<std::string> a(sopClassUids.begin(), sopClassUids.end());
-        std::vector<std::string> b(sopInstanceUids.begin(), sopInstanceUids.end());
-        DicomAssociation::RequestStorageCommitment(parameters, transactionUid, a, b);
-      }
-
-      Json::Value result = Json::objectValue;
-      result["ID"] = transactionUid;
-      result["Path"] = "/storage-commitment/" + transactionUid;
-      call.GetOutput().AnswerJson(result);
-    }
-  }
-
-
-  static void GetStorageCommitmentReport(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    const std::string& transactionUid = call.GetUriComponent("id", "");
-
-    {
-      StorageCommitmentReports::Accessor accessor(
-        context.GetStorageCommitmentReports(), transactionUid);
-
-      if (accessor.IsValid())
-      {
-        Json::Value json;
-        accessor.GetReport().Format(json);
-        call.GetOutput().AnswerJson(json);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InexistentItem,
-                               "No storage commitment transaction with UID: " + transactionUid);
-      }
-    }
-  }
-  
-
-  static void RemoveAfterStorageCommitment(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    const std::string& transactionUid = call.GetUriComponent("id", "");
-
-    {
-      StorageCommitmentReports::Accessor accessor(
-        context.GetStorageCommitmentReports(), transactionUid);
-
-      if (!accessor.IsValid())
-      {
-        throw OrthancException(ErrorCode_InexistentItem,
-                               "No storage commitment transaction with UID: " + transactionUid);
-      }
-      else if (accessor.GetReport().GetStatus() != StorageCommitmentReports::Report::Status_Success)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                               "Cannot remove DICOM instances after failure "
-                               "in storage commitment transaction: " + transactionUid);
-      }
-      else
-      {
-        std::vector<std::string> sopInstanceUids;
-        accessor.GetReport().GetSuccessSopInstanceUids(sopInstanceUids);
-
-        for (size_t i = 0; i < sopInstanceUids.size(); i++)
-        {
-          std::vector<std::string> orthancId;
-          context.GetIndex().LookupIdentifierExact(
-            orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids[i]);
-
-          for (size_t j = 0; j < orthancId.size(); j++)
-          {
-            LOG(INFO) << "Storage commitment - Removing SOP instance UID / Orthanc ID: "
-                      << sopInstanceUids[i] << " / " << orthancId[j];
-
-            Json::Value tmp;
-            context.GetIndex().DeleteResource(tmp, orthancId[j], ResourceType_Instance);
-          }
-        }
-          
-        call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-      }
-    }
-  }
-  
-
-  void OrthancRestApi::RegisterModalities()
-  {
-    Register("/modalities", ListModalities);
-    Register("/modalities/{id}", ListModalityOperations);
-    Register("/modalities/{id}", UpdateModality);
-    Register("/modalities/{id}", DeleteModality);
-    Register("/modalities/{id}/echo", DicomEcho);
-    Register("/modalities/{id}/find-patient", DicomFindPatient);
-    Register("/modalities/{id}/find-study", DicomFindStudy);
-    Register("/modalities/{id}/find-series", DicomFindSeries);
-    Register("/modalities/{id}/find-instance", DicomFindInstance);
-    Register("/modalities/{id}/find", DicomFind);
-    Register("/modalities/{id}/store", DicomStore);
-    Register("/modalities/{id}/store-straight", DicomStoreStraight);  // New in 1.6.1
-    Register("/modalities/{id}/move", DicomMove);
-
-    // For Query/Retrieve
-    Register("/modalities/{id}/query", DicomQuery);
-    Register("/queries", ListQueries);
-    Register("/queries/{id}", DeleteQuery);
-    Register("/queries/{id}", ListQueryOperations);
-    Register("/queries/{id}/answers", ListQueryAnswers);
-    Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations);
-    Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer);
-    Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer);
-    Register("/queries/{id}/answers/{index}/query-instances",
-             QueryAnswerChildren<ResourceType_Instance>);
-    Register("/queries/{id}/answers/{index}/query-series",
-             QueryAnswerChildren<ResourceType_Series>);
-    Register("/queries/{id}/answers/{index}/query-studies",
-             QueryAnswerChildren<ResourceType_Study>);
-    Register("/queries/{id}/level", GetQueryLevel);
-    Register("/queries/{id}/modality", GetQueryModality);
-    Register("/queries/{id}/query", GetQueryArguments);
-    Register("/queries/{id}/retrieve", RetrieveAllAnswers);
-
-    Register("/peers", ListPeers);
-    Register("/peers/{id}", ListPeerOperations);
-    Register("/peers/{id}", UpdatePeer);
-    Register("/peers/{id}", DeletePeer);
-    Register("/peers/{id}/store", PeerStore);
-    Register("/peers/{id}/system", PeerSystem);
-
-    Register("/modalities/{id}/find-worklist", DicomFindWorklist);
-
-    // Storage commitment
-    Register("/modalities/{id}/storage-commitment", StorageCommitmentScu);
-    Register("/storage-commitment/{id}", GetStorageCommitmentReport);
-    Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment);
-  }
-}
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2165 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../../Core/Compression/GzipCompressor.h"
-#include "../../Core/DicomFormat/DicomImageInformation.h"
-#include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../../Core/HttpServer/HttpContentNegociation.h"
-#include "../../Core/Images/Image.h"
-#include "../../Core/Images/ImageProcessing.h"
-#include "../../Core/Logging.h"
-#include "../../Core/MultiThreading/Semaphore.h"
-#include "../OrthancConfiguration.h"
-#include "../Search/DatabaseLookup.h"
-#include "../ServerContext.h"
-#include "../ServerToolbox.h"
-#include "../SliceOrdering.h"
-
-#include "../../Plugins/Engine/OrthancPlugins.h"
-
-// This "include" is mandatory for Release builds using Linux Standard Base
-#include <boost/math/special_functions/round.hpp>
-
-
-/**
- * This semaphore is used to limit the number of concurrent HTTP
- * requests on CPU-intensive routes of the REST API, in order to
- * prevent exhaustion of resources (new in Orthanc 1.7.0).
- **/
-static Orthanc::Semaphore throttlingSemaphore_(4);  // TODO => PARAMETER?
-
-
-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,
-                                    ServerIndex& index,
-                                    const std::list<std::string>& resources,
-                                    ResourceType level,
-                                    bool expand)
-  {
-    Json::Value answer = Json::arrayValue;
-
-    for (std::list<std::string>::const_iterator
-           resource = resources.begin(); resource != resources.end(); ++resource)
-    {
-      if (expand)
-      {
-        Json::Value item;
-        if (index.LookupResource(item, *resource, level))
-        {
-          answer.append(item);
-        }
-      }
-      else
-      {
-        answer.append(*resource);
-      }
-    }
-
-    output.AnswerJson(answer);
-  }
-
-
-  template <enum ResourceType resourceType>
-  static void ListResources(RestApiGetCall& call)
-  {
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-
-    std::list<std::string> result;
-
-    if (call.HasArgument("limit") ||
-        call.HasArgument("since"))
-    {
-      if (!call.HasArgument("limit"))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"limit\" argument for GET request against: " +
-                               call.FlattenUri());
-      }
-
-      if (!call.HasArgument("since"))
-      {
-        throw OrthancException(ErrorCode_BadRequest,
-                               "Missing \"since\" argument for GET request against: " +
-                               call.FlattenUri());
-      }
-
-      size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
-      size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", ""));
-      index.GetAllUuids(result, resourceType, since, limit);
-    }
-    else
-    {
-      index.GetAllUuids(result, resourceType);
-    }
-
-
-    AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"));
-  }
-
-  template <enum ResourceType resourceType>
-  static void GetSingleResource(RestApiGetCall& call)
-  {
-    Json::Value result;
-    if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType))
-    {
-      call.GetOutput().AnswerJson(result);
-    }
-  }
-
-  template <enum ResourceType resourceType>
-  static void DeleteSingleResource(RestApiDeleteCall& call)
-  {
-    Json::Value result;
-    if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType))
-    {
-      call.GetOutput().AnswerJson(result);
-    }
-  }
-
-
-  // Get information about a single patient -----------------------------------
- 
-  static void IsProtectedPatient(RestApiGetCall& call)
-  {
-    std::string publicId = call.GetUriComponent("id", "");
-    bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId);
-    call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText);
-  }
-
-
-  static void SetPatientProtection(RestApiPutCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-
-    std::string body;
-    call.BodyToString(body);
-    body = Toolbox::StripSpaces(body);
-
-    if (body == "0")
-    {
-      context.GetIndex().SetProtectedPatient(publicId, false);
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-    }
-    else if (body == "1")
-    {
-      context.GetIndex().SetProtectedPatient(publicId, true);
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-    }
-    else
-    {
-      // Bad request
-    }
-  }
-
-
-  // Get information about a single instance ----------------------------------
- 
-  static void GetInstanceFile(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-
-    IHttpHandler::Arguments::const_iterator accept = call.GetHttpHeaders().find("accept");
-    if (accept != call.GetHttpHeaders().end())
-    {
-      // New in Orthanc 1.5.4
-      try
-      {
-        MimeType mime = StringToMimeType(accept->second.c_str());
-
-        if (mime == MimeType_DicomWebJson ||
-            mime == MimeType_DicomWebXml)
-        {
-          DicomWebJsonVisitor visitor;
-          
-          {
-            ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
-            locker.GetDicom().Apply(visitor);
-          }
-
-          if (mime == MimeType_DicomWebJson)
-          {
-            std::string s = visitor.GetResult().toStyledString();
-            call.GetOutput().AnswerBuffer(s, MimeType_DicomWebJson);
-          }
-          else
-          {
-            std::string xml;
-            visitor.FormatXml(xml);
-            call.GetOutput().AnswerBuffer(xml, MimeType_DicomWebXml);
-          }
-          
-          return;
-        }
-      }
-      catch (OrthancException&)
-      {
-      }
-    }
-
-    context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
-  }
-
-
-  static void ExportInstanceFile(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-
-    std::string dicom;
-    context.ReadDicom(dicom, publicId);
-
-    std::string target;
-    call.BodyToString(target);
-    SystemToolbox::WriteFile(dicom, target);
-
-    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-  }
-
-
-  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 (format != DicomToJsonFormat_Full ||
-        !ignoreTagLength.empty())
-    {
-      Json::Value full;
-      context.ReadDicomAsJson(full, publicId, ignoreTagLength);
-      AnswerDicomAsJson(call, full, format);
-    }
-    else
-    {
-      // This path allows one 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, MimeType_Json);
-    }
-  }
-
-
-  static void GetInstanceTagsBis(RestApiGetCall& call)
-  {
-    switch (GetDicomFormat(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)
-  {
-    std::string publicId = call.GetUriComponent("id", "");
-
-    unsigned int numberOfFrames;
-      
-    {
-      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);
-  }
-
-
-  namespace
-  {
-    class ImageToEncode
-    {
-    private:
-      std::unique_ptr<ImageAccessor>&  image_;
-      ImageExtractionMode            mode_;
-      bool                           invert_;
-      MimeType                       format_;
-      std::string                    answer_;
-
-    public:
-      ImageToEncode(std::unique_ptr<ImageAccessor>& image,
-                    ImageExtractionMode mode,
-                    bool invert) :
-        image_(image),
-        mode_(mode),
-        invert_(invert)
-      {
-      }
-
-      void Answer(RestApiOutput& output)
-      {
-        output.AnswerBuffer(answer_, format_);
-      }
-
-      void EncodeUsingPng()
-      {
-        format_ = MimeType_Png;
-        DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
-      }
-
-      void EncodeUsingPam()
-      {
-        format_ = MimeType_Pam;
-        DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
-      }
-
-      void EncodeUsingJpeg(uint8_t quality)
-      {
-        format_ = MimeType_Jpeg;
-        DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality);
-      }
-    };
-
-    class EncodePng : public HttpContentNegociation::IHandler
-    {
-    private:
-      ImageToEncode&  image_;
-
-    public:
-      EncodePng(ImageToEncode& image) : image_(image)
-      {
-      }
-
-      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)
-        {
-          throw OrthancException(
-            ErrorCode_BadRequest,
-            "Bad quality for a JPEG encoding (must be a number between 0 and 100): " + v);
-        }
-      }
-
-      virtual void Handle(const std::string& type,
-                          const std::string& subtype)
-      {
-        assert(type == "image");
-        assert(subtype == "jpeg");
-        image_.EncodeUsingJpeg(quality_);
-      }
-    };
-  }
-
-
-  namespace
-  {
-    class IDecodedFrameHandler : public boost::noncopyable
-    {
-    public:
-      virtual ~IDecodedFrameHandler()
-      {
-      }
-
-      virtual void Handle(RestApiGetCall& call,
-                          std::unique_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) = 0;
-
-      virtual bool RequiresDicomTags() const = 0;
-
-      static void Apply(RestApiGetCall& call,
-                        IDecodedFrameHandler& handler)
-      {
-        ServerContext& context = OrthancRestApi::GetContext(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;
-        }
-
-        DicomMap dicom;
-        std::unique_ptr<ImageAccessor> decoded;
-
-        try
-        {
-          std::string publicId = call.GetUriComponent("id", "");
-
-          decoded.reset(context.DecodeDicomFrame(publicId, frame));
-
-          if (decoded.get() == NULL)
-          {
-            throw OrthancException(ErrorCode_NotImplemented,
-                                   "Cannot decode DICOM instance with ID: " + publicId);
-          }
-          
-          if (handler.RequiresDicomTags())
-          {
-            /**
-             * Retrieve a summary of the DICOM tags, which is
-             * necessary to deal with MONOCHROME1 photometric
-             * interpretation, and with windowing parameters.
-             **/ 
-            ServerContext::DicomCacheLocker locker(context, publicId);
-            locker.GetDicom().ExtractDicomSummary(dicom);
-          }
-        }
-        catch (OrthancException& e)
-        {
-          if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange ||
-              e.GetErrorCode() == ErrorCode_UnknownResource)
-          {
-            // The frame number is out of the range for this DICOM
-            // instance, the resource is not existent
-          }
-          else
-          {
-            std::string root = "";
-            for (size_t i = 1; i < call.GetFullUri().size(); i++)
-            {
-              root += "../";
-            }
-
-            call.GetOutput().Redirect(root + "app/images/unsupported.png");
-          }
-          return;
-        }
-
-        handler.Handle(call, decoded, dicom);
-      }
-
-
-      static void DefaultHandler(RestApiGetCall& call,
-                                 std::unique_ptr<ImageAccessor>& decoded,
-                                 ImageExtractionMode mode,
-                                 bool invert)
-      {
-        ImageToEncode image(decoded, mode, invert);
-
-        HttpContentNegociation negociation;
-        EncodePng png(image);
-        negociation.Register(MIME_PNG, png);
-
-        EncodeJpeg jpeg(image, call);
-        negociation.Register(MIME_JPEG, jpeg);
-
-        EncodePam pam(image);
-        negociation.Register(MIME_PAM, pam);
-
-        if (negociation.Apply(call.GetHttpHeaders()))
-        {
-          image.Answer(call.GetOutput());
-        }
-      }
-    };
-
-
-    class GetImageHandler : public IDecodedFrameHandler
-    {
-    private:
-      ImageExtractionMode mode_;
-
-    public:
-      GetImageHandler(ImageExtractionMode mode) :
-        mode_(mode)
-      {
-      }
-
-      virtual void Handle(RestApiGetCall& call,
-                          std::unique_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) ORTHANC_OVERRIDE
-      {
-        bool invert = false;
-
-        if (mode_ == ImageExtractionMode_Preview)
-        {
-          DicomImageInformation info(dicom);
-          invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
-        }
-
-        DefaultHandler(call, decoded, mode_, invert);
-      }
-
-      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
-      {
-        return mode_ == ImageExtractionMode_Preview;
-      }
-    };
-
-
-    class RenderedFrameHandler : public IDecodedFrameHandler
-    {
-    private:
-      static void GetDicomParameters(bool& invert,
-                                     float& rescaleSlope,
-                                     float& rescaleIntercept,
-                                     float& windowWidth,
-                                     float& windowCenter,
-                                     const DicomMap& dicom)
-      {
-        DicomImageInformation info(dicom);
-
-        invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
-
-        rescaleSlope = 1.0f;
-        rescaleIntercept = 0.0f;
-
-        if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) &&
-            dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
-        {
-          dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE);
-          dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
-        }
-
-        windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope;
-        windowCenter = windowWidth / 2.0f + rescaleIntercept;
-
-        if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) &&
-            dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
-        {
-          dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
-          dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
-        }
-      }
-
-      static void GetUserArguments(float& windowWidth /* inout */,
-                                   float& windowCenter /* inout */,
-                                   unsigned int& argWidth,
-                                   unsigned int& argHeight,
-                                   bool& smooth,
-                                   RestApiGetCall& call)
-      {
-        static const char* ARG_WINDOW_CENTER = "window-center";
-        static const char* ARG_WINDOW_WIDTH = "window-width";
-        static const char* ARG_WIDTH = "width";
-        static const char* ARG_HEIGHT = "height";
-        static const char* ARG_SMOOTH = "smooth";
-
-        if (call.HasArgument(ARG_WINDOW_WIDTH))
-        {
-          try
-          {
-            windowWidth = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_WIDTH, ""));
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH));
-          }
-        }
-
-        if (call.HasArgument(ARG_WINDOW_CENTER))
-        {
-          try
-          {
-            windowCenter = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_CENTER, ""));
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_WINDOW_CENTER));
-          }
-        }
-
-        argWidth = 0;
-        argHeight = 0;
-
-        if (call.HasArgument(ARG_WIDTH))
-        {
-          try
-          {
-            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_WIDTH, ""));
-            if (tmp < 0)
-            {
-              throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                     "Argument cannot be negative: " + std::string(ARG_WIDTH));
-            }
-            else
-            {
-              argWidth = static_cast<unsigned int>(tmp);
-            }
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_WIDTH));
-          }
-        }
-
-        if (call.HasArgument(ARG_HEIGHT))
-        {
-          try
-          {
-            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_HEIGHT, ""));
-            if (tmp < 0)
-            {
-              throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                     "Argument cannot be negative: " + std::string(ARG_HEIGHT));
-            }
-            else
-            {
-              argHeight = static_cast<unsigned int>(tmp);
-            }
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Bad value for argument: " + std::string(ARG_HEIGHT));
-          }
-        }
-
-        smooth = false;
-
-        if (call.HasArgument(ARG_SMOOTH))
-        {
-          std::string value = call.GetArgument(ARG_SMOOTH, "");
-          if (value == "0" ||
-              value == "false")
-          {
-            smooth = false;
-          }
-          else if (value == "1" ||
-                   value == "true")
-          {
-            smooth = true;
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Argument must be Boolean: " + std::string(ARG_SMOOTH));
-          }
-        }        
-      }
-                                
-      
-    public:
-      virtual void Handle(RestApiGetCall& call,
-                          std::unique_ptr<ImageAccessor>& decoded,
-                          const DicomMap& dicom) ORTHANC_OVERRIDE
-      {
-        bool invert;
-        float rescaleSlope, rescaleIntercept, windowWidth, windowCenter;
-        GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom);
-
-        unsigned int argWidth, argHeight;
-        bool smooth;
-        GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call);
-
-        unsigned int targetWidth = decoded->GetWidth();
-        unsigned int targetHeight = decoded->GetHeight();
-
-        if (decoded->GetWidth() != 0 &&
-            decoded->GetHeight() != 0)
-        {
-          float ratio = 1;
-
-          if (argWidth != 0 &&
-              argHeight != 0)
-          {
-            float ratioX = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
-            float ratioY = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
-            ratio = std::min(ratioX, ratioY);
-          }
-          else if (argWidth != 0)
-          {
-            ratio = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
-          }
-          else if (argHeight != 0)
-          {
-            ratio = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
-          }
-          
-          targetWidth = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth()));
-          targetHeight = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight()));
-        }
-        
-        if (decoded->GetFormat() == PixelFormat_RGB24)
-        {
-          if (targetWidth == decoded->GetWidth() &&
-              targetHeight == decoded->GetHeight())
-          {
-            DefaultHandler(call, decoded, ImageExtractionMode_Preview, false);
-          }
-          else
-          {
-            std::unique_ptr<ImageAccessor> resized(
-              new Image(decoded->GetFormat(), targetWidth, targetHeight, false));
-            
-            if (smooth &&
-                (targetWidth < decoded->GetWidth() ||
-                 targetHeight < decoded->GetHeight()))
-            {
-              ImageProcessing::SmoothGaussian5x5(*decoded);
-            }
-            
-            ImageProcessing::Resize(*resized, *decoded);
-            DefaultHandler(call, resized, ImageExtractionMode_Preview, false);
-          }
-        }
-        else
-        {
-          // Grayscale image: (1) convert to Float32, (2) apply
-          // windowing to get a Grayscale8, (3) possibly resize
-
-          Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false);
-          ImageProcessing::Convert(converted, *decoded);
-
-          // Avoid divisions by zero
-          if (windowWidth <= 1.0f)
-          {
-            windowWidth = 1;
-          }
-
-          if (std::abs(rescaleSlope) <= 0.1f)
-          {
-            rescaleSlope = 0.1f;
-          }
-
-          const float scaling = 255.0f * rescaleSlope / windowWidth;
-          const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope;
-
-          std::unique_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false));
-          ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false);
-
-          if (targetWidth == decoded->GetWidth() &&
-              targetHeight == decoded->GetHeight())
-          {
-            DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert);
-          }
-          else
-          {
-            std::unique_ptr<ImageAccessor> resized(
-              new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false));
-            
-            if (smooth &&
-                (targetWidth < decoded->GetWidth() ||
-                 targetHeight < decoded->GetHeight()))
-            {
-              ImageProcessing::SmoothGaussian5x5(*rescaled);
-            }
-            
-            ImageProcessing::Resize(*resized, *rescaled);
-            DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert);
-          }
-        }
-      }
-
-      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
-      {
-        return true;
-      }
-    };
-  }
-
-
-  template <enum ImageExtractionMode mode>
-  static void GetImage(RestApiGetCall& call)
-  {
-    Semaphore::Locker locker(throttlingSemaphore_);
-        
-    GetImageHandler handler(mode);
-    IDecodedFrameHandler::Apply(call, handler);
-  }
-
-
-  static void GetRenderedFrame(RestApiGetCall& call)
-  {
-    Semaphore::Locker locker(throttlingSemaphore_);
-        
-    RenderedFrameHandler handler;
-    IDecodedFrameHandler::Apply(call, handler);
-  }
-
-
-  static void GetMatlabImage(RestApiGetCall& call)
-  {
-    Semaphore::Locker locker(throttlingSemaphore_);
-        
-    ServerContext& context = OrthancRestApi::GetContext(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::unique_ptr<ImageAccessor> decoded(context.DecodeDicomFrame(publicId, frame));
-
-    if (decoded.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_NotImplemented,
-                             "Cannot decode DICOM instance with ID: " + publicId);
-    }
-    else
-    {
-      std::string result;
-      decoded->ToMatlabString(result);
-      call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
-    }
-  }
-
-
-  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;
-    MimeType 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, MimeType_Gzip);
-    }
-    else
-    {
-      call.GetOutput().AnswerBuffer(raw, mime);
-    }
-  }
-
-
-  static void GetResourceStatistics(RestApiGetCall& call)
-  {
-    static const uint64_t MEGA_BYTES = 1024 * 1024;
-
-    std::string publicId = call.GetUriComponent("id", "");
-
-    ResourceType type;
-    uint64_t diskSize, uncompressedSize, dicomDiskSize, dicomUncompressedSize;
-    unsigned int countStudies, countSeries, countInstances;
-    OrthancRestApi::GetIndex(call).GetResourceStatistics(
-      type, diskSize, uncompressedSize, countStudies, countSeries, 
-      countInstances, dicomDiskSize, dicomUncompressedSize, publicId);
-
-    Json::Value result = Json::objectValue;
-    result["DiskSize"] = boost::lexical_cast<std::string>(diskSize);
-    result["DiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
-    result["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
-    result["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
-
-    result["DicomDiskSize"] = boost::lexical_cast<std::string>(dicomDiskSize);
-    result["DicomDiskSizeMB"] = static_cast<unsigned int>(dicomDiskSize / MEGA_BYTES);
-    result["DicomUncompressedSize"] = boost::lexical_cast<std::string>(dicomUncompressedSize);
-    result["DicomUncompressedSizeMB"] = static_cast<unsigned int>(dicomUncompressedSize / MEGA_BYTES);
-
-    switch (type)
-    {
-      // Do NOT add "break" below this point!
-      case ResourceType_Patient:
-        result["CountStudies"] = countStudies;
-
-      case ResourceType_Study:
-        result["CountSeries"] = countSeries;
-
-      case ResourceType_Series:
-        result["CountInstances"] = countInstances;
-
-      case ResourceType_Instance:
-      default:
-        break;
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-
-  // Handling of metadata -----------------------------------------------------
-
-  static void CheckValidResourceType(RestApiCall& call)
-  {
-    std::string resourceType = call.GetUriComponent("resourceType", "");
-    StringToResourceType(resourceType.c_str());
-  }
-
-
-  static void ListMetadata(RestApiGetCall& call)
-  {
-    CheckValidResourceType(call);
-    
-    std::string publicId = call.GetUriComponent("id", "");
-    std::map<MetadataType, std::string> metadata;
-
-    OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId);
-
-    Json::Value result;
-
-    if (call.HasArgument("expand"))
-    {
-      result = Json::objectValue;
-      
-      for (std::map<MetadataType, std::string>::const_iterator 
-             it = metadata.begin(); it != metadata.end(); ++it)
-      {
-        std::string key = EnumerationToString(it->first);
-        result[key] = it->second;
-      }      
-    }
-    else
-    {
-      result = Json::arrayValue;
-      
-      for (std::map<MetadataType, std::string>::const_iterator 
-             it = metadata.begin(); it != metadata.end(); ++it)
-      {       
-        result.append(EnumerationToString(it->first));
-      }
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  static void GetMetadata(RestApiGetCall& call)
-  {
-    CheckValidResourceType(call);
-    
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-    MetadataType metadata = StringToMetadata(name);
-
-    std::string value;
-    if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata))
-    {
-      call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
-    }
-  }
-
-
-  static void DeleteMetadata(RestApiDeleteCall& call)
-  {
-    CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-    MetadataType metadata = StringToMetadata(name);
-
-    if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
-    {      
-      OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
-      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
-    }
-  }
-
-
-  static void SetMetadata(RestApiPutCall& call)
-  {
-    CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-    MetadataType metadata = StringToMetadata(name);
-
-    std::string value;
-    call.BodyToString(value);
-
-    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("", MimeType_PlainText);
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
-    }
-  }
-
-
-
-
-  // Handling of attached files -----------------------------------------------
-
-  static void ListAttachments(RestApiGetCall& call)
-  {
-    std::string resourceType = call.GetUriComponent("resourceType", "");
-    std::string publicId = call.GetUriComponent("id", "");
-    std::list<FileContentType> attachments;
-    OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
-
-    Json::Value result = Json::arrayValue;
-
-    for (std::list<FileContentType>::const_iterator 
-           it = attachments.begin(); it != attachments.end(); ++it)
-    {
-      result.append(EnumerationToString(*it));
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call)
-  {
-    CheckValidResourceType(call);
- 
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-    FileContentType contentType = StringToContentType(name);
-
-    return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType);
-  }
-
-
-  static void GetAttachmentOperations(RestApiGetCall& call)
-  {
-    FileInfo info;
-    if (GetAttachmentInfo(info, call))
-    {
-      Json::Value operations = Json::arrayValue;
-
-      operations.append("compress");
-      operations.append("compressed-data");
-
-      if (info.GetCompressedMD5() != "")
-      {
-        operations.append("compressed-md5");
-      }
-
-      operations.append("compressed-size");
-      operations.append("data");
-      operations.append("is-compressed");
-
-      if (info.GetUncompressedMD5() != "")
-      {
-        operations.append("md5");
-      }
-
-      operations.append("size");
-      operations.append("uncompress");
-
-      if (info.GetCompressedMD5() != "" &&
-          info.GetUncompressedMD5() != "")
-      {
-        operations.append("verify-md5");
-      }
-
-      call.GetOutput().AnswerJson(operations);
-    }
-  }
-
-  
-  template <int uncompress>
-  static void GetAttachmentData(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    CheckValidResourceType(call);
- 
-    std::string publicId = call.GetUriComponent("id", "");
-    FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
-
-    if (uncompress)
-    {
-      context.AnswerAttachment(call.GetOutput(), publicId, type);
-    }
-    else
-    {
-      // Return the raw data (possibly compressed), as stored on the filesystem
-      std::string content;
-      context.ReadAttachment(content, publicId, type, false);
-      call.GetOutput().AnswerBuffer(content, MimeType_Binary);
-    }
-  }
-
-
-  static void GetAttachmentSize(RestApiGetCall& call)
-  {
-    FileInfo info;
-    if (GetAttachmentInfo(info, call))
-    {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText);
-    }
-  }
-
-
-  static void GetAttachmentCompressedSize(RestApiGetCall& call)
-  {
-    FileInfo info;
-    if (GetAttachmentInfo(info, call))
-    {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText);
-    }
-  }
-
-
-  static void GetAttachmentMD5(RestApiGetCall& call)
-  {
-    FileInfo info;
-    if (GetAttachmentInfo(info, call) &&
-        info.GetUncompressedMD5() != "")
-    {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText);
-    }
-  }
-
-
-  static void GetAttachmentCompressedMD5(RestApiGetCall& call)
-  {
-    FileInfo info;
-    if (GetAttachmentInfo(info, call) &&
-        info.GetCompressedMD5() != "")
-    {
-      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText);
-    }
-  }
-
-
-  static void VerifyAttachment(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-
-    FileInfo info;
-    if (!GetAttachmentInfo(info, call) ||
-        info.GetCompressedMD5() == "" ||
-        info.GetUncompressedMD5() == "")
-    {
-      // Inexistent resource, or no MD5 available
-      return;
-    }
-
-    bool ok = false;
-
-    // First check whether the compressed data is correctly stored in the disk
-    std::string data;
-    context.ReadAttachment(data, publicId, StringToContentType(name), false);
-
-    std::string actualMD5;
-    Toolbox::ComputeMD5(actualMD5, data);
-    
-    if (actualMD5 == info.GetCompressedMD5())
-    {
-      // The compressed data is OK. If a compression algorithm was
-      // applied to it, now check the MD5 of the uncompressed data.
-      if (info.GetCompressionType() == CompressionType_None)
-      {
-        ok = true;
-      }
-      else
-      {
-        context.ReadAttachment(data, publicId, StringToContentType(name), true);        
-        Toolbox::ComputeMD5(actualMD5, data);
-        ok = (actualMD5 == info.GetUncompressedMD5());
-      }
-    }
-
-    if (ok)
-    {
-      LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5";
-      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-    }
-    else
-    {
-      LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!";
-    }
-  }
-
-
-  static void UploadAttachment(RestApiPutCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    CheckValidResourceType(call);
- 
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-
-    FileContentType contentType = StringToContentType(name);
-    if (IsUserContentType(contentType) &&  // It is forbidden to modify internal attachments
-        context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
-    {
-      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
-    }
-  }
-
-
-  static void DeleteAttachment(RestApiDeleteCall& call)
-  {
-    CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-    FileContentType contentType = StringToContentType(name);
-
-    bool allowed;
-    if (IsUserContentType(contentType))
-    {
-      allowed = true;
-    }
-    else
-    {
-      OrthancConfiguration::ReaderLock lock;
-
-      if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) &&
-          contentType == FileContentType_DicomAsJson)
-      {
-        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("{}", MimeType_Json);
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
-    }
-  }
-
-
-  template <enum CompressionType compression>
-  static void ChangeAttachmentCompression(RestApiPostCall& call)
-  {
-    CheckValidResourceType(call);
-
-    std::string publicId = call.GetUriComponent("id", "");
-    std::string name = call.GetUriComponent("name", "");
-    FileContentType contentType = StringToContentType(name);
-
-    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
-    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
-  }
-
-
-  static void IsAttachmentCompressed(RestApiGetCall& call)
-  {
-    FileInfo info;
-    if (GetAttachmentInfo(info, call))
-    {
-      std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1";
-      call.GetOutput().AnswerBuffer(answer, MimeType_PlainText);
-    }
-  }
-
-
-  // Raw access to the DICOM tags of an instance ------------------------------
-
-  static void GetRawContent(RestApiGetCall& call)
-  {
-    std::string id = call.GetUriComponent("id", "");
-
-    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
-
-    locker.GetDicom().SendPathValue(call.GetOutput(), call.GetTrailingUri());
-  }
-
-
-
-  static bool ExtractSharedTags(Json::Value& shared,
-                                ServerContext& context,
-                                const std::string& publicId)
-  {
-    // Retrieve all the instances of this patient/study/series
-    typedef std::list<std::string> Instances;
-    Instances instances;
-    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
-
-    // Loop over the instances
-    bool isFirst = true;
-    shared = Json::objectValue;
-
-    for (Instances::const_iterator it = instances.begin();
-         it != instances.end(); ++it)
-    {
-      // Get the tags of the current instance, in the simplified format
-      Json::Value tags;
-
-      try
-      {
-        context.ReadDicomAsJson(tags, *it);
-      }
-      catch (OrthancException&)
-      {
-        // Race condition: This instance has been removed since
-        // (*). Ignore this instance.
-        continue;
-      }
-
-      if (tags.type() != Json::objectValue)
-      {
-        return false;   // Error
-      }
-
-      // Only keep the tags that are mapped to a string
-      Json::Value::Members members = tags.getMemberNames();
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        const Json::Value& tag = tags[members[i]];
-        if (tag.type() != Json::objectValue ||
-            tag["Type"].type() != Json::stringValue ||
-            tag["Type"].asString() != "String")
-        {
-          tags.removeMember(members[i]);
-        }
-      }
-
-      if (isFirst)
-      {
-        // This is the first instance, keep its tags as such
-        shared = tags;
-        isFirst = false;
-      }
-      else
-      {
-        // Loop over all the members of the shared tags extracted so
-        // far. If the value of one of these tags does not match its
-        // value in the current instance, remove it.
-        members = shared.getMemberNames();
-        for (size_t i = 0; i < members.size(); i++)
-        {
-          if (!tags.isMember(members[i]) ||
-              tags[members[i]]["Value"].asString() != shared[members[i]]["Value"].asString())
-          {
-            shared.removeMember(members[i]);
-          }
-        }
-      }
-    }
-
-    return true;
-  }
-
-
-  static void GetSharedTags(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    std::string publicId = call.GetUriComponent("id", "");
-
-    Json::Value sharedTags;
-    if (ExtractSharedTags(sharedTags, context, publicId))
-    {
-      // Success: Send the value of the shared tags
-      AnswerDicomAsJson(call, sharedTags);
-    }
-  }
-
-
-  static void GetModuleInternal(RestApiGetCall& call,
-                                ResourceType resourceType,
-                                DicomModule module)
-  {
-    if (!((resourceType == ResourceType_Patient && module == DicomModule_Patient) ||
-          (resourceType == ResourceType_Study && module == DicomModule_Patient) ||
-          (resourceType == ResourceType_Study && module == DicomModule_Study) ||
-          (resourceType == ResourceType_Series && module == DicomModule_Series) ||
-          (resourceType == ResourceType_Instance && module == DicomModule_Instance) ||
-          (resourceType == ResourceType_Instance && module == DicomModule_Image)))
-    {
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    std::string publicId = call.GetUriComponent("id", "");
-
-    std::set<DicomTag> ignoreTagLength;
-    ParseSetOfTags(ignoreTagLength, call, "ignore-length");
-
-    typedef std::set<DicomTag> ModuleTags;
-    ModuleTags moduleTags;
-    DicomTag::AddTagsForModule(moduleTags, module);
-
-    Json::Value tags;
-
-    if (resourceType != ResourceType_Instance)
-    {
-      // Retrieve all the instances of this patient/study/series
-      typedef std::list<std::string> Instances;
-      Instances instances;
-      context.GetIndex().GetChildInstances(instances, publicId);
-
-      if (instances.empty())
-      {
-        return;   // Error: No instance (should never happen)
-      }
-
-      // Select one child instance
-      publicId = instances.front();
-    }
-
-    context.ReadDicomAsJson(tags, publicId, ignoreTagLength);
-    
-    // Filter the tags of the instance according to the module
-    Json::Value result = Json::objectValue;
-    for (ModuleTags::const_iterator tag = moduleTags.begin(); tag != moduleTags.end(); ++tag)
-    {
-      std::string s = tag->Format();
-      if (tags.isMember(s))
-      {
-        result[s] = tags[s];
-      }      
-    }
-
-    AnswerDicomAsJson(call, result);
-  }
-    
-
-
-  template <enum ResourceType resourceType, 
-            enum DicomModule module>
-  static void GetModule(RestApiGetCall& call)
-  {
-    GetModuleInternal(call, resourceType, module);
-  }
-
-
-  namespace
-  {
-    typedef std::list< std::pair<ResourceType, std::string> >  LookupResults;
-  }
-
-
-  static void AccumulateLookupResults(LookupResults& result,
-                                      ServerIndex& index,
-                                      const DicomTag& tag,
-                                      const std::string& value,
-                                      ResourceType level)
-  {
-    std::vector<std::string> tmp;
-    index.LookupIdentifierExact(tmp, level, tag, value);
-
-    for (size_t i = 0; i < tmp.size(); i++)
-    {
-      result.push_back(std::make_pair(level, tmp[i]));
-    }
-  }
-
-
-  static void Lookup(RestApiPostCall& call)
-  {
-    std::string tag;
-    call.BodyToString(tag);
-
-    LookupResults resources;
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-    AccumulateLookupResults(resources, index, DICOM_TAG_PATIENT_ID, tag, ResourceType_Patient);
-    AccumulateLookupResults(resources, index, DICOM_TAG_STUDY_INSTANCE_UID, tag, ResourceType_Study);
-    AccumulateLookupResults(resources, index, DICOM_TAG_SERIES_INSTANCE_UID, tag, ResourceType_Series);
-    AccumulateLookupResults(resources, index, DICOM_TAG_SOP_INSTANCE_UID, tag, ResourceType_Instance);
-
-    Json::Value result = Json::arrayValue;    
-    for (LookupResults::const_iterator 
-           it = resources.begin(); it != resources.end(); ++it)
-    {     
-      ResourceType type = it->first;
-      const std::string& id = it->second;
-      
-      Json::Value item = Json::objectValue;
-      item["Type"] = EnumerationToString(type);
-      item["ID"] = id;
-      item["Path"] = GetBasePath(type, id);
-    
-      result.append(item);
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  namespace 
-  {
-    class FindVisitor : public ServerContext::ILookupVisitor
-    {
-    private:
-      bool                    isComplete_;
-      std::list<std::string>  resources_;
-
-    public:
-      FindVisitor() :
-        isComplete_(false)
-      {
-      }
-
-      virtual bool IsDicomAsJsonNeeded() const
-      {
-        return false;   // (*)
-      }
-      
-      virtual void MarkAsComplete()
-      {
-        isComplete_ = true;  // Unused information as of Orthanc 1.5.0
-      }
-
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId   /* unused     */,
-                         const DicomMap& mainDicomTags   /* unused     */,
-                         const Json::Value* dicomAsJson  /* unused (*) */) 
-      {
-        resources_.push_back(publicId);
-      }
-
-      void Answer(RestApiOutput& output,
-                  ServerIndex& index,
-                  ResourceType level,
-                  bool expand) const
-      {
-        AnswerListOfResources(output, index, resources_, level, expand);
-      }
-    };
-  }
-
-
-  static void Find(RestApiPostCall& call)
-  {
-    static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
-    static const char* const KEY_EXPAND = "Expand";
-    static const char* const KEY_LEVEL = "Level";
-    static const char* const KEY_LIMIT = "Limit";
-    static const char* const KEY_QUERY = "Query";
-    static const char* const KEY_SINCE = "Since";
-
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value request;
-    if (!call.ParseJsonRequest(request) ||
-        request.type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "The body must contain a JSON object");
-    }
-    else if (!request.isMember(KEY_LEVEL) ||
-             request[KEY_LEVEL].type() != Json::stringValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string");
-    }
-    else if (!request.isMember(KEY_QUERY) &&
-             request[KEY_QUERY].type() != Json::objectValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object");
-    }
-    else if (request.isMember(KEY_CASE_SENSITIVE) && 
-             request[KEY_CASE_SENSITIVE].type() != Json::booleanValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean");
-    }
-    else if (request.isMember(KEY_LIMIT) && 
-             request[KEY_LIMIT].type() != Json::intValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer");
-    }
-    else if (request.isMember(KEY_SINCE) &&
-             request[KEY_SINCE].type() != Json::intValue)
-    {
-      throw OrthancException(ErrorCode_BadRequest, 
-                             "Field \"" + std::string(KEY_SINCE) + "\" should be an integer");
-    }
-    else
-    {
-      bool expand = false;
-      if (request.isMember(KEY_EXPAND))
-      {
-        expand = request[KEY_EXPAND].asBool();
-      }
-
-      bool caseSensitive = false;
-      if (request.isMember(KEY_CASE_SENSITIVE))
-      {
-        caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
-      }
-
-      size_t limit = 0;
-      if (request.isMember(KEY_LIMIT))
-      {
-        int tmp = request[KEY_LIMIT].asInt();
-        if (tmp < 0)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer");
-        }
-
-        limit = static_cast<size_t>(tmp);
-      }
-
-      size_t since = 0;
-      if (request.isMember(KEY_SINCE))
-      {
-        int tmp = request[KEY_SINCE].asInt();
-        if (tmp < 0)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                 "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer");
-        }
-
-        since = static_cast<size_t>(tmp);
-      }
-
-      ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
-
-      DatabaseLookup query;
-
-      Json::Value::Members members = request[KEY_QUERY].getMemberNames();
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
-        {
-          throw OrthancException(ErrorCode_BadRequest,
-                                 "Tag \"" + members[i] + "\" should be associated with a string");
-        }
-
-        const std::string value = request[KEY_QUERY][members[i]].asString();
-
-        if (!value.empty())
-        {
-          // An empty string corresponds to an universal constraint,
-          // so we ignore it. This mimics the behavior of class
-          // "OrthancFindRequestHandler"
-          query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), 
-                                  value, caseSensitive, true);
-        }
-      }
-
-      FindVisitor visitor;
-      context.Apply(visitor, query, level, since, limit);
-      visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand);
-    }
-  }
-
-
-  template <enum ResourceType start, 
-            enum ResourceType end>
-  static void GetChildResources(RestApiGetCall& call)
-  {
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-
-    std::list<std::string> a, b, c;
-    a.push_back(call.GetUriComponent("id", ""));
-
-    ResourceType type = start;
-    while (type != end)
-    {
-      b.clear();
-
-      for (std::list<std::string>::const_iterator
-             it = a.begin(); it != a.end(); ++it)
-      {
-        index.GetChildren(c, *it);
-        b.splice(b.begin(), c);
-      }
-
-      type = GetChildResourceType(type);
-
-      a.clear();
-      a.splice(a.begin(), b);
-    }
-
-    Json::Value result = Json::arrayValue;
-
-    for (std::list<std::string>::const_iterator
-           it = a.begin(); it != a.end(); ++it)
-    {
-      Json::Value item;
-
-      if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end))
-      {
-        result.append(item);
-      }
-    }
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  static void GetChildInstancesTags(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    std::string publicId = call.GetUriComponent("id", "");
-    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;
-    Instances instances;
-
-    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
-
-    Json::Value result = Json::objectValue;
-
-    for (Instances::const_iterator it = instances.begin();
-         it != instances.end(); ++it)
-    {
-      Json::Value full;
-      context.ReadDicomAsJson(full, *it, ignoreTagLength);
-
-      if (format != DicomToJsonFormat_Full)
-      {
-        Json::Value simplified;
-        ServerToolbox::SimplifyTags(simplified, full, format);
-        result[*it] = simplified;
-      }
-      else
-      {
-        result[*it] = full;
-      }
-    }
-    
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-
-  template <enum ResourceType start, 
-            enum ResourceType end>
-  static void GetParentResource(RestApiGetCall& call)
-  {
-    assert(start > end);
-
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-    
-    std::string current = call.GetUriComponent("id", "");
-    ResourceType currentType = start;
-    while (currentType > end)
-    {
-      std::string parent;
-      if (!index.LookupParent(parent, current))
-      {
-        // Error that could happen if the resource gets deleted by
-        // another concurrent call
-        return;
-      }
-      
-      current = parent;
-      currentType = GetParentResourceType(currentType);
-    }
-
-    assert(currentType == end);
-
-    Json::Value result;
-    if (index.LookupResource(result, current, end))
-    {
-      call.GetOutput().AnswerJson(result);
-    }
-  }
-
-
-  static void ExtractPdf(RestApiGetCall& call)
-  {
-    const std::string id = call.GetUriComponent("id", "");
-
-    std::string pdf;
-    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
-
-    if (locker.GetDicom().ExtractPdf(pdf))
-    {
-      call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf);
-      return;
-    }
-  }
-
-
-  static void OrderSlices(RestApiGetCall& call)
-  {
-    const std::string id = call.GetUriComponent("id", "");
-
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-    SliceOrdering ordering(index, id);
-
-    Json::Value result;
-    ordering.Format(result);
-    call.GetOutput().AnswerJson(result);
-  }
-
-
-  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("", MimeType_PlainText);
-  }
-
-
-  template <enum ResourceType type>
-  static void ReconstructResource(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""));
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  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("", MimeType_PlainText);
-  }
-
-
-  void OrthancRestApi::RegisterResources()
-  {
-    Register("/instances", ListResources<ResourceType_Instance>);
-    Register("/patients", ListResources<ResourceType_Patient>);
-    Register("/series", ListResources<ResourceType_Series>);
-    Register("/studies", ListResources<ResourceType_Study>);
-
-    Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
-    Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
-    Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
-    Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
-    Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
-    Register("/series/{id}", GetSingleResource<ResourceType_Series>);
-    Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
-    Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
-
-    Register("/instances/{id}/statistics", GetResourceStatistics);
-    Register("/patients/{id}/statistics", GetResourceStatistics);
-    Register("/studies/{id}/statistics", GetResourceStatistics);
-    Register("/series/{id}/statistics", GetResourceStatistics);
-
-    Register("/patients/{id}/shared-tags", GetSharedTags);
-    Register("/series/{id}/shared-tags", GetSharedTags);
-    Register("/studies/{id}/shared-tags", GetSharedTags);
-
-    Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>);
-    Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>);
-    Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>);
-    Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>);
-    Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>);
-
-    Register("/instances/{id}/file", GetInstanceFile);
-    Register("/instances/{id}/export", ExportInstanceFile);
-    Register("/instances/{id}/tags", GetInstanceTagsBis);
-    Register("/instances/{id}/simplified-tags", GetInstanceTags<DicomToJsonFormat_Human>);
-    Register("/instances/{id}/frames", ListFrames);
-
-    Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
-    Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame);
-    Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
-    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}/rendered", GetRenderedFrame);
-    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);
-
-    Register("/{resourceType}/{id}/metadata", ListMetadata);
-    Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata);
-    Register("/{resourceType}/{id}/metadata/{name}", GetMetadata);
-    Register("/{resourceType}/{id}/metadata/{name}", SetMetadata);
-
-    Register("/{resourceType}/{id}/attachments", ListAttachments);
-    Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment);
-    Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations);
-    Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment);
-    Register("/{resourceType}/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
-    Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
-    Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
-    Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
-    Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>);
-    Register("/{resourceType}/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed);
-    Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5);
-    Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize);
-    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);
-
-    Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
-    Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
-    Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>);
-    Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>);
-    Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>);
-    Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>);
-
-    Register("/studies/{id}/patient", GetParentResource<ResourceType_Study, ResourceType_Patient>);
-    Register("/series/{id}/patient", GetParentResource<ResourceType_Series, ResourceType_Patient>);
-    Register("/series/{id}/study", GetParentResource<ResourceType_Series, ResourceType_Study>);
-    Register("/instances/{id}/patient", GetParentResource<ResourceType_Instance, ResourceType_Patient>);
-    Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>);
-    Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>);
-
-    Register("/patients/{id}/instances-tags", GetChildInstancesTags);
-    Register("/studies/{id}/instances-tags", GetChildInstancesTags);
-    Register("/series/{id}/instances-tags", GetChildInstancesTags);
-
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,584 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/MetricsRegistry.h"
-#include "../../Plugins/Engine/OrthancPlugins.h"
-#include "../../Plugins/Engine/PluginsManager.h"
-#include "../OrthancConfiguration.h"
-#include "../ServerContext.h"
-
-
-static const char* LOG_LEVEL_DEFAULT = "default";
-static const char* LOG_LEVEL_VERBOSE = "verbose";
-static const char* LOG_LEVEL_TRACE = "trace";
-
-
-namespace Orthanc
-{
-  // System information -------------------------------------------------------
-
-  static void ServeRoot(RestApiGetCall& call)
-  {
-    call.GetOutput().Redirect("app/explorer.html");
-  }
- 
-  static void GetSystemInformation(RestApiGetCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    Json::Value result = Json::objectValue;
-
-    result["ApiVersion"] = ORTHANC_API_VERSION;
-    result["Version"] = ORTHANC_VERSION;
-    result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
-    result["IsHttpServerSecure"] = context.IsHttpServerSecure();  // New in Orthanc 1.5.8
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      result["DicomAet"] = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
-      result["DicomPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242);
-      result["HttpPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042);
-      result["Name"] = lock.GetConfiguration().GetStringParameter("Name", "");
-    }
-
-    result["StorageAreaPlugin"] = Json::nullValue;
-    result["DatabaseBackendPlugin"] = Json::nullValue;
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    result["PluginsEnabled"] = true;
-    const OrthancPlugins& plugins = context.GetPlugins();
-
-    if (plugins.HasStorageArea())
-    {
-      std::string p = plugins.GetStorageAreaLibrary().GetPath();
-      result["StorageAreaPlugin"] = boost::filesystem::canonical(p).string();
-    }
-
-    if (plugins.HasDatabaseBackend())
-    {
-      std::string p = plugins.GetDatabaseBackendLibrary().GetPath();
-      result["DatabaseBackendPlugin"] = boost::filesystem::canonical(p).string();     
-    }
-#else
-    result["PluginsEnabled"] = false;
-#endif
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-  static void GetStatistics(RestApiGetCall& call)
-  {
-    static const uint64_t MEGA_BYTES = 1024 * 1024;
-
-    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
-    OrthancRestApi::GetIndex(call).GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                                                       countStudies, countSeries, countInstances);
-    
-    Json::Value result = Json::objectValue;
-    result["TotalDiskSize"] = boost::lexical_cast<std::string>(diskSize);
-    result["TotalUncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
-    result["TotalDiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
-    result["TotalUncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
-    result["CountPatients"] = static_cast<unsigned int>(countPatients);
-    result["CountStudies"] = static_cast<unsigned int>(countStudies);
-    result["CountSeries"] = static_cast<unsigned int>(countSeries);
-    result["CountInstances"] = static_cast<unsigned int>(countInstances);
-
-    call.GetOutput().AnswerJson(result);
-  }
-
-  static void GenerateUid(RestApiGetCall& call)
-  {
-    std::string level = call.GetArgument("level", "");
-    if (level == "patient")
-    {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), MimeType_PlainText);
-    }
-    else if (level == "study")
-    {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), MimeType_PlainText);
-    }
-    else if (level == "series")
-    {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), MimeType_PlainText);
-    }
-    else if (level == "instance")
-    {
-      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), MimeType_PlainText);
-    }
-  }
-
-  static void ExecuteScript(RestApiPostCall& call)
-  {
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    if (!context.IsExecuteLuaEnabled())
-    {
-      LOG(ERROR) << "The URI /tools/execute-script is disallowed for security, "
-                 << "check your configuration file";
-      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
-      return;
-    }
-
-    std::string result;
-    std::string command;
-    call.BodyToString(command);
-
-    {
-      LuaScripting::Lock lock(context.GetLuaScripting());
-      lock.GetLua().Execute(result, command);
-    }
-
-    call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
-  }
-
-  template <bool UTC>
-  static void GetNowIsoString(RestApiGetCall& call)
-  {
-    call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), MimeType_PlainText);
-  }
-
-
-  static void GetDicomConformanceStatement(RestApiGetCall& call)
-  {
-    std::string statement;
-    GetFileResource(statement, ServerResources::DICOM_CONFORMANCE_STATEMENT);
-    call.GetOutput().AnswerBuffer(statement, MimeType_PlainText);
-  }
-
-
-  static void GetDefaultEncoding(RestApiGetCall& call)
-  {
-    Encoding encoding = GetDefaultDicomEncoding();
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
-  }
-
-
-  static void SetDefaultEncoding(RestApiPutCall& call)
-  {
-    std::string body;
-    call.BodyToString(body);
-
-    Encoding encoding = StringToEncoding(body.c_str());
-
-    {
-      OrthancConfiguration::WriterLock lock;
-      lock.GetConfiguration().SetDefaultEncoding(encoding);
-    }
-
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
-  }
-
-
-  
-  // Plugins information ------------------------------------------------------
-
-  static void ListPlugins(RestApiGetCall& call)
-  {
-    Json::Value v = Json::arrayValue;
-
-    v.append("explorer.js");
-
-    if (OrthancRestApi::GetContext(call).HasPlugins())
-    {
-#if ORTHANC_ENABLE_PLUGINS == 1
-      std::list<std::string> plugins;
-      OrthancRestApi::GetContext(call).GetPlugins().GetManager().ListPlugins(plugins);
-
-      for (std::list<std::string>::const_iterator 
-             it = plugins.begin(); it != plugins.end(); ++it)
-      {
-        v.append(*it);
-      }
-#endif
-    }
-
-    call.GetOutput().AnswerJson(v);
-  }
-
-
-  static void GetPlugin(RestApiGetCall& call)
-  {
-    if (!OrthancRestApi::GetContext(call).HasPlugins())
-    {
-      return;
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPlugins().GetManager();
-    std::string id = call.GetUriComponent("id", "");
-
-    if (manager.HasPlugin(id))
-    {
-      Json::Value v = Json::objectValue;
-      v["ID"] = id;
-      v["Version"] = manager.GetPluginVersion(id);
-
-      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
-      const char *c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_RootUri);
-      if (c != NULL)
-      {
-        std::string root = c;
-        if (!root.empty())
-        {
-          // Turn the root URI into a URI relative to "/app/explorer.js"
-          if (root[0] == '/')
-          {
-            root = ".." + root;
-          }
-
-          v["RootUri"] = root;
-        }
-      }
-
-      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_Description);
-      if (c != NULL)
-      {
-        v["Description"] = c;
-      }
-
-      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_OrthancExplorer);
-      v["ExtendsOrthancExplorer"] = (c != NULL);
-
-      call.GetOutput().AnswerJson(v);
-    }
-#endif
-  }
-
-
-  static void GetOrthancExplorerPlugins(RestApiGetCall& call)
-  {
-    std::string s = "// Extensions to Orthanc Explorer by the registered plugins\n\n";
-
-    if (OrthancRestApi::GetContext(call).HasPlugins())
-    {
-#if ORTHANC_ENABLE_PLUGINS == 1
-      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
-      const PluginsManager& manager = plugins.GetManager();
-
-      std::list<std::string> lst;
-      manager.ListPlugins(lst);
-
-      for (std::list<std::string>::const_iterator
-             it = lst.begin(); it != lst.end(); ++it)
-      {
-        const char* tmp = plugins.GetProperty(it->c_str(), _OrthancPluginProperty_OrthancExplorer);
-        if (tmp != NULL)
-        {
-          s += "/**\n * From plugin: " + *it + " (version " + manager.GetPluginVersion(*it) + ")\n **/\n\n";
-          s += std::string(tmp) + "\n\n";
-        }
-      }
-#endif
-    }
-
-    call.GetOutput().AnswerBuffer(s, MimeType_JavaScript);
-  }
-
-
-
-
-  // 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);
-    }
-  }
-
-
-  static void GetJobOutput(RestApiGetCall& call)
-  {
-    std::string job = call.GetUriComponent("id", "");
-    std::string key = call.GetUriComponent("key", "");
-
-    std::string value;
-    MimeType mime;
-    
-    if (OrthancRestApi::GetContext(call).GetJobsEngine().
-        GetRegistry().GetJobOutput(value, mime, job, key))
-    {
-      call.GetOutput().AnswerBuffer(value, mime);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InexistentItem,
-                             "Job has no such output: " + key);
-    }
-  }
-
-
-  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("{}", MimeType_Json);
-    }
-  }
-
-  
-  static void GetMetricsPrometheus(RestApiGetCall& call)
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    OrthancRestApi::GetContext(call).GetPlugins().RefreshMetrics();
-#endif
-
-    static const float MEGA_BYTES = 1024 * 1024;
-
-    ServerContext& context = OrthancRestApi::GetContext(call);
-
-    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
-    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                                           countStudies, countSeries, countInstances);
-
-    unsigned int jobsPending, jobsRunning, jobsSuccess, jobsFailed;
-    context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning, jobsSuccess, jobsFailed);
-
-    MetricsRegistry& registry = context.GetMetricsRegistry();
-    registry.SetValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
-    registry.SetValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
-    registry.SetValue("orthanc_count_patients", static_cast<unsigned int>(countPatients));
-    registry.SetValue("orthanc_count_studies", static_cast<unsigned int>(countStudies));
-    registry.SetValue("orthanc_count_series", static_cast<unsigned int>(countSeries));
-    registry.SetValue("orthanc_count_instances", static_cast<unsigned int>(countInstances));
-    registry.SetValue("orthanc_jobs_pending", jobsPending);
-    registry.SetValue("orthanc_jobs_running", jobsRunning);
-    registry.SetValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
-    registry.SetValue("orthanc_jobs_success", jobsSuccess);
-    registry.SetValue("orthanc_jobs_failed", jobsFailed);
-    
-    std::string s;
-    registry.ExportPrometheusText(s);
-
-    call.GetOutput().AnswerBuffer(s, MimeType_PrometheusText);
-  }
-
-
-  static void GetMetricsEnabled(RestApiGetCall& call)
-  {
-    bool enabled = OrthancRestApi::GetContext(call).GetMetricsRegistry().IsEnabled();
-    call.GetOutput().AnswerBuffer(enabled ? "1" : "0", MimeType_PlainText);
-  }
-
-
-  static void PutMetricsEnabled(RestApiPutCall& call)
-  {
-    bool enabled;
-
-    std::string body;
-    call.BodyToString(body);
-
-    if (body == "1")
-    {
-      enabled = true;
-    }
-    else if (body == "0")
-    {
-      enabled = false;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "The HTTP body must be 0 or 1, but found: " + body);
-    }
-
-    // Success
-    OrthancRestApi::GetContext(call).GetMetricsRegistry().SetEnabled(enabled);
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  static void GetLogLevel(RestApiGetCall& call)
-  {
-    std::string s;
-    
-    if (Logging::IsTraceLevelEnabled())
-    {
-      s = LOG_LEVEL_TRACE;
-    }
-    else if (Logging::IsInfoLevelEnabled())
-    {
-      s = LOG_LEVEL_VERBOSE;
-    }
-    else
-    {
-      s = LOG_LEVEL_DEFAULT;
-    }
-    
-    call.GetOutput().AnswerBuffer(s, MimeType_PlainText);
-  }
-
-
-  static void PutLogLevel(RestApiPutCall& call)
-  {
-    std::string body;
-    call.BodyToString(body);
-
-    if (body == LOG_LEVEL_DEFAULT)
-    {
-      Logging::EnableInfoLevel(false);
-      Logging::EnableTraceLevel(false);
-    }
-    else if (body == LOG_LEVEL_VERBOSE)
-    {
-      Logging::EnableInfoLevel(true);
-      Logging::EnableTraceLevel(false);
-    }
-    else if (body == LOG_LEVEL_TRACE)
-    {
-      Logging::EnableInfoLevel(true);
-      Logging::EnableTraceLevel(true);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "The log level must be one of the following values: \"" +
-                             std::string(LOG_LEVEL_DEFAULT) + "\", \"" +
-                             std::string(LOG_LEVEL_VERBOSE) + "\", of \"" +
-                             std::string(LOG_LEVEL_TRACE) + "\"");
-    }
-
-    // Success
-    LOG(WARNING) << "REST API call has switched the log level to: " << body;
-    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
-  }
-
-
-  void OrthancRestApi::RegisterSystem()
-  {
-    Register("/", ServeRoot);
-    Register("/system", GetSystemInformation);
-    Register("/statistics", GetStatistics);
-    Register("/tools/generate-uid", GenerateUid);
-    Register("/tools/execute-script", ExecuteScript);
-    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("/tools/metrics", GetMetricsEnabled);
-    Register("/tools/metrics", PutMetricsEnabled);
-    Register("/tools/metrics-prometheus", GetMetricsPrometheus);
-    Register("/tools/log-level", GetLogLevel);
-    Register("/tools/log-level", PutLogLevel);
-
-    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>);
-    Register("/jobs/{id}/{key}", GetJobOutput);
-  }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/IPluginServiceProvider.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Include/orthanc/OrthancCPlugin.h"
+
+#include "../../Core/SharedLibrary.h"
+
+namespace Orthanc
+{
+  class IPluginServiceProvider : boost::noncopyable
+  {
+  public:
+    virtual ~IPluginServiceProvider()
+    {
+    }
+
+    virtual bool InvokeService(SharedLibrary& plugin,
+                               _OrthancPluginService service,
+                               const void* parameters) = 0;
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1534 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancPluginDatabase.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#error The plugin support is disabled
+#endif
+
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "PluginsEnumerations.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    OrthancPluginDatabase&  that_;
+
+    void CheckSuccess(OrthancPluginErrorCode code) const
+    {
+      if (code != OrthancPluginErrorCode_Success)
+      {
+        that_.errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+
+  public:
+    Transaction(OrthancPluginDatabase& that) :
+    that_(that)
+    {
+    }
+
+    virtual void Begin()
+    {
+      CheckSuccess(that_.backend_.startTransaction(that_.payload_));
+    }
+
+    virtual void Rollback()
+    {
+      CheckSuccess(that_.backend_.rollbackTransaction(that_.payload_));
+    }
+
+    virtual void Commit(int64_t diskSizeDelta)
+    {
+      if (that_.fastGetTotalSize_)
+      {
+        CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
+      }
+      else
+      {
+        if (static_cast<int64_t>(that_.currentDiskSize_) + diskSizeDelta < 0)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta);
+
+        assert(newDiskSize == that_.GetTotalCompressedSize());
+
+        CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
+
+        // The transaction has succeeded, we can commit the new disk size
+        that_.currentDiskSize_ = newDiskSize;
+      }
+    }
+  };
+
+
+  static FileInfo Convert(const OrthancPluginAttachment& attachment)
+  {
+    return FileInfo(attachment.uuid,
+                    static_cast<FileContentType>(attachment.contentType),
+                    attachment.uncompressedSize,
+                    attachment.uncompressedHash,
+                    static_cast<CompressionType>(attachment.compressionType),
+                    attachment.compressedSize,
+                    attachment.compressedHash);
+  }
+
+
+  void OrthancPluginDatabase::CheckSuccess(OrthancPluginErrorCode code)
+  {
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      errorDictionary_.LogError(code, true);
+      throw OrthancException(static_cast<ErrorCode>(code));
+    }
+  }
+
+
+  void OrthancPluginDatabase::ResetAnswers()
+  {
+    type_ = _OrthancPluginDatabaseAnswerType_None;
+
+    answerDicomMap_ = NULL;
+    answerChanges_ = NULL;
+    answerExportedResources_ = NULL;
+    answerDone_ = NULL;
+    answerMatchingResources_ = NULL;
+    answerMatchingInstances_ = NULL;
+    answerMetadata_ = NULL;
+  }
+
+
+  void OrthancPluginDatabase::ForwardAnswers(std::list<int64_t>& target)
+  {
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_Int64)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_Int64)
+    {
+      for (std::list<int64_t>::const_iterator 
+             it = answerInt64_.begin(); it != answerInt64_.end(); ++it)
+      {
+        target.push_back(*it);
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::ForwardAnswers(std::list<std::string>& target)
+  {
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_String)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_String)
+    {
+      for (std::list<std::string>::const_iterator 
+             it = answerStrings_.begin(); it != answerStrings_.end(); ++it)
+      {
+        target.push_back(*it);
+      }
+    }
+  }
+
+
+  bool OrthancPluginDatabase::ForwardSingleAnswer(std::string& target)
+  {
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_String &&
+             answerStrings_.size() == 1)
+    {
+      target = answerStrings_.front();
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
+
+  bool OrthancPluginDatabase::ForwardSingleAnswer(int64_t& target)
+  {
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 &&
+             answerInt64_.size() == 1)
+    {
+      target = answerInt64_.front();
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
+
+  OrthancPluginDatabase::OrthancPluginDatabase(SharedLibrary& library,
+                                               PluginsErrorDictionary&  errorDictionary,
+                                               const OrthancPluginDatabaseBackend& backend,
+                                               const OrthancPluginDatabaseExtensions* extensions,
+                                               size_t extensionsSize,
+                                               void *payload) : 
+    library_(library),
+    errorDictionary_(errorDictionary),
+    backend_(backend),
+    payload_(payload),
+    listener_(NULL)
+  {
+    static const char* const MISSING = "  Missing extension in database index plugin: ";
+    
+    ResetAnswers();
+
+    memset(&extensions_, 0, sizeof(extensions_));
+
+    size_t size = sizeof(extensions_);
+    if (extensionsSize < size)
+    {
+      size = extensionsSize;  // Not all the extensions are available
+    }
+
+    memcpy(&extensions_, extensions, size);
+
+    bool isOptimal = true;
+
+    if (extensions_.lookupResources == NULL)
+    {
+      LOG(INFO) << MISSING << "LookupIdentifierRange()";
+      isOptimal = false;
+    }
+
+    if (extensions_.createInstance == NULL)
+    {
+      LOG(INFO) << MISSING << "CreateInstance()";
+      isOptimal = false;
+    }
+
+    if (extensions_.setResourcesContent == NULL)
+    {
+      LOG(INFO) << MISSING << "SetResourcesContent()";
+      isOptimal = false;
+    }
+
+    if (extensions_.getChildrenMetadata == NULL)
+    {
+      LOG(INFO) << MISSING << "GetChildrenMetadata()";
+      isOptimal = false;
+    }
+
+    if (extensions_.getAllMetadata == NULL)
+    {
+      LOG(INFO) << MISSING << "GetAllMetadata()";
+      isOptimal = false;
+    }
+
+    if (extensions_.lookupResourceAndParent == NULL)
+    {
+      LOG(INFO) << MISSING << "LookupResourceAndParent()";
+      isOptimal = false;
+    }
+
+    if (isOptimal)
+    {
+      LOG(INFO) << "The performance of the database index plugin "
+                << "is optimal for this version of Orthanc";
+    }
+    else
+    {
+      LOG(WARNING) << "Performance warning in the database index: "
+                   << "Some extensions are missing in the plugin";
+    }
+
+    if (extensions_.getLastChangeIndex == NULL)
+    {
+      LOG(WARNING) << "The database extension GetLastChangeIndex() is missing";
+    }
+
+    if (extensions_.tagMostRecentPatient == NULL)
+    {
+      LOG(WARNING) << "The database extension TagMostRecentPatient() is missing "
+                   << "(affected by issue 58)";
+    }
+  }
+
+
+  void OrthancPluginDatabase::Open()
+  {
+    CheckSuccess(backend_.open(payload_));
+
+    {
+      Transaction transaction(*this);
+      transaction.Begin();
+
+      std::string tmp;
+      fastGetTotalSize_ =
+        (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) &&
+         tmp == "1");
+      
+      if (fastGetTotalSize_)
+      {
+        currentDiskSize_ = 0;   // Unused
+      }
+      else
+      {
+        // This is the case of database plugins using Orthanc SDK <= 1.5.2
+        LOG(WARNING) << "Your database index plugin is not compatible with multiple Orthanc writers";
+        currentDiskSize_ = GetTotalCompressedSize();
+      }
+
+      transaction.Commit(0);
+    }
+  }
+
+
+  void OrthancPluginDatabase::AddAttachment(int64_t id,
+                                            const FileInfo& attachment)
+  {
+    OrthancPluginAttachment tmp;
+    tmp.uuid = attachment.GetUuid().c_str();
+    tmp.contentType = static_cast<int32_t>(attachment.GetContentType());
+    tmp.uncompressedSize = attachment.GetUncompressedSize();
+    tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str();
+    tmp.compressionType = static_cast<int32_t>(attachment.GetCompressionType());
+    tmp.compressedSize = attachment.GetCompressedSize();
+    tmp.compressedHash = attachment.GetCompressedMD5().c_str();
+
+    CheckSuccess(backend_.addAttachment(payload_, id, &tmp));
+  }
+
+
+  void OrthancPluginDatabase::AttachChild(int64_t parent,
+                                          int64_t child)
+  {
+    CheckSuccess(backend_.attachChild(payload_, parent, child));
+  }
+
+
+  void OrthancPluginDatabase::ClearChanges()
+  {
+    CheckSuccess(backend_.clearChanges(payload_));
+  }
+
+
+  void OrthancPluginDatabase::ClearExportedResources()
+  {
+    CheckSuccess(backend_.clearExportedResources(payload_));
+  }
+
+
+  int64_t OrthancPluginDatabase::CreateResource(const std::string& publicId,
+                                                ResourceType type)
+  {
+    int64_t id;
+    CheckSuccess(backend_.createResource(&id, payload_, publicId.c_str(), Plugins::Convert(type)));
+    return id;
+  }
+
+
+  void OrthancPluginDatabase::DeleteAttachment(int64_t id,
+                                               FileContentType attachment)
+  {
+    CheckSuccess(backend_.deleteAttachment(payload_, id, static_cast<int32_t>(attachment)));
+  }
+
+
+  void OrthancPluginDatabase::DeleteMetadata(int64_t id,
+                                             MetadataType type)
+  {
+    CheckSuccess(backend_.deleteMetadata(payload_, id, static_cast<int32_t>(type)));
+  }
+
+
+  void OrthancPluginDatabase::DeleteResource(int64_t id)
+  {
+    CheckSuccess(backend_.deleteResource(payload_, id));
+  }
+
+
+  void OrthancPluginDatabase::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                             int64_t id)
+  {
+    if (extensions_.getAllMetadata == NULL)
+    {
+      // Fallback implementation if extension is missing
+      target.clear();
+
+      ResetAnswers();
+      CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id));
+
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_Int32)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      target.clear();
+
+      if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+      {
+        for (std::list<int32_t>::const_iterator 
+               it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+        {
+          MetadataType type = static_cast<MetadataType>(*it);
+
+          std::string value;
+          if (LookupMetadata(value, id, type))
+          {
+            target[type] = value;
+          }
+        }
+      }
+    }
+    else
+    {
+      ResetAnswers();
+
+      answerMetadata_ = &target;
+      target.clear();
+      
+      CheckSuccess(extensions_.getAllMetadata(GetContext(), payload_, id));
+
+      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+          type_ != _OrthancPluginDatabaseAnswerType_Metadata)
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::GetAllInternalIds(std::list<int64_t>& target,
+                                                ResourceType resourceType)
+  {
+    if (extensions_.getAllInternalIds == NULL)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "The database plugin does not implement the mandatory GetAllInternalIds() extension");
+    }
+
+    ResetAnswers();
+    CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType)));
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
+                                              ResourceType resourceType)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.getAllPublicIds(GetContext(), payload_, Plugins::Convert(resourceType)));
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
+                                              ResourceType resourceType,
+                                              size_t since,
+                                              size_t limit)
+  {
+    if (extensions_.getAllPublicIdsWithLimit != NULL)
+    {
+      // This extension is available since Orthanc 0.9.4
+      ResetAnswers();
+      CheckSuccess(extensions_.getAllPublicIdsWithLimit
+                   (GetContext(), payload_, Plugins::Convert(resourceType), since, limit));
+      ForwardAnswers(target);
+    }
+    else
+    {
+      // The extension is not available in the database plugin, use a
+      // fallback implementation
+      target.clear();
+
+      if (limit == 0)
+      {
+        return;
+      }
+
+      std::list<std::string> tmp;
+      GetAllPublicIds(tmp, resourceType);
+    
+      if (tmp.size() <= since)
+      {
+        // Not enough results => empty answer
+        return;
+      }
+
+      std::list<std::string>::iterator current = tmp.begin();
+      std::advance(current, since);
+
+      while (limit > 0 && current != tmp.end())
+      {
+        target.push_back(*current);
+        --limit;
+        ++current;
+      }
+    }
+  }
+
+
+
+  void OrthancPluginDatabase::GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                                         bool& done /*out*/,
+                                         int64_t since,
+                                         uint32_t maxResults)
+  {
+    ResetAnswers();
+    answerChanges_ = &target;
+    answerDone_ = &done;
+    done = false;
+
+    CheckSuccess(backend_.getChanges(GetContext(), payload_, since, maxResults));
+  }
+
+
+  void OrthancPluginDatabase::GetChildrenInternalId(std::list<int64_t>& target,
+                                                    int64_t id)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.getChildrenInternalId(GetContext(), payload_, id));
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::GetChildrenPublicId(std::list<std::string>& target,
+                                                  int64_t id)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.getChildrenPublicId(GetContext(), payload_, id));
+    ForwardAnswers(target);
+  }
+
+
+  void OrthancPluginDatabase::GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                                   bool& done /*out*/,
+                                                   int64_t since,
+                                                   uint32_t maxResults)
+  {
+    ResetAnswers();
+    answerExportedResources_ = &target;
+    answerDone_ = &done;
+    done = false;
+
+    CheckSuccess(backend_.getExportedResources(GetContext(), payload_, since, maxResults));
+  }
+
+
+  void OrthancPluginDatabase::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+  {
+    bool ignored = false;
+
+    ResetAnswers();
+    answerChanges_ = &target;
+    answerDone_ = &ignored;
+
+    CheckSuccess(backend_.getLastChange(GetContext(), payload_));
+  }
+
+
+  void OrthancPluginDatabase::GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
+  {
+    bool ignored = false;
+
+    ResetAnswers();
+    answerExportedResources_ = &target;
+    answerDone_ = &ignored;
+
+    CheckSuccess(backend_.getLastExportedResource(GetContext(), payload_));
+  }
+
+
+  void OrthancPluginDatabase::GetMainDicomTags(DicomMap& map,
+                                               int64_t id)
+  {
+    ResetAnswers();
+    answerDicomMap_ = &map;
+
+    CheckSuccess(backend_.getMainDicomTags(GetContext(), payload_, id));
+  }
+
+
+  std::string OrthancPluginDatabase::GetPublicId(int64_t resourceId)
+  {
+    ResetAnswers();
+    std::string s;
+
+    CheckSuccess(backend_.getPublicId(GetContext(), payload_, resourceId));
+
+    if (!ForwardSingleAnswer(s))
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    return s;
+  }
+
+
+  uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType)
+  {
+    uint64_t count;
+    CheckSuccess(backend_.getResourceCount(&count, payload_, Plugins::Convert(resourceType)));
+    return count;
+  }
+
+
+  ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId)
+  {
+    OrthancPluginResourceType type;
+    CheckSuccess(backend_.getResourceType(&type, payload_, resourceId));
+    return Plugins::Convert(type);
+  }
+
+
+  uint64_t OrthancPluginDatabase::GetTotalCompressedSize()
+  {
+    uint64_t size;
+    CheckSuccess(backend_.getTotalCompressedSize(&size, payload_));
+    return size;
+  }
+
+    
+  uint64_t OrthancPluginDatabase::GetTotalUncompressedSize()
+  {
+    uint64_t size;
+    CheckSuccess(backend_.getTotalUncompressedSize(&size, payload_));
+    return size;
+  }
+
+
+  bool OrthancPluginDatabase::IsExistingResource(int64_t internalId)
+  {
+    int32_t existing;
+    CheckSuccess(backend_.isExistingResource(&existing, payload_, internalId));
+    return (existing != 0);
+  }
+
+
+  bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId)
+  {
+    int32_t isProtected;
+    CheckSuccess(backend_.isProtectedPatient(&isProtected, payload_, internalId));
+    return (isProtected != 0);
+  }
+
+
+  void OrthancPluginDatabase::ListAvailableAttachments(std::list<FileContentType>& target,
+                                                       int64_t id)
+  {
+    ResetAnswers();
+
+    CheckSuccess(backend_.listAvailableAttachments(GetContext(), payload_, id));
+
+    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
+        type_ != _OrthancPluginDatabaseAnswerType_Int32)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    target.clear();
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
+    {
+      for (std::list<int32_t>::const_iterator 
+             it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
+      {
+        target.push_back(static_cast<FileContentType>(*it));
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::LogChange(int64_t internalId,
+                                        const ServerIndexChange& change)
+  {
+    OrthancPluginChange tmp;
+    tmp.seq = change.GetSeq();
+    tmp.changeType = static_cast<int32_t>(change.GetChangeType());
+    tmp.resourceType = Plugins::Convert(change.GetResourceType());
+    tmp.publicId = change.GetPublicId().c_str();
+    tmp.date = change.GetDate().c_str();
+
+    CheckSuccess(backend_.logChange(payload_, &tmp));
+  }
+
+
+  void OrthancPluginDatabase::LogExportedResource(const ExportedResource& resource)
+  {
+    OrthancPluginExportedResource tmp;
+    tmp.seq = resource.GetSeq();
+    tmp.resourceType = Plugins::Convert(resource.GetResourceType());
+    tmp.publicId = resource.GetPublicId().c_str();
+    tmp.modality = resource.GetModality().c_str();
+    tmp.date = resource.GetDate().c_str();
+    tmp.patientId = resource.GetPatientId().c_str();
+    tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str();
+    tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str();
+    tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str();
+
+    CheckSuccess(backend_.logExportedResource(payload_, &tmp));
+  }
+
+    
+  bool OrthancPluginDatabase::LookupAttachment(FileInfo& attachment,
+                                               int64_t id,
+                                               FileContentType contentType)
+  {
+    ResetAnswers();
+
+    CheckSuccess(backend_.lookupAttachment
+                 (GetContext(), payload_, id, static_cast<int32_t>(contentType)));
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment &&
+             answerAttachments_.size() == 1)
+    {
+      attachment = answerAttachments_.front();
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
+
+  bool OrthancPluginDatabase::LookupGlobalProperty(std::string& target,
+                                                   GlobalProperty property)
+  {
+    ResetAnswers();
+
+    CheckSuccess(backend_.lookupGlobalProperty
+                 (GetContext(), payload_, static_cast<int32_t>(property)));
+
+    return ForwardSingleAnswer(target);
+  }
+
+
+  bool OrthancPluginDatabase::LookupMetadata(std::string& target,
+                                             int64_t id,
+                                             MetadataType type)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.lookupMetadata(GetContext(), payload_, id, static_cast<int32_t>(type)));
+    return ForwardSingleAnswer(target);
+  }
+
+
+  bool OrthancPluginDatabase::LookupParent(int64_t& parentId,
+                                           int64_t resourceId)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.lookupParent(GetContext(), payload_, resourceId));
+    return ForwardSingleAnswer(parentId);
+  }
+
+
+  bool OrthancPluginDatabase::LookupResource(int64_t& id,
+                                             ResourceType& type,
+                                             const std::string& publicId)
+  {
+    ResetAnswers();
+
+    CheckSuccess(backend_.lookupResource(GetContext(), payload_, publicId.c_str()));
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      return false;
+    }
+    else if (type_ == _OrthancPluginDatabaseAnswerType_Resource &&
+             answerResources_.size() == 1)
+    {
+      id = answerResources_.front().first;
+      type = answerResources_.front().second;
+      return true; 
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
+
+  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.selectPatientToRecycle(GetContext(), payload_));
+    return ForwardSingleAnswer(internalId);
+  }
+
+
+  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId,
+                                                     int64_t patientIdToAvoid)
+  {
+    ResetAnswers();
+    CheckSuccess(backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid));
+    return ForwardSingleAnswer(internalId);
+  }
+
+
+  void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property,
+                                                const std::string& value)
+  {
+    CheckSuccess(backend_.setGlobalProperty
+                 (payload_, static_cast<int32_t>(property), value.c_str()));
+  }
+
+
+  void OrthancPluginDatabase::ClearMainDicomTags(int64_t id)
+  {
+    if (extensions_.clearMainDicomTags == NULL)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension");
+    }
+
+    CheckSuccess(extensions_.clearMainDicomTags(payload_, id));
+  }
+
+
+  void OrthancPluginDatabase::SetMainDicomTag(int64_t id,
+                                              const DicomTag& tag,
+                                              const std::string& value)
+  {
+    OrthancPluginDicomTag tmp;
+    tmp.group = tag.GetGroup();
+    tmp.element = tag.GetElement();
+    tmp.value = value.c_str();
+
+    CheckSuccess(backend_.setMainDicomTag(payload_, id, &tmp));
+  }
+
+
+  void OrthancPluginDatabase::SetIdentifierTag(int64_t id,
+                                               const DicomTag& tag,
+                                               const std::string& value)
+  {
+    OrthancPluginDicomTag tmp;
+    tmp.group = tag.GetGroup();
+    tmp.element = tag.GetElement();
+    tmp.value = value.c_str();
+
+    CheckSuccess(backend_.setIdentifierTag(payload_, id, &tmp));
+  }
+
+
+  void OrthancPluginDatabase::SetMetadata(int64_t id,
+                                          MetadataType type,
+                                          const std::string& value)
+  {
+    CheckSuccess(backend_.setMetadata
+                 (payload_, id, static_cast<int32_t>(type), value.c_str()));
+  }
+
+
+  void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, 
+                                                  bool isProtected)
+  {
+    CheckSuccess(backend_.setProtectedPatient(payload_, internalId, isProtected));
+  }
+
+
+  IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction()
+  {
+    return new Transaction(*this);
+  }
+
+
+  static void ProcessEvent(IDatabaseListener& listener,
+                           const _OrthancPluginDatabaseAnswer& answer)
+  {
+    switch (answer.type)
+    {
+      case _OrthancPluginDatabaseAnswerType_DeletedAttachment:
+      {
+        const OrthancPluginAttachment& attachment = 
+          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
+        listener.SignalFileDeleted(Convert(attachment));
+        break;
+      }
+        
+      case _OrthancPluginDatabaseAnswerType_RemainingAncestor:
+      {
+        ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
+        listener.SignalRemainingAncestor(type, answer.valueString);
+        break;
+      }
+      
+      case _OrthancPluginDatabaseAnswerType_DeletedResource:
+      {
+        ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
+        ServerIndexChange change(ChangeType_Deleted, type, answer.valueString);
+        listener.SignalChange(change);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+  }
+
+
+  unsigned int OrthancPluginDatabase::GetDatabaseVersion()
+  {
+    if (extensions_.getDatabaseVersion != NULL)
+    {
+      uint32_t version;
+      CheckSuccess(extensions_.getDatabaseVersion(&version, payload_));
+      return version;
+    }
+    else
+    {
+      // Before adding the "GetDatabaseVersion()" extension in plugins
+      // (OrthancPostgreSQL <= 1.2), the only supported DB schema was
+      // version 5.
+      return 5;
+    }
+  }
+
+
+  void OrthancPluginDatabase::Upgrade(unsigned int targetVersion,
+                                      IStorageArea& storageArea)
+  {
+    if (extensions_.upgradeDatabase != NULL)
+    {
+      Transaction transaction(*this);
+      transaction.Begin();
+
+      OrthancPluginErrorCode code = extensions_.upgradeDatabase(
+        payload_, targetVersion, 
+        reinterpret_cast<OrthancPluginStorageArea*>(&storageArea));
+
+      if (code == OrthancPluginErrorCode_Success)
+      {
+        transaction.Commit(0);
+      }
+      else
+      {
+        transaction.Rollback();
+        errorDictionary_.LogError(code, true);
+        throw OrthancException(static_cast<ErrorCode>(code));
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::AnswerReceived(const _OrthancPluginDatabaseAnswer& answer)
+  {
+    if (answer.type == _OrthancPluginDatabaseAnswerType_None)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin);
+    }
+
+    if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment ||
+        answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource ||
+        answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor)
+    {
+      assert(listener_ != NULL);
+      ProcessEvent(*listener_, answer);
+      return;
+    }
+
+    if (type_ == _OrthancPluginDatabaseAnswerType_None)
+    {
+      type_ = answer.type;
+
+      switch (type_)
+      {
+        case _OrthancPluginDatabaseAnswerType_Int32:
+          answerInt32_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Int64:
+          answerInt64_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Resource:
+          answerResources_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Attachment:
+          answerAttachments_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_String:
+          answerStrings_.clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_DicomTag:
+          assert(answerDicomMap_ != NULL);
+          answerDicomMap_->Clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Change:
+          assert(answerChanges_ != NULL);
+          answerChanges_->clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_ExportedResource:
+          assert(answerExportedResources_ != NULL);
+          answerExportedResources_->clear();
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_MatchingResource:
+          assert(answerMatchingResources_ != NULL);
+          answerMatchingResources_->clear();
+
+          if (answerMatchingInstances_ != NULL)
+          {
+            answerMatchingInstances_->clear();
+          }
+          
+          break;
+
+        case _OrthancPluginDatabaseAnswerType_Metadata:
+          assert(answerMetadata_ != NULL);
+          answerMetadata_->clear();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_DatabasePlugin,
+                                 "Unhandled type of answer for custom index plugin: " +
+                                 boost::lexical_cast<std::string>(answer.type));
+      }
+    }
+    else if (type_ != answer.type)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "Error in the plugin protocol: Cannot change the answer type");
+    }
+
+    switch (answer.type)
+    {
+      case _OrthancPluginDatabaseAnswerType_Int32:
+      {
+        answerInt32_.push_back(answer.valueInt32);
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Int64:
+      {
+        answerInt64_.push_back(answer.valueInt64);
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Resource:
+      {
+        OrthancPluginResourceType type = static_cast<OrthancPluginResourceType>(answer.valueInt32);
+        answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type)));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Attachment:
+      {
+        const OrthancPluginAttachment& attachment = 
+          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
+
+        answerAttachments_.push_back(Convert(attachment));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_DicomTag:
+      {
+        const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric);
+        assert(answerDicomMap_ != NULL);
+        answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false);
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_String:
+      {
+        if (answer.valueString == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        if (type_ == _OrthancPluginDatabaseAnswerType_None)
+        {
+          type_ = _OrthancPluginDatabaseAnswerType_String;
+          answerStrings_.clear();
+        }
+        else if (type_ != _OrthancPluginDatabaseAnswerType_String)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        answerStrings_.push_back(std::string(answer.valueString));
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Change:
+      {
+        assert(answerDone_ != NULL);
+        if (answer.valueUint32 == 1)
+        {
+          *answerDone_ = true;
+        }
+        else if (*answerDone_)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          const OrthancPluginChange& change =
+            *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
+          assert(answerChanges_ != NULL);
+          answerChanges_->push_back
+            (ServerIndexChange(change.seq,
+                               static_cast<ChangeType>(change.changeType),
+                               Plugins::Convert(change.resourceType),
+                               change.publicId,
+                               change.date));                                   
+        }
+
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_ExportedResource:
+      {
+        assert(answerDone_ != NULL);
+        if (answer.valueUint32 == 1)
+        {
+          *answerDone_ = true;
+        }
+        else if (*answerDone_)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+        else
+        {
+          const OrthancPluginExportedResource& exported = 
+            *reinterpret_cast<const OrthancPluginExportedResource*>(answer.valueGeneric);
+          assert(answerExportedResources_ != NULL);
+          answerExportedResources_->push_back
+            (ExportedResource(exported.seq,
+                              Plugins::Convert(exported.resourceType),
+                              exported.publicId,
+                              exported.modality,
+                              exported.date,
+                              exported.patientId,
+                              exported.studyInstanceUid,
+                              exported.seriesInstanceUid,
+                              exported.sopInstanceUid));
+        }
+
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_MatchingResource:
+      {
+        const OrthancPluginMatchingResource& match = 
+          *reinterpret_cast<const OrthancPluginMatchingResource*>(answer.valueGeneric);
+
+        if (match.resourceId == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        assert(answerMatchingResources_ != NULL);
+        answerMatchingResources_->push_back(match.resourceId);
+
+        if (answerMatchingInstances_ != NULL)
+        {
+          if (match.someInstanceId == NULL)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          answerMatchingInstances_->push_back(match.someInstanceId);
+        }
+ 
+        break;
+      }
+
+      case _OrthancPluginDatabaseAnswerType_Metadata:
+      {
+        const OrthancPluginResourcesContentMetadata& metadata =
+          *reinterpret_cast<const OrthancPluginResourcesContentMetadata*>(answer.valueGeneric);
+
+        MetadataType type = static_cast<MetadataType>(metadata.metadata);
+
+        if (metadata.value == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        assert(answerMetadata_ != NULL &&
+               answerMetadata_->find(type) == answerMetadata_->end());
+        (*answerMetadata_) [type] = metadata.value;
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_DatabasePlugin,
+                               "Unhandled type of answer for custom index plugin: " +
+                               boost::lexical_cast<std::string>(answer.type));
+    }
+  }
+
+    
+  bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold)
+  {
+    if (fastGetTotalSize_)
+    {
+      return GetTotalCompressedSize() > threshold;
+    }
+    else
+    {
+      assert(GetTotalCompressedSize() == currentDiskSize_);
+      return currentDiskSize_ > threshold;
+    }      
+  }
+
+
+  void OrthancPluginDatabase::ApplyLookupResources(std::list<std::string>& resourcesId,
+                                                   std::list<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& lookup,
+                                                   ResourceType queryLevel,
+                                                   size_t limit)
+  {
+    if (extensions_.lookupResources == NULL)
+    {
+      // Fallback to compatibility mode
+      ILookupResources::Apply
+        (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit);
+    }
+    else
+    {
+      std::vector<OrthancPluginDatabaseConstraint> constraints;
+      std::vector< std::vector<const char*> > constraintsValues;
+
+      constraints.resize(lookup.size());
+      constraintsValues.resize(lookup.size());
+
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
+      }
+
+      ResetAnswers();
+      answerMatchingResources_ = &resourcesId;
+      answerMatchingInstances_ = instancesId;
+      
+      CheckSuccess(extensions_.lookupResources(GetContext(), payload_, lookup.size(),
+                                               (lookup.empty() ? NULL : &constraints[0]),
+                                               Plugins::Convert(queryLevel),
+                                               limit, (instancesId == NULL ? 0 : 1)));
+    }
+  }
+
+
+  bool OrthancPluginDatabase::CreateInstance(
+    IDatabaseWrapper::CreateInstanceResult& result,
+    int64_t& instanceId,
+    const std::string& patient,
+    const std::string& study,
+    const std::string& series,
+    const std::string& instance)
+  {
+    if (extensions_.createInstance == NULL)
+    {
+      // Fallback to compatibility mode
+      return ICreateInstance::Apply
+        (*this, result, instanceId, patient, study, series, instance);
+    }
+    else
+    {
+      OrthancPluginCreateInstanceResult output;
+      memset(&output, 0, sizeof(output));
+
+      CheckSuccess(extensions_.createInstance(&output, payload_, patient.c_str(),
+                                              study.c_str(), series.c_str(), instance.c_str()));
+
+      instanceId = output.instanceId;
+      
+      if (output.isNewInstance)
+      {
+        result.isNewPatient_ = output.isNewPatient;
+        result.isNewStudy_ = output.isNewStudy;
+        result.isNewSeries_ = output.isNewSeries;
+        result.patientId_ = output.patientId;
+        result.studyId_ = output.studyId;
+        result.seriesId_ = output.seriesId;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+
+
+  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& result,
+                                               ResourceType level,
+                                               const DicomTag& tag,
+                                               Compatibility::IdentifierConstraintType type,
+                                               const std::string& value)
+  {
+    if (extensions_.lookupIdentifier3 == NULL)
+    {
+      throw OrthancException(ErrorCode_DatabasePlugin,
+                             "The database plugin does not implement the mandatory LookupIdentifier3() extension");
+    }
+
+    OrthancPluginDicomTag tmp;
+    tmp.group = tag.GetGroup();
+    tmp.element = tag.GetElement();
+    tmp.value = value.c_str();
+
+    ResetAnswers();
+    CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level),
+                                               &tmp, Compatibility::Convert(type)));
+    ForwardAnswers(result);
+  }
+
+
+  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, Compatibility::IdentifierConstraintType_GreaterOrEqual, start);
+
+      std::list<int64_t> b;
+      LookupIdentifier(result, level, tag, Compatibility::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);
+    }
+  }
+
+
+  void OrthancPluginDatabase::SetResourcesContent(const Orthanc::ResourcesContent& content)
+  {
+    if (extensions_.setResourcesContent == NULL)
+    {
+      ISetResourcesContent::Apply(*this, content);
+    }
+    else
+    {
+      std::vector<OrthancPluginResourcesContentTags> identifierTags;
+      std::vector<OrthancPluginResourcesContentTags> mainDicomTags;
+      std::vector<OrthancPluginResourcesContentMetadata> metadata;
+
+      identifierTags.reserve(content.GetListTags().size());
+      mainDicomTags.reserve(content.GetListTags().size());
+      metadata.reserve(content.GetListMetadata().size());
+
+      for (ResourcesContent::ListTags::const_iterator
+             it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
+      {
+        OrthancPluginResourcesContentTags tmp;
+        tmp.resource = it->resourceId_;
+        tmp.group = it->tag_.GetGroup();
+        tmp.element = it->tag_.GetElement();
+        tmp.value = it->value_.c_str();
+
+        if (it->isIdentifier_)
+        {
+          identifierTags.push_back(tmp);
+        }
+        else
+        {
+          mainDicomTags.push_back(tmp);
+        }
+      }
+
+      for (ResourcesContent::ListMetadata::const_iterator
+             it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
+      {
+        OrthancPluginResourcesContentMetadata tmp;
+        tmp.resource = it->resourceId_;
+        tmp.metadata = it->metadata_;
+        tmp.value = it->value_.c_str();
+        metadata.push_back(tmp);
+      }
+
+      assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() &&
+             metadata.size() == content.GetListMetadata().size());
+       
+      CheckSuccess(extensions_.setResourcesContent(
+                     payload_,
+                     identifierTags.size(),
+                     (identifierTags.empty() ? NULL : &identifierTags[0]),
+                     mainDicomTags.size(),
+                     (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
+                     metadata.size(),
+                     (metadata.empty() ? NULL : &metadata[0])));
+    }
+  }
+
+
+
+  void OrthancPluginDatabase::GetChildrenMetadata(std::list<std::string>& target,
+                                                  int64_t resourceId,
+                                                  MetadataType metadata)
+  {
+    if (extensions_.getChildrenMetadata == NULL)
+    {
+      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
+    }
+    else
+    {
+      ResetAnswers();
+      CheckSuccess(extensions_.getChildrenMetadata
+                   (GetContext(), payload_, resourceId, static_cast<int32_t>(metadata)));
+      ForwardAnswers(target);
+    }
+  }
+
+
+  int64_t OrthancPluginDatabase::GetLastChangeIndex()
+  {
+    if (extensions_.getLastChangeIndex == NULL)
+    {
+      // This was the default behavior in Orthanc <= 1.5.1
+      // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ
+      return 0;
+    }
+    else
+    {
+      int64_t result = 0;
+      CheckSuccess(extensions_.getLastChangeIndex(&result, payload_));
+      return result;
+    }
+  }
+
+  
+  void OrthancPluginDatabase::TagMostRecentPatient(int64_t patient)
+  {
+    if (extensions_.tagMostRecentPatient != NULL)
+    {
+      CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient));
+    }
+  }
+
+
+  bool OrthancPluginDatabase::LookupResourceAndParent(int64_t& id,
+                                                      ResourceType& type,
+                                                      std::string& parentPublicId,
+                                                      const std::string& publicId)
+  {
+    if (extensions_.lookupResourceAndParent == NULL)
+    {
+      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
+    }
+    else
+    {
+      std::list<std::string> parent;
+
+      uint8_t isExisting;
+      OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient;
+      
+      ResetAnswers();
+      CheckSuccess(extensions_.lookupResourceAndParent
+                   (GetContext(), &isExisting, &id, &pluginType, payload_, publicId.c_str()));
+      ForwardAnswers(parent);
+
+      if (isExisting)
+      {
+        type = Plugins::Convert(pluginType);
+
+        if (parent.empty())
+        {
+          if (type != ResourceType_Patient)
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+        }
+        else if (parent.size() == 1)
+        {
+          if ((type != ResourceType_Study &&
+               type != ResourceType_Series &&
+               type != ResourceType_Instance) ||
+              parent.front().empty())
+          {
+            throw OrthancException(ErrorCode_DatabasePlugin);
+          }
+
+          parentPublicId = parent.front();
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin);
+        }
+
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPluginDatabase.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,376 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/SharedLibrary.h"
+#include "../../OrthancServer/Database/Compatibility/ICreateInstance.h"
+#include "../../OrthancServer/Database/Compatibility/IGetChildrenMetadata.h"
+#include "../../OrthancServer/Database/Compatibility/ILookupResources.h"
+#include "../../OrthancServer/Database/Compatibility/ILookupResourceAndParent.h"
+#include "../../OrthancServer/Database/Compatibility/ISetResourcesContent.h"
+#include "../Include/orthanc/OrthancCDatabasePlugin.h"
+#include "PluginsErrorDictionary.h"
+
+namespace Orthanc
+{
+  class OrthancPluginDatabase :
+    public IDatabaseWrapper,
+    public Compatibility::ICreateInstance,
+    public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ILookupResources,
+    public Compatibility::ILookupResourceAndParent,
+    public Compatibility::ISetResourcesContent
+  {
+  private:
+    class Transaction;
+
+    typedef std::pair<int64_t, ResourceType>     AnswerResource;
+    typedef std::map<MetadataType, std::string>  AnswerMetadata;
+
+    SharedLibrary&  library_;
+    PluginsErrorDictionary&  errorDictionary_;
+    _OrthancPluginDatabaseAnswerType type_;
+    OrthancPluginDatabaseBackend backend_;
+    OrthancPluginDatabaseExtensions extensions_;
+    void* payload_;
+    IDatabaseListener* listener_;
+
+    bool      fastGetTotalSize_;
+    uint64_t  currentDiskSize_;
+
+    std::list<std::string>         answerStrings_;
+    std::list<int32_t>             answerInt32_;
+    std::list<int64_t>             answerInt64_;
+    std::list<AnswerResource>      answerResources_;
+    std::list<FileInfo>            answerAttachments_;
+
+    DicomMap*                      answerDicomMap_;
+    std::list<ServerIndexChange>*  answerChanges_;
+    std::list<ExportedResource>*   answerExportedResources_;
+    bool*                          answerDone_;
+    std::list<std::string>*        answerMatchingResources_;
+    std::list<std::string>*        answerMatchingInstances_;
+    AnswerMetadata*                answerMetadata_;
+
+    OrthancPluginDatabaseContext* GetContext()
+    {
+      return reinterpret_cast<OrthancPluginDatabaseContext*>(this);
+    }
+
+    void CheckSuccess(OrthancPluginErrorCode code);
+
+    void ResetAnswers();
+
+    void ForwardAnswers(std::list<int64_t>& target);
+
+    void ForwardAnswers(std::list<std::string>& target);
+
+    bool ForwardSingleAnswer(std::string& target);
+
+    bool ForwardSingleAnswer(int64_t& target);
+
+  public:
+    OrthancPluginDatabase(SharedLibrary& library,
+                          PluginsErrorDictionary&  errorDictionary,
+                          const OrthancPluginDatabaseBackend& backend,
+                          const OrthancPluginDatabaseExtensions* extensions,
+                          size_t extensionsSize,
+                          void *payload);
+
+    virtual void Open() 
+      ORTHANC_OVERRIDE;
+
+    virtual void Close() 
+      ORTHANC_OVERRIDE
+    {
+      CheckSuccess(backend_.close(payload_));
+    }
+
+    const SharedLibrary& GetSharedLibrary() const
+    {
+      return library_;
+    }
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment) 
+      ORTHANC_OVERRIDE;
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child) 
+      ORTHANC_OVERRIDE;
+
+    virtual void ClearChanges() 
+      ORTHANC_OVERRIDE;
+
+    virtual void ClearExportedResources() 
+      ORTHANC_OVERRIDE;
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type) 
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) 
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) 
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteResource(int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void FlushToDisk() 
+      ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual bool HasFlushToDisk() const 
+      ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) 
+      ORTHANC_OVERRIDE;
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual std::string GetPublicId(int64_t resourceId) 
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
+
+    virtual ResourceType GetResourceType(int64_t resourceId) 
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetTotalCompressedSize() 
+      ORTHANC_OVERRIDE;
+    
+    virtual uint64_t GetTotalUncompressedSize() 
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsExistingResource(int64_t internalId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsProtectedPatient(int64_t internalId) 
+      ORTHANC_OVERRIDE;
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) 
+      ORTHANC_OVERRIDE;
+
+    virtual void LogExportedResource(const ExportedResource& resource) 
+      ORTHANC_OVERRIDE;
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) 
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void ClearMainDicomTags(int64_t id) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value) 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) 
+      ORTHANC_OVERRIDE;
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction() 
+      ORTHANC_OVERRIDE;
+
+    virtual void SetListener(IDatabaseListener& listener) 
+      ORTHANC_OVERRIDE
+    {
+      listener_ = &listener;
+    }
+
+    virtual unsigned int GetDatabaseVersion() 
+      ORTHANC_OVERRIDE;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) 
+      ORTHANC_OVERRIDE;
+
+    void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold) 
+      ORTHANC_OVERRIDE;
+
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit)
+      ORTHANC_OVERRIDE;
+
+    virtual bool CreateInstance(CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance)
+      ORTHANC_OVERRIDE;
+
+    // From the "ILookupResources" interface
+    virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                   ResourceType resourceType) 
+      ORTHANC_OVERRIDE;
+
+    // From the "ILookupResources" interface
+    virtual void LookupIdentifier(std::list<int64_t>& result,
+                                  ResourceType level,
+                                  const DicomTag& tag,
+                                  Compatibility::IdentifierConstraintType type,
+                                  const std::string& value)
+      ORTHANC_OVERRIDE;
+    
+    // From the "ILookupResources" interface
+    virtual void LookupIdentifierRange(std::list<int64_t>& result,
+                                       ResourceType level,
+                                       const DicomTag& tag,
+                                       const std::string& start,
+                                       const std::string& end)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata)
+      ORTHANC_OVERRIDE;
+
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
+  
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId)
+      ORTHANC_OVERRIDE;
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,5256 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancPlugins.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#error The plugin support is disabled
+#endif
+
+#if !defined(DCMTK_VERSION_NUMBER)
+#  error The macro DCMTK_VERSION_NUMBER must be defined
+#endif
+
+
+#include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/Compression/ZlibCompressor.h"
+#include "../../Core/DicomFormat/DicomArray.h"
+#include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../../Core/HttpServer/HttpToolbox.h"
+#include "../../Core/Images/Image.h"
+#include "../../Core/Images/ImageProcessing.h"
+#include "../../Core/Images/JpegReader.h"
+#include "../../Core/Images/JpegWriter.h"
+#include "../../Core/Images/PngReader.h"
+#include "../../Core/Images/PngWriter.h"
+#include "../../Core/Logging.h"
+#include "../../Core/MetricsRegistry.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../../Core/Toolbox.h"
+#include "../../OrthancServer/OrthancConfiguration.h"
+#include "../../OrthancServer/OrthancFindRequestHandler.h"
+#include "../../OrthancServer/Search/HierarchicalMatcher.h"
+#include "../../OrthancServer/ServerContext.h"
+#include "../../OrthancServer/ServerToolbox.h"
+#include "PluginsEnumerations.h"
+#include "PluginsJob.h"
+
+#include <boost/regex.hpp>
+#include <dcmtk/dcmdata/dcdict.h>
+#include <dcmtk/dcmdata/dcdicent.h>
+
+#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary"
+
+
+namespace Orthanc
+{
+  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
+                                 const void* data,
+                                 size_t size)
+  {
+    if (static_cast<uint32_t>(size) != size)
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
+    }
+
+    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)
+  {
+    if (static_cast<uint32_t>(str.size()) != str.size())
+    {
+      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
+    }
+
+    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
+    {
+    private:
+      _OrthancPluginRegisterStorageArea callbacks_;
+      PluginsErrorDictionary&  errorDictionary_;
+
+      void Free(void* buffer) const
+      {
+        if (buffer != NULL)
+        {
+          callbacks_.free(buffer);
+        }
+      }
+
+    public:
+      PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks,
+                        PluginsErrorDictionary&  errorDictionary) : 
+        callbacks_(callbacks),
+        errorDictionary_(errorDictionary)
+      {
+      }
+
+
+      virtual void Create(const std::string& uuid,
+                          const void* content, 
+                          size_t size,
+                          FileContentType type)
+      {
+        OrthancPluginErrorCode error = callbacks_.create
+          (uuid.c_str(), content, size, Plugins::Convert(type));
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+
+
+      virtual void Read(std::string& content,
+                        const std::string& uuid,
+                        FileContentType type)
+      {
+        void* buffer = NULL;
+        int64_t size = 0;
+
+        OrthancPluginErrorCode error = callbacks_.read
+          (&buffer, &size, uuid.c_str(), Plugins::Convert(type));
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+
+        try
+        {
+          content.resize(static_cast<size_t>(size));
+        }
+        catch (...)
+        {
+          Free(buffer);
+          throw OrthancException(ErrorCode_NotEnoughMemory);
+        }
+
+        if (size > 0)
+        {
+          memcpy(&content[0], buffer, static_cast<size_t>(size));
+        }
+
+        Free(buffer);
+      }
+
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type) 
+      {
+        OrthancPluginErrorCode error = callbacks_.remove
+          (uuid.c_str(), Plugins::Convert(type));
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+      }
+    };
+
+
+    class StorageAreaFactory : public boost::noncopyable
+    {
+    private:
+      SharedLibrary&   sharedLibrary_;
+      _OrthancPluginRegisterStorageArea  callbacks_;
+      PluginsErrorDictionary&  errorDictionary_;
+
+    public:
+      StorageAreaFactory(SharedLibrary& sharedLibrary,
+                         const _OrthancPluginRegisterStorageArea& callbacks,
+                         PluginsErrorDictionary&  errorDictionary) :
+        sharedLibrary_(sharedLibrary),
+        callbacks_(callbacks),
+        errorDictionary_(errorDictionary)
+      {
+      }
+
+      SharedLibrary&  GetSharedLibrary()
+      {
+        return sharedLibrary_;
+      }
+
+      IStorageArea* Create() const
+      {
+        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()
+      {
+        OrthancConfiguration::ReaderLock lock;
+
+        std::set<std::string> peers;
+        lock.GetConfiguration().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 (lock.GetConfiguration().LookupOrthancPeer(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];
+      }
+    };
+
+
+    class DicomWebBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter
+    {
+    private:
+      OrthancPluginDicomWebBinaryCallback   oldCallback_;
+      OrthancPluginDicomWebBinaryCallback2  newCallback_;  // New in Orthanc 1.7.0
+      void*                                 newPayload_;   // New in Orthanc 1.7.0
+      DicomWebJsonVisitor::BinaryMode       currentMode_;
+      std::string                           currentBulkDataUri_;
+
+      static void Setter(OrthancPluginDicomWebNode*       node,
+                         OrthancPluginDicomWebBinaryMode  mode,
+                         const char*                      bulkDataUri)
+      {
+        DicomWebBinaryFormatter& that = *reinterpret_cast<DicomWebBinaryFormatter*>(node);
+
+        switch (mode)
+        {
+          case OrthancPluginDicomWebBinaryMode_Ignore:
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
+            break;
+              
+          case OrthancPluginDicomWebBinaryMode_InlineBinary:
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_InlineBinary;
+            break;
+              
+          case OrthancPluginDicomWebBinaryMode_BulkDataUri:
+            if (bulkDataUri == NULL)
+            {
+              throw OrthancException(ErrorCode_NullPointer);
+            }              
+            
+            that.currentBulkDataUri_ = bulkDataUri;
+            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_BulkDataUri;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+      
+    public:
+      DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback callback) :
+        oldCallback_(callback),
+        newCallback_(NULL),
+        newPayload_(NULL)
+      {
+      }
+      
+      DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback2 callback,
+                              void* payload) :
+        oldCallback_(NULL),
+        newCallback_(callback),
+        newPayload_(payload)
+      {
+      }
+      
+      virtual DicomWebJsonVisitor::BinaryMode Format(std::string& bulkDataUri,
+                                                     const std::vector<DicomTag>& parentTags,
+                                                     const std::vector<size_t>& parentIndexes,
+                                                     const DicomTag& tag,
+                                                     ValueRepresentation vr)
+      {
+        if (oldCallback_ == NULL &&
+            newCallback_ == NULL)
+        {
+          return DicomWebJsonVisitor::BinaryMode_InlineBinary;
+        }
+        else
+        {
+          assert(parentTags.size() == parentIndexes.size());
+          std::vector<uint16_t> groups(parentTags.size());
+          std::vector<uint16_t> elements(parentTags.size());
+          std::vector<uint32_t> indexes(parentTags.size());
+
+          for (size_t i = 0; i < parentTags.size(); i++)
+          {
+            groups[i] = parentTags[i].GetGroup();
+            elements[i] = parentTags[i].GetElement();
+            indexes[i] = static_cast<uint32_t>(parentIndexes[i]);
+          }
+          bool empty = parentTags.empty();
+
+          currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
+
+          if (oldCallback_ != NULL)
+          {
+            oldCallback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
+                         DicomWebBinaryFormatter::Setter,
+                         static_cast<uint32_t>(parentTags.size()),
+                         (empty ? NULL : &groups[0]),
+                         (empty ? NULL : &elements[0]),
+                         (empty ? NULL : &indexes[0]),
+                         tag.GetGroup(),
+                         tag.GetElement(),
+                         Plugins::Convert(vr));
+          }
+          else
+          {
+            assert(newCallback_ != NULL);
+            newCallback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
+                         DicomWebBinaryFormatter::Setter,
+                         static_cast<uint32_t>(parentTags.size()),
+                         (empty ? NULL : &groups[0]),
+                         (empty ? NULL : &elements[0]),
+                         (empty ? NULL : &indexes[0]),
+                         tag.GetGroup(),
+                         tag.GetElement(),
+                         Plugins::Convert(vr),
+                         newPayload_);
+          }          
+
+          bulkDataUri = currentBulkDataUri_;          
+          return currentMode_;
+        }
+      }
+
+      void Apply(char** target,
+                 bool isJson,
+                 ParsedDicomFile& dicom)
+      {
+        DicomWebJsonVisitor visitor;
+        visitor.SetFormatter(*this);
+
+        dicom.Apply(visitor);
+
+        std::string s;
+
+        if (isJson)
+        {
+          s = visitor.GetResult().toStyledString();
+        }
+        else
+        {
+          visitor.FormatXml(s);
+        }
+
+        *target = CopyString(s);
+      }
+
+  
+      void Apply(char** target,
+                 bool isJson,
+                 const void* dicom,
+                 size_t dicomSize) 
+      {
+        ParsedDicomFile parsed(dicom, dicomSize);
+        Apply(target, isJson, parsed);
+      }
+    };
+  }
+
+
+  class OrthancPlugins::PImpl
+  {
+  private:
+    boost::mutex   contextMutex_;
+    ServerContext* context_;
+    
+  public:
+    class PluginHttpOutput : public boost::noncopyable
+    {
+    private:
+      enum MultipartState
+      {
+        MultipartState_None,
+        MultipartState_FirstPart,
+        MultipartState_SecondPart,
+        MultipartState_NextParts
+      };
+
+      HttpOutput&                 output_;
+      std::unique_ptr<std::string>  errorDetails_;
+      bool                        logDetails_;
+      MultipartState              multipartState_;
+      std::string                 multipartSubType_;
+      std::string                 multipartContentType_;
+      std::string                 multipartFirstPart_;
+      std::map<std::string, std::string>  multipartFirstHeaders_;
+
+    public:
+      PluginHttpOutput(HttpOutput& output) :
+        output_(output),
+        logDetails_(false),
+        multipartState_(MultipartState_None)
+      {
+      }
+
+      HttpOutput& GetOutput()
+      {
+        if (multipartState_ == MultipartState_None)
+        {
+          return output_;
+        }
+        else
+        {
+          // Must use "SendMultipartItem()" on multipart streams
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+      }
+
+      void SetErrorDetails(const std::string& details,
+                           bool logDetails)
+      {
+        errorDetails_.reset(new std::string(details));
+        logDetails_ = logDetails;
+      }
+
+      bool HasErrorDetails() const
+      {
+        return errorDetails_.get() != NULL;
+      }
+
+      bool IsLogDetails() const
+      {
+        return logDetails_;
+      }
+
+      const std::string& GetErrorDetails() const
+      {
+        if (errorDetails_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          return *errorDetails_;
+        }
+      }
+
+      void StartMultipart(const char* subType,
+                          const char* contentType)
+      {
+        if (multipartState_ != MultipartState_None)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+        else
+        {
+          multipartState_ = MultipartState_FirstPart;
+          multipartSubType_ = subType;
+          multipartContentType_ = contentType;
+        }
+      }
+
+      void SendMultipartItem(const void* data,
+                             size_t size,
+                             const std::map<std::string, std::string>& headers)
+      {
+        if (size != 0 && data == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        switch (multipartState_)
+        {
+          case MultipartState_None:
+            // Must call "StartMultipart()" before
+            throw OrthancException(ErrorCode_BadSequenceOfCalls);
+
+          case MultipartState_FirstPart:
+            multipartFirstPart_.assign(reinterpret_cast<const char*>(data), size);
+            multipartFirstHeaders_ = headers;
+            multipartState_ = MultipartState_SecondPart;
+            break;
+
+          case MultipartState_SecondPart:
+            // Start an actual stream for chunked transfer as soon as
+            // there are more than 2 elements in the multipart stream
+            output_.StartMultipart(multipartSubType_, multipartContentType_);
+            output_.SendMultipartItem(multipartFirstPart_.c_str(), multipartFirstPart_.size(), 
+                                      multipartFirstHeaders_);
+            multipartFirstPart_.clear();  // Release memory
+
+            output_.SendMultipartItem(data, size, headers);
+            multipartState_ = MultipartState_NextParts;
+            break;
+
+          case MultipartState_NextParts:
+            output_.SendMultipartItem(data, size, headers);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+
+      void Close(OrthancPluginErrorCode error,
+                 PluginsErrorDictionary& dictionary)
+      {
+        if (error == OrthancPluginErrorCode_Success)
+        {
+          switch (multipartState_)
+          {
+            case MultipartState_None:
+              assert(!output_.IsWritingMultipart());
+              break;
+
+            case MultipartState_FirstPart:   // Multipart started, but no part was sent
+            case MultipartState_SecondPart:  // Multipart started, first part is pending
+            {
+              assert(!output_.IsWritingMultipart());
+              std::vector<const void*> parts;
+              std::vector<size_t> sizes;
+              std::vector<const std::map<std::string, std::string>*> headers;
+
+              if (multipartState_ == MultipartState_SecondPart)
+              {
+                parts.push_back(multipartFirstPart_.c_str());
+                sizes.push_back(multipartFirstPart_.size());
+                headers.push_back(&multipartFirstHeaders_);
+              }
+
+              output_.AnswerMultipartWithoutChunkedTransfer(multipartSubType_, multipartContentType_,
+                                                            parts, sizes, headers);
+              break;
+            }
+
+            case MultipartState_NextParts:
+              assert(output_.IsWritingMultipart());
+              output_.CloseMultipart();
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+        else
+        {
+          dictionary.LogError(error, false);
+
+          if (HasErrorDetails())
+          {
+            throw OrthancException(static_cast<ErrorCode>(error),
+                                   GetErrorDetails(),
+                                   IsLogDetails());
+          }
+          else
+          {
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+      }
+    };
+
+    
+    class RestCallback : public boost::noncopyable
+    {
+    private:
+      boost::regex              regex_;
+      OrthancPluginRestCallback callback_;
+      bool                      lock_;
+
+      OrthancPluginErrorCode InvokeInternal(PluginHttpOutput& output,
+                                            const std::string& flatUri,
+                                            const OrthancPluginHttpRequest& request)
+      {
+        return callback_(reinterpret_cast<OrthancPluginRestOutput*>(&output), 
+                         flatUri.c_str(), 
+                         &request);
+      }
+
+    public:
+      RestCallback(const char* regex,
+                   OrthancPluginRestCallback callback,
+                   bool lockRestCallbacks) :
+        regex_(regex),
+        callback_(callback),
+        lock_(lockRestCallbacks)
+      {
+      }
+
+      const boost::regex& GetRegularExpression() const
+      {
+        return regex_;
+      }
+
+      OrthancPluginErrorCode Invoke(boost::recursive_mutex& restCallbackMutex,
+                                    PluginHttpOutput& output,
+                                    const std::string& flatUri,
+                                    const OrthancPluginHttpRequest& request)
+      {
+        if (lock_)
+        {
+          boost::recursive_mutex::scoped_lock lock(restCallbackMutex);
+          return InvokeInternal(output, flatUri, request);
+        }
+        else
+        {
+          return InvokeInternal(output, flatUri, request);
+        }
+      }
+    };
+
+
+    class ChunkedRestCallback : public boost::noncopyable
+    {
+    private:
+      _OrthancPluginChunkedRestCallback parameters_;
+      boost::regex                      regex_;
+
+    public:
+      ChunkedRestCallback(_OrthancPluginChunkedRestCallback parameters) :
+        parameters_(parameters),
+        regex_(parameters.pathRegularExpression)
+      {
+      }
+
+      const boost::regex& GetRegularExpression() const
+      {
+        return regex_;
+      }
+
+      const _OrthancPluginChunkedRestCallback& GetParameters() const
+      {
+        return parameters_;
+      }
+    };
+
+
+
+    class StorageCommitmentScp : public IStorageCommitmentFactory
+    {
+    private:
+      class Handler : public IStorageCommitmentFactory::ILookupHandler
+      {
+      private:
+        _OrthancPluginRegisterStorageCommitmentScpCallback  parameters_;
+        void*    handler_;
+
+      public:
+        Handler(_OrthancPluginRegisterStorageCommitmentScpCallback  parameters,
+                void* handler) :
+          parameters_(parameters),
+          handler_(handler)
+        {
+          if (handler == NULL)
+          {
+            throw OrthancException(ErrorCode_NullPointer);
+          }
+        }
+
+        virtual ~Handler()
+        {
+          assert(handler_ != NULL);
+          parameters_.destructor(handler_);
+          handler_ = NULL;
+        }
+
+        virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                      const std::string& sopInstanceUid)
+        {
+          assert(handler_ != NULL);
+          OrthancPluginStorageCommitmentFailureReason reason =
+            OrthancPluginStorageCommitmentFailureReason_Success;
+          OrthancPluginErrorCode error = parameters_.lookup(
+            &reason, handler_, sopClassUid.c_str(), sopInstanceUid.c_str());
+          if (error == OrthancPluginErrorCode_Success)
+          {
+            return Plugins::Convert(reason);
+          }
+          else
+          {
+            throw OrthancException(static_cast<ErrorCode>(error));
+          }
+        }
+      };
+      
+      _OrthancPluginRegisterStorageCommitmentScpCallback  parameters_;
+
+    public:
+      StorageCommitmentScp(_OrthancPluginRegisterStorageCommitmentScpCallback parameters) :
+        parameters_(parameters)
+      {
+      }
+
+      virtual ILookupHandler* CreateStorageCommitment(
+        const std::string& jobId,
+        const std::string& transactionUid,
+        const std::vector<std::string>& sopClassUids,
+        const std::vector<std::string>& sopInstanceUids,
+        const std::string& remoteAet,
+        const std::string& calledAet) ORTHANC_OVERRIDE
+      {
+        const size_t n = sopClassUids.size();
+        
+        if (sopInstanceUids.size() != n)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        
+        std::vector<const char*> a, b;
+        a.resize(n);
+        b.resize(n);
+
+        for (size_t i = 0; i < n; i++)
+        {
+          a[i] = sopClassUids[i].c_str();
+          b[i] = sopInstanceUids[i].c_str();
+        }
+
+        void* handler = NULL;
+        OrthancPluginErrorCode error = parameters_.factory(
+          &handler, jobId.c_str(), transactionUid.c_str(),
+          a.empty() ? NULL : &a[0], b.empty() ? NULL : &b[0], static_cast<uint32_t>(n),
+          remoteAet.c_str(), calledAet.c_str());
+
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          throw OrthancException(static_cast<ErrorCode>(error));          
+        }
+        else if (handler == NULL)
+        {
+          // This plugin won't handle this storage commitment request
+          return NULL;
+        }
+        else
+        {
+          return new Handler(parameters_, handler);
+        }
+      }
+    };
+
+
+    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<ChunkedRestCallback*>  ChunkedRestCallbacks;
+    typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
+    typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
+    typedef std::list<OrthancPluginIncomingHttpRequestFilter>  IncomingHttpRequestFilters;
+    typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
+    typedef std::list<OrthancPluginIncomingDicomInstanceFilter>  IncomingDicomInstanceFilters;
+    typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
+    typedef std::list<OrthancPluginTranscoderCallback>  TranscoderCallbacks;
+    typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
+    typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
+    typedef std::list<StorageCommitmentScp*>  StorageCommitmentScpCallbacks;
+    typedef std::map<Property, std::string>  Properties;
+
+    PluginsManager manager_;
+
+    RestCallbacks restCallbacks_;
+    ChunkedRestCallbacks chunkedRestCallbacks_;
+    OnStoredCallbacks  onStoredCallbacks_;
+    OnChangeCallbacks  onChangeCallbacks_;
+    OrthancPluginFindCallback  findCallback_;
+    OrthancPluginWorklistCallback  worklistCallback_;
+    DecodeImageCallbacks  decodeImageCallbacks_;
+    TranscoderCallbacks  transcoderCallbacks_;
+    JobsUnserializers  jobsUnserializers_;
+    _OrthancPluginMoveCallback moveCallbacks_;
+    IncomingHttpRequestFilters  incomingHttpRequestFilters_;
+    IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
+    IncomingDicomInstanceFilters  incomingDicomInstanceFilters_;
+    RefreshMetricsCallbacks refreshMetricsCallbacks_;
+    StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
+    std::unique_ptr<StorageAreaFactory>  storageArea_;
+
+    boost::recursive_mutex restCallbackMutex_;
+    boost::recursive_mutex storedCallbackMutex_;
+    boost::recursive_mutex changeCallbackMutex_;
+    boost::mutex findCallbackMutex_;
+    boost::mutex worklistCallbackMutex_;
+    boost::shared_mutex decoderTranscoderMutex_;  // Changed from "boost::mutex" in Orthanc 1.7.0
+    boost::mutex jobsUnserializersMutex_;
+    boost::mutex refreshMetricsMutex_;
+    boost::mutex storageCommitmentScpMutex_;
+    boost::recursive_mutex invokeServiceMutex_;
+
+    Properties properties_;
+    int argc_;
+    char** argv_;
+    std::unique_ptr<OrthancPluginDatabase>  database_;
+    PluginsErrorDictionary  dictionary_;
+
+    PImpl() : 
+      context_(NULL), 
+      findCallback_(NULL),
+      worklistCallback_(NULL),
+      argc_(1),
+      argv_(NULL)
+    {
+      memset(&moveCallbacks_, 0, sizeof(moveCallbacks_));
+    }
+  };
+
+
+  
+  class OrthancPlugins::WorklistHandler : public IWorklistRequestHandler
+  {
+  private:
+    OrthancPlugins&  that_;
+    std::unique_ptr<HierarchicalMatcher> matcher_;
+    std::unique_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,
+                                                          "" /* no private creator */));
+          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
+    {
+      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::unique_ptr<ParsedDicomFile> summary(matcher_->Extract(f));
+      reinterpret_cast<DicomFindAnswers*>(answers)->Add(*summary);
+    }
+  };
+
+  
+  class OrthancPlugins::FindHandler : public IFindRequestHandler
+  {
+  private:
+    OrthancPlugins&            that_;
+    std::unique_ptr<DicomArray>  currentQuery_;
+
+    void Reset()
+    {
+      currentQuery_.reset(NULL);
+    }
+
+  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
+    {
+      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);
+      }
+    }
+  };
+  
+
+
+  class OrthancPlugins::MoveHandler : public IMoveRequestHandler
+  {
+  private:
+    class Driver : public IMoveRequestIterator
+    {
+    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();
+      }
+    }
+                        
+
+
+  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)
+      {
+        throw OrthancException(ErrorCode_Plugin,
+                               "Plugin cannot create a driver for an incoming C-MOVE request");
+      }
+
+      unsigned int size = params_.getMoveSize(driver);
+
+      return new Driver(driver, size, params_.applyMove, params_.freeMove);
+    }
+  };
+
+
+
+  class OrthancPlugins::HttpClientChunkedRequest : public HttpClient::IRequestBody
+  {
+  private:
+    const _OrthancPluginChunkedHttpClient&  params_;
+    PluginsErrorDictionary&                 errorDictionary_;
+
+  public:
+    HttpClientChunkedRequest(const _OrthancPluginChunkedHttpClient& params,
+                             PluginsErrorDictionary&  errorDictionary) :
+      params_(params),
+      errorDictionary_(errorDictionary)
+    {
+    }
+
+    virtual bool ReadNextChunk(std::string& chunk)
+    {
+      if (params_.requestIsDone(params_.request))
+      {
+        return false;
+      }
+      else
+      {
+        size_t size = params_.requestChunkSize(params_.request);
+
+        chunk.resize(size);
+        
+        if (size != 0)
+        {
+          const void* data = params_.requestChunkData(params_.request);
+          memcpy(&chunk[0], data, size);
+        }
+
+        OrthancPluginErrorCode error = params_.requestNext(params_.request);
+        
+        if (error != OrthancPluginErrorCode_Success)
+        {
+          errorDictionary_.LogError(error, true);
+          throw OrthancException(static_cast<ErrorCode>(error));
+        }
+        else
+        {
+          return true;
+        }
+      }
+    }
+  };
+
+
+  class OrthancPlugins::HttpClientChunkedAnswer : public HttpClient::IAnswer
+  {
+  private:
+    const _OrthancPluginChunkedHttpClient&  params_;
+    PluginsErrorDictionary&                 errorDictionary_;
+
+  public:
+    HttpClientChunkedAnswer(const _OrthancPluginChunkedHttpClient& params,
+                            PluginsErrorDictionary&  errorDictionary) :
+      params_(params),
+      errorDictionary_(errorDictionary)
+    {
+    }
+
+    virtual void AddHeader(const std::string& key,
+                           const std::string& value)
+    {
+      OrthancPluginErrorCode error = params_.answerAddHeader(params_.answer, key.c_str(), value.c_str());
+        
+      if (error != OrthancPluginErrorCode_Success)
+      {
+        errorDictionary_.LogError(error, true);
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
+    }
+      
+    virtual void AddChunk(const void* data,
+                          size_t size)
+    {
+      OrthancPluginErrorCode error = params_.answerAddChunk(params_.answer, data, size);
+        
+      if (error != OrthancPluginErrorCode_Success)
+      {
+        errorDictionary_.LogError(error, true);
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
+    }
+  };
+
+
+  OrthancPlugins::OrthancPlugins()
+  {
+    /* Sanity check of the compiler */
+    if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
+        sizeof(int32_t) != sizeof(_OrthancPluginService) ||
+        sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
+        sizeof(int32_t) != sizeof(OrthancPluginPixelFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginContentType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginResourceType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginChangeType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
+        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) ||
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
+        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>(OrthancPluginCreateDicomFlags_DecodeDataUriScheme) != static_cast<int>(DicomFromJsonFlags_DecodeDataUriScheme) ||
+        static_cast<int>(OrthancPluginCreateDicomFlags_GenerateIdentifiers) != static_cast<int>(DicomFromJsonFlags_GenerateIdentifiers))
+
+    {
+      throw OrthancException(ErrorCode_Plugin);
+    }
+
+    pimpl_.reset(new PImpl());
+    pimpl_->manager_.RegisterServiceProvider(*this);
+  }
+
+  
+  void OrthancPlugins::SetServerContext(ServerContext& context)
+  {
+    pimpl_->SetServerContext(&context);
+  }
+
+
+  void OrthancPlugins::ResetServerContext()
+  {
+    pimpl_->SetServerContext(NULL);
+  }
+
+  
+  OrthancPlugins::~OrthancPlugins()
+  {
+    for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); 
+         it != pimpl_->restCallbacks_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    for (PImpl::ChunkedRestCallbacks::iterator it = pimpl_->chunkedRestCallbacks_.begin(); 
+         it != pimpl_->chunkedRestCallbacks_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    for (PImpl::StorageCommitmentScpCallbacks::iterator
+           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
+         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
+    {
+      delete *it;
+    } 
+  }
+
+
+  static void ArgumentsToPlugin(std::vector<const char*>& keys,
+                                std::vector<const char*>& values,
+                                const IHttpHandler::Arguments& arguments)
+  {
+    keys.resize(arguments.size());
+    values.resize(arguments.size());
+
+    size_t pos = 0;
+    for (IHttpHandler::Arguments::const_iterator 
+           it = arguments.begin(); it != arguments.end(); ++it)
+    {
+      keys[pos] = it->first.c_str();
+      values[pos] = it->second.c_str();
+      pos++;
+    }
+  }
+
+
+  static void ArgumentsToPlugin(std::vector<const char*>& keys,
+                                std::vector<const char*>& values,
+                                const IHttpHandler::GetArguments& arguments)
+  {
+    keys.resize(arguments.size());
+    values.resize(arguments.size());
+
+    for (size_t i = 0; i < arguments.size(); i++)
+    {
+      keys[i] = arguments[i].first.c_str();
+      values[i] = arguments[i].second.c_str();
+    }
+  }
+
+
+  namespace
+  {
+    class RestCallbackMatcher : public boost::noncopyable
+    {
+    private:
+      std::string               flatUri_;
+      std::vector<std::string>  groups_;
+      std::vector<const char*>  cgroups_;
+      
+    public:
+      RestCallbackMatcher(const UriComponents& uri) :
+        flatUri_(Toolbox::FlattenUri(uri))
+      {
+      }
+
+      bool IsMatch(const boost::regex& re)
+      {
+        // Check whether the regular expression associated to this
+        // callback matches the URI
+        boost::cmatch what;
+
+        if (boost::regex_match(flatUri_.c_str(), what, re))
+        {
+          // Extract the value of the free parameters of the regular expression
+          if (what.size() > 1)
+          {
+            groups_.resize(what.size() - 1);
+            cgroups_.resize(what.size() - 1);
+            for (size_t i = 1; i < what.size(); i++)
+            {
+              groups_[i - 1] = what[i];
+              cgroups_[i - 1] = groups_[i - 1].c_str();
+            }
+          }
+
+          return true;
+        }
+        else
+        {
+          // Not a match
+          return false;
+        }
+      }
+
+      uint32_t GetGroupsCount() const
+      {
+        return cgroups_.size();
+      }
+
+      const char* const* GetGroups() const
+      {
+        return cgroups_.empty() ? NULL : &cgroups_[0];
+      }
+
+      const std::string& GetFlatUri() const
+      {
+        return flatUri_;
+      }
+    };
+
+
+    // WARNING - The lifetime of this internal object must be smaller
+    // than "matcher", "headers" and "getArguments" objects
+    class HttpRequestConverter
+    {
+    private:
+      std::vector<const char*>    getKeys_;
+      std::vector<const char*>    getValues_;
+      std::vector<const char*>    headersKeys_;
+      std::vector<const char*>    headersValues_;
+      OrthancPluginHttpRequest    converted_;
+
+    public:
+      HttpRequestConverter(const RestCallbackMatcher& matcher,
+                           HttpMethod method,
+                           const IHttpHandler::Arguments& headers)
+      {
+        memset(&converted_, 0, sizeof(OrthancPluginHttpRequest));
+
+        ArgumentsToPlugin(headersKeys_, headersValues_, headers);
+        assert(headersKeys_.size() == headersValues_.size());
+
+        switch (method)
+        {
+          case HttpMethod_Get:
+            converted_.method = OrthancPluginHttpMethod_Get;
+            break;
+
+          case HttpMethod_Post:
+            converted_.method = OrthancPluginHttpMethod_Post;
+            break;
+
+          case HttpMethod_Delete:
+            converted_.method = OrthancPluginHttpMethod_Delete;
+            break;
+
+          case HttpMethod_Put:
+            converted_.method = OrthancPluginHttpMethod_Put;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        converted_.groups = matcher.GetGroups();
+        converted_.groupsCount = matcher.GetGroupsCount();
+        converted_.getCount = 0;
+        converted_.getKeys = NULL;
+        converted_.getValues = NULL;
+        converted_.body = NULL;
+        converted_.bodySize = 0;
+        converted_.headersCount = headers.size();
+       
+        if (headers.size() > 0)
+        {
+          converted_.headersKeys = &headersKeys_[0];
+          converted_.headersValues = &headersValues_[0];
+        }
+      }
+
+      void SetGetArguments(const IHttpHandler::GetArguments& getArguments)
+      {
+        ArgumentsToPlugin(getKeys_, getValues_, getArguments);
+        assert(getKeys_.size() == getValues_.size());
+
+        converted_.getCount = getArguments.size();
+
+        if (getArguments.size() > 0)
+        {
+          converted_.getKeys = &getKeys_[0];
+          converted_.getValues = &getValues_[0];
+        }
+      }
+
+      OrthancPluginHttpRequest& GetRequest()
+      {
+        return converted_;
+      }
+    };
+  }
+
+
+  static std::string GetAllowedMethods(_OrthancPluginChunkedRestCallback parameters)
+  {
+    std::string s;
+
+    if (parameters.getHandler != NULL)
+    {
+      s += "GET";
+    }
+
+    if (parameters.postHandler != NULL)
+    {
+      if (!s.empty())
+      {
+        s+= ",";
+      }
+      
+      s += "POST";
+    }
+
+    if (parameters.deleteHandler != NULL)
+    {
+      if (!s.empty())
+      {
+        s+= ",";
+      }
+      
+      s += "DELETE";
+    }
+
+    if (parameters.putHandler != NULL)
+    {
+      if (!s.empty())
+      {
+        s+= ",";
+      }
+      
+      s += "PUT";
+    }
+
+    return s;
+  }
+
+
+  bool OrthancPlugins::HandleChunkedGetDelete(HttpOutput& output,
+                                              HttpMethod method,
+                                              const UriComponents& uri,
+                                              const Arguments& headers,
+                                              const GetArguments& getArguments)
+  {
+    RestCallbackMatcher matcher(uri);
+
+    PImpl::ChunkedRestCallback* callback = NULL;
+
+    // Loop over the callbacks registered by the plugins
+    for (PImpl::ChunkedRestCallbacks::const_iterator it = pimpl_->chunkedRestCallbacks_.begin(); 
+         it != pimpl_->chunkedRestCallbacks_.end(); ++it)
+    {
+      if (matcher.IsMatch((*it)->GetRegularExpression()))
+      {
+        callback = *it;
+        break;
+      }
+    }
+
+    if (callback == NULL)
+    {
+      return false;
+    }
+    else
+    {
+      LOG(INFO) << "Delegating HTTP request to plugin for URI: " << matcher.GetFlatUri();
+
+      OrthancPluginRestCallback handler;
+
+      switch (method)
+      {
+        case HttpMethod_Get:
+          handler = callback->GetParameters().getHandler;
+          break;
+
+        case HttpMethod_Delete:
+          handler = callback->GetParameters().deleteHandler;
+          break;
+
+        default:
+          handler = NULL;
+          break;
+      }
+
+      if (handler == NULL)
+      {
+        output.SendMethodNotAllowed(GetAllowedMethods(callback->GetParameters()));
+      }
+      else
+      {
+        HttpRequestConverter converter(matcher, method, headers);
+        converter.SetGetArguments(getArguments);
+      
+        PImpl::PluginHttpOutput pluginOutput(output);
+        
+        OrthancPluginErrorCode error = handler(
+          reinterpret_cast<OrthancPluginRestOutput*>(&pluginOutput), 
+          matcher.GetFlatUri().c_str(), &converter.GetRequest());
+        
+        pluginOutput.Close(error, GetErrorDictionary());
+      }
+      
+      return true;
+    }
+  }
+
+
+  bool OrthancPlugins::Handle(HttpOutput& output,
+                              RequestOrigin /*origin*/,
+                              const char* /*remoteIp*/,
+                              const char* /*username*/,
+                              HttpMethod method,
+                              const UriComponents& uri,
+                              const Arguments& headers,
+                              const GetArguments& getArguments,
+                              const void* bodyData,
+                              size_t bodySize)
+  {
+    RestCallbackMatcher matcher(uri);
+
+    PImpl::RestCallback* callback = NULL;
+
+    // Loop over the callbacks registered by the plugins
+    for (PImpl::RestCallbacks::const_iterator it = pimpl_->restCallbacks_.begin(); 
+         it != pimpl_->restCallbacks_.end(); ++it)
+    {
+      if (matcher.IsMatch((*it)->GetRegularExpression()))
+      {
+        callback = *it;
+        break;
+      }
+    }
+
+    if (callback == NULL)
+    {
+      // Callback not found, try to find a chunked callback
+      return HandleChunkedGetDelete(output, method, uri, headers, getArguments);
+    }
+
+    LOG(INFO) << "Delegating HTTP request to plugin for URI: " << matcher.GetFlatUri();
+
+    HttpRequestConverter converter(matcher, method, headers);
+    converter.SetGetArguments(getArguments);
+    converter.GetRequest().body = bodyData;
+    converter.GetRequest().bodySize = bodySize;
+
+    PImpl::PluginHttpOutput pluginOutput(output);
+
+    assert(callback != NULL);
+    OrthancPluginErrorCode error = callback->Invoke
+      (pimpl_->restCallbackMutex_, pluginOutput, matcher.GetFlatUri(), converter.GetRequest());
+
+    pluginOutput.Close(error, GetErrorDictionary());
+    return true;
+  }
+
+
+  class OrthancPlugins::IDicomInstance : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomInstance()
+    {
+    }
+
+    virtual bool CanBeFreed() const = 0;
+
+    virtual const DicomInstanceToStore& GetInstance() const = 0;
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance
+  {
+  private:
+    const DicomInstanceToStore&  instance_;
+
+  public:
+    DicomInstanceFromCallback(const DicomInstanceToStore& instance) :
+      instance_(instance)
+    {
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance
+  {
+  private:
+    std::string           buffer_;
+    DicomInstanceToStore  instance_;
+
+  public:
+    DicomInstanceFromBuffer(const void* buffer,
+                            size_t size)
+    {
+      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
+      instance_.SetBuffer(buffer_.empty() ? NULL : buffer_.c_str(), buffer_.size());
+      instance_.SetOrigin(DicomInstanceOrigin::FromPlugins());
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return instance_;
+    };
+  };
+
+
+  class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance
+  {
+  private:
+    std::unique_ptr<ParsedDicomFile>  parsed_;
+    DicomInstanceToStore              instance_;
+
+  public:
+    DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) :
+      parsed_(transcoded.ReleaseAsParsedDicomFile())
+    {
+      instance_.SetParsedDicomFile(*parsed_);
+      instance_.SetOrigin(DicomInstanceOrigin::FromPlugins());
+    }
+
+    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
+    {
+      return instance_;
+    };
+  };
+
+
+  void OrthancPlugins::SignalStoredInstance(const std::string& instanceId,
+                                            const DicomInstanceToStore& instance,
+                                            const Json::Value& simplifiedTags)
+  {
+    DicomInstanceFromCallback wrapped(instance);
+    
+    boost::recursive_mutex::scoped_lock lock(pimpl_->storedCallbackMutex_);
+
+    for (PImpl::OnStoredCallbacks::const_iterator
+           callback = pimpl_->onStoredCallbacks_.begin(); 
+         callback != pimpl_->onStoredCallbacks_.end(); ++callback)
+    {
+      OrthancPluginErrorCode error = (*callback) (
+        reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped),
+        instanceId.c_str());
+
+      if (error != OrthancPluginErrorCode_Success)
+      {
+        GetErrorDictionary().LogError(error, true);
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
+    }
+  }
+
+
+  bool OrthancPlugins::FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                              const Json::Value& simplified)
+  {
+    DicomInstanceFromCallback wrapped(instance);
+    
+    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    
+    for (PImpl::IncomingDicomInstanceFilters::const_iterator
+           filter = pimpl_->incomingDicomInstanceFilters_.begin();
+         filter != pimpl_->incomingDicomInstanceFilters_.end(); ++filter)
+    {
+      int32_t allowed = (*filter) (reinterpret_cast<const OrthancPluginDicomInstance*>(&wrapped));
+
+      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;
+  }
+
+  
+  void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType,
+                                            OrthancPluginResourceType resourceType,
+                                            const char* resource)
+  {
+    boost::recursive_mutex::scoped_lock lock(pimpl_->changeCallbackMutex_);
+
+    for (std::list<OrthancPluginOnChangeCallback>::const_iterator 
+           callback = pimpl_->onChangeCallbacks_.begin(); 
+         callback != pimpl_->onChangeCallbacks_.end(); ++callback)
+    {
+      OrthancPluginErrorCode error = (*callback) (changeType, resourceType, resource);
+
+      if (error != OrthancPluginErrorCode_Success)
+      {
+        GetErrorDictionary().LogError(error, true);
+        throw OrthancException(static_cast<ErrorCode>(error));
+      }
+    }
+  }
+
+
+
+  void OrthancPlugins::SignalChange(const ServerIndexChange& change)
+  {
+    SignalChangeInternal(Plugins::Convert(change.GetChangeType()),
+                         Plugins::Convert(change.GetResourceType()),
+                         change.GetPublicId().c_str());
+  }
+
+
+
+  void OrthancPlugins::RegisterRestCallback(const void* parameters,
+                                            bool lock)
+  {
+    const _OrthancPluginRestCallback& p = 
+      *reinterpret_cast<const _OrthancPluginRestCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a REST callback "
+              << (lock ? "with" : "without")
+              << " mutual exclusion on: " 
+              << p.pathRegularExpression;
+
+    pimpl_->restCallbacks_.push_back(new PImpl::RestCallback(p.pathRegularExpression, p.callback, lock));
+  }
+
+
+  void OrthancPlugins::RegisterChunkedRestCallback(const void* parameters)
+  {
+    const _OrthancPluginChunkedRestCallback& p = 
+      *reinterpret_cast<const _OrthancPluginChunkedRestCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a REST callback for chunked streams on: " 
+              << p.pathRegularExpression;
+
+    pimpl_->chunkedRestCallbacks_.push_back(new PImpl::ChunkedRestCallback(p));
+  }
+
+
+  void OrthancPlugins::RegisterOnStoredInstanceCallback(const void* parameters)
+  {
+    const _OrthancPluginOnStoredInstanceCallback& p = 
+      *reinterpret_cast<const _OrthancPluginOnStoredInstanceCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered an OnStoredInstance callback";
+    pimpl_->onStoredCallbacks_.push_back(p.callback);
+  }
+
+
+  void OrthancPlugins::RegisterOnChangeCallback(const void* parameters)
+  {
+    const _OrthancPluginOnChangeCallback& p = 
+      *reinterpret_cast<const _OrthancPluginOnChangeCallback*>(parameters);
+
+    LOG(INFO) << "Plugin has registered an OnChange callback";
+    pimpl_->onChangeCallbacks_.push_back(p.callback);
+  }
+
+
+  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)
+    {
+      throw OrthancException(ErrorCode_Plugin,
+                             "Can only register one plugin to handle modality worklists");
+    }
+    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)
+    {
+      throw OrthancException(ErrorCode_Plugin,
+                             "Can only register one plugin to handle C-FIND requests");
+    }
+    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)
+    {
+      throw OrthancException(ErrorCode_Plugin,
+                             "Can only register one plugin to handle C-MOVE requests");
+    }
+    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::unique_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+
+    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::RegisterTranscoderCallback(const void* parameters)
+  {
+    const _OrthancPluginTranscoderCallback& p = 
+      *reinterpret_cast<const _OrthancPluginTranscoderCallback*>(parameters);
+
+    boost::unique_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+
+    pimpl_->transcoderCallbacks_.push_back(p.callback);
+    LOG(INFO) << "Plugin has registered a callback to transcode DICOM images (" 
+              << pimpl_->transcoderCallbacks_.size() << " transcoder(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::RegisterIncomingDicomInstanceFilter(const void* parameters)
+  {
+    const _OrthancPluginIncomingDicomInstanceFilter& p = 
+      *reinterpret_cast<const _OrthancPluginIncomingDicomInstanceFilter*>(parameters);
+
+    LOG(INFO) << "Plugin has registered a callback to filter incoming DICOM instances";
+    pimpl_->incomingDicomInstanceFilters_.push_back(p.callback);
+  }
+
+
+  void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters)
+  {
+    const _OrthancPluginRegisterRefreshMetricsCallback& p = 
+      *reinterpret_cast<const _OrthancPluginRegisterRefreshMetricsCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->refreshMetricsMutex_);
+
+    LOG(INFO) << "Plugin has registered a callback to refresh its metrics";
+    pimpl_->refreshMetricsCallbacks_.push_back(p.callback);
+  }
+
+
+  void OrthancPlugins::RegisterStorageCommitmentScpCallback(const void* parameters)
+  {
+    const _OrthancPluginRegisterStorageCommitmentScpCallback& p = 
+      *reinterpret_cast<const _OrthancPluginRegisterStorageCommitmentScpCallback*>(parameters);
+
+    boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_);
+    LOG(INFO) << "Plugin has registered a storage commitment callback";
+
+    pimpl_->storageCommitmentScpCallbacks_.push_back(new PImpl::StorageCommitmentScp(p));
+  }
+
+
+  void OrthancPlugins::AnswerBuffer(const void* parameters)
+  {
+    const _OrthancPluginAnswerBuffer& p = 
+      *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SetContentType(p.mimeType);
+    translatedOutput.Answer(p.answer, p.answerSize);
+  }
+
+
+  void OrthancPlugins::Redirect(const void* parameters)
+  {
+    const _OrthancPluginOutputPlusArgument& p = 
+      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.Redirect(p.argument);
+  }
+
+
+  void OrthancPlugins::SendHttpStatusCode(const void* parameters)
+  {
+    const _OrthancPluginSendHttpStatusCode& p = 
+      *reinterpret_cast<const _OrthancPluginSendHttpStatusCode*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SendStatus(static_cast<HttpStatus>(p.status));
+  }
+
+
+  void OrthancPlugins::SendHttpStatus(const void* parameters)
+  {
+    const _OrthancPluginSendHttpStatus& p = 
+      *reinterpret_cast<const _OrthancPluginSendHttpStatus*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    HttpStatus status = static_cast<HttpStatus>(p.status);
+
+    if (p.bodySize > 0 && p.body != NULL)
+    {
+      translatedOutput.SendStatus(status, p.body, p.bodySize);
+    }
+    else
+    {
+      translatedOutput.SendStatus(status);
+    }
+  }
+
+
+  void OrthancPlugins::SendUnauthorized(const void* parameters)
+  {
+    const _OrthancPluginOutputPlusArgument& p = 
+      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SendUnauthorized(p.argument);
+  }
+
+
+  void OrthancPlugins::SendMethodNotAllowed(const void* parameters)
+  {
+    const _OrthancPluginOutputPlusArgument& p = 
+      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SendMethodNotAllowed(p.argument);
+  }
+
+
+  void OrthancPlugins::SetCookie(const void* parameters)
+  {
+    const _OrthancPluginSetHttpHeader& p = 
+      *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.SetCookie(p.key, p.value);
+  }
+
+
+  void OrthancPlugins::SetHttpHeader(const void* parameters)
+  {
+    const _OrthancPluginSetHttpHeader& p = 
+      *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+    translatedOutput.AddHeader(p.key, p.value);
+  }
+
+
+  void OrthancPlugins::SetHttpErrorDetails(const void* parameters)
+  {
+    const _OrthancPluginSetHttpErrorDetails& p = 
+      *reinterpret_cast<const _OrthancPluginSetHttpErrorDetails*>(parameters);
+
+    PImpl::PluginHttpOutput* output =
+      reinterpret_cast<PImpl::PluginHttpOutput*>(p.output);
+    output->SetErrorDetails(p.details, p.log);
+  }
+
+
+  void OrthancPlugins::CompressAndAnswerPngImage(const void* parameters)
+  {
+    // Bridge for backward compatibility with Orthanc <= 0.9.3
+    const _OrthancPluginCompressAndAnswerPngImage& p = 
+      *reinterpret_cast<const _OrthancPluginCompressAndAnswerPngImage*>(parameters);
+
+    _OrthancPluginCompressAndAnswerImage p2;
+    p2.output = p.output;
+    p2.imageFormat = OrthancPluginImageFormat_Png;
+    p2.pixelFormat = p.format;
+    p2.width = p.width;
+    p2.height = p.height;
+    p2.pitch = p.height;
+    p2.buffer = p.buffer;
+    p2.quality = 0;
+
+    CompressAndAnswerImage(&p2);
+  }
+
+
+  void OrthancPlugins::CompressAndAnswerImage(const void* parameters)
+  {
+    const _OrthancPluginCompressAndAnswerImage& p = 
+      *reinterpret_cast<const _OrthancPluginCompressAndAnswerImage*>(parameters);
+
+    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
+
+    ImageAccessor accessor;
+    accessor.AssignReadOnly(Plugins::Convert(p.pixelFormat), p.width, p.height, p.pitch, p.buffer);
+
+    std::string compressed;
+
+    switch (p.imageFormat)
+    {
+      case OrthancPluginImageFormat_Png:
+      {
+        PngWriter writer;
+        writer.WriteToMemory(compressed, accessor);
+        translatedOutput.SetContentType(MimeType_Png);
+        break;
+      }
+
+      case OrthancPluginImageFormat_Jpeg:
+      {
+        JpegWriter writer;
+        writer.SetQuality(p.quality);
+        writer.WriteToMemory(compressed, accessor);
+        translatedOutput.SetContentType(MimeType_Jpeg);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    translatedOutput.Answer(compressed);
+  }
+
+
+  void OrthancPlugins::GetDicomForInstance(const void* parameters)
+  {
+    const _OrthancPluginGetDicomForInstance& p = 
+      *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters);
+
+    std::string dicom;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      lock.GetContext().ReadDicom(dicom, p.instanceId);
+    }
+
+    CopyToMemoryBuffer(*p.target, dicom);
+  }
+
+
+  void OrthancPlugins::RestApiGet(const void* parameters,
+                                  bool afterPlugins)
+  {
+    const _OrthancPluginRestApiGet& p = 
+      *reinterpret_cast<const _OrthancPluginRestApiGet*>(parameters);
+        
+    LOG(INFO) << "Plugin making REST GET call on URI " << p.uri
+              << (afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    IHttpHandler* handler;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    }
+
+    std::map<std::string, std::string> httpHeaders;
+
+    std::string result;
+    if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri, httpHeaders))
+    {
+      CopyToMemoryBuffer(*p.target, result);
+    }
+    else
+    {
+      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);
+    }
+  }
+
+
+  void OrthancPlugins::RestApiPostPut(bool isPost, 
+                                      const void* parameters,
+                                      bool afterPlugins)
+  {
+    const _OrthancPluginRestApiPostPut& p = 
+      *reinterpret_cast<const _OrthancPluginRestApiPostPut*>(parameters);
+
+    LOG(INFO) << "Plugin making REST " << EnumerationToString(isPost ? HttpMethod_Post : HttpMethod_Put)
+              << " call on URI " << p.uri << (afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    IHttpHandler* handler;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    }
+      
+    std::map<std::string, std::string> httpHeaders;
+
+    std::string result;
+    if (isPost ? 
+        HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders) :
+        HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders))
+    {
+      CopyToMemoryBuffer(*p.target, result);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  void OrthancPlugins::RestApiDelete(const void* parameters,
+                                     bool afterPlugins)
+  {
+    const char* uri = reinterpret_cast<const char*>(parameters);
+    LOG(INFO) << "Plugin making REST DELETE call on URI " << uri
+              << (afterPlugins ? " (after plugins)" : " (built-in API)");
+
+    IHttpHandler* handler;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
+    }
+      
+    std::map<std::string, std::string> httpHeaders;
+
+    if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri, httpHeaders))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  void OrthancPlugins::LookupResource(_OrthancPluginService service,
+                                      const void* parameters)
+  {
+    const _OrthancPluginRetrieveDynamicString& p = 
+      *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters);
+
+    /**
+     * The enumeration below only uses the tags that are indexed in
+     * the Orthanc database. It reflects the
+     * "CandidateResources::ApplyFilter()" method of the
+     * "OrthancFindRequestHandler" class.
+     **/
+
+    DicomTag tag(0, 0);
+    ResourceType level;
+    switch (service)
+    {
+      case _OrthancPluginService_LookupPatient:
+        tag = DICOM_TAG_PATIENT_ID;
+        level = ResourceType_Patient;
+        break;
+
+      case _OrthancPluginService_LookupStudy:
+        tag = DICOM_TAG_STUDY_INSTANCE_UID;
+        level = ResourceType_Study;
+        break;
+
+      case _OrthancPluginService_LookupStudyWithAccessionNumber:
+        tag = DICOM_TAG_ACCESSION_NUMBER;
+        level = ResourceType_Study;
+        break;
+
+      case _OrthancPluginService_LookupSeries:
+        tag = DICOM_TAG_SERIES_INSTANCE_UID;
+        level = ResourceType_Series;
+        break;
+
+      case _OrthancPluginService_LookupInstance:
+        tag = DICOM_TAG_SOP_INSTANCE_UID;
+        level = ResourceType_Instance;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::vector<std::string> result;
+
+    {
+      PImpl::ServerContextLock lock(*pimpl_);
+      lock.GetContext().GetIndex().LookupIdentifierExact(result, level, tag, p.argument);
+    }
+
+    if (result.size() == 1)
+    {
+      *p.result = CopyString(result[0]);
+    }
+    else
+    {
+      if (result.size() > 1)
+      {
+        LOG(WARNING) << "LookupResource(): Multiple resources match the query (instead of 0 or 1), which indicates "
+                     << "your DICOM database breaks the DICOM model of the real world";
+      }
+      
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  static void AccessInstanceMetadataInternal(bool checkExistence,
+                                             const _OrthancPluginAccessDicomInstance& params,
+                                             const DicomInstanceToStore& instance)
+  {
+    MetadataType metadata;
+
+    try
+    {
+      metadata = StringToMetadata(params.key);
+    }
+    catch (OrthancException&)
+    {
+      // Unknown metadata
+      if (checkExistence)
+      {
+        *params.resultInt64 = -1;
+      }
+      else
+      {
+        *params.resultString = NULL;
+      }
+
+      return;
+    }
+
+    ServerIndex::MetadataMap::const_iterator it = 
+      instance.GetMetadata().find(std::make_pair(ResourceType_Instance, metadata));
+
+    if (checkExistence)
+    {
+      if (it != instance.GetMetadata().end())
+      {
+        *params.resultInt64 = 1;
+      }
+      else
+      {
+        *params.resultInt64 = 0;
+      }
+    }
+    else
+    {
+      if (it != instance.GetMetadata().end())
+      {      
+        *params.resultString = it->second.c_str();
+      }
+      else
+      {
+        // Error: Missing metadata
+        *params.resultString = NULL;
+      }
+    }
+  }
+
+
+  void OrthancPlugins::AccessDicomInstance(_OrthancPluginService service,
+                                           const void* parameters)
+  {
+    const _OrthancPluginAccessDicomInstance& p = 
+      *reinterpret_cast<const _OrthancPluginAccessDicomInstance*>(parameters);
+
+    if (p.instance == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    const DicomInstanceToStore& instance =
+      reinterpret_cast<const IDicomInstance*>(p.instance)->GetInstance();
+
+    switch (service)
+    {
+      case _OrthancPluginService_GetInstanceRemoteAet:
+        *p.resultString = instance.GetOrigin().GetRemoteAetC();
+        return;
+
+      case _OrthancPluginService_GetInstanceSize:
+        *p.resultInt64 = instance.GetBufferSize();
+        return;
+
+      case _OrthancPluginService_GetInstanceData:
+        *p.resultString = reinterpret_cast<const char*>(instance.GetBufferData());
+        return;
+
+      case _OrthancPluginService_HasInstanceMetadata:
+        AccessInstanceMetadataInternal(true, p, instance);
+        return;
+
+      case _OrthancPluginService_GetInstanceMetadata:
+        AccessInstanceMetadataInternal(false, p, instance);
+        return;
+
+      case _OrthancPluginService_GetInstanceJson:
+      case _OrthancPluginService_GetInstanceSimplifiedJson:
+      {
+        Json::StyledWriter writer;
+        std::string s;
+
+        if (service == _OrthancPluginService_GetInstanceJson)
+        {
+          s = writer.write(instance.GetJson());
+        }
+        else
+        {
+          Json::Value simplified;
+          ServerToolbox::SimplifyTags(simplified, instance.GetJson(), DicomToJsonFormat_Human);
+          s = writer.write(simplified);
+        }
+
+        *p.resultStringToFree = CopyString(s);
+        return;
+      }
+
+      case _OrthancPluginService_GetInstanceOrigin:   // New in Orthanc 0.9.5
+        *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin());
+        return;
+
+      case _OrthancPluginService_GetInstanceTransferSyntaxUid:   // New in Orthanc 1.6.1
+      {
+        std::string s;
+        if (!instance.LookupTransferSyntax(s))
+        {
+          s.clear();
+        }
+        
+        *p.resultStringToFree = CopyString(s);
+        return;
+      }
+
+      case _OrthancPluginService_HasInstancePixelData:   // New in Orthanc 1.6.1
+        *p.resultInt64 = instance.HasPixelData();
+        return;
+
+      case _OrthancPluginService_GetInstanceFramesCount:  // New in Orthanc 1.7.0
+        *p.resultInt64 = instance.GetParsedDicomFile().GetFramesCount();
+        return;
+        
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void OrthancPlugins::BufferCompression(const void* parameters)
+  {
+    const _OrthancPluginBufferCompression& p = 
+      *reinterpret_cast<const _OrthancPluginBufferCompression*>(parameters);
+
+    std::string result;
+
+    {
+      std::unique_ptr<DeflateBaseCompressor> compressor;
+
+      switch (p.compression)
+      {
+        case OrthancPluginCompressionType_Zlib:
+        {
+          compressor.reset(new ZlibCompressor);
+          compressor->SetPrefixWithUncompressedSize(false);
+          break;
+        }
+
+        case OrthancPluginCompressionType_ZlibWithSize:
+        {
+          compressor.reset(new ZlibCompressor);
+          compressor->SetPrefixWithUncompressedSize(true);
+          break;
+        }
+
+        case OrthancPluginCompressionType_Gzip:
+        {
+          compressor.reset(new GzipCompressor);
+          compressor->SetPrefixWithUncompressedSize(false);
+          break;
+        }
+
+        case OrthancPluginCompressionType_GzipWithSize:
+        {
+          compressor.reset(new GzipCompressor);
+          compressor->SetPrefixWithUncompressedSize(true);
+          break;
+        }
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      if (p.uncompress)
+      {
+        compressor->Uncompress(result, p.source, p.size);
+      }
+      else
+      {
+        compressor->Compress(result, p.source, p.size);
+      }
+    }
+
+    CopyToMemoryBuffer(*p.target, result);
+  }
+
+
+  static OrthancPluginImage* ReturnImage(std::unique_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.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    if (image->IsReadOnly())
+    {
+      std::unique_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::AccessDicomInstance2(_OrthancPluginService service,
+                                            const void* parameters)
+  {
+    const _OrthancPluginAccessDicomInstance2& p = 
+      *reinterpret_cast<const _OrthancPluginAccessDicomInstance2*>(parameters);
+
+    if (p.instance == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    const DicomInstanceToStore& instance =
+      reinterpret_cast<const IDicomInstance*>(p.instance)->GetInstance();
+
+    switch (service)
+    {
+      case _OrthancPluginService_GetInstanceFramesCount:
+        *p.targetUint32 = instance.GetParsedDicomFile().GetFramesCount();
+        return;
+        
+      case _OrthancPluginService_GetInstanceRawFrame:
+      {
+        if (p.targetBuffer == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        p.targetBuffer->data = NULL;
+        p.targetBuffer->size = 0;
+        
+        MimeType mime;
+        std::string frame;
+        instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex);
+        CopyToMemoryBuffer(*p.targetBuffer, frame);
+        return;
+      }
+        
+      case _OrthancPluginService_GetInstanceDecodedFrame:
+      {
+        if (p.targetImage == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        std::unique_ptr<ImageAccessor> decoded;
+        {
+          PImpl::ServerContextLock lock(*pimpl_);
+          decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex));
+        }
+        
+        *(p.targetImage) = ReturnImage(decoded);
+        return;
+      }
+        
+      case _OrthancPluginService_SerializeDicomInstance:
+      {
+        if (p.targetBuffer == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        p.targetBuffer->data = NULL;
+        p.targetBuffer->size = 0;
+        
+        std::string serialized;
+        instance.GetParsedDicomFile().SaveToMemoryBuffer(serialized);
+        CopyToMemoryBuffer(*p.targetBuffer, serialized);
+        return;
+      }
+
+      case _OrthancPluginService_GetInstanceAdvancedJson:
+      {
+        if (p.targetStringToFree == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        
+        Json::Value json;
+        instance.GetParsedDicomFile().DatasetToJson(
+          json, Plugins::Convert(p.format), 
+          static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
+
+        Json::FastWriter writer;
+        *p.targetStringToFree = CopyString(writer.write(json));        
+        return;
+      }
+      
+      case _OrthancPluginService_GetInstanceDicomWebJson:
+      case _OrthancPluginService_GetInstanceDicomWebXml:
+      {
+        if (p.targetStringToFree == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+
+        DicomWebBinaryFormatter formatter(p.dicomWebCallback, p.dicomWebPayload);
+        formatter.Apply(p.targetStringToFree,
+                        (service == _OrthancPluginService_GetInstanceDicomWebJson),
+                        instance.GetParsedDicomFile());
+        return;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void OrthancPlugins::UncompressImage(const void* parameters)
+  {
+    const _OrthancPluginUncompressImage& p = *reinterpret_cast<const _OrthancPluginUncompressImage*>(parameters);
+
+    std::unique_ptr<ImageAccessor> image;
+
+    switch (p.format)
+    {
+      case OrthancPluginImageFormat_Png:
+      {
+        image.reset(new PngReader);
+        reinterpret_cast<PngReader&>(*image).ReadFromMemory(p.data, p.size);
+        break;
+      }
+
+      case OrthancPluginImageFormat_Jpeg:
+      {
+        image.reset(new JpegReader);
+        reinterpret_cast<JpegReader&>(*image).ReadFromMemory(p.data, p.size);
+        break;
+      }
+
+      case OrthancPluginImageFormat_Dicom:
+      {
+        PImpl::ServerContextLock lock(*pimpl_);
+        image.reset(lock.GetContext().DecodeDicomFrame(p.data, p.size, 0));
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    *(p.target) = ReturnImage(image);
+  }
+
+
+  void OrthancPlugins::CompressImage(const void* parameters)
+  {
+    const _OrthancPluginCompressImage& p = *reinterpret_cast<const _OrthancPluginCompressImage*>(parameters);
+
+    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, accessor);
+        break;
+      }
+
+      case OrthancPluginImageFormat_Jpeg:
+      {
+        JpegWriter writer;
+        writer.SetQuality(p.quality);
+        writer.WriteToMemory(compressed, accessor);
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    CopyToMemoryBuffer(*p.target, compressed.size() > 0 ? compressed.c_str() : NULL, compressed.size());
+  }
+
+
+  static void SetupHttpClient(HttpClient& client,
+                              const _OrthancPluginCallHttpClient2& parameters)
+  {
+    client.SetUrl(parameters.url);
+    client.SetConvertHeadersToLowerCase(false);
+
+    if (parameters.timeout != 0)
+    {
+      client.SetTimeout(parameters.timeout);
+    }
+
+    if (parameters.username != NULL && 
+        parameters.password != NULL)
+    {
+      client.SetCredentials(parameters.username, parameters.password);
+    }
+
+    if (parameters.certificateFile != NULL)
+    {
+      std::string certificate(parameters.certificateFile);
+      std::string key, password;
+
+      if (parameters.certificateKeyFile)
+      {
+        key.assign(parameters.certificateKeyFile);
+      }
+
+      if (parameters.certificateKeyPassword)
+      {
+        password.assign(parameters.certificateKeyPassword);
+      }
+
+      client.SetClientCertificate(certificate, key, password);
+    }
+
+    client.SetPkcs11Enabled(parameters.pkcs11 ? true : false);
+
+    for (uint32_t i = 0; i < parameters.headersCount; i++)
+    {
+      if (parameters.headersKeys[i] == NULL ||
+          parameters.headersValues[i] == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      client.AddHeader(parameters.headersKeys[i], parameters.headersValues[i]);
+    }
+
+    switch (parameters.method)
+    {
+      case OrthancPluginHttpMethod_Get:
+        client.SetMethod(HttpMethod_Get);
+        break;
+
+      case OrthancPluginHttpMethod_Post:
+        client.SetMethod(HttpMethod_Post);
+        client.GetBody().assign(reinterpret_cast<const char*>(parameters.body), parameters.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Put:
+        client.SetMethod(HttpMethod_Put);
+        client.GetBody().assign(reinterpret_cast<const char*>(parameters.body), parameters.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Delete:
+        client.SetMethod(HttpMethod_Delete);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  static void ExecuteHttpClientWithoutChunkedBody(uint16_t& httpStatus,
+                                                  OrthancPluginMemoryBuffer* answerBody,
+                                                  OrthancPluginMemoryBuffer* answerHeaders,
+                                                  HttpClient& client)
+  {
+    if (answerBody == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    std::string body;
+    HttpClient::HttpHeaders headers;
+
+    bool success = client.Apply(body, headers);
+
+    // The HTTP request has succeeded
+    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 (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(*answerHeaders, s);
+    }
+
+    // Copy the body of the answer if it makes sense
+    if (client.GetMethod() != HttpMethod_Delete)
+    {
+      CopyToMemoryBuffer(*answerBody, body);
+    }
+  }
+
+
+  void OrthancPlugins::CallHttpClient(const void* parameters)
+  {
+    const _OrthancPluginCallHttpClient& p = *reinterpret_cast<const _OrthancPluginCallHttpClient*>(parameters);
+
+    HttpClient client;
+
+    {    
+      _OrthancPluginCallHttpClient2 converted;
+      memset(&converted, 0, sizeof(converted));
+
+      converted.answerBody = NULL;
+      converted.answerHeaders = NULL;
+      converted.httpStatus = NULL;
+      converted.method = p.method;
+      converted.url = p.url;
+      converted.headersCount = 0;
+      converted.headersKeys = NULL;
+      converted.headersValues = NULL;
+      converted.body = p.body;
+      converted.bodySize = p.bodySize;
+      converted.username = p.username;
+      converted.password = p.password;
+      converted.timeout = 0;  // Use default timeout
+      converted.certificateFile = NULL;
+      converted.certificateKeyFile = NULL;
+      converted.certificateKeyPassword = NULL;
+      converted.pkcs11 = false;
+
+      SetupHttpClient(client, converted);
+    }
+
+    uint16_t status;
+    ExecuteHttpClientWithoutChunkedBody(status, p.target, NULL, client);
+  }
+
+
+  void OrthancPlugins::CallHttpClient2(const void* parameters)
+  {
+    const _OrthancPluginCallHttpClient2& p = *reinterpret_cast<const _OrthancPluginCallHttpClient2*>(parameters);
+    
+    if (p.httpStatus == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    HttpClient client;
+
+    if (p.method == OrthancPluginHttpMethod_Post ||
+        p.method == OrthancPluginHttpMethod_Put)
+    {
+      client.GetBody().assign(reinterpret_cast<const char*>(p.body), p.bodySize);
+    }
+    
+    SetupHttpClient(client, p);
+    ExecuteHttpClientWithoutChunkedBody(*p.httpStatus, p.answerBody, p.answerHeaders, client);
+  }
+
+
+  void OrthancPlugins::ChunkedHttpClient(const void* parameters)
+  {
+    const _OrthancPluginChunkedHttpClient& p =
+      *reinterpret_cast<const _OrthancPluginChunkedHttpClient*>(parameters);
+        
+    if (p.httpStatus == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    HttpClient client;
+
+    {
+      _OrthancPluginCallHttpClient2 converted;
+      memset(&converted, 0, sizeof(converted));
+
+      converted.answerBody = NULL;
+      converted.answerHeaders = NULL;
+      converted.httpStatus = NULL;
+      converted.method = p.method;
+      converted.url = p.url;
+      converted.headersCount = p.headersCount;
+      converted.headersKeys = p.headersKeys;
+      converted.headersValues = p.headersValues;
+      converted.body = NULL;
+      converted.bodySize = 0;
+      converted.username = p.username;
+      converted.password = p.password;
+      converted.timeout = p.timeout;
+      converted.certificateFile = p.certificateFile;
+      converted.certificateKeyFile = p.certificateKeyFile;
+      converted.certificateKeyPassword = p.certificateKeyPassword;
+      converted.pkcs11 = p.pkcs11;
+
+      SetupHttpClient(client, converted);
+    }
+    
+    HttpClientChunkedRequest body(p, pimpl_->dictionary_);
+    client.SetBody(body);
+
+    HttpClientChunkedAnswer answer(p, pimpl_->dictionary_);
+
+    bool success = client.Apply(answer);
+
+    *p.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
+
+    if (!success)
+    {
+      HttpClient::ThrowException(client.GetLastStatus());
+    }
+  }
+
+
+  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(reinterpret_cast<const char*>(p.body), p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Put:
+        client.SetMethod(HttpMethod_Put);
+        client.GetBody().assign(reinterpret_cast<const char*>(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::unique_ptr<ImageAccessor> target(new Image(Plugins::Convert(p.targetFormat), source.GetWidth(), source.GetHeight(), false));
+    ImageProcessing::Convert(*target, source);
+
+    *(p.target) = ReturnImage(target);
+  }
+
+
+
+  void OrthancPlugins::GetFontInfo(const void* parameters)
+  {
+    const _OrthancPluginGetFontInfo& p = *reinterpret_cast<const _OrthancPluginGetFontInfo*>(parameters);
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+
+      const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex);
+
+      if (p.name != NULL)
+      {
+        *(p.name) = font.GetName().c_str();
+      }
+      else if (p.size != NULL)
+      {
+        *(p.size) = font.GetSize();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+
+
+  void OrthancPlugins::DrawText(const void* parameters)
+  {
+    const _OrthancPluginDrawText& p = *reinterpret_cast<const _OrthancPluginDrawText*>(parameters);
+
+    ImageAccessor& target = *reinterpret_cast<ImageAccessor*>(p.image);
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex);
+      font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b);
+    }
+  }
+
+
+  void OrthancPlugins::ApplyDicomToJson(_OrthancPluginService service,
+                                        const void* parameters)
+  {
+    const _OrthancPluginDicomToJson& p =
+      *reinterpret_cast<const _OrthancPluginDicomToJson*>(parameters);
+
+    std::unique_ptr<ParsedDicomFile> dicom;
+
+    if (service == _OrthancPluginService_DicomBufferToJson)
+    {
+      dicom.reset(new ParsedDicomFile(p.buffer, p.size));
+    }
+    else
+    {
+      if (p.instanceId == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      std::string content;
+
+      {
+        PImpl::ServerContextLock lock(*pimpl_);
+        lock.GetContext().ReadDicom(content, p.instanceId);
+      }
+
+      dicom.reset(new ParsedDicomFile(content));
+    }
+
+    Json::Value json;
+    dicom->DatasetToJson(json, Plugins::Convert(p.format), 
+                         static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
+
+    Json::FastWriter writer;
+    *p.result = CopyString(writer.write(json));
+  }
+        
+
+  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;
+
+    {
+      // Fix issue 168 (Plugins can't read private tags from the
+      // configuration file)
+      // https://bitbucket.org/sjodogne/orthanc/issues/168/
+      std::string privateCreator;
+      {
+        OrthancConfiguration::ReaderLock lock;
+        privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
+      }
+      
+      std::unique_ptr<ParsedDicomFile> file
+        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(p.flags),
+                                         privateCreator));
+
+      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::GetTagName(const void* parameters)
+  {
+    const _OrthancPluginGetTagName& p =
+      *reinterpret_cast<const _OrthancPluginGetTagName*>(parameters);
+
+    std::string privateCreator;
+    
+    if (p.privateCreator != NULL)
+    {
+      privateCreator = p.privateCreator;
+    }
+   
+    DicomTag tag(p.group, p.element);
+    *p.result = CopyString(FromDcmtkBridge::GetTagName(tag, privateCreator));
+  }
+
+
+  void OrthancPlugins::ApplyCreateImage(_OrthancPluginService service,
+                                        const void* parameters)
+  {
+    const _OrthancPluginCreateImage& p =
+      *reinterpret_cast<const _OrthancPluginCreateImage*>(parameters);
+
+    std::unique_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:
+      {
+        PImpl::ServerContextLock lock(*pimpl_);
+        result.reset(lock.GetContext().DecodeDicomFrame(p.constBuffer, p.bufferSize, p.frameIndex));
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    *(p.target) = ReturnImage(result);
+  }
+
+
+  void OrthancPlugins::ApplySendMultipartItem(const void* parameters)
+  {
+    // 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);
+
+    std::map<std::string, std::string> headers;  // No custom headers
+    reinterpret_cast<PImpl::PluginHttpOutput*>(p.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);
+    
+    std::map<std::string, std::string> headers;
+    for (uint32_t i = 0; i < p.headersCount; i++)
+    {
+      headers[p.headersKeys[i]] = p.headersValues[i];
+    }
+    
+    reinterpret_cast<PImpl::PluginHttpOutput*>(p.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
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "Cannot invoke this service without a custom database back-end");
+    }
+  }
+
+
+  namespace
+  {
+    class DictionaryReadLocker
+    {
+    private:
+      const DcmDataDictionary& dictionary_;
+
+    public:
+      DictionaryReadLocker() : dictionary_(dcmDataDict.rdlock())
+      {
+      }
+
+      ~DictionaryReadLocker()
+      {
+#if DCMTK_VERSION_NUMBER >= 364
+        dcmDataDict.rdunlock();
+#else
+        dcmDataDict.unlock();
+#endif
+      }
+
+      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 = NULL;
+
+    if (tag.IsPrivate())
+    {
+      // Fix issue 168 (Plugins can't read private tags from the
+      // configuration file)
+      // https://bitbucket.org/sjodogne/orthanc/issues/168/
+      std::string privateCreator;
+      {
+        OrthancConfiguration::ReaderLock lock;
+        privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
+      }
+
+      entry = locker->findEntry(tag2, privateCreator.c_str());
+    }
+    else
+    {
+      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 = SystemToolbox::GetPathToExecutable();
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
+        return true;
+      }
+
+      case _OrthancPluginService_GetOrthancDirectory:
+      {
+        std::string s = SystemToolbox::GetDirectoryOfExecutable();
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
+        return true;
+      }
+
+      case _OrthancPluginService_GetConfigurationPath:
+      {
+        std::string s;
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          s = lock.GetConfiguration().GetConfigurationAbsolutePath();
+        }
+
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
+        return true;
+      }
+
+      case _OrthancPluginService_GetConfiguration:
+      {
+        std::string s;
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          lock.GetConfiguration().Format(s);
+        }
+
+        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
+        return true;
+      }
+
+      case _OrthancPluginService_BufferCompression:
+        BufferCompression(parameters);
+        return true;
+
+      case _OrthancPluginService_AnswerBuffer:
+        AnswerBuffer(parameters);
+        return true;
+
+      case _OrthancPluginService_CompressAndAnswerPngImage:
+        CompressAndAnswerPngImage(parameters);
+        return true;
+
+      case _OrthancPluginService_CompressAndAnswerImage:
+        CompressAndAnswerImage(parameters);
+        return true;
+
+      case _OrthancPluginService_GetDicomForInstance:
+        GetDicomForInstance(parameters);
+        return true;
+
+      case _OrthancPluginService_RestApiGet:
+        RestApiGet(parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiGetAfterPlugins:
+        RestApiGet(parameters, true);
+        return true;
+
+      case _OrthancPluginService_RestApiGet2:
+        RestApiGet2(parameters);
+        return true;
+
+      case _OrthancPluginService_RestApiPost:
+        RestApiPostPut(true, parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiPostAfterPlugins:
+        RestApiPostPut(true, parameters, true);
+        return true;
+
+      case _OrthancPluginService_RestApiDelete:
+        RestApiDelete(parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiDeleteAfterPlugins:
+        RestApiDelete(parameters, true);
+        return true;
+
+      case _OrthancPluginService_RestApiPut:
+        RestApiPostPut(false, parameters, false);
+        return true;
+
+      case _OrthancPluginService_RestApiPutAfterPlugins:
+        RestApiPostPut(false, parameters, true);
+        return true;
+
+      case _OrthancPluginService_Redirect:
+        Redirect(parameters);
+        return true;
+
+      case _OrthancPluginService_SendUnauthorized:
+        SendUnauthorized(parameters);
+        return true;
+
+      case _OrthancPluginService_SendMethodNotAllowed:
+        SendMethodNotAllowed(parameters);
+        return true;
+
+      case _OrthancPluginService_SendHttpStatus:
+        SendHttpStatus(parameters);
+        return true;
+
+      case _OrthancPluginService_SendHttpStatusCode:
+        SendHttpStatusCode(parameters);
+        return true;
+
+      case _OrthancPluginService_SetCookie:
+        SetCookie(parameters);
+        return true;
+
+      case _OrthancPluginService_SetHttpHeader:
+        SetHttpHeader(parameters);
+        return true;
+
+      case _OrthancPluginService_SetHttpErrorDetails:
+        SetHttpErrorDetails(parameters);
+        return true;
+
+      case _OrthancPluginService_LookupPatient:
+      case _OrthancPluginService_LookupStudy:
+      case _OrthancPluginService_LookupStudyWithAccessionNumber:
+      case _OrthancPluginService_LookupSeries:
+      case _OrthancPluginService_LookupInstance:
+        LookupResource(service, parameters);
+        return true;
+
+      case _OrthancPluginService_GetInstanceRemoteAet:
+      case _OrthancPluginService_GetInstanceSize:
+      case _OrthancPluginService_GetInstanceData:
+      case _OrthancPluginService_GetInstanceJson:
+      case _OrthancPluginService_GetInstanceSimplifiedJson:
+      case _OrthancPluginService_HasInstanceMetadata:
+      case _OrthancPluginService_GetInstanceMetadata:
+      case _OrthancPluginService_GetInstanceOrigin:
+      case _OrthancPluginService_GetInstanceTransferSyntaxUid:
+      case _OrthancPluginService_HasInstancePixelData:
+        AccessDicomInstance(service, parameters);
+        return true;
+
+      case _OrthancPluginService_GetInstanceFramesCount:
+      case _OrthancPluginService_GetInstanceRawFrame:
+      case _OrthancPluginService_GetInstanceDecodedFrame:
+      case _OrthancPluginService_SerializeDicomInstance:
+      case _OrthancPluginService_GetInstanceAdvancedJson:
+      case _OrthancPluginService_GetInstanceDicomWebJson:
+      case _OrthancPluginService_GetInstanceDicomWebXml:
+        AccessDicomInstance2(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);
+        reinterpret_cast<PImpl::PluginHttpOutput*>(p.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_ChunkedHttpClient:
+        ChunkedHttpClient(parameters);
+        return true;
+
+      case _OrthancPluginService_ConvertPixelFormat:
+        ConvertPixelFormat(parameters);
+        return true;
+
+      case _OrthancPluginService_GetFontsCount:
+      {
+        const _OrthancPluginReturnSingleValue& p =
+          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          *(p.resultUint32) = lock.GetConfiguration().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;
+      }
+
+      case _OrthancPluginService_AutodetectMimeType:
+      {
+        const _OrthancPluginRetrieveStaticString& p =
+          *reinterpret_cast<const _OrthancPluginRetrieveStaticString*>(parameters);
+        *p.result = EnumerationToString(SystemToolbox::AutodetectMimeType(p.argument));
+        return true;
+      }
+
+      case _OrthancPluginService_SetMetricsValue:
+      {
+        const _OrthancPluginSetMetricsValue& p =
+          *reinterpret_cast<const _OrthancPluginSetMetricsValue*>(parameters);
+
+        MetricsType type;
+        switch (p.type)
+        {
+          case OrthancPluginMetricsType_Default:
+            type = MetricsType_Default;
+            break;
+
+          case OrthancPluginMetricsType_Timer:
+            type = MetricsType_MaxOver10Seconds;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+        
+        {
+          PImpl::ServerContextLock lock(*pimpl_);
+          lock.GetContext().GetMetricsRegistry().SetValue(p.name, p.value, type);
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_EncodeDicomWebJson:
+      case _OrthancPluginService_EncodeDicomWebXml:
+      {
+        const _OrthancPluginEncodeDicomWeb& p =
+          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb*>(parameters);
+
+        DicomWebBinaryFormatter formatter(p.callback);
+        formatter.Apply(p.target,
+                        (service == _OrthancPluginService_EncodeDicomWebJson),
+                        p.dicom, p.dicomSize);
+        return true;
+      }
+
+      case _OrthancPluginService_EncodeDicomWebJson2:
+      case _OrthancPluginService_EncodeDicomWebXml2:
+      {
+        const _OrthancPluginEncodeDicomWeb2& p =
+          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb2*>(parameters);
+
+        DicomWebBinaryFormatter formatter(p.callback, p.payload);
+        formatter.Apply(p.target,
+                        (service == _OrthancPluginService_EncodeDicomWebJson2),
+                        p.dicom, p.dicomSize);
+        return true;
+      }
+
+      case _OrthancPluginService_GetTagName:
+        GetTagName(parameters);
+        return true;
+
+      case _OrthancPluginService_CreateDicomInstance:
+      {
+        const _OrthancPluginCreateDicomInstance& p =
+          *reinterpret_cast<const _OrthancPluginCreateDicomInstance*>(parameters);
+        *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
+          new DicomInstanceFromBuffer(p.buffer, p.size));
+        return true;
+      }
+        
+      case _OrthancPluginService_FreeDicomInstance:
+      {
+        const _OrthancPluginFreeDicomInstance& p =
+          *reinterpret_cast<const _OrthancPluginFreeDicomInstance*>(parameters);
+
+        if (p.dicom != NULL)
+        {
+          IDicomInstance* obj = reinterpret_cast<IDicomInstance*>(p.dicom);
+          
+          if (obj->CanBeFreed())
+          {
+            delete obj;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_Plugin, "Cannot free a DICOM instance provided to a callback");
+          }
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_TranscodeDicomInstance:
+      {
+        const _OrthancPluginCreateDicomInstance& p =
+          *reinterpret_cast<const _OrthancPluginCreateDicomInstance*>(parameters);
+
+        DicomTransferSyntax transferSyntax;
+        if (p.transferSyntax == NULL ||
+            !LookupTransferSyntax(transferSyntax, p.transferSyntax))
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange, "Unsupported transfer syntax: " +
+                                 std::string(p.transferSyntax == NULL ? "(null)" : p.transferSyntax));
+        }
+        else
+        {
+          std::set<DicomTransferSyntax> syntaxes;
+          syntaxes.insert(transferSyntax);
+
+          IDicomTranscoder::DicomImage source;
+          source.SetExternalBuffer(p.buffer, p.size);
+
+          IDicomTranscoder::DicomImage transcoded;
+          bool success;
+          
+          {
+            PImpl::ServerContextLock lock(*pimpl_);
+            success = lock.GetContext().Transcode(
+              transcoded, source, syntaxes, true /* allow new sop */);
+          }
+
+          if (success)
+          {
+            *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
+              new DicomInstanceFromTranscoded(transcoded));
+            return true;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode image");
+          }
+        }
+      }
+
+      case _OrthancPluginService_CreateMemoryBuffer:
+      {
+        const _OrthancPluginCreateMemoryBuffer& p =
+          *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer*>(parameters);
+
+        p.target->size = p.size;
+        
+        if (p.size == 0)
+        {
+          p.target->data = NULL;
+        }
+        else
+        {
+          p.target->data = malloc(p.size);
+        }          
+        
+        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_RegisterChunkedRestCallback:
+        RegisterChunkedRestCallback(parameters);
+        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_RegisterTranscoderCallback:
+        RegisterTranscoderCallback(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_RegisterIncomingDicomInstanceFilter:
+        RegisterIncomingDicomInstanceFilter(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterRefreshMetricsCallback:
+        RegisterRefreshMetricsCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterStorageCommitmentScpCallback:
+        RegisterStorageCommitmentScpCallback(parameters);
+        return true;
+
+      case _OrthancPluginService_RegisterStorageArea:
+      {
+        LOG(INFO) << "Plugin has registered a custom storage area";
+        const _OrthancPluginRegisterStorageArea& p = 
+          *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters);
+        
+        if (pimpl_->storageArea_.get() == NULL)
+        {
+          pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_StorageAreaAlreadyRegistered);
+        }
+
+        return true;
+      }
+
+      case _OrthancPluginService_SetPluginProperty:
+      {
+        const _OrthancPluginSetPluginProperty& p = 
+          *reinterpret_cast<const _OrthancPluginSetPluginProperty*>(parameters);
+        pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value;
+        return true;
+      }
+
+      case _OrthancPluginService_GetCommandLineArgumentsCount:
+      {
+        const _OrthancPluginReturnSingleValue& p =
+          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
+        *(p.resultUint32) = pimpl_->argc_ - 1;
+        return true;
+      }
+
+      case _OrthancPluginService_GetCommandLineArgument:
+      {
+        const _OrthancPluginGlobalProperty& p =
+          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
+        
+        if (p.property + 1 > pimpl_->argc_)
+        {
+          return false;
+        }
+        else
+        {
+          std::string arg = std::string(pimpl_->argv_[p.property + 1]);
+          *(p.result) = CopyString(arg);
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_RegisterDatabaseBackend:
+      {
+        LOG(INFO) << "Plugin has registered a custom database back-end";
+
+        const _OrthancPluginRegisterDatabaseBackend& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters);
+
+        if (pimpl_->database_.get() == NULL)
+        {
+          pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), 
+                                                            *p.backend, NULL, 0, p.payload));
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered);
+        }
+
+        *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get());
+
+        return true;
+      }
+
+      case _OrthancPluginService_RegisterDatabaseBackendV2:
+      {
+        LOG(INFO) << "Plugin has registered a custom database back-end";
+
+        const _OrthancPluginRegisterDatabaseBackendV2& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters);
+
+        if (pimpl_->database_.get() == NULL)
+        {
+          pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(),
+                                                            *p.backend, p.extensions,
+                                                            p.extensionsSize, p.payload));
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered);
+        }
+
+        *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get());
+
+        return true;
+      }
+
+      case _OrthancPluginService_DatabaseAnswer:
+        throw OrthancException(ErrorCode_InternalError);   // Implemented before locking (*)
+
+      case _OrthancPluginService_RegisterErrorCode:
+      {
+        const _OrthancPluginRegisterErrorCode& p =
+          *reinterpret_cast<const _OrthancPluginRegisterErrorCode*>(parameters);
+        *(p.target) = pimpl_->dictionary_.Register(plugin, p.code, p.httpStatus, p.message);
+        return true;
+      }
+
+      case _OrthancPluginService_RegisterDictionaryTag:
+      {
+        const _OrthancPluginRegisterDictionaryTag& p =
+          *reinterpret_cast<const _OrthancPluginRegisterDictionaryTag*>(parameters);
+        FromDcmtkBridge::RegisterDictionaryTag(DicomTag(p.group, p.element),
+                                               Plugins::Convert(p.vr), p.name,
+                                               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;
+      }
+
+      case _OrthancPluginService_ReconstructMainDicomTags:
+      {
+        const _OrthancPluginReconstructMainDicomTags& p =
+          *reinterpret_cast<const _OrthancPluginReconstructMainDicomTags*>(parameters);
+
+        if (pimpl_->database_.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_DatabasePlugin,
+                                 "The service ReconstructMainDicomTags can only be invoked by custom database plugins");
+        }
+
+        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
+        ServerToolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level));
+
+        return true;
+      }
+
+      default:
+      {
+        // This service is unknown to the Orthanc plugin engine
+        return false;
+      }
+    }
+  }
+
+
+
+  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;
+  }
+
+
+  IStorageArea* OrthancPlugins::CreateStorageArea()
+  {
+    if (!HasStorageArea())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return pimpl_->storageArea_->Create();
+    }
+  }
+
+
+  const SharedLibrary& OrthancPlugins::GetStorageAreaLibrary() const
+  {
+    if (!HasStorageArea())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return pimpl_->storageArea_->GetSharedLibrary();
+    }
+  }
+
+
+  IDatabaseWrapper& OrthancPlugins::GetDatabaseBackend()
+  {
+    if (!HasDatabaseBackend())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *pimpl_->database_;
+    }
+  }
+
+
+  const SharedLibrary& OrthancPlugins::GetDatabaseBackendLibrary() const
+  {
+    if (!HasDatabaseBackend())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return pimpl_->database_->GetSharedLibrary();
+    }
+  }
+
+
+  const char* OrthancPlugins::GetProperty(const char* plugin,
+                                          _OrthancPluginProperty property) const
+  {
+    PImpl::Property p = std::make_pair(plugin, property);
+    PImpl::Properties::const_iterator it = pimpl_->properties_.find(p);
+
+    if (it == pimpl_->properties_.end())
+    {
+      return NULL;
+    }
+    else
+    {
+      return it->second.c_str();
+    }
+  }
+
+
+  void OrthancPlugins::SetCommandLineArguments(int argc, char* argv[])
+  {
+    if (argc < 1 || argv == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    pimpl_->argc_ = argc;
+    pimpl_->argv_ = argv;
+  }
+
+
+  PluginsManager& OrthancPlugins::GetManager()
+  {
+    return pimpl_->manager_;
+  }
+
+
+  const PluginsManager& OrthancPlugins::GetManager() const
+  {
+    return pimpl_->manager_;
+  }
+
+
+  PluginsErrorDictionary&  OrthancPlugins::GetErrorDictionary()
+  {
+    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::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+    return !pimpl_->decodeImageCallbacks_.empty();
+  }
+
+
+  bool OrthancPlugins::HasCustomTranscoder()
+  {
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+    return !pimpl_->transcoderCallbacks_.empty();
+  }
+
+
+  ImageAccessor* OrthancPlugins::Decode(const void* dicom,
+                                        size_t size,
+                                        unsigned int frame)
+  {
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+
+    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;
+  }
+
+  
+  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();
+    }
+
+    {
+      boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
+    
+      // 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;
+  }
+
+
+  void OrthancPlugins::RefreshMetrics()
+  {
+    boost::mutex::scoped_lock lock(pimpl_->refreshMetricsMutex_);
+
+    for (PImpl::RefreshMetricsCallbacks::iterator 
+           it = pimpl_->refreshMetricsCallbacks_.begin();
+         it != pimpl_->refreshMetricsCallbacks_.end(); ++it)
+    {
+      if (*it != NULL)
+      {
+        (*it) ();
+      }
+    }
+  }
+
+
+  class OrthancPlugins::HttpServerChunkedReader : public IHttpHandler::IChunkedRequestReader
+  {
+  private:
+    OrthancPluginServerChunkedRequestReader*  reader_;
+    _OrthancPluginChunkedRestCallback         parameters_;
+    PluginsErrorDictionary&                   errorDictionary_;
+
+  public:
+    HttpServerChunkedReader(OrthancPluginServerChunkedRequestReader* reader,
+                            const _OrthancPluginChunkedRestCallback& parameters,
+                            PluginsErrorDictionary& errorDictionary) :
+      reader_(reader),
+      parameters_(parameters),
+      errorDictionary_(errorDictionary)
+    {
+      assert(reader_ != NULL);
+    }
+
+    virtual ~HttpServerChunkedReader()
+    {
+      assert(reader_ != NULL);
+      parameters_.finalize(reader_);
+    }
+
+    virtual void AddBodyChunk(const void* data,
+                              size_t size)
+    {
+      if (static_cast<uint32_t>(size) != size)
+      {
+        throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
+      }
+
+      assert(reader_ != NULL);
+      parameters_.addChunk(reader_, data, size);
+    }    
+
+    virtual void Execute(HttpOutput& output)
+    {
+      assert(reader_ != NULL);
+
+      PImpl::PluginHttpOutput pluginOutput(output);
+
+      OrthancPluginErrorCode error = parameters_.execute(
+        reader_, reinterpret_cast<OrthancPluginRestOutput*>(&pluginOutput));
+
+      pluginOutput.Close(error, errorDictionary_);
+    }
+  };
+
+
+  bool OrthancPlugins::CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                                  RequestOrigin origin,
+                                                  const char* remoteIp,
+                                                  const char* username,
+                                                  HttpMethod method,
+                                                  const UriComponents& uri,
+                                                  const Arguments& headers)
+  {
+    if (method != HttpMethod_Post &&
+        method != HttpMethod_Put)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    RestCallbackMatcher matcher(uri);
+
+    PImpl::ChunkedRestCallback* callback = NULL;
+
+    // Loop over the callbacks registered by the plugins
+    for (PImpl::ChunkedRestCallbacks::const_iterator it = pimpl_->chunkedRestCallbacks_.begin(); 
+         it != pimpl_->chunkedRestCallbacks_.end(); ++it)
+    {
+      if (matcher.IsMatch((*it)->GetRegularExpression()))
+      {
+        callback = *it;
+        break;
+      }
+    }
+
+    if (callback == NULL)
+    {
+      // Callback not found
+      return false;
+    }
+    else
+    {
+      OrthancPluginServerChunkedRequestReaderFactory handler;
+
+      switch (method)
+      {
+        case HttpMethod_Post:
+          handler = callback->GetParameters().postHandler;
+          break;
+
+        case HttpMethod_Put:
+          handler = callback->GetParameters().putHandler;
+          break;
+
+        default:
+          handler = NULL;
+          break;
+      }
+
+      if (handler == NULL)
+      {
+        return false;
+      }
+      else
+      {
+        LOG(INFO) << "Delegating chunked HTTP request to plugin for URI: " << matcher.GetFlatUri();
+
+        HttpRequestConverter converter(matcher, method, headers);
+        converter.GetRequest().body = NULL;
+        converter.GetRequest().bodySize = 0;
+
+        OrthancPluginServerChunkedRequestReader* reader = NULL;
+    
+        OrthancPluginErrorCode errorCode = handler(
+          &reader, matcher.GetFlatUri().c_str(), &converter.GetRequest());
+    
+        if (reader == NULL)
+        {
+          // The plugin has not created a reader for chunked body
+          return false;
+        }
+        else if (errorCode != OrthancPluginErrorCode_Success)
+        {
+          throw OrthancException(static_cast<ErrorCode>(errorCode));
+        }
+        else
+        {
+          target.reset(new HttpServerChunkedReader(reader, callback->GetParameters(), GetErrorDictionary()));
+          return true;
+        }
+      }
+    }
+  }
+
+
+  IStorageCommitmentFactory::ILookupHandler* OrthancPlugins::CreateStorageCommitment(
+    const std::string& jobId,
+    const std::string& transactionUid,
+    const std::vector<std::string>& sopClassUids,
+    const std::vector<std::string>& sopInstanceUids,
+    const std::string& remoteAet,
+    const std::string& calledAet)
+  {
+    boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_);
+
+    for (PImpl::StorageCommitmentScpCallbacks::iterator
+           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
+         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
+    {
+      assert(*it != NULL);
+      IStorageCommitmentFactory::ILookupHandler* handler = (*it)->CreateStorageCommitment
+        (jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet);
+
+      if (handler != NULL)
+      {
+        return handler;
+      }
+    } 
+    
+    return NULL;
+  }
+
+
+  class MemoryBufferRaii : public boost::noncopyable
+  {
+  private:
+    OrthancPluginMemoryBuffer  buffer_;
+
+  public:
+    MemoryBufferRaii()
+    {
+      buffer_.size = 0;
+      buffer_.data = NULL;
+    }
+
+    ~MemoryBufferRaii()
+    {
+      if (buffer_.size != 0)
+      {
+        free(buffer_.data);
+      }
+    }
+
+    OrthancPluginMemoryBuffer* GetObject()
+    {
+      return &buffer_;
+    }
+
+    void ToString(std::string& target) const
+    {
+      target.resize(buffer_.size);
+
+      if (buffer_.size != 0)
+      {
+        memcpy(&target[0], buffer_.data, buffer_.size);
+      }
+    }
+  };
+  
+
+  bool OrthancPlugins::TranscodeBuffer(std::string& target,
+                                       const void* buffer,
+                                       size_t size,
+                                       const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                       bool allowNewSopInstanceUid)
+  {
+    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
+
+    if (pimpl_->transcoderCallbacks_.empty())
+    {
+      return false;
+    }
+
+    std::vector<const char*> uids;
+    uids.reserve(allowedSyntaxes.size());
+    for (std::set<DicomTransferSyntax>::const_iterator
+           it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
+    {
+      uids.push_back(GetTransferSyntaxUid(*it));
+    }
+    
+    for (PImpl::TranscoderCallbacks::const_iterator
+           transcoder = pimpl_->transcoderCallbacks_.begin();
+         transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder)
+    {
+      MemoryBufferRaii a;
+
+      if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0],
+                         static_cast<uint32_t>(uids.size()), allowNewSopInstanceUid) ==
+          OrthancPluginErrorCode_Success)
+      {
+        a.ToString(target);
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/OrthancPlugins.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,391 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PluginsErrorDictionary.h"
+
+#if !defined(ORTHANC_ENABLE_PLUGINS)
+#  error The macro ORTHANC_ENABLE_PLUGINS must be defined
+#endif
+
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class OrthancPlugins : public boost::noncopyable
+  {
+  };
+}
+
+#else
+
+#include "../../Core/DicomNetworking/IFindRequestHandlerFactory.h"
+#include "../../Core/DicomNetworking/IMoveRequestHandlerFactory.h"
+#include "../../Core/DicomNetworking/IWorklistRequestHandlerFactory.h"
+#include "../../Core/DicomParsing/MemoryBufferTranscoder.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 "../../OrthancServer/ServerJobs/IStorageCommitmentFactory.h"
+#include "OrthancPluginDatabase.h"
+#include "PluginsManager.h"
+
+#include <list>
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  class OrthancPlugins : 
+    public IHttpHandler, 
+    public IPluginServiceProvider, 
+    public IServerListener,
+    public IWorklistRequestHandlerFactory,
+    public IDicomImageDecoder,
+    public IIncomingHttpRequestFilter,
+    public IFindRequestHandlerFactory,
+    public IMoveRequestHandlerFactory,
+    public IStorageCommitmentFactory,
+    public MemoryBufferTranscoder
+  {
+  private:
+    class PImpl;
+    boost::shared_ptr<PImpl> pimpl_;
+
+    class WorklistHandler;
+    class FindHandler;
+    class MoveHandler;
+    class HttpClientChunkedRequest;
+    class HttpClientChunkedAnswer;
+    class HttpServerChunkedReader;
+    class IDicomInstance;
+    class DicomInstanceFromCallback;
+    class DicomInstanceFromBuffer;
+    class DicomInstanceFromTranscoded;
+    
+    void RegisterRestCallback(const void* parameters,
+                              bool lock);
+
+    void RegisterChunkedRestCallback(const void* parameters);
+
+    bool HandleChunkedGetDelete(HttpOutput& output,
+                                HttpMethod method,
+                                const UriComponents& uri,
+                                const Arguments& headers,
+                                const GetArguments& getArguments);
+
+    void RegisterOnStoredInstanceCallback(const void* parameters);
+
+    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 RegisterTranscoderCallback(const void* parameters);
+
+    void RegisterJobsUnserializer(const void* parameters);
+
+    void RegisterIncomingHttpRequestFilter(const void* parameters);
+
+    void RegisterIncomingHttpRequestFilter2(const void* parameters);
+
+    void RegisterIncomingDicomInstanceFilter(const void* parameters);
+
+    void RegisterRefreshMetricsCallback(const void* parameters);
+
+    void RegisterStorageCommitmentScpCallback(const void* parameters);
+
+    void AnswerBuffer(const void* parameters);
+
+    void Redirect(const void* parameters);
+
+    void CompressAndAnswerPngImage(const void* parameters);
+
+    void CompressAndAnswerImage(const void* parameters);
+
+    void GetDicomForInstance(const void* parameters);
+
+    void RestApiGet(const void* parameters,
+                    bool afterPlugins);
+
+    void RestApiGet2(const void* parameters);
+
+    void RestApiPostPut(bool isPost, 
+                        const void* parameters,
+                        bool afterPlugins);
+
+    void RestApiDelete(const void* parameters,
+                       bool afterPlugins);
+
+    void LookupResource(_OrthancPluginService service,
+                        const void* parameters);
+
+    void AccessDicomInstance(_OrthancPluginService service,
+                             const void* parameters);
+    
+    void AccessDicomInstance2(_OrthancPluginService service,
+                              const void* parameters);
+    
+    void SendHttpStatusCode(const void* parameters);
+
+    void SendHttpStatus(const void* parameters);
+
+    void SendUnauthorized(const void* parameters);
+
+    void SendMethodNotAllowed(const void* parameters);
+
+    void SetCookie(const void* parameters);
+
+    void SetHttpHeader(const void* parameters);
+
+    void SetHttpErrorDetails(const void* parameters);
+
+    void BufferCompression(const void* parameters);
+
+    void UncompressImage(const void* parameters);
+
+    void CompressImage(const void* parameters);
+
+    void ConvertPixelFormat(const void* parameters);
+
+    void CallHttpClient(const void* parameters);
+
+    void CallHttpClient2(const void* parameters);
+
+    void ChunkedHttpClient(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 GetTagName(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);
+
+  protected:
+    // From "MemoryBufferTranscoder"
+    virtual bool TranscodeBuffer(std::string& target,
+                                 const void* buffer,
+                                 size_t size,
+                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                 bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+    
+  public:
+    OrthancPlugins();
+
+    virtual ~OrthancPlugins();
+
+    void SetServerContext(ServerContext& context);
+
+    void ResetServerContext();
+
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const void* bodyData,
+                        size_t bodySize) ORTHANC_OVERRIDE;
+
+    virtual bool InvokeService(SharedLibrary& plugin,
+                               _OrthancPluginService service,
+                               const void* parameters) ORTHANC_OVERRIDE;
+
+    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE;
+    
+    virtual void SignalStoredInstance(const std::string& instanceId,
+                                      const DicomInstanceToStore& instance,
+                                      const Json::Value& simplifiedTags) ORTHANC_OVERRIDE;
+
+    virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                        const Json::Value& simplified) ORTHANC_OVERRIDE;
+
+    bool HasStorageArea() const;
+
+    IStorageArea* CreateStorageArea();  // To be freed after use
+
+    const SharedLibrary& GetStorageAreaLibrary() const;
+
+    bool HasDatabaseBackend() const;
+
+    IDatabaseWrapper& GetDatabaseBackend();
+
+    const SharedLibrary& GetDatabaseBackendLibrary() const;
+
+    const char* GetProperty(const char* plugin,
+                            _OrthancPluginProperty property) const;
+
+    void SetCommandLineArguments(int argc, char* argv[]);
+
+    PluginsManager& GetManager();
+
+    const PluginsManager& GetManager() const;
+
+    PluginsErrorDictionary& GetErrorDictionary();
+
+    void SignalOrthancStarted()
+    {
+      SignalChangeInternal(OrthancPluginChangeType_OrthancStarted, OrthancPluginResourceType_None, NULL);
+    }
+
+    void SignalOrthancStopped()
+    {
+      SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL);
+    }
+
+    void SignalJobSubmitted(const std::string& jobId)
+    {
+      SignalChangeInternal(OrthancPluginChangeType_JobSubmitted, OrthancPluginResourceType_None, jobId.c_str());
+    }
+
+    void SignalJobSuccess(const std::string& jobId)
+    {
+      SignalChangeInternal(OrthancPluginChangeType_JobSuccess, OrthancPluginResourceType_None, jobId.c_str());
+    }
+
+    void SignalJobFailure(const std::string& jobId)
+    {
+      SignalChangeInternal(OrthancPluginChangeType_JobFailure, OrthancPluginResourceType_None, jobId.c_str());
+    }
+
+    void SignalUpdatedPeers()
+    {
+      SignalChangeInternal(OrthancPluginChangeType_UpdatedPeers, OrthancPluginResourceType_None, NULL);
+    }
+
+    void SignalUpdatedModalities()
+    {
+      SignalChangeInternal(OrthancPluginChangeType_UpdatedModalities, OrthancPluginResourceType_None, NULL);
+    }
+
+    bool HasWorklistHandler();
+
+    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() ORTHANC_OVERRIDE;
+
+    bool HasCustomImageDecoder();
+
+    bool HasCustomTranscoder();
+
+    virtual ImageAccessor* Decode(const void* dicom,
+                                  size_t size,
+                                  unsigned int frame) ORTHANC_OVERRIDE;
+
+    virtual bool IsAllowed(HttpMethod method,
+                           const char* uri,
+                           const char* ip,
+                           const char* username,
+                           const IHttpHandler::Arguments& httpHeaders,
+                           const IHttpHandler::GetArguments& getArguments) ORTHANC_OVERRIDE;
+
+    bool HasFindHandler();
+
+    virtual IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE;
+
+    bool HasMoveHandler();
+
+    virtual IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE;
+
+    IJob* UnserializeJob(const std::string& type,
+                         const Json::Value& value);
+
+    void RefreshMetrics();
+
+    // New in Orthanc 1.5.7
+    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                            RequestOrigin origin,
+                                            const char* remoteIp,
+                                            const char* username,
+                                            HttpMethod method,
+                                            const UriComponents& uri,
+                                            const Arguments& headers) ORTHANC_OVERRIDE;
+
+    // New in Orthanc 1.6.0
+    IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment(
+      const std::string& jobId,
+      const std::string& transactionUid,
+      const std::vector<std::string>& sopClassUids,
+      const std::vector<std::string>& sopInstanceUids,
+      const std::string& remoteAet,
+      const std::string& calledAet) ORTHANC_OVERRIDE;
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,584 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PluginsEnumerations.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#error The plugin support is disabled
+#endif
+
+
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case Compatibility::IdentifierConstraintType_Equal:
+          return OrthancPluginIdentifierConstraint_Equal;
+
+        case Compatibility::IdentifierConstraintType_GreaterOrEqual:
+          return OrthancPluginIdentifierConstraint_GreaterOrEqual;
+
+        case Compatibility::IdentifierConstraintType_SmallerOrEqual:
+          return OrthancPluginIdentifierConstraint_SmallerOrEqual;
+
+        case Compatibility::IdentifierConstraintType_Wildcard:
+          return OrthancPluginIdentifierConstraint_Wildcard;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint)
+    {
+      switch (constraint)
+      {
+        case OrthancPluginIdentifierConstraint_Equal:
+          return Compatibility::IdentifierConstraintType_Equal;
+
+        case OrthancPluginIdentifierConstraint_GreaterOrEqual:
+          return Compatibility::IdentifierConstraintType_GreaterOrEqual;
+
+        case OrthancPluginIdentifierConstraint_SmallerOrEqual:
+          return Compatibility::IdentifierConstraintType_SmallerOrEqual;
+
+        case OrthancPluginIdentifierConstraint_Wildcard:
+          return Compatibility::IdentifierConstraintType_Wildcard;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+
+
+  namespace Plugins
+  {
+    OrthancPluginChangeType Convert(ChangeType type)
+    {
+      switch (type)
+      {
+        case ChangeType_CompletedSeries:
+          return OrthancPluginChangeType_CompletedSeries;
+
+        case ChangeType_Deleted:
+          return OrthancPluginChangeType_Deleted;
+
+        case ChangeType_NewChildInstance:
+          return OrthancPluginChangeType_NewChildInstance;
+
+        case ChangeType_NewInstance:
+          return OrthancPluginChangeType_NewInstance;
+
+        case ChangeType_NewPatient:
+          return OrthancPluginChangeType_NewPatient;
+
+        case ChangeType_NewSeries:
+          return OrthancPluginChangeType_NewSeries;
+
+        case ChangeType_NewStudy:
+          return OrthancPluginChangeType_NewStudy;
+
+        case ChangeType_StablePatient:
+          return OrthancPluginChangeType_StablePatient;
+
+        case ChangeType_StableSeries:
+          return OrthancPluginChangeType_StableSeries;
+
+        case ChangeType_StableStudy:
+          return OrthancPluginChangeType_StableStudy;
+
+        case ChangeType_UpdatedAttachment:
+          return OrthancPluginChangeType_UpdatedAttachment;
+
+        case ChangeType_UpdatedMetadata:
+          return OrthancPluginChangeType_UpdatedMetadata;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    OrthancPluginPixelFormat Convert(PixelFormat format)
+    {
+      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;
+
+        case PixelFormat_SignedGrayscale16:
+          return OrthancPluginPixelFormat_SignedGrayscale16;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    PixelFormat Convert(OrthancPluginPixelFormat format)
+    {
+      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;
+
+        case OrthancPluginPixelFormat_SignedGrayscale16:
+          return PixelFormat_SignedGrayscale16;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    OrthancPluginContentType Convert(FileContentType type)
+    {
+      switch (type)
+      {
+        case FileContentType_Dicom:
+          return OrthancPluginContentType_Dicom;
+
+        case FileContentType_DicomAsJson:
+          return OrthancPluginContentType_DicomAsJson;
+
+        default:
+          return OrthancPluginContentType_Unknown;
+      }
+    }
+
+
+    FileContentType Convert(OrthancPluginContentType type)
+    {
+      switch (type)
+      {
+        case OrthancPluginContentType_Dicom:
+          return FileContentType_Dicom;
+
+        case OrthancPluginContentType_DicomAsJson:
+          return FileContentType_DicomAsJson;
+
+        default:
+          return FileContentType_Unknown;
+      }
+    }
+
+
+    DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format)
+    {
+      switch (format)
+      {
+        case OrthancPluginDicomToJsonFormat_Full:
+          return DicomToJsonFormat_Full;
+
+        case OrthancPluginDicomToJsonFormat_Short:
+          return DicomToJsonFormat_Short;
+
+        case OrthancPluginDicomToJsonFormat_Human:
+          return DicomToJsonFormat_Human;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    OrthancPluginInstanceOrigin Convert(RequestOrigin origin)
+    {
+      switch (origin)
+      {
+        case RequestOrigin_DicomProtocol:
+          return OrthancPluginInstanceOrigin_DicomProtocol;
+
+        case RequestOrigin_RestApi:
+          return OrthancPluginInstanceOrigin_RestApi;
+
+        case RequestOrigin_Lua:
+          return OrthancPluginInstanceOrigin_Lua;
+
+        case RequestOrigin_Plugins:
+          return OrthancPluginInstanceOrigin_Plugin;
+
+        case RequestOrigin_Unknown:
+          return OrthancPluginInstanceOrigin_Unknown;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    OrthancPluginHttpMethod Convert(HttpMethod method)
+    {
+      switch (method)
+      {
+        case HttpMethod_Get:
+          return OrthancPluginHttpMethod_Get;
+
+        case HttpMethod_Post:
+          return OrthancPluginHttpMethod_Post;
+
+        case HttpMethod_Put:
+          return OrthancPluginHttpMethod_Put;
+
+        case HttpMethod_Delete:
+          return OrthancPluginHttpMethod_Delete;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    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);
+      }
+    }
+
+
+    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason)
+    {
+      switch (reason)
+      {
+        case OrthancPluginStorageCommitmentFailureReason_Success:
+          return StorageCommitmentFailureReason_Success;
+          
+        case OrthancPluginStorageCommitmentFailureReason_ProcessingFailure:
+          return StorageCommitmentFailureReason_ProcessingFailure;
+
+        case OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance:
+          return StorageCommitmentFailureReason_NoSuchObjectInstance;
+
+        case OrthancPluginStorageCommitmentFailureReason_ResourceLimitation:
+          return StorageCommitmentFailureReason_ResourceLimitation;
+
+        case OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
+          return StorageCommitmentFailureReason_ReferencedSOPClassNotSupported;
+
+        case OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict:
+          return StorageCommitmentFailureReason_ClassInstanceConflict;
+
+        case OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID:
+          return StorageCommitmentFailureReason_DuplicateTransactionUID;
+             
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsEnumerations.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+
+/**
+ * NB: Conversions to/from "OrthancPluginConstraintType" and
+ * "OrthancPluginResourceType" are located in file
+ * "../../OrthancServer/Search/DatabaseConstraint.h" to be shared with
+ * the "orthanc-databases" project.
+ **/
+
+#include "../Include/orthanc/OrthancCPlugin.h"
+#include "../../OrthancServer/Search/DatabaseConstraint.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint);
+
+    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint);
+  }
+
+  namespace Plugins
+  {
+    OrthancPluginChangeType Convert(ChangeType type);
+
+    OrthancPluginPixelFormat Convert(PixelFormat format);
+
+    PixelFormat Convert(OrthancPluginPixelFormat format);
+
+    OrthancPluginContentType Convert(FileContentType type);
+
+    FileContentType Convert(OrthancPluginContentType type);
+
+    DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format);
+
+    OrthancPluginInstanceOrigin Convert(RequestOrigin origin);
+
+    OrthancPluginHttpMethod Convert(HttpMethod method);
+
+    ValueRepresentation Convert(OrthancPluginValueRepresentation vr);
+
+    OrthancPluginValueRepresentation Convert(ValueRepresentation vr);
+
+    OrthancPluginJobStepStatus Convert(JobStepCode step);
+
+    JobStepCode Convert(OrthancPluginJobStepStatus step);
+
+    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
+  }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsErrorDictionary.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PluginsErrorDictionary.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#error The plugin support is disabled
+#endif
+
+
+
+#include "PluginsEnumerations.h"
+#include "PluginsManager.h"
+#include "../../Core/Logging.h"
+
+#include <memory>
+
+
+namespace Orthanc
+{
+  PluginsErrorDictionary::PluginsErrorDictionary() : 
+    pos_(ErrorCode_START_PLUGINS)
+  {
+  }
+
+
+  PluginsErrorDictionary::~PluginsErrorDictionary()
+  {
+    for (Errors::iterator it = errors_.begin(); it != errors_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  OrthancPluginErrorCode PluginsErrorDictionary::Register(SharedLibrary& library,
+                                                          int32_t  pluginCode,
+                                                          uint16_t httpStatus,
+                                                          const char* message)
+  {
+    std::unique_ptr<Error> error(new Error);
+
+    error->pluginName_ = PluginsManager::GetPluginName(library);
+    error->pluginCode_ = pluginCode;
+    error->message_ = message;
+    error->httpStatus_ = static_cast<HttpStatus>(httpStatus);
+
+    OrthancPluginErrorCode code;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      errors_[pos_] = error.release();
+      code = static_cast<OrthancPluginErrorCode>(pos_);
+      pos_ += 1;
+    }
+
+    return code;
+  }
+
+
+  void  PluginsErrorDictionary::LogError(ErrorCode code,
+                                         bool ignoreBuiltinErrors)
+  {
+    if (code >= ErrorCode_START_PLUGINS)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      Errors::const_iterator error = errors_.find(static_cast<int32_t>(code));
+      
+      if (error != errors_.end())
+      {
+        LOG(ERROR) << "Error code " << error->second->pluginCode_
+                   << " inside plugin \"" << error->second->pluginName_
+                   << "\": " << error->second->message_;
+        return;
+      }
+    }
+
+    if (!ignoreBuiltinErrors)
+    {
+      LOG(ERROR) << "Exception inside the plugin engine: "
+                 << EnumerationToString(code);
+    }
+  }
+
+
+  bool  PluginsErrorDictionary::Format(Json::Value& message,    /* out */
+                                       HttpStatus& httpStatus,  /* out */
+                                       const OrthancException& exception)
+  {
+    if (exception.GetErrorCode() >= ErrorCode_START_PLUGINS)
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      Errors::const_iterator error = errors_.find(static_cast<int32_t>(exception.GetErrorCode()));
+      
+      if (error != errors_.end())
+      {
+        httpStatus = error->second->httpStatus_;
+        message["PluginName"] = error->second->pluginName_;
+        message["PluginCode"] = error->second->pluginCode_;
+        message["Message"] = error->second->message_;
+
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsErrorDictionary.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../Include/orthanc/OrthancCPlugin.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SharedLibrary.h"
+
+#include <map>
+#include <string>
+#include <boost/noncopyable.hpp>
+#include <boost/thread/mutex.hpp>
+#include <json/value.h>
+
+
+namespace Orthanc
+{
+  class PluginsErrorDictionary : public boost::noncopyable
+  {
+  private:
+    struct Error
+    {
+      std::string  pluginName_;
+      int32_t      pluginCode_;
+      HttpStatus   httpStatus_;
+      std::string  message_;
+    };
+    
+    typedef std::map<int32_t, Error*>  Errors;
+
+    boost::mutex  mutex_;
+    int32_t  pos_;
+    Errors   errors_;
+
+  public:
+    PluginsErrorDictionary();
+
+    ~PluginsErrorDictionary();
+
+    OrthancPluginErrorCode  Register(SharedLibrary& library,
+                                     int32_t  pluginCode,
+                                     uint16_t httpStatus,
+                                     const char* message);
+
+    void  LogError(ErrorCode code,
+                   bool ignoreBuiltinErrors);
+
+    void  LogError(OrthancPluginErrorCode code,
+                   bool ignoreBuiltinErrors)
+    {
+      LogError(static_cast<ErrorCode>(code), ignoreBuiltinErrors);
+    }
+
+    bool  Format(Json::Value& message,    /* out */
+                 HttpStatus& httpStatus,  /* out */
+                 const OrthancException& exception);
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsJob.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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(const std::string& jobId)
+  {
+    OrthancPluginJobStepStatus status = parameters_.step(parameters_.job);
+
+    switch (status)
+    {
+      case OrthancPluginJobStepStatus_Success:
+        return JobStepResult::Success();
+
+      case OrthancPluginJobStepStatus_Failure:
+        return JobStepResult::Failure(ErrorCode_Plugin, NULL);
+
+      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)
+      {
+        throw OrthancException(ErrorCode_Plugin,
+                               "A job plugin must provide a JSON object as its public content");
+      }
+    }
+  }
+
+  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)
+      {
+        throw OrthancException(ErrorCode_Plugin,
+                               "A job plugin must provide a JSON object as its serialized content");
+      }
+
+
+      static const char* KEY_TYPE = "Type";
+      
+      if (value.isMember(KEY_TYPE))
+      {
+        throw OrthancException(ErrorCode_Plugin,
+                               "The \"Type\" field is for reserved use for serialized job");
+      }
+
+      value[KEY_TYPE] = type_;
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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() ORTHANC_OVERRIDE
+    {
+    }
+    
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
+
+    virtual float GetProgress() ORTHANC_OVERRIDE;
+
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    {
+      target = type_;
+    }
+    
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      // TODO
+      return false;
+    }
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,360 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PluginsManager.h"
+
+#if ORTHANC_ENABLE_PLUGINS != 1
+#error The plugin support is disabled
+#endif
+
+#include "../../Core/HttpServer/HttpOutput.h"
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+
+#include <cassert>
+#include <memory>
+#include <boost/filesystem.hpp>
+
+#ifdef WIN32
+#define PLUGIN_EXTENSION ".dll"
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+#define PLUGIN_EXTENSION ".so"
+#elif defined(__APPLE__) && defined(__MACH__)
+#define PLUGIN_EXTENSION ".dylib"
+#else
+#error Support your platform here
+#endif
+
+
+namespace Orthanc
+{
+  PluginsManager::Plugin::Plugin(PluginsManager& pluginManager,
+                                 const std::string& path) : 
+    library_(path),
+    pluginManager_(pluginManager)
+  {
+    memset(&context_, 0, sizeof(context_));
+    context_.pluginsManager = this;
+    context_.orthancVersion = ORTHANC_VERSION;
+    context_.Free = ::free;
+    context_.InvokeService = InvokeService;
+  }
+
+
+  static void CallInitialize(SharedLibrary& plugin,
+                             const OrthancPluginContext& context)
+  {
+    typedef int32_t (*Initialize) (const OrthancPluginContext*);
+
+#if defined(_WIN32)
+    Initialize initialize = (Initialize) plugin.GetFunction("OrthancPluginInitialize");
+#else
+    /**
+     * gcc would complain about "ISO C++ forbids casting between
+     * pointer-to-function and pointer-to-object" without the trick
+     * below, that is known as "the POSIX.1-2003 (Technical Corrigendum
+     * 1) workaround". See the man page of "dlsym()".
+     * http://www.trilithium.com/johan/2004/12/problem-with-dlsym/
+     * http://stackoverflow.com/a/14543811/881731
+     **/
+
+    Initialize initialize;
+    *(void **) (&initialize) = plugin.GetFunction("OrthancPluginInitialize");
+#endif
+
+    assert(initialize != NULL);
+    int32_t error = initialize(&context);
+
+    if (error != 0)
+    {
+      LOG(ERROR) << "Error while initializing plugin " << plugin.GetPath()
+                 << " (code " << error << ")";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+  }
+
+
+  static void CallFinalize(SharedLibrary& plugin)
+  {
+    typedef void (*Finalize) ();
+
+#if defined(_WIN32)
+    Finalize finalize = (Finalize) plugin.GetFunction("OrthancPluginFinalize");
+#else
+    Finalize finalize;
+    *(void **) (&finalize) = plugin.GetFunction("OrthancPluginFinalize");
+#endif
+
+    assert(finalize != NULL);
+    finalize();
+  }
+
+
+  static const char* CallGetName(SharedLibrary& plugin)
+  {
+    typedef const char* (*GetName) ();
+
+#if defined(_WIN32)
+    GetName getName = (GetName) plugin.GetFunction("OrthancPluginGetName");
+#else
+    GetName getName;
+    *(void **) (&getName) = plugin.GetFunction("OrthancPluginGetName");
+#endif
+
+    assert(getName != NULL);
+    return getName();
+  }
+
+
+  static const char* CallGetVersion(SharedLibrary& plugin)
+  {
+    typedef const char* (*GetVersion) ();
+
+#if defined(_WIN32)
+    GetVersion getVersion = (GetVersion) plugin.GetFunction("OrthancPluginGetVersion");
+#else
+    GetVersion getVersion;
+    *(void **) (&getVersion) = plugin.GetFunction("OrthancPluginGetVersion");
+#endif
+
+    assert(getVersion != NULL);
+    return getVersion();
+  }
+
+
+  OrthancPluginErrorCode PluginsManager::InvokeService(OrthancPluginContext* context,
+                                                       _OrthancPluginService service, 
+                                                       const void* params)
+  {
+    switch (service)
+    {
+      case _OrthancPluginService_LogError:
+        LOG(ERROR) << reinterpret_cast<const char*>(params);
+        return OrthancPluginErrorCode_Success;
+
+      case _OrthancPluginService_LogWarning:
+        LOG(WARNING) << reinterpret_cast<const char*>(params);
+        return OrthancPluginErrorCode_Success;
+
+      case _OrthancPluginService_LogInfo:
+        LOG(INFO) << reinterpret_cast<const char*>(params);
+        return OrthancPluginErrorCode_Success;
+
+      default:
+        break;
+    }
+
+    Plugin* that = reinterpret_cast<Plugin*>(context->pluginsManager);
+
+    for (std::list<IPluginServiceProvider*>::iterator
+           it = that->GetPluginManager().serviceProviders_.begin(); 
+         it != that->GetPluginManager().serviceProviders_.end(); ++it)
+    {
+      try
+      {
+        if ((*it)->InvokeService(that->GetSharedLibrary(), service, params))
+        {
+          return OrthancPluginErrorCode_Success;
+        }
+      }
+      catch (OrthancException& e)
+      {
+        // This service provider has failed
+        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());
+      }
+    }
+
+    LOG(ERROR) << "Plugin invoking unknown service: " << service;
+    return OrthancPluginErrorCode_UnknownPluginService;
+  }
+
+
+  PluginsManager::PluginsManager()
+  {
+  }
+
+  PluginsManager::~PluginsManager()
+  {
+    for (Plugins::iterator it = plugins_.begin(); it != plugins_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        LOG(WARNING) << "Unregistering plugin '" << it->first
+                     << "' (version " << it->second->GetVersion() << ")";
+
+        CallFinalize(it->second->GetSharedLibrary());
+        delete it->second;
+      }
+    }
+  }
+
+
+  static bool IsOrthancPlugin(SharedLibrary& library)
+  {
+    return (library.HasFunction("OrthancPluginInitialize") &&
+            library.HasFunction("OrthancPluginFinalize") &&
+            library.HasFunction("OrthancPluginGetName") &&
+            library.HasFunction("OrthancPluginGetVersion"));
+  }
+
+  
+  void PluginsManager::RegisterPlugin(const std::string& path)
+  {
+    if (!boost::filesystem::exists(path))
+    {
+      LOG(ERROR) << "Inexistent path to plugins: " << path;
+      return;
+    }
+
+    if (boost::filesystem::is_directory(path))
+    {
+      ScanFolderForPlugins(path, false);
+      return;
+    }
+
+    std::unique_ptr<Plugin> plugin(new Plugin(*this, path));
+
+    if (!IsOrthancPlugin(plugin->GetSharedLibrary()))
+    {
+      LOG(ERROR) << "Plugin " << plugin->GetSharedLibrary().GetPath()
+                 << " does not declare the proper entry functions";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+    std::string name(CallGetName(plugin->GetSharedLibrary()));
+    if (plugins_.find(name) != plugins_.end())
+    {
+      LOG(ERROR) << "Plugin '" << name << "' already registered";
+      throw OrthancException(ErrorCode_SharedLibrary);
+    }
+
+    plugin->SetVersion(CallGetVersion(plugin->GetSharedLibrary()));
+    LOG(WARNING) << "Registering plugin '" << name
+                 << "' (version " << plugin->GetVersion() << ")";
+
+    CallInitialize(plugin->GetSharedLibrary(), plugin->GetContext());
+
+    plugins_[name] = plugin.release();
+  }
+
+
+  void PluginsManager::ScanFolderForPlugins(const std::string& folder,
+                                            bool isRecursive)
+  {
+    using namespace boost::filesystem;
+
+    if (!exists(folder))
+    {
+      return;
+    }
+
+    LOG(INFO) << "Scanning folder " << folder << " for plugins";
+
+    directory_iterator end_it; // default construction yields past-the-end
+    for (directory_iterator it(folder);
+          it != end_it;
+          ++it)
+    {
+      std::string path = it->path().string();
+
+      if (is_directory(it->status()))
+      {
+        if (isRecursive)
+        {
+          ScanFolderForPlugins(path, true);
+        }
+      }
+      else
+      {
+        std::string extension = boost::filesystem::extension(it->path());
+        Toolbox::ToLowerCase(extension);
+
+        if (extension == PLUGIN_EXTENSION)
+        {
+          LOG(INFO) << "Found a shared library: " << it->path();
+
+          SharedLibrary plugin(path);
+          if (IsOrthancPlugin(plugin))
+          {
+            RegisterPlugin(path);
+          }
+        }
+      }
+    }
+  }
+
+
+  void PluginsManager::ListPlugins(std::list<std::string>& result) const
+  {
+    result.clear();
+
+    for (Plugins::const_iterator it = plugins_.begin(); 
+         it != plugins_.end(); ++it)
+    {
+      result.push_back(it->first);
+    }
+  }
+
+
+  bool PluginsManager::HasPlugin(const std::string& name) const
+  {
+    return plugins_.find(name) != plugins_.end();
+  }
+
+
+  const std::string& PluginsManager::GetPluginVersion(const std::string& name) const
+  {
+    Plugins::const_iterator it = plugins_.find(name);
+    if (it == plugins_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return it->second->GetVersion();
+    }
+  }
+
+  
+  std::string PluginsManager::GetPluginName(SharedLibrary& library)
+  {
+    return CallGetName(library);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Engine/PluginsManager.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IPluginServiceProvider.h"
+
+#include <map>
+#include <list>
+
+namespace Orthanc
+{
+  class PluginsManager : public boost::noncopyable
+  {
+  private:
+    class Plugin : public boost::noncopyable
+    {
+    private:
+      OrthancPluginContext  context_;
+      SharedLibrary         library_;
+      std::string           version_;
+      PluginsManager&       pluginManager_;
+
+    public:
+      Plugin(PluginsManager& pluginManager,
+             const std::string& path);
+
+      SharedLibrary& GetSharedLibrary()
+      {
+        return library_;
+      }
+
+      void SetVersion(const std::string& version)
+      {
+        version_ = version;
+      }
+
+      const std::string& GetVersion() const
+      {
+        return version_;
+      }
+
+      PluginsManager& GetPluginManager()
+      {
+        return pluginManager_;
+      }
+
+      OrthancPluginContext& GetContext()
+      {
+        return context_;
+      }
+    };
+
+    typedef std::map<std::string, Plugin*>  Plugins;
+
+    Plugins  plugins_;
+    std::list<IPluginServiceProvider*> serviceProviders_;
+
+    static OrthancPluginErrorCode InvokeService(OrthancPluginContext* context,
+                                                _OrthancPluginService service,
+                                                const void* parameters);
+
+  public:
+    PluginsManager();
+
+    ~PluginsManager();
+
+    void RegisterPlugin(const std::string& path);
+
+    void ScanFolderForPlugins(const std::string& path,
+                              bool isRecursive);
+
+    void RegisterServiceProvider(IPluginServiceProvider& provider)
+    {
+      serviceProviders_.push_back(&provider);
+    }
+
+    void ListPlugins(std::list<std::string>& result) const;
+
+    bool HasPlugin(const std::string& name) const;
+
+    const std::string& GetPluginVersion(const std::string& name) const;
+
+    static std::string GetPluginName(SharedLibrary& library);
+  };
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,987 @@
+/**
+ * @ingroup CInterface
+ **/
+
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancCPlugin.h"
+
+
+/** @{ */
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+
+  /**
+   * Opaque structure that represents the context of a custom database engine.
+   * @ingroup Callbacks
+   **/
+  typedef struct _OrthancPluginDatabaseContext_t OrthancPluginDatabaseContext;
+
+
+/*<! @cond Doxygen_Suppress */
+  typedef enum
+  {
+    _OrthancPluginDatabaseAnswerType_None = 0,
+
+    /* Events */
+    _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1,
+    _OrthancPluginDatabaseAnswerType_DeletedResource = 2,
+    _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3,
+
+    /* Return value */
+    _OrthancPluginDatabaseAnswerType_Attachment = 10,
+    _OrthancPluginDatabaseAnswerType_Change = 11,
+    _OrthancPluginDatabaseAnswerType_DicomTag = 12,
+    _OrthancPluginDatabaseAnswerType_ExportedResource = 13,
+    _OrthancPluginDatabaseAnswerType_Int32 = 14,
+    _OrthancPluginDatabaseAnswerType_Int64 = 15,
+    _OrthancPluginDatabaseAnswerType_Resource = 16,
+    _OrthancPluginDatabaseAnswerType_String = 17,
+    _OrthancPluginDatabaseAnswerType_MatchingResource = 18,  /* New in Orthanc 1.5.2 */
+    _OrthancPluginDatabaseAnswerType_Metadata = 19,          /* New in Orthanc 1.5.4 */
+
+    _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff
+  } _OrthancPluginDatabaseAnswerType;
+
+
+  typedef struct
+  {
+    const char* uuid;
+    int32_t     contentType;
+    uint64_t    uncompressedSize;
+    const char* uncompressedHash;
+    int32_t     compressionType;
+    uint64_t    compressedSize;
+    const char* compressedHash;
+  } OrthancPluginAttachment;
+
+  typedef struct
+  {
+    uint16_t     group;
+    uint16_t     element;
+    const char*  value;
+  } OrthancPluginDicomTag;
+
+  typedef struct
+  {
+    int64_t                    seq;
+    int32_t                    changeType;
+    OrthancPluginResourceType  resourceType;
+    const char*                publicId;
+    const char*                date;
+  } OrthancPluginChange;
+
+  typedef struct
+  {
+    int64_t                    seq;
+    OrthancPluginResourceType  resourceType;
+    const char*                publicId;
+    const char*                modality;
+    const char*                date;
+    const char*                patientId;
+    const char*                studyInstanceUid;
+    const char*                seriesInstanceUid;
+    const char*                sopInstanceUid;
+  } OrthancPluginExportedResource;
+
+  typedef struct   /* New in Orthanc 1.5.2 */
+  {
+    OrthancPluginResourceType    level;
+    uint16_t                     tagGroup;
+    uint16_t                     tagElement;
+    uint8_t                      isIdentifierTag;
+    uint8_t                      isCaseSensitive;
+    uint8_t                      isMandatory;
+    OrthancPluginConstraintType  type;
+    uint32_t                     valuesCount;
+    const char* const*           values;
+  } OrthancPluginDatabaseConstraint;
+
+  typedef struct   /* New in Orthanc 1.5.2 */
+  {
+    const char*  resourceId;
+    const char*  someInstanceId;  /* Can be NULL if not requested */
+  } OrthancPluginMatchingResource;
+
+  typedef struct   /* New in Orthanc 1.5.2 */
+  {
+    /* Mandatory field */
+    uint8_t  isNewInstance;
+    int64_t  instanceId;
+
+    /* The following fields must only be set if "isNewInstance" is "true" */
+    uint8_t  isNewPatient;
+    uint8_t  isNewStudy;
+    uint8_t  isNewSeries;
+    int64_t  patientId;
+    int64_t  studyId;
+    int64_t  seriesId;
+  } OrthancPluginCreateInstanceResult;
+
+  typedef struct  /* New in Orthanc 1.5.2 */
+  {
+    int64_t      resource;
+    uint16_t     group;
+    uint16_t     element;
+    const char*  value;
+  } OrthancPluginResourcesContentTags;
+    
+  typedef struct  /* New in Orthanc 1.5.2 */
+  {
+    int64_t      resource;
+    int32_t      metadata;
+    const char*  value;
+  } OrthancPluginResourcesContentMetadata;
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseContext* database;
+    _OrthancPluginDatabaseAnswerType  type;
+    int32_t      valueInt32;
+    uint32_t     valueUint32;
+    int64_t      valueInt64;
+    const char  *valueString;
+    const void  *valueGeneric;
+  } _OrthancPluginDatabaseAnswer;
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerString(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const char*                    value)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_String;
+    params.valueString = value;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChange(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginChange*     change)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Change;
+    params.valueUint32 = 0;
+    params.valueGeneric = change;
+
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChangesDone(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Change;
+    params.valueUint32 = 1;
+    params.valueGeneric = NULL;
+
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt32(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int32_t                        value)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Int32;
+    params.valueInt32 = value;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt64(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int64_t                        value)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Int64;
+    params.valueInt64 = value;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResource(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database,
+    const OrthancPluginExportedResource*  exported)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_ExportedResource;
+    params.valueUint32 = 0;
+    params.valueGeneric = exported;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResourcesDone(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_ExportedResource;
+    params.valueUint32 = 1;
+    params.valueGeneric = NULL;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerDicomTag(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginDicomTag*   tag)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DicomTag;
+    params.valueGeneric = tag;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Attachment;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int64_t                        id,
+    OrthancPluginResourceType      resourceType)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Resource;
+    params.valueInt64 = id;
+    params.valueInt32 = (int32_t) resourceType;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMatchingResource(
+    OrthancPluginContext*                 context,
+    OrthancPluginDatabaseContext*         database,
+    const OrthancPluginMatchingResource*  match)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_MatchingResource;
+    params.valueGeneric = match;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMetadata(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    int64_t                        resourceId,
+    int32_t                        type,
+    const char*                    value)
+  {
+    OrthancPluginResourcesContentMetadata metadata;
+    _OrthancPluginDatabaseAnswer params;
+    metadata.resource = resourceId;
+    metadata.metadata = type;
+    metadata.value = value;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_Metadata;
+    params.valueGeneric = &metadata;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const OrthancPluginAttachment* attachment)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment;
+    params.valueGeneric = attachment;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const char*                    publicId,
+    OrthancPluginResourceType      resourceType)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_DeletedResource;
+    params.valueString = publicId;
+    params.valueInt32 = (int32_t) resourceType;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalRemainingAncestor(
+    OrthancPluginContext*          context,
+    OrthancPluginDatabaseContext*  database,
+    const char*                    ancestorId,
+    OrthancPluginResourceType      ancestorType)
+  {
+    _OrthancPluginDatabaseAnswer params;
+    memset(&params, 0, sizeof(params));
+    params.database = database;
+    params.type = _OrthancPluginDatabaseAnswerType_RemainingAncestor;
+    params.valueString = ancestorId;
+    params.valueInt32 = (int32_t) ancestorType;
+    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
+  }
+
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginErrorCode  (*addAttachment) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      const OrthancPluginAttachment* attachment);
+                             
+    OrthancPluginErrorCode  (*attachChild) (
+      /* inputs */
+      void* payload,
+      int64_t parent,
+      int64_t child);
+                             
+    OrthancPluginErrorCode  (*clearChanges) (
+      /* inputs */
+      void* payload);
+                             
+    OrthancPluginErrorCode  (*clearExportedResources) (
+      /* inputs */
+      void* payload);
+
+    OrthancPluginErrorCode  (*createResource) (
+      /* outputs */
+      int64_t* id, 
+      /* inputs */
+      void* payload,
+      const char* publicId,
+      OrthancPluginResourceType resourceType);           
+                   
+    OrthancPluginErrorCode  (*deleteAttachment) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t contentType);
+   
+    OrthancPluginErrorCode  (*deleteMetadata) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t metadataType);
+   
+    OrthancPluginErrorCode  (*deleteResource) (
+      /* inputs */
+      void* payload,
+      int64_t id);    
+
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    OrthancPluginErrorCode  (*getAllPublicIds) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType);
+
+    /* Output: Use OrthancPluginDatabaseAnswerChange() and
+     * OrthancPluginDatabaseAnswerChangesDone() */
+    OrthancPluginErrorCode  (*getChanges) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t since,
+      uint32_t maxResult);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*getChildrenInternalId) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    OrthancPluginErrorCode  (*getChildrenPublicId) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerExportedResource() and
+     * OrthancPluginDatabaseAnswerExportedResourcesDone() */
+    OrthancPluginErrorCode  (*getExportedResources) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t  since,
+      uint32_t  maxResult);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerChange() */
+    OrthancPluginErrorCode  (*getLastChange) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload);
+
+    /* Output: Use OrthancPluginDatabaseAnswerExportedResource() */
+    OrthancPluginErrorCode  (*getLastExportedResource) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerDicomTag() */
+    OrthancPluginErrorCode  (*getMainDicomTags) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    OrthancPluginErrorCode  (*getPublicId) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    OrthancPluginErrorCode  (*getResourceCount) (
+      /* outputs */
+      uint64_t* target,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType  resourceType);
+                   
+    OrthancPluginErrorCode  (*getResourceType) (
+      /* outputs */
+      OrthancPluginResourceType* resourceType,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    OrthancPluginErrorCode  (*getTotalCompressedSize) (
+      /* outputs */
+      uint64_t* target,
+      /* inputs */
+      void* payload);
+                   
+    OrthancPluginErrorCode  (*getTotalUncompressedSize) (
+      /* outputs */
+      uint64_t* target,
+      /* inputs */
+      void* payload);
+                   
+    OrthancPluginErrorCode  (*isExistingResource) (
+      /* outputs */
+      int32_t* existing,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    OrthancPluginErrorCode  (*isProtectedPatient) (
+      /* outputs */
+      int32_t* isProtected,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt32() */
+    OrthancPluginErrorCode  (*listAvailableMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerInt32() */
+    OrthancPluginErrorCode  (*listAvailableAttachments) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    OrthancPluginErrorCode  (*logChange) (
+      /* inputs */
+      void* payload,
+      const OrthancPluginChange* change);
+                   
+    OrthancPluginErrorCode  (*logExportedResource) (
+      /* inputs */
+      void* payload,
+      const OrthancPluginExportedResource* exported);
+                   
+    /* Output: Use OrthancPluginDatabaseAnswerAttachment() */
+    OrthancPluginErrorCode  (*lookupAttachment) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t contentType);
+
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    OrthancPluginErrorCode  (*lookupGlobalProperty) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int32_t property);
+
+    /* Use "OrthancPluginDatabaseExtensions::lookupIdentifier3" 
+       instead of this function as of Orthanc 0.9.5 (db v6), can be set to NULL.
+       Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*lookupIdentifier) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      const OrthancPluginDicomTag* tag);
+
+    /* Unused starting with Orthanc 0.9.5 (db v6), can be set to NULL.
+       Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*lookupIdentifier2) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      const char* value);
+
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    OrthancPluginErrorCode  (*lookupMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t metadata);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*lookupParent) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerResource() */
+    OrthancPluginErrorCode  (*lookupResource) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      const char* publicId);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*selectPatientToRecycle) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*selectPatientToRecycle2) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t patientIdToAvoid);
+
+    OrthancPluginErrorCode  (*setGlobalProperty) (
+      /* inputs */
+      void* payload,
+      int32_t property,
+      const char* value);
+
+    OrthancPluginErrorCode  (*setMainDicomTag) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      const OrthancPluginDicomTag* tag);
+
+    OrthancPluginErrorCode  (*setIdentifierTag) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      const OrthancPluginDicomTag* tag);
+
+    OrthancPluginErrorCode  (*setMetadata) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t metadata,
+      const char* value);
+
+    OrthancPluginErrorCode  (*setProtectedPatient) (
+      /* inputs */
+      void* payload,
+      int64_t id,
+      int32_t isProtected);
+
+    OrthancPluginErrorCode  (*startTransaction) (
+      /* inputs */
+      void* payload);
+
+    OrthancPluginErrorCode  (*rollbackTransaction) (
+      /* inputs */
+      void* payload);
+
+    OrthancPluginErrorCode  (*commitTransaction) (
+      /* inputs */
+      void* payload);
+
+    OrthancPluginErrorCode  (*open) (
+      /* inputs */
+      void* payload);
+
+    OrthancPluginErrorCode  (*close) (
+      /* inputs */
+      void* payload);
+
+  } OrthancPluginDatabaseBackend;
+
+
+  typedef struct
+  {
+    /**
+     * Base extensions since Orthanc 1.0.0
+     **/
+    
+    /* Output: Use OrthancPluginDatabaseAnswerString() */
+    OrthancPluginErrorCode  (*getAllPublicIdsWithLimit) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType,
+      uint64_t since,
+      uint64_t limit);
+
+    OrthancPluginErrorCode  (*getDatabaseVersion) (
+      /* outputs */
+      uint32_t* version,
+      /* inputs */
+      void* payload);
+
+    OrthancPluginErrorCode  (*upgradeDatabase) (
+      /* inputs */
+      void* payload,
+      uint32_t targetVersion,
+      OrthancPluginStorageArea* storageArea);
+ 
+    OrthancPluginErrorCode  (*clearMainDicomTags) (
+      /* inputs */
+      void* payload,
+      int64_t id);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*getAllInternalIds) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType);
+
+    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
+    OrthancPluginErrorCode  (*lookupIdentifier3) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      OrthancPluginResourceType resourceType,
+      const OrthancPluginDicomTag* tag,
+      OrthancPluginIdentifierConstraint constraint);
+
+
+    /**
+     * Extensions since Orthanc 1.4.0
+     **/
+    
+    /* 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);
+
+    
+    /**
+     * Extensions since Orthanc 1.5.2
+     **/
+    
+    /* Ouput: Use OrthancPluginDatabaseAnswerMatchingResource */
+    OrthancPluginErrorCode  (*lookupResources) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      uint32_t constraintsCount,
+      const OrthancPluginDatabaseConstraint* constraints,
+      OrthancPluginResourceType queryLevel,
+      uint32_t limit,
+      uint8_t requestSomeInstance);
+    
+    OrthancPluginErrorCode  (*createInstance) (
+      /* output */
+      OrthancPluginCreateInstanceResult* output,
+      /* inputs */
+      void* payload,
+      const char* hashPatient,
+      const char* hashStudy,
+      const char* hashSeries,
+      const char* hashInstance);
+
+    OrthancPluginErrorCode  (*setResourcesContent) (
+      /* inputs */
+      void* payload,
+      uint32_t countIdentifierTags,
+      const OrthancPluginResourcesContentTags* identifierTags,
+      uint32_t countMainDicomTags,
+      const OrthancPluginResourcesContentTags* mainDicomTags,
+      uint32_t countMetadata,
+      const OrthancPluginResourcesContentMetadata* metadata);
+
+    /* Ouput: Use OrthancPluginDatabaseAnswerString */
+    OrthancPluginErrorCode  (*getChildrenMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t resourceId,
+      int32_t metadata);
+
+    OrthancPluginErrorCode  (*getLastChangeIndex) (
+      /* outputs */
+      int64_t* target,
+      /* inputs */
+      void* payload);
+                   
+    OrthancPluginErrorCode  (*tagMostRecentPatient) (
+      /* inputs */
+      void* payload,
+      int64_t patientId);
+                   
+    
+    /**
+     * Extensions since Orthanc 1.5.4
+     **/
+
+    /* Ouput: Use OrthancPluginDatabaseAnswerMetadata */
+    OrthancPluginErrorCode  (*getAllMetadata) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      /* inputs */
+      void* payload,
+      int64_t resourceId);
+    
+    /* Ouput: Use OrthancPluginDatabaseAnswerString to send 
+       the public ID of the parent (if the resource is not a patient) */
+    OrthancPluginErrorCode  (*lookupResourceAndParent) (
+      /* outputs */
+      OrthancPluginDatabaseContext* context,
+      uint8_t* isExisting,
+      int64_t* id,
+      OrthancPluginResourceType* type,
+      
+      /* inputs */
+      void* payload,
+      const char* publicId);
+
+  } OrthancPluginDatabaseExtensions;
+
+/*<! @endcond */
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseContext**       result;
+    const OrthancPluginDatabaseBackend*  backend;
+    void*                                payload;
+  } _OrthancPluginRegisterDatabaseBackend;
+
+  /**
+   * Register a custom database back-end (for legacy plugins).
+   *
+   * @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.
+   * @return The context of the database engine (it must not be manually freed).
+   * @ingroup Callbacks
+   * @deprecated
+   * @see OrthancPluginRegisterDatabaseBackendV2
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginDatabaseContext* OrthancPluginRegisterDatabaseBackend(
+    OrthancPluginContext*                context,
+    const OrthancPluginDatabaseBackend*  backend,
+    void*                                payload)
+  {
+    OrthancPluginDatabaseContext* result = NULL;
+    _OrthancPluginRegisterDatabaseBackend params;
+
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    {
+      return NULL;
+    }
+
+    memset(&params, 0, sizeof(params));
+    params.backend = backend;
+    params.result = &result;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackend, &params) ||
+        result == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginDatabaseContext**          result;
+    const OrthancPluginDatabaseBackend*     backend;
+    void*                                   payload;
+    const OrthancPluginDatabaseExtensions*  extensions;
+    uint32_t                                extensionsSize;
+  } _OrthancPluginRegisterDatabaseBackendV2;
+
+
+  /**
+   * Register a custom database back-end.
+   *
+   * @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.
+   * @param extensions Extensions to the base database SDK that was shipped until Orthanc 0.9.3.
+   * @return The context of the database engine (it must not be manually freed).
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginDatabaseContext* OrthancPluginRegisterDatabaseBackendV2(
+    OrthancPluginContext*                   context,
+    const OrthancPluginDatabaseBackend*     backend,
+    const OrthancPluginDatabaseExtensions*  extensions,
+    void*                                   payload)
+  {
+    OrthancPluginDatabaseContext* result = NULL;
+    _OrthancPluginRegisterDatabaseBackendV2 params;
+
+    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
+    {
+      return NULL;
+    }
+
+    memset(&params, 0, sizeof(params));
+    params.backend = backend;
+    params.result = &result;
+    params.payload = payload;
+    params.extensions = extensions;
+    params.extensionsSize = sizeof(OrthancPluginDatabaseExtensions);
+
+    if (context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV2, &params) ||
+        result == NULL)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+#ifdef  __cplusplus
+}
+#endif
+
+
+/** @} */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,8193 @@
+/**
+ * \mainpage
+ *
+ * This C/C++ SDK allows external developers to create plugins that
+ * can be loaded into Orthanc to extend its functionality. Each
+ * Orthanc plugin must expose 4 public functions with the following
+ * signatures:
+ * 
+ * -# <tt>int32_t OrthancPluginInitialize(const OrthancPluginContext* context)</tt>:
+ *    This function is invoked by Orthanc when it loads the plugin on startup.
+ *    The plugin must:
+ *    - Check its compatibility with the Orthanc version using
+ *      ::OrthancPluginCheckVersion().
+ *    - Store the context pointer so that it can use the plugin 
+ *      services of Orthanc.
+ *    - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback().
+ *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
+ *    - 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().
+ *    - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback().
+ *    - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback().
+ *    - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback().
+ *    - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter().
+ *    - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback().
+ * -# <tt>void OrthancPluginFinalize()</tt>:
+ *    This function is invoked by Orthanc during its shutdown. The plugin
+ *    must free all its memory.
+ * -# <tt>const char* OrthancPluginGetName()</tt>:
+ *    The plugin must return a short string to identify itself.
+ * -# <tt>const char* OrthancPluginGetVersion()</tt>:
+ *    The plugin must return a string containing its version number.
+ *
+ * The name and the version of a plugin is only used to prevent it
+ * from being loaded twice. Note that, in C++, it is mandatory to
+ * declare these functions within an <tt>extern "C"</tt> section.
+ * 
+ * To ensure multi-threading safety, the various REST callbacks are
+ * guaranteed to be executed in mutual exclusion since Orthanc
+ * 0.8.5. If this feature is undesired (notably when developing
+ * high-performance plugins handling simultaneous requests), use
+ * ::OrthancPluginRegisterRestCallbackNoLock().
+ **/
+
+
+
+/**
+ * @defgroup Images Images and compression
+ * @brief Functions to deal with images and compressed buffers.
+ *
+ * @defgroup REST REST
+ * @brief Functions to answer REST requests in a callback.
+ *
+ * @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, storage commitment).
+ *
+ * @defgroup Orthanc Orthanc
+ * @brief Functions to access the content of the Orthanc server.
+ *
+ * @defgroup DicomInstance DicomInstance
+ * @brief Functions to access DICOM images that are managed by the Orthanc core.
+ **/
+
+
+
+/**
+ * @defgroup Toolbox Toolbox
+ * @brief Generic functions to help with the creation of plugins.
+ **/
+
+
+
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <stdio.h>
+#include <string.h>
+
+#ifdef WIN32
+#  define ORTHANC_PLUGINS_API __declspec(dllexport)
+#elif __GNUC__ >= 4
+#  define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default")))
+#else
+#  define ORTHANC_PLUGINS_API
+#endif
+
+#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     7
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  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
+
+
+
+/********************************************************************
+ ** Check that function inlining is properly supported. The use of
+ ** inlining is required, to avoid the duplication of object code
+ ** between two compilation modules that would use the Orthanc Plugin
+ ** API.
+ ********************************************************************/
+
+/* If the auto-detection of the "inline" keyword below does not work
+   automatically and that your compiler is known to properly support
+   inlining, uncomment the following #define and adapt the definition
+   of "static inline". */
+
+/* #define ORTHANC_PLUGIN_INLINE static inline */
+
+#ifndef ORTHANC_PLUGIN_INLINE
+#  if __STDC_VERSION__ >= 199901L
+/*   This is C99 or above: http://predef.sourceforge.net/prestd.html */
+#    define ORTHANC_PLUGIN_INLINE static inline
+#  elif defined(__cplusplus)
+/*   This is C++ */
+#    define ORTHANC_PLUGIN_INLINE static inline
+#  elif defined(__GNUC__)
+/*   This is GCC running in C89 mode */
+#    define ORTHANC_PLUGIN_INLINE static __inline
+#  elif defined(_MSC_VER)
+/*   This is Visual Studio running in C89 mode */
+#    define ORTHANC_PLUGIN_INLINE static __inline
+#  else
+#    error Your compiler is not known to support the "inline" keyword
+#  endif
+#endif
+
+
+
+/********************************************************************
+ ** Inclusion of standard libraries.
+ ********************************************************************/
+
+/**
+ * For Microsoft Visual Studio, a compatibility "stdint.h" can be
+ * downloaded at the following URL:
+ * https://hg.orthanc-server.com/orthanc/raw-file/tip/Resources/ThirdParty/VisualStudio/stdint.h
+ **/
+#include <stdint.h>
+
+#include <stdlib.h>
+
+
+
+/********************************************************************
+ ** Definition of the Orthanc Plugin API.
+ ********************************************************************/
+
+/** @{ */
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+  /**
+   * The various error codes that can be returned by the Orthanc core.
+   **/
+  typedef enum
+  {
+    OrthancPluginErrorCode_InternalError = -1    /*!< Internal error */,
+    OrthancPluginErrorCode_Success = 0    /*!< Success */,
+    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    /*!< 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 */,
+    OrthancPluginErrorCode_BadRequest = 8    /*!< Bad request */,
+    OrthancPluginErrorCode_NetworkProtocol = 9    /*!< Error in the network protocol */,
+    OrthancPluginErrorCode_SystemCommand = 10    /*!< Error while calling a system command */,
+    OrthancPluginErrorCode_Database = 11    /*!< Error with the database engine */,
+    OrthancPluginErrorCode_UriSyntax = 12    /*!< Badly formatted URI */,
+    OrthancPluginErrorCode_InexistentFile = 13    /*!< Inexistent file */,
+    OrthancPluginErrorCode_CannotWriteFile = 14    /*!< Cannot write to file */,
+    OrthancPluginErrorCode_BadFileFormat = 15    /*!< Bad file format */,
+    OrthancPluginErrorCode_Timeout = 16    /*!< Timeout */,
+    OrthancPluginErrorCode_UnknownResource = 17    /*!< Unknown resource */,
+    OrthancPluginErrorCode_IncompatibleDatabaseVersion = 18    /*!< Incompatible version of the database */,
+    OrthancPluginErrorCode_FullStorage = 19    /*!< The file storage is full */,
+    OrthancPluginErrorCode_CorruptedFile = 20    /*!< Corrupted file (e.g. inconsistent MD5 hash) */,
+    OrthancPluginErrorCode_InexistentTag = 21    /*!< Inexistent tag */,
+    OrthancPluginErrorCode_ReadOnly = 22    /*!< Cannot modify a read-only data structure */,
+    OrthancPluginErrorCode_IncompatibleImageFormat = 23    /*!< Incompatible format of the images */,
+    OrthancPluginErrorCode_IncompatibleImageSize = 24    /*!< Incompatible size of the images */,
+    OrthancPluginErrorCode_SharedLibrary = 25    /*!< Error while using a shared library (plugin) */,
+    OrthancPluginErrorCode_UnknownPluginService = 26    /*!< Plugin invoking an unknown service */,
+    OrthancPluginErrorCode_UnknownDicomTag = 27    /*!< Unknown DICOM tag */,
+    OrthancPluginErrorCode_BadJson = 28    /*!< Cannot parse a JSON document */,
+    OrthancPluginErrorCode_Unauthorized = 29    /*!< Bad credentials were provided to an HTTP request */,
+    OrthancPluginErrorCode_BadFont = 30    /*!< Badly formatted font file */,
+    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_BadGeometry = 38    /*!< Geometry error encountered in Stone */,
+    OrthancPluginErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
+    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 */,
+    OrthancPluginErrorCode_SQLiteStatementAlreadyUsed = 1003    /*!< SQLite: This cached statement is already being referred to */,
+    OrthancPluginErrorCode_SQLiteExecute = 1004    /*!< SQLite: Cannot execute a command */,
+    OrthancPluginErrorCode_SQLiteRollbackWithoutTransaction = 1005    /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */,
+    OrthancPluginErrorCode_SQLiteCommitWithoutTransaction = 1006    /*!< SQLite: Committing a nonexistent transaction */,
+    OrthancPluginErrorCode_SQLiteRegisterFunction = 1007    /*!< SQLite: Unable to register a function */,
+    OrthancPluginErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
+    OrthancPluginErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
+    OrthancPluginErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
+    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
+    OrthancPluginErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
+    OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
+    OrthancPluginErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
+    OrthancPluginErrorCode_SQLiteTransactionBegin = 1015    /*!< SQLite: Cannot start a transaction */,
+    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 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 */,
+    OrthancPluginErrorCode_MakeDirectory = 2008    /*!< Cannot create a directory */,
+    OrthancPluginErrorCode_BadApplicationEntityTitle = 2009    /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */,
+    OrthancPluginErrorCode_NoCFindHandler = 2010    /*!< No request handler factory for DICOM C-FIND SCP */,
+    OrthancPluginErrorCode_NoCMoveHandler = 2011    /*!< No request handler factory for DICOM C-MOVE SCP */,
+    OrthancPluginErrorCode_NoCStoreHandler = 2012    /*!< No request handler factory for DICOM C-STORE SCP */,
+    OrthancPluginErrorCode_NoApplicationEntityFilter = 2013    /*!< No application entity filter */,
+    OrthancPluginErrorCode_NoSopClassOrInstance = 2014    /*!< DicomUserConnection: Unable to find the SOP class and instance */,
+    OrthancPluginErrorCode_NoPresentationContext = 2015    /*!< DicomUserConnection: No acceptable presentation context for modality */,
+    OrthancPluginErrorCode_DicomFindUnavailable = 2016    /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */,
+    OrthancPluginErrorCode_DicomMoveUnavailable = 2017    /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */,
+    OrthancPluginErrorCode_CannotStoreInstance = 2018    /*!< Cannot store an instance */,
+    OrthancPluginErrorCode_CreateDicomNotString = 2019    /*!< Only string values are supported when creating DICOM instances */,
+    OrthancPluginErrorCode_CreateDicomOverrideTag = 2020    /*!< Trying to override a value inherited from a parent module */,
+    OrthancPluginErrorCode_CreateDicomUseContent = 2021    /*!< Use \"Content\" to inject an image into a new DICOM instance */,
+    OrthancPluginErrorCode_CreateDicomNoPayload = 2022    /*!< No payload is present for one instance in the series */,
+    OrthancPluginErrorCode_CreateDicomUseDataUriScheme = 2023    /*!< The payload of the DICOM instance must be specified according to Data URI scheme */,
+    OrthancPluginErrorCode_CreateDicomBadParent = 2024    /*!< Trying to attach a new DICOM instance to an inexistent resource */,
+    OrthancPluginErrorCode_CreateDicomParentIsInstance = 2025    /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */,
+    OrthancPluginErrorCode_CreateDicomParentEncoding = 2026    /*!< Unable to get the encoding of the parent resource */,
+    OrthancPluginErrorCode_UnknownModality = 2027    /*!< Unknown modality */,
+    OrthancPluginErrorCode_BadJobOrdering = 2028    /*!< Bad ordering of filters in a job */,
+    OrthancPluginErrorCode_JsonToLuaTable = 2029    /*!< Cannot convert the given JSON object to a Lua table */,
+    OrthancPluginErrorCode_CannotCreateLua = 2030    /*!< Cannot create the Lua context */,
+    OrthancPluginErrorCode_CannotExecuteLua = 2031    /*!< Cannot execute a Lua command */,
+    OrthancPluginErrorCode_LuaAlreadyExecuted = 2032    /*!< Arguments cannot be pushed after the Lua function is executed */,
+    OrthancPluginErrorCode_LuaBadOutput = 2033    /*!< The Lua function does not give the expected number of outputs */,
+    OrthancPluginErrorCode_NotLuaPredicate = 2034    /*!< The Lua function is not a predicate (only true/false outputs allowed) */,
+    OrthancPluginErrorCode_LuaReturnsNoString = 2035    /*!< The Lua function does not return a string */,
+    OrthancPluginErrorCode_StorageAreaAlreadyRegistered = 2036    /*!< Another plugin has already registered a custom storage area */,
+    OrthancPluginErrorCode_DatabaseBackendAlreadyRegistered = 2037    /*!< Another plugin has already registered a custom database back-end */,
+    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_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
+    OrthancPluginErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
+    OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
+
+    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
+  } OrthancPluginErrorCode;
+
+
+  /**
+   * Forward declaration of one of the mandatory functions for Orthanc
+   * plugins.
+   **/
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName();
+
+
+  /**
+   * The various HTTP methods for a REST call.
+   **/
+  typedef enum
+  {
+    OrthancPluginHttpMethod_Get = 1,    /*!< GET request */
+    OrthancPluginHttpMethod_Post = 2,   /*!< POST request */
+    OrthancPluginHttpMethod_Put = 3,    /*!< PUT request */
+    OrthancPluginHttpMethod_Delete = 4, /*!< DELETE request */
+
+    _OrthancPluginHttpMethod_INTERNAL = 0x7fffffff
+  } OrthancPluginHttpMethod;
+
+
+  /**
+   * @brief The parameters of a REST request.
+   * @ingroup Callbacks
+   **/
+  typedef struct
+  {
+    /**
+     * @brief The HTTP method.
+     **/
+    OrthancPluginHttpMethod method;
+
+    /**
+     * @brief The number of groups of the regular expression.
+     **/
+    uint32_t                groupsCount;
+
+    /**
+     * @brief The matched values for the groups of the regular expression.
+     **/
+    const char* const*      groups;
+
+    /**
+     * @brief For a GET request, the number of GET parameters.
+     **/
+    uint32_t                getCount;
+
+    /**
+     * @brief For a GET request, the keys of the GET parameters.
+     **/
+    const char* const*      getKeys;
+
+    /**
+     * @brief For a GET request, the values of the GET parameters.
+     **/
+    const char* const*      getValues;
+
+    /**
+     * @brief For a PUT or POST request, the content of the body.
+     **/
+    const void*             body;
+
+    /**
+     * @brief For a PUT or POST request, the number of bytes of the body.
+     **/
+    uint32_t                bodySize;
+
+
+    /* --------------------------------------------------
+       New in version 0.8.1
+       -------------------------------------------------- */
+
+    /**
+     * @brief The number of HTTP headers.
+     **/
+    uint32_t                headersCount;
+
+    /**
+     * @brief The keys of the HTTP headers (always converted to low-case).
+     **/
+    const char* const*      headersKeys;
+
+    /**
+     * @brief The values of the HTTP headers.
+     **/
+    const char* const*      headersValues;
+
+  } OrthancPluginHttpRequest;
+
+
+  typedef enum 
+  {
+    /* Generic services */
+    _OrthancPluginService_LogInfo = 1,
+    _OrthancPluginService_LogWarning = 2,
+    _OrthancPluginService_LogError = 3,
+    _OrthancPluginService_GetOrthancPath = 4,
+    _OrthancPluginService_GetOrthancDirectory = 5,
+    _OrthancPluginService_GetConfigurationPath = 6,
+    _OrthancPluginService_SetPluginProperty = 7,
+    _OrthancPluginService_GetGlobalProperty = 8,
+    _OrthancPluginService_SetGlobalProperty = 9,
+    _OrthancPluginService_GetCommandLineArgumentsCount = 10,
+    _OrthancPluginService_GetCommandLineArgument = 11,
+    _OrthancPluginService_GetExpectedDatabaseVersion = 12,
+    _OrthancPluginService_GetConfiguration = 13,
+    _OrthancPluginService_BufferCompression = 14,
+    _OrthancPluginService_ReadFile = 15,
+    _OrthancPluginService_WriteFile = 16,
+    _OrthancPluginService_GetErrorDescription = 17,
+    _OrthancPluginService_CallHttpClient = 18,
+    _OrthancPluginService_RegisterErrorCode = 19,
+    _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,
+    _OrthancPluginService_AutodetectMimeType = 30,
+    _OrthancPluginService_SetMetricsValue = 31,
+    _OrthancPluginService_EncodeDicomWebJson = 32,
+    _OrthancPluginService_EncodeDicomWebXml = 33,
+    _OrthancPluginService_ChunkedHttpClient = 34,    /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_GetTagName = 35,           /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_EncodeDicomWebJson2 = 36,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_EncodeDicomWebXml2 = 37,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_CreateMemoryBuffer = 38,   /* New in Orthanc 1.7.0 */
+    
+    /* Registration of callbacks */
+    _OrthancPluginService_RegisterRestCallback = 1000,
+    _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001,
+    _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,
+    _OrthancPluginService_RegisterRefreshMetricsCallback = 1011,
+    _OrthancPluginService_RegisterChunkedRestCallback = 1012,  /* New in Orthanc 1.5.7 */
+    _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013,
+    _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014,
+    _OrthancPluginService_RegisterTranscoderCallback = 1015,   /* New in Orthanc 1.7.0 */
+    
+    /* Sending answers to REST calls */
+    _OrthancPluginService_AnswerBuffer = 2000,
+    _OrthancPluginService_CompressAndAnswerPngImage = 2001,  /* Unused as of Orthanc 0.9.4 */
+    _OrthancPluginService_Redirect = 2002,
+    _OrthancPluginService_SendHttpStatusCode = 2003,
+    _OrthancPluginService_SendUnauthorized = 2004,
+    _OrthancPluginService_SendMethodNotAllowed = 2005,
+    _OrthancPluginService_SetCookie = 2006,
+    _OrthancPluginService_SetHttpHeader = 2007,
+    _OrthancPluginService_StartMultipartAnswer = 2008,
+    _OrthancPluginService_SendMultipartItem = 2009,
+    _OrthancPluginService_SendHttpStatus = 2010,
+    _OrthancPluginService_CompressAndAnswerImage = 2011,
+    _OrthancPluginService_SendMultipartItem2 = 2012,
+    _OrthancPluginService_SetHttpErrorDetails = 2013,
+
+    /* Access to the Orthanc database and API */
+    _OrthancPluginService_GetDicomForInstance = 3000,
+    _OrthancPluginService_RestApiGet = 3001,
+    _OrthancPluginService_RestApiPost = 3002,
+    _OrthancPluginService_RestApiDelete = 3003,
+    _OrthancPluginService_RestApiPut = 3004,
+    _OrthancPluginService_LookupPatient = 3005,
+    _OrthancPluginService_LookupStudy = 3006,
+    _OrthancPluginService_LookupSeries = 3007,
+    _OrthancPluginService_LookupInstance = 3008,
+    _OrthancPluginService_LookupStudyWithAccessionNumber = 3009,
+    _OrthancPluginService_RestApiGetAfterPlugins = 3010,
+    _OrthancPluginService_RestApiPostAfterPlugins = 3011,
+    _OrthancPluginService_RestApiDeleteAfterPlugins = 3012,
+    _OrthancPluginService_RestApiPutAfterPlugins = 3013,
+    _OrthancPluginService_ReconstructMainDicomTags = 3014,
+    _OrthancPluginService_RestApiGet2 = 3015,
+
+    /* Access to DICOM instances */
+    _OrthancPluginService_GetInstanceRemoteAet = 4000,
+    _OrthancPluginService_GetInstanceSize = 4001,
+    _OrthancPluginService_GetInstanceData = 4002,
+    _OrthancPluginService_GetInstanceJson = 4003,
+    _OrthancPluginService_GetInstanceSimplifiedJson = 4004,
+    _OrthancPluginService_HasInstanceMetadata = 4005,
+    _OrthancPluginService_GetInstanceMetadata = 4006,
+    _OrthancPluginService_GetInstanceOrigin = 4007,
+    _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008,
+    _OrthancPluginService_HasInstancePixelData = 4009,
+    _OrthancPluginService_CreateDicomInstance = 4010,      /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_FreeDicomInstance = 4011,        /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceFramesCount = 4012,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceRawFrame = 4013,      /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDecodedFrame = 4014,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_TranscodeDicomInstance = 4015,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_SerializeDicomInstance = 4016,   /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceAdvancedJson = 4017,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDicomWebJson = 4018,  /* New in Orthanc 1.7.0 */
+    _OrthancPluginService_GetInstanceDicomWebXml = 4019,   /* New in Orthanc 1.7.0 */
+    
+    /* Services for plugins implementing a database back-end */
+    _OrthancPluginService_RegisterDatabaseBackend = 5000,
+    _OrthancPluginService_DatabaseAnswer = 5001,
+    _OrthancPluginService_RegisterDatabaseBackendV2 = 5002,
+    _OrthancPluginService_StorageAreaCreate = 5003,
+    _OrthancPluginService_StorageAreaRead = 5004,
+    _OrthancPluginService_StorageAreaRemove = 5005,
+
+    /* Primitives for handling images */
+    _OrthancPluginService_GetImagePixelFormat = 6000,
+    _OrthancPluginService_GetImageWidth = 6001,
+    _OrthancPluginService_GetImageHeight = 6002,
+    _OrthancPluginService_GetImagePitch = 6003,
+    _OrthancPluginService_GetImageBuffer = 6004,
+    _OrthancPluginService_UncompressImage = 6005,
+    _OrthancPluginService_FreeImage = 6006,
+    _OrthancPluginService_CompressImage = 6007,
+    _OrthancPluginService_ConvertPixelFormat = 6008,
+    _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;
+
+
+  typedef enum
+  {
+    _OrthancPluginProperty_Description = 1,
+    _OrthancPluginProperty_RootUri = 2,
+    _OrthancPluginProperty_OrthancExplorer = 3,
+
+    _OrthancPluginProperty_INTERNAL = 0x7fffffff
+  } _OrthancPluginProperty;
+
+
+
+  /**
+   * The memory layout of the pixels of an image.
+   * @ingroup Images
+   **/
+  typedef enum
+  {
+    /**
+     * @brief Graylevel 8bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * one byte.
+     **/
+    OrthancPluginPixelFormat_Grayscale8 = 1,
+
+    /**
+     * @brief Graylevel, unsigned 16bpp image.
+     *
+     * The image is graylevel. Each pixel is unsigned and stored in
+     * two bytes.
+     **/
+    OrthancPluginPixelFormat_Grayscale16 = 2,
+
+    /**
+     * @brief Graylevel, signed 16bpp image.
+     *
+     * The image is graylevel. Each pixel is signed and stored in two
+     * bytes.
+     **/
+    OrthancPluginPixelFormat_SignedGrayscale16 = 3,
+
+    /**
+     * @brief Color image in RGB24 format.
+     *
+     * This format describes a color image. The pixels are stored in 3
+     * consecutive bytes. The memory layout is RGB.
+     **/
+    OrthancPluginPixelFormat_RGB24 = 4,
+
+    /**
+     * @brief Color image in RGBA32 format.
+     *
+     * This format describes a color image. The pixels are stored in 4
+     * consecutive bytes. The memory layout is RGBA.
+     **/
+    OrthancPluginPixelFormat_RGBA32 = 5,
+
+    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;
+
+
+
+  /**
+   * The content types that are supported by Orthanc plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginContentType_Unknown = 0,      /*!< Unknown content type */
+    OrthancPluginContentType_Dicom = 1,        /*!< DICOM */
+    OrthancPluginContentType_DicomAsJson = 2,  /*!< JSON summary of a DICOM file */
+
+    _OrthancPluginContentType_INTERNAL = 0x7fffffff
+  } OrthancPluginContentType;
+
+
+
+  /**
+   * The supported types of DICOM resources.
+   **/
+  typedef enum
+  {
+    OrthancPluginResourceType_Patient = 0,     /*!< Patient */
+    OrthancPluginResourceType_Study = 1,       /*!< Study */
+    OrthancPluginResourceType_Series = 2,      /*!< Series */
+    OrthancPluginResourceType_Instance = 3,    /*!< Instance */
+    OrthancPluginResourceType_None = 4,        /*!< Unavailable resource type */
+
+    _OrthancPluginResourceType_INTERNAL = 0x7fffffff
+  } OrthancPluginResourceType;
+
+
+
+  /**
+   * The supported types of changes that can be signaled to the change callback.
+   * @ingroup Callbacks
+   **/
+  typedef enum
+  {
+    OrthancPluginChangeType_CompletedSeries = 0,    /*!< Series is now complete */
+    OrthancPluginChangeType_Deleted = 1,            /*!< Deleted resource */
+    OrthancPluginChangeType_NewChildInstance = 2,   /*!< A new instance was added to this resource */
+    OrthancPluginChangeType_NewInstance = 3,        /*!< New instance received */
+    OrthancPluginChangeType_NewPatient = 4,         /*!< New patient created */
+    OrthancPluginChangeType_NewSeries = 5,          /*!< New series created */
+    OrthancPluginChangeType_NewStudy = 6,           /*!< New study created */
+    OrthancPluginChangeType_StablePatient = 7,      /*!< Timeout: No new instance in this patient */
+    OrthancPluginChangeType_StableSeries = 8,       /*!< Timeout: No new instance in this series */
+    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_JobSubmitted = 16,      /*!< New Job submitted */
+    OrthancPluginChangeType_JobSuccess = 17,        /*!< A Job has completed successfully */
+    OrthancPluginChangeType_JobFailure = 18,        /*!< A Job has failed */
+
+    _OrthancPluginChangeType_INTERNAL = 0x7fffffff
+  } OrthancPluginChangeType;
+
+
+  /**
+   * The compression algorithms that are supported by the Orthanc core.
+   * @ingroup Images
+   **/
+  typedef enum
+  {
+    OrthancPluginCompressionType_Zlib = 0,          /*!< Standard zlib compression */
+    OrthancPluginCompressionType_ZlibWithSize = 1,  /*!< zlib, prefixed with uncompressed size (uint64_t) */
+    OrthancPluginCompressionType_Gzip = 2,          /*!< Standard gzip compression */
+    OrthancPluginCompressionType_GzipWithSize = 3,  /*!< gzip, prefixed with uncompressed size (uint64_t) */
+
+    _OrthancPluginCompressionType_INTERNAL = 0x7fffffff
+  } OrthancPluginCompressionType;
+
+
+  /**
+   * The image formats that are supported by the Orthanc core.
+   * @ingroup Images
+   **/
+  typedef enum
+  {
+    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;
+
+
+  /**
+   * The value representations present in the DICOM standard (version 2013).
+   * @ingroup Toolbox
+   **/
+  typedef enum
+  {
+    OrthancPluginValueRepresentation_AE = 1,   /*!< Application Entity */
+    OrthancPluginValueRepresentation_AS = 2,   /*!< Age String */
+    OrthancPluginValueRepresentation_AT = 3,   /*!< Attribute Tag */
+    OrthancPluginValueRepresentation_CS = 4,   /*!< Code String */
+    OrthancPluginValueRepresentation_DA = 5,   /*!< Date */
+    OrthancPluginValueRepresentation_DS = 6,   /*!< Decimal String */
+    OrthancPluginValueRepresentation_DT = 7,   /*!< Date Time */
+    OrthancPluginValueRepresentation_FD = 8,   /*!< Floating Point Double */
+    OrthancPluginValueRepresentation_FL = 9,   /*!< Floating Point Single */
+    OrthancPluginValueRepresentation_IS = 10,  /*!< Integer String */
+    OrthancPluginValueRepresentation_LO = 11,  /*!< Long String */
+    OrthancPluginValueRepresentation_LT = 12,  /*!< Long Text */
+    OrthancPluginValueRepresentation_OB = 13,  /*!< Other Byte String */
+    OrthancPluginValueRepresentation_OF = 14,  /*!< Other Float String */
+    OrthancPluginValueRepresentation_OW = 15,  /*!< Other Word String */
+    OrthancPluginValueRepresentation_PN = 16,  /*!< Person Name */
+    OrthancPluginValueRepresentation_SH = 17,  /*!< Short String */
+    OrthancPluginValueRepresentation_SL = 18,  /*!< Signed Long */
+    OrthancPluginValueRepresentation_SQ = 19,  /*!< Sequence of Items */
+    OrthancPluginValueRepresentation_SS = 20,  /*!< Signed Short */
+    OrthancPluginValueRepresentation_ST = 21,  /*!< Short Text */
+    OrthancPluginValueRepresentation_TM = 22,  /*!< Time */
+    OrthancPluginValueRepresentation_UI = 23,  /*!< Unique Identifier (UID) */
+    OrthancPluginValueRepresentation_UL = 24,  /*!< Unsigned Long */
+    OrthancPluginValueRepresentation_UN = 25,  /*!< Unknown */
+    OrthancPluginValueRepresentation_US = 26,  /*!< Unsigned Short */
+    OrthancPluginValueRepresentation_UT = 27,  /*!< Unlimited Text */
+
+    _OrthancPluginValueRepresentation_INTERNAL = 0x7fffffff
+  } OrthancPluginValueRepresentation;
+
+
+  /**
+   * 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_Human = 3,   /*!< Human-readable JSON */
+
+    _OrthancPluginDicomToJsonFormat_INTERNAL = 0x7fffffff
+  } OrthancPluginDicomToJsonFormat;
+
+
+  /**
+   * Flags to customize a DICOM-to-JSON conversion. By default, binary
+   * tags are formatted using Data URI scheme.
+   * @ingroup Toolbox
+   **/
+  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 */
+    OrthancPluginDicomToJsonFlags_IncludePixelData      = (1 << 3),  /*!< Include the pixel data */
+    OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),  /*!< Output binary tags as-is, dropping non-ASCII */
+    OrthancPluginDicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),  /*!< Signal binary tags as null values */
+
+    _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff
+  } OrthancPluginDicomToJsonFlags;
+
+
+  /**
+   * 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.
+   * @deprecated Plugins using OrthancPluginConstraintType will be faster
+   **/
+  typedef enum
+  {
+    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 constraints on the tags (main DICOM tags and identifier tags)
+   * that must be supported by the database plugins.
+   **/
+  typedef enum
+  {
+    OrthancPluginConstraintType_Equal = 1,           /*!< Equal */
+    OrthancPluginConstraintType_SmallerOrEqual = 2,  /*!< Less or equal */
+    OrthancPluginConstraintType_GreaterOrEqual = 3,  /*!< More or equal */
+    OrthancPluginConstraintType_Wildcard = 4,        /*!< Wildcard matching */
+    OrthancPluginConstraintType_List = 5,            /*!< List of values */
+
+    _OrthancPluginConstraintType_INTERNAL = 0x7fffffff
+  } OrthancPluginConstraintType;
+
+
+  /**
+   * 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;
+
+
+  /**
+   * The available types of metrics.
+   **/
+  typedef enum
+  {
+    OrthancPluginMetricsType_Default = 0,   /*!< Default metrics */
+
+    /**
+     * This metrics represents a time duration. Orthanc will keep the
+     * maximum value of the metrics over a sliding window of ten
+     * seconds, which is useful if the metrics is sampled frequently.
+     **/
+    OrthancPluginMetricsType_Timer = 1
+  } OrthancPluginMetricsType;
+  
+
+  /**
+   * The available modes to export a binary DICOM tag into a DICOMweb
+   * JSON or XML document.
+   **/
+  typedef enum
+  {
+    OrthancPluginDicomWebBinaryMode_Ignore = 0,        /*!< Don't include binary tags */
+    OrthancPluginDicomWebBinaryMode_InlineBinary = 1,  /*!< Inline encoding using Base64 */
+    OrthancPluginDicomWebBinaryMode_BulkDataUri = 2    /*!< Use a bulk data URI field */
+  } OrthancPluginDicomWebBinaryMode;
+
+
+  /**
+   * The available values for the Failure Reason (0008,1197) during
+   * storage commitment.
+   * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
+   **/
+  typedef enum
+  {
+    OrthancPluginStorageCommitmentFailureReason_Success = 0,
+    /*!< Success: The DICOM instance is properly stored in the SCP */
+
+    OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1,
+    /*!< 0110H: A general failure in processing the operation was encountered */
+
+    OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2,
+    /*!< 0112H: One or more of the elements in the Referenced SOP
+      Instance Sequence was not available */
+
+    OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3,
+    /*!< 0213H: The SCP does not currently have enough resources to
+      store the requested SOP Instance(s) */
+
+    OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4,
+    /*!< 0122H: Storage Commitment has been requested for a SOP
+      Instance with a SOP Class that is not supported by the SCP */
+
+    OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5,
+    /*!< 0119H: The SOP Class of an element in the Referenced SOP
+      Instance Sequence did not correspond to the SOP class registered
+      for this SOP Instance at the SCP */
+
+    OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6
+    /*!< 0131H: The Transaction UID of the Storage Commitment Request
+      is already in use */
+  } OrthancPluginStorageCommitmentFailureReason;
+
+  
+
+  /**
+   * @brief A memory buffer allocated by the core system of Orthanc.
+   *
+   * A memory buffer allocated by the core system of Orthanc. When the
+   * content of the buffer is not useful anymore, it must be free by a
+   * call to ::OrthancPluginFreeMemoryBuffer().
+   **/
+  typedef struct
+  {
+    /**
+     * @brief The content of the buffer.
+     **/
+    void*      data;
+
+    /**
+     * @brief The number of bytes in the buffer.
+     **/
+    uint32_t   size;
+  } OrthancPluginMemoryBuffer;
+
+
+
+
+  /**
+   * @brief Opaque structure that represents the HTTP connection to the client application.
+   * @ingroup Callback
+   **/
+  typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput;
+
+
+
+  /**
+   * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core.
+   * @ingroup DicomInstance
+   **/
+  typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance;
+
+
+
+  /**
+   * @brief Opaque structure that represents an image that is uncompressed in memory.
+   * @ingroup Images
+   **/
+  typedef struct _OrthancPluginImage_t OrthancPluginImage;
+
+
+
+  /**
+   * @brief Opaque structure that represents the storage area that is actually used by Orthanc.
+   * @ingroup Images
+   **/
+  typedef struct _OrthancPluginStorageArea_t OrthancPluginStorageArea;
+
+
+
+  /**
+   * @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 _OrthancPluginFindMatcher_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 Opaque structure that represents a node in a JSON or XML
+   * document used in DICOMweb.
+   * @ingroup Toolbox
+   **/
+  typedef struct _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode;
+
+  
+
+  /**
+   * @brief Signature of a callback function that answers to a REST request.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginRestCallback) (
+    OrthancPluginRestOutput* output,
+    const char* url,
+    const OrthancPluginHttpRequest* request);
+
+
+
+  /**
+   * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) (
+    const OrthancPluginDicomInstance* instance,
+    const char* instanceId);
+
+
+
+  /**
+   * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginOnChangeCallback) (
+    OrthancPluginChangeType changeType,
+    OrthancPluginResourceType resourceType,
+    const char* resourceId);
+
+
+
+  /**
+   * @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.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginFree) (void* buffer);
+
+
+
+  /**
+   * @brief Signature of a function to set the content of a node
+   * encoding a binary DICOM tag, into a JSON or XML document
+   * generated for DICOMweb.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebSetBinaryNode) (
+    OrthancPluginDicomWebNode*       node,
+    OrthancPluginDicomWebBinaryMode  mode,
+    const char*                      bulkDataUri);
+    
+
+
+  /**
+   * @brief Callback for writing to the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
+   *
+   * @param uuid The UUID of the file.
+   * @param content The content of the file.
+   * @param size The size of the file.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate) (
+    const char* uuid,
+    const void* content,
+    int64_t size,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for reading from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc reads a file from the storage area.
+   *
+   * @param content The content of the file (output).
+   * @param size The size of the file (output).
+   * @param uuid The UUID of the file of interest.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   * 
+   * @warning The "content" buffer *must* have been allocated using
+   * the "malloc()" function of your C standard library (i.e. nor
+   * "new[]", neither a pointer to a buffer). The "free()" function of
+   * your C standard library will automatically be invoked on the
+   * "content" pointer.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRead) (
+    void** content,
+    int64_t* size,
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @brief Callback for removing a file from the storage area.
+   *
+   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
+   *
+   * @param uuid The UUID of the file to be removed.
+   * @param type The content type corresponding to this file. 
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove) (
+    const char* uuid,
+    OrthancPluginContentType type);
+
+
+
+  /**
+   * @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 unserialize 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 Callback executed to update the metrics of the plugin.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a monitoring tool (such as Prometheus) asks the current
+   * values of the metrics. This callback gives the plugin a chance to
+   * update its metrics, by calling OrthancPluginSetMetricsValue().
+   * This is typically useful for metrics that are expensive to
+   * acquire.
+   * 
+   * @see OrthancPluginRegisterRefreshMetrics()
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginRefreshMetricsCallback) ();
+
+  
+
+  /**
+   * @brief Callback executed to encode a binary tag in DICOMweb.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a DICOM tag that contains a binary value must be written
+   * to a JSON or XML node, while a DICOMweb document is being
+   * generated. The value representation (VR) of the DICOM tag can be
+   * OB, OD, OF, OL, OW, or UN.
+   * 
+   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
+   * @param node The node being generated, as provided by Orthanc.
+   * @param setter The setter to be used to encode the content of the node. If
+   * the setter is not called, the binary tag is not written to the output document.
+   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
+   * This parameter gives the number of elements in the "levelTagGroup", 
+   * "levelTagElement", and "levelIndex" arrays.
+   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
+   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
+   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
+   * @param tagGroup The group of the DICOM tag of interest.
+   * @param tagElement The element of the DICOM tag of interest.
+   * @param vr The value representation of the binary DICOM node.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebBinaryCallback) (
+    OrthancPluginDicomWebNode*          node,
+    OrthancPluginDicomWebSetBinaryNode  setter,
+    uint32_t                            levelDepth,
+    const uint16_t*                     levelTagGroup,
+    const uint16_t*                     levelTagElement,
+    const uint32_t*                     levelIndex,
+    uint16_t                            tagGroup,
+    uint16_t                            tagElement,
+    OrthancPluginValueRepresentation    vr);
+
+
+
+  /**
+   * @brief Callback executed to encode a binary tag in DICOMweb.
+   * 
+   * Signature of a callback function that is called by Orthanc
+   * whenever a DICOM tag that contains a binary value must be written
+   * to a JSON or XML node, while a DICOMweb document is being
+   * generated. The value representation (VR) of the DICOM tag can be
+   * OB, OD, OF, OL, OW, or UN.
+   * 
+   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
+   * @param node The node being generated, as provided by Orthanc.
+   * @param setter The setter to be used to encode the content of the node. If
+   * the setter is not called, the binary tag is not written to the output document.
+   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
+   * This parameter gives the number of elements in the "levelTagGroup", 
+   * "levelTagElement", and "levelIndex" arrays.
+   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
+   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
+   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
+   * @param tagGroup The group of the DICOM tag of interest.
+   * @param tagElement The element of the DICOM tag of interest.
+   * @param vr The value representation of the binary DICOM node.
+   * @param payload The user payload.
+   * @ingroup Callbacks
+   **/
+  typedef void (*OrthancPluginDicomWebBinaryCallback2) (
+    OrthancPluginDicomWebNode*          node,
+    OrthancPluginDicomWebSetBinaryNode  setter,
+    uint32_t                            levelDepth,
+    const uint16_t*                     levelTagGroup,
+    const uint16_t*                     levelTagElement,
+    const uint32_t*                     levelIndex,
+    uint16_t                            tagGroup,
+    uint16_t                            tagElement,
+    OrthancPluginValueRepresentation    vr,
+    void*                               payload);
+
+
+
+  /**
+   * @brief Data structure that contains information about the Orthanc core.
+   **/
+  typedef struct _OrthancPluginContext_t
+  {
+    void*                     pluginsManager;
+    const char*               orthancVersion;
+    OrthancPluginFree         Free;
+    OrthancPluginErrorCode  (*InvokeService) (struct _OrthancPluginContext_t* context,
+                                              _OrthancPluginService service,
+                                              const void* params);
+  } 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.
+   * 
+   * Free a string that was allocated by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param str The string to be freed.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeString(
+    OrthancPluginContext* context, 
+    char* str)
+  {
+    if (str != NULL)
+    {
+      context->Free(str);
+    }
+  }
+
+
+  /**
+   * @brief Check that the version of the hosting Orthanc is above a given version.
+   * 
+   * 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  OrthancPluginCheckVersionAdvanced(
+    OrthancPluginContext* context,
+    int expectedMajor,
+    int expectedMinor,
+    int expectedRevision)
+  {
+    int major, minor, revision;
+
+    if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
+        sizeof(int32_t) != sizeof(_OrthancPluginService) ||
+        sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
+        sizeof(int32_t) != sizeof(OrthancPluginPixelFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginContentType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginResourceType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginChangeType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
+        sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
+        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
+        sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
+        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
+        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
+        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
+        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason))
+    {
+      /* Mismatch in the size of the enumerations */
+      return 0;
+    }
+
+    /* Assume compatibility with the mainline */
+    if (!strcmp(context->orthancVersion, "mainline"))
+    {
+      return 1;
+    }
+
+    /* Parse the version of the Orthanc core */
+    if ( 
+#ifdef _MSC_VER
+      sscanf_s
+#else
+      sscanf
+#endif
+      (context->orthancVersion, "%4d.%4d.%4d", &major, &minor, &revision) != 3)
+    {
+      return 0;
+    }
+
+    /* Check the major number of the version */
+
+    if (major > expectedMajor)
+    {
+      return 1;
+    }
+
+    if (major < expectedMajor)
+    {
+      return 0;
+    }
+
+    /* Check the minor number of the version */
+
+    if (minor > expectedMinor)
+    {
+      return 1;
+    }
+
+    if (minor < expectedMinor)
+    {
+      return 0;
+    }
+
+    /* Check the revision number of the version */
+
+    if (revision >= expectedRevision)
+    {
+      return 1;
+    }
+    else
+    {
+      return 0;
+    }
+  }
+
+
+  /**
+   * @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.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer to release.
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeMemoryBuffer(
+    OrthancPluginContext* context, 
+    OrthancPluginMemoryBuffer* buffer)
+  {
+    context->Free(buffer->data);
+  }
+
+
+  /**
+   * @brief Log an error.
+   *
+   * Log an error message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogError(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogError, message);
+  }
+
+
+  /**
+   * @brief Log a warning.
+   *
+   * Log a warning message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogWarning(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogWarning, message);
+  }
+
+
+  /**
+   * @brief Log an information.
+   *
+   * Log an information message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogInfo(
+    OrthancPluginContext* context,
+    const char* message)
+  {
+    context->InvokeService(context, _OrthancPluginService_LogInfo, message);
+  }
+
+
+
+  typedef struct
+  {
+    const char* pathRegularExpression;
+    OrthancPluginRestCallback callback;
+  } _OrthancPluginRestCallback;
+
+  /**
+   * @brief Register a REST callback.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Each REST callback is guaranteed to run in mutual exclusion.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
+   * @param callback The callback function to handle the REST call.
+   * @see OrthancPluginRegisterRestCallbackNoLock()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback(
+    OrthancPluginContext*     context,
+    const char*               pathRegularExpression,
+    OrthancPluginRestCallback callback)
+  {
+    _OrthancPluginRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRestCallback, &params);
+  }
+
+
+
+  /**
+   * @brief Register a REST callback, without locking.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Contrarily to OrthancPluginRegisterRestCallback(), the callback
+   * will NOT be invoked in mutual exclusion. This can be useful for
+   * high-performance plugins that must handle concurrent requests
+   * (Orthanc uses a pool of threads, one thread being assigned to
+   * each incoming HTTP request). Of course, if using this function,
+   * it is up to the plugin to implement the required locking
+   * mechanisms.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups.
+   * @param callback The callback function to handle the REST call.
+   * @see OrthancPluginRegisterRestCallback()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallbackNoLock(
+    OrthancPluginContext*     context,
+    const char*               pathRegularExpression,
+    OrthancPluginRestCallback callback)
+  {
+    _OrthancPluginRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRestCallbackNoLock, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginOnStoredInstanceCallback callback;
+  } _OrthancPluginOnStoredInstanceCallback;
+
+  /**
+   * @brief Register a callback for received instances.
+   *
+   * This function registers a callback function that is called
+   * whenever a new DICOM instance is stored into the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnStoredInstanceCallback(
+    OrthancPluginContext*                  context,
+    OrthancPluginOnStoredInstanceCallback  callback)
+  {
+    _OrthancPluginOnStoredInstanceCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterOnStoredInstanceCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const void*              answer;
+    uint32_t                 answerSize;
+    const char*              mimeType;
+  } _OrthancPluginAnswerBuffer;
+
+  /**
+   * @brief Answer to a REST request.
+   *
+   * This function answers to a REST request with the content of a memory buffer.
+   * 
+   * @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 answer.
+   * @param answerSize Number of bytes of the answer.
+   * @param mimeType The MIME type of the answer.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const void*              answer,
+    uint32_t                 answerSize,
+    const char*              mimeType)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = mimeType;
+    context->InvokeService(context, _OrthancPluginService_AnswerBuffer, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput*  output;
+    OrthancPluginPixelFormat  format;
+    uint32_t                  width;
+    uint32_t                  height;
+    uint32_t                  pitch;
+    const void*               buffer;
+  } _OrthancPluginCompressAndAnswerPngImage;
+
+  typedef struct
+  {
+    OrthancPluginRestOutput*  output;
+    OrthancPluginImageFormat  imageFormat;
+    OrthancPluginPixelFormat  pixelFormat;
+    uint32_t                  width;
+    uint32_t                  height;
+    uint32_t                  pitch;
+    const void*               buffer;
+    uint8_t                   quality;
+  } _OrthancPluginCompressAndAnswerImage;
+
+
+  /**
+   * @brief Answer to a REST request with a PNG image.
+   *
+   * This function answers to a REST request with a PNG image. The
+   * parameters of this function describe a memory buffer that
+   * contains an uncompressed image. The image will be automatically compressed
+   * as a PNG image by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param format The memory layout of the uncompressed image.
+   * @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 containing the uncompressed image.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerPngImage(
+    OrthancPluginContext*     context,
+    OrthancPluginRestOutput*  output,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    const void*               buffer)
+  {
+    _OrthancPluginCompressAndAnswerImage params;
+    params.output = output;
+    params.imageFormat = OrthancPluginImageFormat_Png;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = 0;  /* No quality for PNG */
+    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 instanceId;
+  } _OrthancPluginGetDicomForInstance;
+
+  /**
+   * @brief Retrieve a DICOM instance using its Orthanc identifier.
+   * 
+   * Retrieve a DICOM instance using its Orthanc identifier. The DICOM
+   * 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. 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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetDicomForInstance(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 instanceId)
+  {
+    _OrthancPluginGetDicomForInstance params;
+    params.target = target;
+    params.instanceId = instanceId;
+    return context->InvokeService(context, _OrthancPluginService_GetDicomForInstance, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+  } _OrthancPluginRestApiGet;
+
+  /**
+   * @brief Make a GET call to the built-in Orthanc REST API.
+   * 
+   * Make a GET call to the built-in Orthanc REST API. 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.
+   * @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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri)
+  {
+    _OrthancPluginRestApiGet params;
+    params.target = target;
+    params.uri = uri;
+    return context->InvokeService(context, _OrthancPluginService_RestApiGet, &params);
+  }
+
+
+
+  /**
+   * @brief Make a GET call to the REST API, as tainted by the plugins.
+   * 
+   * Make a GET call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. 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.
+   * @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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGetAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri)
+  {
+    _OrthancPluginRestApiGet params;
+    params.target = target;
+    params.uri = uri;
+    return context->InvokeService(context, _OrthancPluginService_RestApiGetAfterPlugins, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 uri;
+    const void*                 body;
+    uint32_t                    bodySize;
+  } _OrthancPluginRestApiPostPut;
+
+  /**
+   * @brief Make a POST call to the built-in Orthanc REST API.
+   * 
+   * Make a POST call to the built-in Orthanc REST API. 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 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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPost(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPost, &params);
+  }
+
+
+  /**
+   * @brief Make a POST call to the REST API, as tainted by the plugins.
+   * 
+   * Make a POST call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. 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 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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPostAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPostAfterPlugins, &params);
+  }
+
+
+
+  /**
+   * @brief Make a DELETE call to the built-in Orthanc REST API.
+   * 
+   * Make a DELETE call to the built-in Orthanc REST API.
+   * 
+   * @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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDelete(
+    OrthancPluginContext*       context,
+    const char*                 uri)
+  {
+    return context->InvokeService(context, _OrthancPluginService_RestApiDelete, uri);
+  }
+
+
+  /**
+   * @brief Make a DELETE call to the REST API, as tainted by the plugins.
+   * 
+   * Make a DELETE call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. 
+   * 
+   * @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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDeleteAfterPlugins(
+    OrthancPluginContext*       context,
+    const char*                 uri)
+  {
+    return context->InvokeService(context, _OrthancPluginService_RestApiDeleteAfterPlugins, uri);
+  }
+
+
+
+  /**
+   * @brief Make a PUT call to the built-in Orthanc REST API.
+   * 
+   * Make a PUT call to the built-in Orthanc REST API. 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 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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPut(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPut, &params);
+  }
+
+
+
+  /**
+   * @brief Make a PUT call to the REST API, as tainted by the plugins.
+   * 
+   * Make a PUT call to the Orthanc REST API, after all the plugins
+   * are applied. In other words, if some plugin overrides or adds the
+   * called URI to the built-in Orthanc REST API, this call will
+   * return the result provided by this plugin. 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 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
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPutAfterPlugins(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 uri,
+    const void*                 body,
+    uint32_t                    bodySize)
+  {
+    _OrthancPluginRestApiPostPut params;
+    params.target = target;
+    params.uri = uri;
+    params.body = body;
+    params.bodySize = bodySize;
+    return context->InvokeService(context, _OrthancPluginService_RestApiPutAfterPlugins, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              argument;
+  } _OrthancPluginOutputPlusArgument;
+
+  /**
+   * @brief Redirect a REST request.
+   *
+   * This function answers to a REST request by redirecting the user
+   * to another URI using HTTP status 301.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param redirection Where to redirect.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRedirect(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              redirection)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = redirection;
+    context->InvokeService(context, _OrthancPluginService_Redirect, &params);
+  }
+
+
+
+  typedef struct
+  {
+    char**       result;
+    const char*  argument;
+  } _OrthancPluginRetrieveDynamicString;
+
+  /**
+   * @brief Look for a patient.
+   *
+   * Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored patients).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param patientID The Patient ID of interest.
+   * @return The NULL value if the patient is non-existent, or a string containing the 
+   * Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupPatient(
+    OrthancPluginContext*  context,
+    const char*            patientID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = patientID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupPatient, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a study.
+   *
+   * Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored studies).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param studyUID The Study Instance UID of interest.
+   * @return The NULL value if the study is non-existent, or a string containing the 
+   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudy(
+    OrthancPluginContext*  context,
+    const char*            studyUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = studyUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupStudy, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a study, using the accession number.
+   *
+   * Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored studies).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param accessionNumber The Accession Number of interest.
+   * @return The NULL value if the study is non-existent, or a string containing the 
+   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudyWithAccessionNumber(
+    OrthancPluginContext*  context,
+    const char*            accessionNumber)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = accessionNumber;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupStudyWithAccessionNumber, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for a series.
+   *
+   * Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored series).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param seriesUID The Series Instance UID of interest.
+   * @return The NULL value if the series is non-existent, or a string containing the 
+   * Orthanc ID of the series. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupSeries(
+    OrthancPluginContext*  context,
+    const char*            seriesUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = seriesUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupSeries, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Look for an instance.
+   *
+   * Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018).
+   * This function uses the database index to run as fast as possible (it does not loop
+   * over all the stored instances).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param sopInstanceUID The SOP Instance UID of interest.
+   * @return The NULL value if the instance is non-existent, or a string containing the 
+   * Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupInstance(
+    OrthancPluginContext*  context,
+    const char*            sopInstanceUID)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = sopInstanceUID;
+
+    if (context->InvokeService(context, _OrthancPluginService_LookupInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    uint16_t                 status;
+  } _OrthancPluginSendHttpStatusCode;
+
+  /**
+   * @brief Send a HTTP status code.
+   *
+   * This function answers to a REST request by sending a HTTP status
+   * code (such as "400 - Bad Request"). Note that:
+   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
+   * - Redirections (status 301) must use ::OrthancPluginRedirect().
+   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
+   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param status The HTTP status code to be sent.
+   * @ingroup REST
+   * @see OrthancPluginSendHttpStatus()
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatusCode(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    uint16_t                 status)
+  {
+    _OrthancPluginSendHttpStatusCode params;
+    params.output = output;
+    params.status = status;
+    context->InvokeService(context, _OrthancPluginService_SendHttpStatusCode, &params);
+  }
+
+
+  /**
+   * @brief Signal that a REST request is not authorized.
+   *
+   * This function answers to a REST request by signaling that it is
+   * not authorized.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param realm The realm for the authorization process.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendUnauthorized(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              realm)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = realm;
+    context->InvokeService(context, _OrthancPluginService_SendUnauthorized, &params);
+  }
+
+
+  /**
+   * @brief Signal that this URI does not support this HTTP method.
+   *
+   * This function answers to a REST request by signaling that the
+   * queried URI does not support this method.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param allowedMethods The allowed methods for this URI (e.g. "GET,POST" after a PUT or a POST request).
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendMethodNotAllowed(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              allowedMethods)
+  {
+    _OrthancPluginOutputPlusArgument params;
+    params.output = output;
+    params.argument = allowedMethods;
+    context->InvokeService(context, _OrthancPluginService_SendMethodNotAllowed, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              key;
+    const char*              value;
+  } _OrthancPluginSetHttpHeader;
+
+  /**
+   * @brief Set a cookie.
+   *
+   * This function sets a cookie in the HTTP client.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param cookie The cookie to be set.
+   * @param value The value of the cookie.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetCookie(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              cookie,
+    const char*              value)
+  {
+    _OrthancPluginSetHttpHeader params;
+    params.output = output;
+    params.key = cookie;
+    params.value = value;
+    context->InvokeService(context, _OrthancPluginService_SetCookie, &params);
+  }
+
+
+  /**
+   * @brief Set some HTTP header.
+   *
+   * This function sets a HTTP header in the HTTP answer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param key The HTTP header to be set.
+   * @param value The value of the HTTP header.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpHeader(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              key,
+    const char*              value)
+  {
+    _OrthancPluginSetHttpHeader params;
+    params.output = output;
+    params.key = key;
+    params.value = value;
+    context->InvokeService(context, _OrthancPluginService_SetHttpHeader, &params);
+  }
+
+
+  typedef struct
+  {
+    char**                             resultStringToFree;
+    const char**                       resultString;
+    int64_t*                           resultInt64;
+    const char*                        key;
+    const OrthancPluginDicomInstance*  instance;
+    OrthancPluginInstanceOrigin*       resultOrigin;   /* New in Orthanc 0.9.5 SDK */
+  } _OrthancPluginAccessDicomInstance;
+
+
+  /**
+   * @brief Get the AET of a DICOM instance.
+   *
+   * This function returns the Application Entity Title (AET) of the
+   * DICOM modality from which a DICOM instance originates.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The AET if success, NULL if error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceRemoteAet, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the size of a DICOM file.
+   *
+   * This function returns the number of bytes of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The size of the file, -1 in case of error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    int64_t size;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &size;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSize, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return size;
+    }
+  }
+
+
+  /**
+   * @brief Get the data of a DICOM file.
+   *
+   * This function returns a pointer to the content of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The pointer to the DICOM data, NULL in case of error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceData, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the DICOM tag hierarchy as a JSON file.
+   *
+   * This function returns a pointer to a newly created string
+   * containing a JSON file. This JSON file encodes the tag hierarchy
+   * of the given DICOM instance.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the JSON file.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the DICOM tag hierarchy as a JSON file (with simplification).
+   *
+   * This function returns a pointer to a newly created string
+   * containing a JSON file. This JSON file encodes the tag hierarchy
+   * of the given DICOM instance. In contrast with
+   * ::OrthancPluginGetInstanceJson(), the returned JSON file is in
+   * its simplified version.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the JSON file.
+   * This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSimplifiedJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Check whether a DICOM instance is associated with some metadata.
+   *
+   * This function checks whether the DICOM instance of interest is
+   * associated with some metadata. As of Orthanc 0.8.1, in the
+   * callbacks registered by
+   * ::OrthancPluginRegisterOnStoredInstanceCallback(), the only
+   * possibly available metadata are "ReceptionDate", "RemoteAET" and
+   * "IndexInSeries".
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param metadata The metadata of interest.
+   * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE int  OrthancPluginHasInstanceMetadata(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    const char*                        metadata)
+  {
+    int64_t result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &result;
+    params.instance = instance;
+    params.key = metadata;
+
+    if (context->InvokeService(context, _OrthancPluginService_HasInstanceMetadata, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return (result != 0);
+    }
+  }
+
+
+  /**
+   * @brief Get the value of some metadata associated with a given DICOM instance.
+   *
+   * This functions returns the value of some metadata that is associated with the DICOM instance of interest.
+   * Before calling this function, the existence of the metadata must have been checked with
+   * ::OrthancPluginHasInstanceMetadata().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param metadata The metadata of interest.
+   * @return The metadata value if success, NULL if error. Please note that the 
+   *         returned string belongs to the instance object and must NOT be 
+   *         deallocated. Please make a copy of the string if you wish to access 
+   *         it later.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    const char*                        metadata)
+  {
+    const char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultString = &result;
+    params.instance = instance;
+    params.key = metadata;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceMetadata, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginStorageCreate  create;
+    OrthancPluginStorageRead    read;
+    OrthancPluginStorageRemove  remove;
+    OrthancPluginFree           free;
+  } _OrthancPluginRegisterStorageArea;
+
+  /**
+   * @brief Register a custom storage area.
+   *
+   * This function registers a custom storage area, to replace the
+   * built-in way Orthanc stores its files on the filesystem. This
+   * function must be called during the initialization of the plugin,
+   * i.e. inside the OrthancPluginInitialize() public function.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param create The callback function to store a file on the custom storage area.
+   * @param read The callback function to read a file from the custom storage area.
+   * @param remove The callback function to remove a file from the custom storage area.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageCreate  create,
+    OrthancPluginStorageRead    read,
+    OrthancPluginStorageRemove  remove)
+  {
+    _OrthancPluginRegisterStorageArea params;
+    params.create = create;
+    params.read = read;
+    params.remove = remove;
+
+#ifdef  __cplusplus
+    params.free = ::free;
+#else
+    params.free = free;
+#endif
+
+    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea, &params);
+  }
+
+
+
+  /**
+   * @brief Return the path to the Orthanc executable.
+   *
+   * This function returns the path to the Orthanc executable.
+   * 
+   * @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 path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancPath(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetOrthancPath, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the directory containing the Orthanc.
+   *
+   * This function returns the path to the directory containing the Orthanc executable.
+   * 
+   * @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 path. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancDirectory(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetOrthancDirectory, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the path to the configuration file(s).
+   *
+   * This function returns the path to the configuration file(s) that
+   * was specified when starting Orthanc. Since version 0.9.1, this
+   * path can refer to a folder that stores a set of configuration
+   * files. This function is deprecated in favor of
+   * OrthancPluginGetConfiguration().
+   * 
+   * @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 path. This string must be freed by
+   * OrthancPluginFreeString().
+   * @see OrthancPluginGetConfiguration()
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetConfigurationPath, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginOnChangeCallback callback;
+  } _OrthancPluginOnChangeCallback;
+
+  /**
+   * @brief Register a callback to monitor changes.
+   *
+   * This function registers a callback function that is called
+   * whenever a change happens to some DICOM resource.
+   *
+   * @warning If your change callback has to call the REST API of
+   * 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
+   * scripts.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback(
+    OrthancPluginContext*          context,
+    OrthancPluginOnChangeCallback  callback)
+  {
+    _OrthancPluginOnChangeCallback params;
+    params.callback = callback;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char* plugin;
+    _OrthancPluginProperty property;
+    const char* value;
+  } _OrthancPluginSetPluginProperty;
+
+
+  /**
+   * @brief Set the URI where the plugin provides its Web interface.
+   *
+   * For plugins that come with a Web interface, this function
+   * declares the entry path where to find this interface. This
+   * information is notably used in the "Plugins" page of Orthanc
+   * Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param uri The root URI for this plugin.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
+    OrthancPluginContext*  context,
+    const char*            uri)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_RootUri;
+    params.value = uri;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Set a description for this plugin.
+   *
+   * Set a description for this plugin. It is displayed in the
+   * "Plugins" page of Orthanc Explorer.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param description The description.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
+    OrthancPluginContext*  context,
+    const char*            description)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_Description;
+    params.value = description;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  /**
+   * @brief Extend the JavaScript code of Orthanc Explorer.
+   *
+   * Add JavaScript code to customize the default behavior of Orthanc
+   * Explorer. This can for instance be used to add new buttons.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param javascript The custom JavaScript code.
+   **/ 
+  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
+    OrthancPluginContext*  context,
+    const char*            javascript)
+  {
+    _OrthancPluginSetPluginProperty params;
+    params.plugin = OrthancPluginGetName();
+    params.property = _OrthancPluginProperty_OrthancExplorer;
+    params.value = javascript;
+
+    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
+  }
+
+
+  typedef struct
+  {
+    char**       result;
+    int32_t      property;
+    const char*  value;
+  } _OrthancPluginGlobalProperty;
+
+
+  /**
+   * @brief Get the value of a global property.
+   *
+   * Get the value of a global property that is stored in the Orthanc database. Global
+   * properties whose index is below 1024 are reserved by Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param property The global property of interest.
+   * @param defaultValue The value to return, if the global property is unset.
+   * @return The value of the global property, or NULL in the case of an error. This
+   * string must be freed by OrthancPluginFreeString().
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetGlobalProperty(
+    OrthancPluginContext*  context,
+    int32_t                property,
+    const char*            defaultValue)
+  {
+    char* result;
+
+    _OrthancPluginGlobalProperty params;
+    params.result = &result;
+    params.property = property;
+    params.value = defaultValue;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetGlobalProperty, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Set the value of a global property.
+   *
+   * Set the value of a global property into the Orthanc
+   * database. Setting a global property can be used by plugins to
+   * save their internal parameters. Plugins are only allowed to set
+   * properties whose index are above or equal to 1024 (properties
+   * below 1024 are read-only and reserved by Orthanc).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param property The global property of interest.
+   * @param value The value to be set in the global property.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Orthanc
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetGlobalProperty(
+    OrthancPluginContext*  context,
+    int32_t                property,
+    const char*            value)
+  {
+    _OrthancPluginGlobalProperty params;
+    params.result = NULL;
+    params.property = property;
+    params.value = value;
+
+    return context->InvokeService(context, _OrthancPluginService_SetGlobalProperty, &params);
+  }
+
+
+
+  typedef struct
+  {
+    int32_t   *resultInt32;
+    uint32_t  *resultUint32;
+    int64_t   *resultInt64;
+    uint64_t  *resultUint64;
+  } _OrthancPluginReturnSingleValue;
+
+  /**
+   * @brief Get the number of command-line arguments.
+   *
+   * Retrieve the number of command-line arguments that were used to launch Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The number of arguments.
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetCommandLineArgumentsCount(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgumentsCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+  /**
+   * @brief Get the value of a command-line argument.
+   *
+   * Get the value of one of the command-line arguments that were used
+   * to launch Orthanc. The number of available arguments can be
+   * retrieved by OrthancPluginGetCommandLineArgumentsCount().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param argument The index of the argument.
+   * @return The value of the argument, or NULL in the case of an error. This
+   * string must be freed by OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetCommandLineArgument(
+    OrthancPluginContext*  context,
+    uint32_t               argument)
+  {
+    char* result;
+
+    _OrthancPluginGlobalProperty params;
+    params.result = &result;
+    params.property = (int32_t) argument;
+    params.value = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgument, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the expected version of the database schema.
+   *
+   * Retrieve the expected version of the database schema.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The version.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetExpectedDatabaseVersion(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetExpectedDatabaseVersion, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the content of the configuration file(s).
+   *
+   * This function returns the content of the configuration that is
+   * used by Orthanc, formatted as a JSON string.
+   * 
+   * @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 configuration. This string must be freed by
+   * OrthancPluginFreeString().
+   **/
+  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfiguration(OrthancPluginContext* context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetConfiguration, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              subType;
+    const char*              contentType;
+  } _OrthancPluginStartMultipartAnswer;
+
+  /**
+   * @brief Start an HTTP multipart answer.
+   *
+   * Initiates a HTTP multipart answer, as the result of a REST request.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @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(), OrthancPluginSendMultipartItem2()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              subType,
+    const char*              contentType)
+  {
+    _OrthancPluginStartMultipartAnswer params;
+    params.output = output;
+    params.subType = subType;
+    params.contentType = contentType;
+    return context->InvokeService(context, _OrthancPluginService_StartMultipartAnswer, &params);
+  }
+
+
+  /**
+   * @brief Send an item as a part of some HTTP multipart answer.
+   *
+   * This function sends an item as a part of some HTTP multipart
+   * answer that was initiated by OrthancPluginStartMultipartAnswer().
+   * 
+   * @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.
+   * @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(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              answer,
+    uint32_t                 answerSize)
+  {
+    _OrthancPluginAnswerBuffer params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.mimeType = NULL;
+    return context->InvokeService(context, _OrthancPluginService_SendMultipartItem, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*    target;
+    const void*                   source;
+    uint32_t                      size;
+    OrthancPluginCompressionType  compression;
+    uint8_t                       uncompress;
+  } _OrthancPluginBufferCompression;
+
+
+  /**
+   * @brief Compress or decompress a buffer.
+   *
+   * This function compresses or decompresses a buffer, using the
+   * 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. 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.
+   * @param uncompress If set to "0", the buffer must be compressed. 
+   * If set to "1", the buffer must be uncompressed.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginBufferCompression(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    target,
+    const void*                   source,
+    uint32_t                      size,
+    OrthancPluginCompressionType  compression,
+    uint8_t                       uncompress)
+  {
+    _OrthancPluginBufferCompression params;
+    params.target = target;
+    params.source = source;
+    params.size = size;
+    params.compression = compression;
+    params.uncompress = uncompress;
+
+    return context->InvokeService(context, _OrthancPluginService_BufferCompression, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    const char*                 path;
+  } _OrthancPluginReadFile;
+
+  /**
+   * @brief Read a file.
+   * 
+   * Read the content of a file on the filesystem, and returns it 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 path The path of the file to be read.
+   * @return 0 if success, or the error code if failure.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginReadFile(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 path)
+  {
+    _OrthancPluginReadFile params;
+    params.target = target;
+    params.path = path;
+    return context->InvokeService(context, _OrthancPluginService_ReadFile, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char*  path;
+    const void*  data;
+    uint32_t     size;
+  } _OrthancPluginWriteFile;
+
+  /**
+   * @brief Write a file.
+   * 
+   * Write the content of a memory buffer to the filesystem.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param path The path of the file to be written.
+   * @param data The content of the memory buffer.
+   * @param size The size of the memory buffer.
+   * @return 0 if success, or the error code if failure.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWriteFile(
+    OrthancPluginContext*  context,
+    const char*            path,
+    const void*            data,
+    uint32_t               size)
+  {
+    _OrthancPluginWriteFile params;
+    params.path = path;
+    params.data = data;
+    params.size = size;
+    return context->InvokeService(context, _OrthancPluginService_WriteFile, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char**            target;
+    OrthancPluginErrorCode  error;
+  } _OrthancPluginGetErrorDescription;
+
+  /**
+   * @brief Get the description of a given error code.
+   *
+   * This function returns the description of a given error code.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param error The error code of interest.
+   * @return The error description. This is a statically-allocated
+   * string, do not free it.
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetErrorDescription(
+    OrthancPluginContext*    context,
+    OrthancPluginErrorCode   error)
+  {
+    const char* result = NULL;
+
+    _OrthancPluginGetErrorDescription params;
+    params.target = &result;
+    params.error = error;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetErrorDescription, &params) != OrthancPluginErrorCode_Success ||
+        result == NULL)
+    {
+      return "Unknown error code";
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    uint16_t                 status;
+    const char*              body;
+    uint32_t                 bodySize;
+  } _OrthancPluginSendHttpStatus;
+
+  /**
+   * @brief Send a HTTP status, with a custom body.
+   *
+   * This function answers to a HTTP request by sending a HTTP status
+   * code (such as "400 - Bad Request"), together with a body
+   * describing the error. The body will only be returned if the
+   * configuration option "HttpDescribeErrors" of Orthanc is set to "true".
+   * 
+   * Note that:
+   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
+   * - Redirections (status 301) must use ::OrthancPluginRedirect().
+   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
+   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param status The HTTP status code to be sent.
+   * @param body The body of the answer.
+   * @param bodySize The size of the body.
+   * @see OrthancPluginSendHttpStatusCode()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatus(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    uint16_t                 status,
+    const char*              body,
+    uint32_t                 bodySize)
+  {
+    _OrthancPluginSendHttpStatus params;
+    params.output = output;
+    params.status = status;
+    params.body = body;
+    params.bodySize = bodySize;
+    context->InvokeService(context, _OrthancPluginService_SendHttpStatus, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const OrthancPluginImage*  image;
+    uint32_t*                  resultUint32;
+    OrthancPluginPixelFormat*  resultPixelFormat;
+    void**                     resultBuffer;
+  } _OrthancPluginGetImageInfo;
+
+
+  /**
+   * @brief Return the pixel format of an image.
+   *
+   * This function returns the type of memory layout for the pixels of the given image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The pixel format.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginPixelFormat  OrthancPluginGetImagePixelFormat(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    OrthancPluginPixelFormat target;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultPixelFormat = &target;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImagePixelFormat, &params) != OrthancPluginErrorCode_Success)
+    {
+      return OrthancPluginPixelFormat_Unknown;
+    }
+    else
+    {
+      return (OrthancPluginPixelFormat) target;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the width of an image.
+   *
+   * This function returns the width of the given image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The width.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImageWidth(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    uint32_t width;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultUint32 = &width;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImageWidth, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return width;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the height of an image.
+   *
+   * This function returns the height of the given image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The height.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImageHeight(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    uint32_t height;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultUint32 = &height;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImageHeight, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return height;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the pitch of an image.
+   *
+   * This function returns the pitch of the given image. The pitch is
+   * defined as the number of bytes between 2 successive lines of the
+   * image in the memory buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The pitch.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImagePitch(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    uint32_t pitch;
+    
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.resultUint32 = &pitch;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImagePitch, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return pitch;
+    }
+  }
+
+
+
+  /**
+   * @brief Return a pointer to the content of an image.
+   *
+   * This function returns a pointer to the memory buffer that
+   * contains the pixels of the image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image of interest.
+   * @return The pointer.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE void*  OrthancPluginGetImageBuffer(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  image)
+  {
+    void* target = NULL;
+
+    _OrthancPluginGetImageInfo params;
+    memset(&params, 0, sizeof(params));
+    params.resultBuffer = &target;
+    params.image = image;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetImageBuffer, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginImage**       target;
+    const void*                data;
+    uint32_t                   size;
+    OrthancPluginImageFormat   format;
+  } _OrthancPluginUncompressImage;
+
+
+  /**
+   * @brief Decode a compressed image.
+   *
+   * This function decodes a compressed image from a memory buffer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param data Pointer to a memory buffer containing the compressed image.
+   * @param size Size of the memory buffer containing the compressed image.
+   * @param format The file format of the compressed image.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginUncompressImage(
+    OrthancPluginContext*      context,
+    const void*                data,
+    uint32_t                   size,
+    OrthancPluginImageFormat   format)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginUncompressImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.data = data;
+    params.size = size;
+    params.format = format;
+
+    if (context->InvokeService(context, _OrthancPluginService_UncompressImage, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginImage*   image;
+  } _OrthancPluginFreeImage;
+
+  /**
+   * @brief Free an image.
+   *
+   * This function frees an image that was decoded with OrthancPluginUncompressImage().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeImage(
+    OrthancPluginContext* context, 
+    OrthancPluginImage*   image)
+  {
+    _OrthancPluginFreeImage params;
+    params.image = image;
+
+    context->InvokeService(context, _OrthancPluginService_FreeImage, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer* target;
+    OrthancPluginImageFormat   imageFormat;
+    OrthancPluginPixelFormat   pixelFormat;
+    uint32_t                   width;
+    uint32_t                   height;
+    uint32_t                   pitch;
+    const void*                buffer;
+    uint8_t                    quality;
+  } _OrthancPluginCompressImage;
+
+
+  /**
+   * @brief Encode a PNG image.
+   *
+   * This function compresses the given memory buffer containing an
+   * image using the PNG specification, and stores the result of the
+   * compression 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 format The memory layout of the uncompressed image.
+   * @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 containing the uncompressed image.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginCompressAndAnswerPngImage()
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressPngImage(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    target,
+    OrthancPluginPixelFormat      format,
+    uint32_t                      width,
+    uint32_t                      height,
+    uint32_t                      pitch,
+    const void*                   buffer)
+  {
+    _OrthancPluginCompressImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = target;
+    params.imageFormat = OrthancPluginImageFormat_Png;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = 0;  /* Unused for PNG */
+
+    return context->InvokeService(context, _OrthancPluginService_CompressImage, &params);
+  }
+
+
+  /**
+   * @brief Encode a JPEG image.
+   *
+   * This function compresses the given memory buffer containing an
+   * image using the JPEG specification, and stores the result of the
+   * compression 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 format The memory layout of the uncompressed image.
+   * @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 containing the uncompressed image.
+   * @param quality The quality of the JPEG encoding, between 1 (worst
+   * quality, best compression) and 100 (best quality, worst
+   * compression).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressJpegImage(
+    OrthancPluginContext*         context,
+    OrthancPluginMemoryBuffer*    target,
+    OrthancPluginPixelFormat      format,
+    uint32_t                      width,
+    uint32_t                      height,
+    uint32_t                      pitch,
+    const void*                   buffer,
+    uint8_t                       quality)
+  {
+    _OrthancPluginCompressImage params;
+    memset(&params, 0, sizeof(params));
+    params.target = target;
+    params.imageFormat = OrthancPluginImageFormat_Jpeg;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = quality;
+
+    return context->InvokeService(context, _OrthancPluginService_CompressImage, &params);
+  }
+
+
+
+  /**
+   * @brief Answer to a REST request with a JPEG image.
+   *
+   * This function answers to a REST request with a JPEG image. The
+   * parameters of this function describe a memory buffer that
+   * contains an uncompressed image. The image will be automatically compressed
+   * as a JPEG image by the core system of Orthanc.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param format The memory layout of the uncompressed image.
+   * @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 containing the uncompressed image.
+   * @param quality The quality of the JPEG encoding, between 1 (worst
+   * quality, best compression) and 100 (best quality, worst
+   * compression).
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerJpegImage(
+    OrthancPluginContext*     context,
+    OrthancPluginRestOutput*  output,
+    OrthancPluginPixelFormat  format,
+    uint32_t                  width,
+    uint32_t                  height,
+    uint32_t                  pitch,
+    const void*               buffer,
+    uint8_t                   quality)
+  {
+    _OrthancPluginCompressAndAnswerImage params;
+    params.output = output;
+    params.imageFormat = OrthancPluginImageFormat_Jpeg;
+    params.pixelFormat = format;
+    params.width = width;
+    params.height = height;
+    params.pitch = pitch;
+    params.buffer = buffer;
+    params.quality = quality;
+    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    OrthancPluginHttpMethod     method;
+    const char*                 url;
+    const char*                 username;
+    const char*                 password;
+    const void*                 body;
+    uint32_t                    bodySize;
+  } _OrthancPluginCallHttpClient;
+
+
+  /**
+   * @brief Issue a HTTP GET call.
+   * 
+   * Make a HTTP GET call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. Favor
+   * OrthancPluginRestApiGet() if calling the built-in REST API of the
+   * Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @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).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpGet(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 url,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = target;
+    params.method = OrthancPluginHttpMethod_Get;
+    params.url = url;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+  /**
+   * @brief Issue a HTTP POST call.
+   * 
+   * Make a HTTP POST call to the given URL. The result to the query
+   * is stored into a newly allocated memory buffer. Favor
+   * OrthancPluginRestApiPost() if calling the built-in REST API of
+   * the Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @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.
+   * @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).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPost(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 url,
+    const void*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = target;
+    params.method = OrthancPluginHttpMethod_Post;
+    params.url = url;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+  /**
+   * @brief Issue a HTTP PUT call.
+   * 
+   * Make a HTTP PUT call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. Favor
+   * OrthancPluginRestApiPut() if calling the built-in REST API of the
+   * Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @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.
+   * @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).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPut(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    const char*                 url,
+    const void*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.target = target;
+    params.method = OrthancPluginHttpMethod_Put;
+    params.url = url;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+  /**
+   * @brief Issue a HTTP DELETE call.
+   * 
+   * Make a HTTP DELETE call to the given URL. Favor
+   * OrthancPluginRestApiDelete() if calling the built-in REST API of
+   * the Orthanc instance that hosts this plugin.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @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).
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpDelete(
+    OrthancPluginContext*       context,
+    const char*                 url,
+    const char*                 username,
+    const char*                 password)
+  {
+    _OrthancPluginCallHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    params.method = OrthancPluginHttpMethod_Delete;
+    params.url = url;
+    params.username = username;
+    params.password = password;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginImage**       target;
+    const OrthancPluginImage*  source;
+    OrthancPluginPixelFormat   targetFormat;
+  } _OrthancPluginConvertPixelFormat;
+
+
+  /**
+   * @brief Change the pixel format of an image.
+   *
+   * This function creates a new image, changing the memory layout of the pixels.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param source The source image.
+   * @param targetFormat The target pixel format.
+   * @return The resulting image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginConvertPixelFormat(
+    OrthancPluginContext*      context,
+    const OrthancPluginImage*  source,
+    OrthancPluginPixelFormat   targetFormat)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginConvertPixelFormat params;
+    params.target = &target;
+    params.source = source;
+    params.targetFormat = targetFormat;
+
+    if (context->InvokeService(context, _OrthancPluginService_ConvertPixelFormat, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Return the number of available fonts.
+   *
+   * This function returns the number of fonts that are built in the
+   * Orthanc core. These fonts can be used to draw texts on images
+   * through OrthancPluginDrawText().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return The number of fonts.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontsCount(
+    OrthancPluginContext*  context)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginReturnSingleValue params;
+    memset(&params, 0, sizeof(params));
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFontsCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    uint32_t      fontIndex; /* in */
+    const char**  name; /* out */
+    uint32_t*     size; /* out */
+  } _OrthancPluginGetFontInfo;
+
+  /**
+   * @brief Return the name of a font.
+   *
+   * This function returns the name of a font that is built in the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
+   * @return The font name. This is a statically-allocated string, do not free it.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetFontName(
+    OrthancPluginContext*  context,
+    uint32_t               fontIndex)
+  {
+    const char* result = NULL;
+
+    _OrthancPluginGetFontInfo params;
+    memset(&params, 0, sizeof(params));
+    params.name = &result;
+    params.fontIndex = fontIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Return the size of a font.
+   *
+   * This function returns the size of a font that is built in the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
+   * @return The font size.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontSize(
+    OrthancPluginContext*  context,
+    uint32_t               fontIndex)
+  {
+    uint32_t result;
+
+    _OrthancPluginGetFontInfo params;
+    memset(&params, 0, sizeof(params));
+    params.size = &result;
+    params.fontIndex = fontIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, &params) != OrthancPluginErrorCode_Success)
+    {
+      return 0;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginImage*   image;
+    uint32_t              fontIndex;
+    const char*           utf8Text;
+    int32_t               x;
+    int32_t               y;
+    uint8_t               r;
+    uint8_t               g;
+    uint8_t               b;
+  } _OrthancPluginDrawText;
+
+
+  /**
+   * @brief Draw text on an image.
+   *
+   * This function draws some text on some image.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param image The image upon which to draw the text.
+   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
+   * @param utf8Text The text to be drawn, encoded as an UTF-8 zero-terminated string.
+   * @param x The X position of the text over the image.
+   * @param y The Y position of the text over the image.
+   * @param r The value of the red color channel of the text.
+   * @param g The value of the green color channel of the text.
+   * @param b The value of the blue color channel of the text.
+   * @return 0 if success, other value if error.
+   * @ingroup Images
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginDrawText(
+    OrthancPluginContext*  context,
+    OrthancPluginImage*    image,
+    uint32_t               fontIndex,
+    const char*            utf8Text,
+    int32_t                x,
+    int32_t                y,
+    uint8_t                r,
+    uint8_t                g,
+    uint8_t                b)
+  {
+    _OrthancPluginDrawText params;
+    memset(&params, 0, sizeof(params));
+    params.image = image;
+    params.fontIndex = fontIndex;
+    params.utf8Text = utf8Text;
+    params.x = x;
+    params.y = y;
+    params.r = r;
+    params.g = g;
+    params.b = b;
+
+    return context->InvokeService(context, _OrthancPluginService_DrawText, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginStorageArea*   storageArea;
+    const char*                 uuid;
+    const void*                 content;
+    uint64_t                    size;
+    OrthancPluginContentType    type;
+  } _OrthancPluginStorageAreaCreate;
+
+
+  /**
+   * @brief Create a file inside the storage area.
+   *
+   * This function creates a new file inside the storage area that is
+   * currently used by Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storageArea The storage area.
+   * @param uuid The identifier of the file to be created.
+   * @param content The content to store in the newly created file.
+   * @param size The size of the content.
+   * @param type The type of the file content.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaCreate(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageArea*   storageArea,
+    const char*                 uuid,
+    const void*                 content,
+    uint64_t                    size,
+    OrthancPluginContentType    type)
+  {
+    _OrthancPluginStorageAreaCreate params;
+    params.storageArea = storageArea;
+    params.uuid = uuid;
+    params.content = content;
+    params.size = size;
+    params.type = type;
+
+    return context->InvokeService(context, _OrthancPluginService_StorageAreaCreate, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    OrthancPluginStorageArea*   storageArea;
+    const char*                 uuid;
+    OrthancPluginContentType    type;
+  } _OrthancPluginStorageAreaRead;
+
+
+  /**
+   * @brief Read a file from the storage area.
+   *
+   * This function reads the content of a given file from the storage
+   * area that is currently used by Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param storageArea The storage area.
+   * @param uuid The identifier of the file to be read.
+   * @param type The type of the file content.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRead(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    OrthancPluginStorageArea*   storageArea,
+    const char*                 uuid,
+    OrthancPluginContentType    type)
+  {
+    _OrthancPluginStorageAreaRead params;
+    params.target = target;
+    params.storageArea = storageArea;
+    params.uuid = uuid;
+    params.type = type;
+
+    return context->InvokeService(context, _OrthancPluginService_StorageAreaRead, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginStorageArea*   storageArea;
+    const char*                 uuid;
+    OrthancPluginContentType    type;
+  } _OrthancPluginStorageAreaRemove;
+
+  /**
+   * @brief Remove a file from the storage area.
+   *
+   * This function removes a given file from the storage area that is
+   * currently used by Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storageArea The storage area.
+   * @param uuid The identifier of the file to be removed.
+   * @param type The type of the file content.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRemove(
+    OrthancPluginContext*       context,
+    OrthancPluginStorageArea*   storageArea,
+    const char*                 uuid,
+    OrthancPluginContentType    type)
+  {
+    _OrthancPluginStorageAreaRemove params;
+    params.storageArea = storageArea;
+    params.uuid = uuid;
+    params.type = type;
+
+    return context->InvokeService(context, _OrthancPluginService_StorageAreaRemove, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginErrorCode*  target;
+    int32_t                  code;
+    uint16_t                 httpStatus;
+    const char*              message;
+  } _OrthancPluginRegisterErrorCode;
+  
+  /**
+   * @brief Declare a custom error code for this plugin.
+   *
+   * This function declares a custom error code that can be generated
+   * by this plugin. This declaration is used to enrich the body of
+   * the HTTP answer in the case of an error, and to set the proper
+   * HTTP status code.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param code The error code that is internal to this plugin.
+   * @param httpStatus The HTTP status corresponding to this error.
+   * @param message The description of the error.
+   * @return The error code that has been assigned inside the Orthanc core.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterErrorCode(
+    OrthancPluginContext*    context,
+    int32_t                  code,
+    uint16_t                 httpStatus,
+    const char*              message)
+  {
+    OrthancPluginErrorCode target;
+
+    _OrthancPluginRegisterErrorCode params;
+    params.target = &target;
+    params.code = code;
+    params.httpStatus = httpStatus;
+    params.message = message;
+
+    if (context->InvokeService(context, _OrthancPluginService_RegisterErrorCode, &params) == OrthancPluginErrorCode_Success)
+    {
+      return target;
+    }
+    else
+    {
+      /* There was an error while assigned the error. Use a generic code. */
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    uint16_t                          group;
+    uint16_t                          element;
+    OrthancPluginValueRepresentation  vr;
+    const char*                       name;
+    uint32_t                          minMultiplicity;
+    uint32_t                          maxMultiplicity;
+  } _OrthancPluginRegisterDictionaryTag;
+  
+  /**
+   * @brief Register a new tag into the DICOM dictionary.
+   *
+   * 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.
+   * @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>").
+   * @return 0 if success, other value if error.
+   * @see OrthancPluginRegisterPrivateDictionaryTag()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterDictionaryTag(
+    OrthancPluginContext*             context,
+    uint16_t                          group,
+    uint16_t                          element,
+    OrthancPluginValueRepresentation  vr,
+    const char*                       name,
+    uint32_t                          minMultiplicity,
+    uint32_t                          maxMultiplicity)
+  {
+    _OrthancPluginRegisterDictionaryTag params;
+    params.group = group;
+    params.element = element;
+    params.vr = vr;
+    params.name = name;
+    params.minMultiplicity = minMultiplicity;
+    params.maxMultiplicity = maxMultiplicity;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterDictionaryTag, &params);
+  }
+
+
+
+  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
+  {
+    OrthancPluginStorageArea*  storageArea;
+    OrthancPluginResourceType  level;
+  } _OrthancPluginReconstructMainDicomTags;
+
+  /**
+   * @brief Reconstruct the main DICOM tags.
+   *
+   * 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. A database transaction will be automatically setup.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param storageArea The storage area.
+   * @param level The type of the resources of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginReconstructMainDicomTags(
+    OrthancPluginContext*      context,
+    OrthancPluginStorageArea*  storageArea,
+    OrthancPluginResourceType  level)
+  {
+    _OrthancPluginReconstructMainDicomTags params;
+    params.level = level;
+    params.storageArea = storageArea;
+
+    return context->InvokeService(context, _OrthancPluginService_ReconstructMainDicomTags, &params);
+  }
+
+
+  typedef struct
+  {
+    char**                          result;
+    const char*                     instanceId;
+    const void*                     buffer;
+    uint32_t                        size;
+    OrthancPluginDicomToJsonFormat  format;
+    OrthancPluginDicomToJsonFlags   flags;
+    uint32_t                        maxStringLength;
+  } _OrthancPluginDicomToJson;
+
+
+  /**
+   * @brief Format a DICOM memory buffer as a JSON string.
+   *
+   * This function takes as input a memory buffer containing a DICOM
+   * file, and outputs a JSON string representing the tags of this
+   * DICOM file.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM file.
+   * @param size The size of the memory buffer.
+   * @param format The output format.
+   * @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
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   * @see OrthancPluginDicomInstanceToJson
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson(
+    OrthancPluginContext*           context,
+    const void*                     buffer,
+    uint32_t                        size,
+    OrthancPluginDicomToJsonFormat  format,
+    OrthancPluginDicomToJsonFlags   flags, 
+    uint32_t                        maxStringLength)
+  {
+    char* result;
+
+    _OrthancPluginDicomToJson params;
+    memset(&params, 0, sizeof(params));
+    params.result = &result;
+    params.buffer = buffer;
+    params.size = size;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_DicomBufferToJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Format a DICOM instance as a JSON string.
+   *
+   * This function formats a DICOM instance that is stored in Orthanc,
+   * and outputs a JSON string representing the tags of this DICOM
+   * instance.
+   *
+   * @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 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
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   * @see OrthancPluginDicomInstanceToJson
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson(
+    OrthancPluginContext*           context,
+    const char*                     instanceId,
+    OrthancPluginDicomToJsonFormat  format,
+    OrthancPluginDicomToJsonFlags   flags, 
+    uint32_t                        maxStringLength)
+  {
+    char* result;
+
+    _OrthancPluginDicomToJson params;
+    memset(&params, 0, sizeof(params));
+    params.result = &result;
+    params.instanceId = instanceId;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_DicomInstanceToJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  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 DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin(
+    OrthancPluginContext*             context,
+    const 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 decode DICOM images,
+   * extending the built-in decoder of Orthanc that uses
+   * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is
+   * affected by the configuration option
+   * "BuiltinDecoderTranscoderOrder" 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
+   * @see OrthancPluginGetInstanceDecodedFrame()
+   **/
+  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 void*                 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()
+   * @ingroup Toolbox
+   **/
+  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 void*                 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 suboperation.
+   * @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 void*                 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 void*                 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);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              details;
+    uint8_t                  log;
+  } _OrthancPluginSetHttpErrorDetails;
+
+  /**
+   * @brief Provide a detailed description for an HTTP error.
+   *
+   * This function sets the detailed description associated with an
+   * HTTP error. This description will be displayed in the "Details"
+   * field of the JSON body of the HTTP answer. It is only taken into
+   * consideration if the REST callback returns an error code that is
+   * different from "OrthancPluginErrorCode_Success", and if the
+   * "HttpDescribeErrors" configuration option of Orthanc is set to
+   * "true".
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param details The details of the error message.
+   * @param log Whether to also write the detailed error to the Orthanc logs.
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              details,
+    uint8_t                  log)
+  {
+    _OrthancPluginSetHttpErrorDetails params;
+    params.output = output;
+    params.details = details;
+    params.log = log;
+    context->InvokeService(context, _OrthancPluginService_SetHttpErrorDetails, &params);
+  }
+
+
+
+  typedef struct
+  {
+    const char** result;
+    const char*  argument;
+  } _OrthancPluginRetrieveStaticString;
+
+  /**
+   * @brief Detect the MIME type of a file.
+   *
+   * This function returns the MIME type of a file by inspecting its extension.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param path Path to the file.
+   * @return The MIME type. This is a statically-allocated
+   * string, do not free it.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginAutodetectMimeType(
+    OrthancPluginContext*  context,
+    const char*            path)
+  {
+    const char* result = NULL;
+
+    _OrthancPluginRetrieveStaticString params;
+    params.result = &result;
+    params.argument = path;
+
+    if (context->InvokeService(context, _OrthancPluginService_AutodetectMimeType, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    const char*               name;
+    float                     value;
+    OrthancPluginMetricsType  type;
+  } _OrthancPluginSetMetricsValue;
+
+  /**
+   * @brief Set the value of a metrics.
+   *
+   * This function sets the value of a metrics to monitor the behavior
+   * of the plugin through tools such as Prometheus. The values of all
+   * the metrics are stored within the Orthanc context.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param name The name of the metrics to be set.
+   * @param value The value of the metrics.
+   * @param type The type of the metrics. This parameter is only taken into consideration
+   * the first time this metrics is set.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue(
+    OrthancPluginContext*     context,
+    const char*               name,
+    float                     value,
+    OrthancPluginMetricsType  type)
+  {
+    _OrthancPluginSetMetricsValue params;
+    params.name = name;
+    params.value = value;
+    params.type = type;
+    context->InvokeService(context, _OrthancPluginService_SetMetricsValue, &params);
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginRefreshMetricsCallback  callback;
+  } _OrthancPluginRegisterRefreshMetricsCallback;
+
+  /**
+   * @brief Register a callback to refresh the metrics.
+   *
+   * This function registers a callback to refresh the metrics. The
+   * callback must make calls to OrthancPluginSetMetricsValue().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback function to handle the refresh.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback(
+    OrthancPluginContext*               context,
+    OrthancPluginRefreshMetricsCallback callback)
+  {
+    _OrthancPluginRegisterRefreshMetricsCallback params;
+    params.callback = callback;
+    context->InvokeService(context, _OrthancPluginService_RegisterRefreshMetricsCallback, &params);
+  }
+
+
+
+
+  typedef struct
+  {
+    char**                               target;
+    const void*                          dicom;
+    uint32_t                             dicomSize;
+    OrthancPluginDicomWebBinaryCallback  callback;
+  } _OrthancPluginEncodeDicomWeb;
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @see OrthancPluginCreateDicom()
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @deprecated OrthancPluginEncodeDicomWebJson2()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
+    OrthancPluginContext*                context,
+    const void*                          dicom,
+    uint32_t                             dicomSize,
+    OrthancPluginDicomWebBinaryCallback  callback)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @return The NULL value in case of error, or the XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @deprecated OrthancPluginEncodeDicomWebXml2()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
+    OrthancPluginContext*                context,
+    const void*                          dicom,
+    uint32_t                             dicomSize,
+    OrthancPluginDicomWebBinaryCallback  callback)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+
+  typedef struct
+  {
+    char**                                target;
+    const void*                           dicom;
+    uint32_t                              dicomSize;
+    OrthancPluginDicomWebBinaryCallback2  callback;
+    void*                                 payload;
+  } _OrthancPluginEncodeDicomWeb2;
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb2 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a memory buffer containing a DICOM instance,
+   * into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom Pointer to the DICOM instance.
+   * @param dicomSize Size of the DICOM instance.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @see OrthancPluginCreateDicom()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2(
+    OrthancPluginContext*                 context,
+    const void*                           dicom,
+    uint32_t                              dicomSize,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginEncodeDicomWeb2 params;
+    params.target = &target;
+    params.dicom = dicom;
+    params.dicomSize = dicomSize;
+    params.callback = callback;
+    params.payload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+
+  /**
+   * @brief Callback executed when a HTTP header is received during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, as soon as it
+   * receives one HTTP header from the answer of the remote HTTP
+   * server.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param answer The user payload, as provided by the calling plugin.
+   * @param key The key of the HTTP header.
+   * @param value The value of the HTTP header.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) (
+    void* answer,
+    const char* key,
+    const char* value);
+
+
+  /**
+   * @brief Callback executed when an answer chunk is received during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, as soon as it
+   * receives one data chunk from the answer of the remote HTTP
+   * server.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param answer The user payload, as provided by the calling plugin.
+   * @param data The content of the data chunk.
+   * @param size The size of the data chunk.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) (
+    void* answer,
+    const void* data,
+    uint32_t size);
+  
+
+  /**
+   * @brief Callback to know whether the request body is entirely read during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must answer "1" as
+   * soon as the body is entirely read: The "request" data structure
+   * must act as an iterator.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return "1" if the body is over, or "0" if there is still data to be read.
+   * @ingroup Toolbox
+   **/
+  typedef uint8_t (*OrthancPluginChunkedClientRequestIsDone) (void* request);
+
+
+  /**
+   * @brief Callback to advance in the request body during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. This function asks the plugin
+   * to advance to the next chunk of data of the request body: The
+   * "request" data structure must act as an iterator.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientRequestNext) (void* request);
+
+
+  /**
+   * @brief Callback to read the current chunk of the request body during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must provide the
+   * content of the current chunk of data of the request body.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return The content of the current request chunk.
+   * @ingroup Toolbox
+   **/
+  typedef const void* (*OrthancPluginChunkedClientRequestGetChunkData) (void* request);
+
+
+  /**
+   * @brief Callback to read the size of the current request chunk during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must provide the
+   * size of the current chunk of data of the request body.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return The size of the current request chunk.
+   * @ingroup Toolbox
+   **/
+  typedef uint32_t (*OrthancPluginChunkedClientRequestGetChunkSize) (void* request);
+
+  
+  typedef struct
+  {
+    void*                                          answer;
+    OrthancPluginChunkedClientAnswerAddChunk       answerAddChunk;
+    OrthancPluginChunkedClientAnswerAddHeader      answerAddHeader;
+    uint16_t*                                      httpStatus;
+    OrthancPluginHttpMethod                        method;
+    const char*                                    url;
+    uint32_t                                       headersCount;
+    const char* const*                             headersKeys;
+    const char* const*                             headersValues;
+    void*                                          request;
+    OrthancPluginChunkedClientRequestIsDone        requestIsDone;
+    OrthancPluginChunkedClientRequestGetChunkData  requestChunkData;
+    OrthancPluginChunkedClientRequestGetChunkSize  requestChunkSize;
+    OrthancPluginChunkedClientRequestNext          requestNext;
+    const char*                                    username;
+    const char*                                    password;
+    uint32_t                                       timeout;
+    const char*                                    certificateFile;
+    const char*                                    certificateKeyFile;
+    const char*                                    certificateKeyPassword;
+    uint8_t                                        pkcs11;
+  } _OrthancPluginChunkedHttpClient;
+
+  
+  /**
+   * @brief Issue a HTTP call, using chunked HTTP transfers.
+   * 
+   * Make a HTTP call to the given URL using chunked HTTP
+   * transfers. The request body is provided as an iterator over data
+   * chunks. The answer is provided as a sequence of function calls
+   * with the individual HTTP headers and answer chunks.
+   * 
+   * Contrarily to OrthancPluginHttpClient() that entirely stores the
+   * request body and the answer body in memory buffers, this function
+   * uses chunked HTTP transfers. This results in a lower memory
+   * consumption. Pay attention to the fact that Orthanc servers with
+   * version <= 1.5.6 do not support chunked transfers: You must use
+   * OrthancPluginHttpClient() if contacting such older servers.
+   *
+   * 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 answer The user payload for the answer body. It will be provided to the callbacks for the answer.
+   * @param answerAddChunk Callback function to report a data chunk from the answer body.
+   * @param answerAddHeader Callback function to report an HTTP header sent by the remote server.
+   * @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 request The user payload containing the request body, and acting as an iterator.
+   * It will be provided to the callbacks for the request.
+   * @param requestIsDone Callback function to tell whether the request body is entirely read.
+   * @param requestChunkData Callback function to get the content of the current data chunk of the request body.
+   * @param requestChunkSize Callback function to get the size of the current data chunk of the request body.
+   * @param requestNext Callback function to advance to the next data chunk of the request body.
+   * @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 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 OrthancPluginHttpClient()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginChunkedHttpClient(
+    OrthancPluginContext*                          context,
+    void*                                          answer,
+    OrthancPluginChunkedClientAnswerAddChunk       answerAddChunk,
+    OrthancPluginChunkedClientAnswerAddHeader      answerAddHeader,
+    uint16_t*                                      httpStatus,
+    OrthancPluginHttpMethod                        method,
+    const char*                                    url,
+    uint32_t                                       headersCount,
+    const char* const*                             headersKeys,
+    const char* const*                             headersValues,
+    void*                                          request,
+    OrthancPluginChunkedClientRequestIsDone        requestIsDone,
+    OrthancPluginChunkedClientRequestGetChunkData  requestChunkData,
+    OrthancPluginChunkedClientRequestGetChunkSize  requestChunkSize,
+    OrthancPluginChunkedClientRequestNext          requestNext,
+    const char*                                    username,
+    const char*                                    password,
+    uint32_t                                       timeout,
+    const char*                                    certificateFile,
+    const char*                                    certificateKeyFile,
+    const char*                                    certificateKeyPassword,
+    uint8_t                                        pkcs11)
+  {
+    _OrthancPluginChunkedHttpClient params;
+    memset(&params, 0, sizeof(params));
+
+    /* In common with OrthancPluginHttpClient() */
+    params.httpStatus = httpStatus;
+    params.method = method;
+    params.url = url;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.username = username;
+    params.password = password;
+    params.timeout = timeout;
+    params.certificateFile = certificateFile;
+    params.certificateKeyFile = certificateKeyFile;
+    params.certificateKeyPassword = certificateKeyPassword;
+    params.pkcs11 = pkcs11;
+
+    /* For chunked body/answer */
+    params.answer = answer;
+    params.answerAddChunk = answerAddChunk;
+    params.answerAddHeader = answerAddHeader;
+    params.request = request;
+    params.requestIsDone = requestIsDone;
+    params.requestChunkData = requestChunkData;
+    params.requestChunkSize = requestChunkSize;
+    params.requestNext = requestNext;
+
+    return context->InvokeService(context, _OrthancPluginService_ChunkedHttpClient, &params);
+  }
+
+
+
+  /**
+   * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer.
+   * @ingroup Callback
+   **/
+  typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader;
+
+
+
+  /**
+   * @brief Callback to create a reader to handle incoming chunked HTTP transfers.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is only invoked if the HTTP method is POST or PUT. The
+   * callback must create an user-specific "reader" object that will
+   * be fed with the body of the incoming body.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader Memory location that must be filled with the newly-created reader.
+   * @param url The URI that is accessed.
+   * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) (
+    OrthancPluginServerChunkedRequestReader**  reader,
+    const char*                                url,
+    const OrthancPluginHttpRequest*            request);
+
+  
+  /**
+   * @brief Callback invoked whenever a new data chunk is available during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This callback
+   * is invoked as soon as a new data chunk is available for the request body.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   * @param data The content of the data chunk.
+   * @param size The size of the data chunk.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderAddChunk) (
+    OrthancPluginServerChunkedRequestReader* reader,
+    const void*                              data,
+    uint32_t                                 size);
+    
+
+  /**
+   * @brief Callback invoked whenever the request body is entirely received.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is invoked as soon as the full body of the HTTP request
+   * is available. The plugin can then send its answer thanks to the
+   * provided "output" object.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   * @param output The HTTP connection to the client application.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderExecute) (
+    OrthancPluginServerChunkedRequestReader* reader,
+    OrthancPluginRestOutput*                 output);
+    
+
+  /**
+   * @brief Callback invoked to release the resources associated with an incoming HTTP chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is invoked to release all the resources allocated by the
+   * given reader. Note that this function might be invoked even if
+   * the entire body was not read, to deal with client error or
+   * disconnection.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   **/
+  typedef void (*OrthancPluginServerChunkedRequestReaderFinalize) (
+    OrthancPluginServerChunkedRequestReader* reader);
+  
+  typedef struct
+  {
+    const char*                                      pathRegularExpression;
+    OrthancPluginRestCallback                        getHandler;
+    OrthancPluginServerChunkedRequestReaderFactory   postHandler;
+    OrthancPluginRestCallback                        deleteHandler;
+    OrthancPluginServerChunkedRequestReaderFactory   putHandler;
+    OrthancPluginServerChunkedRequestReaderAddChunk  addChunk;
+    OrthancPluginServerChunkedRequestReaderExecute   execute;
+    OrthancPluginServerChunkedRequestReaderFinalize  finalize;
+  } _OrthancPluginChunkedRestCallback;
+
+
+  /**
+   * @brief Register a REST callback to handle chunked HTTP transfers.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Contrarily to OrthancPluginRegisterRestCallback(), the callbacks
+   * will NOT be invoked in mutual exclusion, so it is up to the
+   * plugin to implement the required locking mechanisms.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
+   * @param getHandler The callback function to handle REST calls using the GET HTTP method.
+   * @param postHandler The callback function to handle REST calls using the GET POST method.
+   * @param deleteHandler The callback function to handle REST calls using the GET DELETE method.
+   * @param putHandler The callback function to handle REST calls using the GET PUT method.
+   * @param addChunk The callback invoked when a new chunk is available for the request body of a POST or PUT call.
+   * @param execute The callback invoked once the entire body of a POST or PUT call is read.
+   * @param finalize The callback invoked to release the resources associated with a POST or PUT call.
+   * @see OrthancPluginRegisterRestCallbackNoLock()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback(
+    OrthancPluginContext*                            context,
+    const char*                                      pathRegularExpression,
+    OrthancPluginRestCallback                        getHandler,
+    OrthancPluginServerChunkedRequestReaderFactory   postHandler,
+    OrthancPluginRestCallback                        deleteHandler,
+    OrthancPluginServerChunkedRequestReaderFactory   putHandler,
+    OrthancPluginServerChunkedRequestReaderAddChunk  addChunk,
+    OrthancPluginServerChunkedRequestReaderExecute   execute,
+    OrthancPluginServerChunkedRequestReaderFinalize  finalize)
+  {
+    _OrthancPluginChunkedRestCallback params;
+    params.pathRegularExpression = pathRegularExpression;
+    params.getHandler = getHandler;
+    params.postHandler = postHandler;
+    params.deleteHandler = deleteHandler;
+    params.putHandler = putHandler;
+    params.addChunk = addChunk;
+    params.execute = execute;
+    params.finalize = finalize;
+
+    context->InvokeService(context, _OrthancPluginService_RegisterChunkedRestCallback, &params);
+  }
+
+
+
+
+
+  typedef struct
+  {
+    char**       result;
+    uint16_t     group;
+    uint16_t     element;
+    const char*  privateCreator;
+  } _OrthancPluginGetTagName;
+
+  /**
+   * @brief Returns the symbolic name of a DICOM tag.
+   *
+   * This function makes a lookup to the dictionary of DICOM tags that
+   * are known to Orthanc, and returns the symbolic name of a DICOM tag.
+   *
+   * @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 privateCreator For private tags, the name of the private creator (can be NULL).
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName(
+    OrthancPluginContext*  context,
+    uint16_t               group,
+    uint16_t               element,
+    const char*            privateCreator)
+  {
+    char* result;
+
+    _OrthancPluginGetTagName params;
+    params.result = &result;
+    params.group = group;
+    params.element = element;
+    params.privateCreator = privateCreator;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetTagName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+  /**
+   * @brief Callback executed by the storage commitment SCP.
+   *
+   * Signature of a factory function that creates an object to handle
+   * one incoming storage commitment request.
+   *
+   * @remark The factory receives the list of the SOP class/instance
+   * UIDs of interest to the remote storage commitment SCU. This gives
+   * the factory the possibility to start some prefetch process
+   * upfront in the background, before the handler object is actually
+   * queried about the status of these DICOM instances.
+   *
+   * @param handler Output variable where the factory puts the handler object it created.
+   * @param jobId ID of the Orthanc job that is responsible for handling 
+   * the storage commitment request. This job will successively look for the
+   * status of all the individual queried DICOM instances.
+   * @param transactionUid UID of the storage commitment transaction
+   * provided by the storage commitment SCU. It contains the value of the
+   * (0008,1195) DICOM tag.
+   * @param sopClassUids Array of the SOP class UIDs (0008,0016) that are queried by the SCU.
+   * @param sopInstanceUids Array of the SOP instance UIDs (0008,0018) that are queried by the SCU.
+   * @param countInstances Number of DICOM instances that are queried. This is the size
+   * of the `sopClassUids` and `sopInstanceUids` arrays.
+   * @param remoteAet The AET of the storage commitment SCU.
+   * @param calledAet The AET used by the SCU to contact the storage commitment SCP (i.e. Orthanc).
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentFactory) (
+    void**              handler /* out */,
+    const char*         jobId,
+    const char*         transactionUid,
+    const char* const*  sopClassUids,
+    const char* const*  sopInstanceUids,
+    uint32_t            countInstances,
+    const char*         remoteAet,
+    const char*         calledAet);
+
+  
+  /**
+   * @brief Callback to free one storage commitment SCP handler.
+   * 
+   * Signature of a callback function that releases the resources
+   * allocated by the factory of the storage commitment SCP. The
+   * handler is the return value of a previous call to the
+   * OrthancPluginStorageCommitmentFactory() callback.
+   *
+   * @param handler The handler object to be destructed.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler);
+
+
+  /**
+   * @brief Callback to get the status of one DICOM instance in the
+   * storage commitment SCP.
+   *
+   * Signature of a callback function that is successively invoked for
+   * each DICOM instance that is queried by the remote storage
+   * commitment SCU.  The function must be tought of as a method of
+   * the handler object that was created by a previous call to the
+   * OrthancPluginStorageCommitmentFactory() callback. After each call
+   * to this method, the progress of the associated Orthanc job is
+   * updated.
+   * 
+   * @param target Output variable where to put the status for the queried instance.
+   * @param handler The handler object associated with this storage commitment request.
+   * @param sopClassUid The SOP class UID (0008,0016) of interest.
+   * @param sopInstanceUid The SOP instance UID (0008,0018) of interest.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) (
+    OrthancPluginStorageCommitmentFailureReason* target,
+    void* handler,
+    const char* sopClassUid,
+    const char* sopInstanceUid);
+    
+    
+  typedef struct
+  {
+    OrthancPluginStorageCommitmentFactory     factory;
+    OrthancPluginStorageCommitmentDestructor  destructor;
+    OrthancPluginStorageCommitmentLookup      lookup;
+  } _OrthancPluginRegisterStorageCommitmentScpCallback;
+
+  /**
+   * @brief Register a callback to handle incoming requests to the storage commitment SCP.
+   *
+   * This function registers a callback to handle storage commitment SCP requests.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param factory Factory function that creates the handler object
+   * for incoming storage commitment requests.
+   * @param destructor Destructor function to destroy the handler object.
+   * @param lookup Callback method to get the status of one DICOM instance.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback(
+    OrthancPluginContext*                     context,
+    OrthancPluginStorageCommitmentFactory     factory,
+    OrthancPluginStorageCommitmentDestructor  destructor,
+    OrthancPluginStorageCommitmentLookup      lookup)
+  {
+    _OrthancPluginRegisterStorageCommitmentScpCallback params;
+    params.factory = factory;
+    params.destructor = destructor;
+    params.lookup = lookup;
+    return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, &params);
+  }
+  
+
+
+  /**
+   * @brief Callback to filter incoming DICOM instances received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a new DICOM instance (e.g. through REST API or
+   * DICOM protocol), and that answers whether this DICOM instance
+   * should be accepted or discarded by Orthanc.
+   *
+   * Note that the metadata information is not available
+   * (i.e. GetInstanceMetadata() should not be used on "instance").
+   *
+   * @param instance The received DICOM instance.
+   * @return 0 to discard the instance, 1 to store the instance, -1 if error.
+   * @ingroup Callback
+   **/
+  typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) (
+    const OrthancPluginDicomInstance* instance);
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingDicomInstanceFilter callback;
+  } _OrthancPluginIncomingDicomInstanceFilter;
+
+  /**
+   * @brief Register a callback to filter incoming DICOM instance.
+   *
+   * This function registers a custom callback to filter incoming
+   * DICOM instances received by Orthanc (either through the REST API
+   * or through the DICOM protocol).
+   *
+   * @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 OrthancPluginRegisterIncomingDicomInstanceFilter(
+    OrthancPluginContext*                     context,
+    OrthancPluginIncomingDicomInstanceFilter  callback)
+  {
+    _OrthancPluginIncomingDicomInstanceFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingDicomInstanceFilter, &params);
+  }
+
+
+  /**
+   * @brief Get the transfer syntax of a DICOM file.
+   *
+   * This function returns a pointer to a newly created string that
+   * contains the transfer syntax UID of the DICOM instance. The empty
+   * string might be returned if this information is unknown.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The NULL value in case of error, or a string containing the
+   * transfer syntax UID. This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance)
+  {
+    char* result;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultStringToFree = &result;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceTransferSyntaxUid, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Check whether the DICOM file has pixel data.
+   *
+   * This function returns a Boolean value indicating whether the
+   * DICOM instance contains the pixel data (7FE0,0010) tag.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return "1" if the DICOM instance contains pixel data, or "0" if
+   * the tag is missing, or "-1" in the case of an error.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    int64_t hasPixelData;
+
+    _OrthancPluginAccessDicomInstance params;
+    memset(&params, 0, sizeof(params));
+    params.resultInt64 = &hasPixelData;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_HasInstancePixelData, &params) != OrthancPluginErrorCode_Success ||
+        hasPixelData < 0 ||
+        hasPixelData > 1)
+    {
+      /* Error */
+      return -1;
+    }
+    else
+    {
+      return (hasPixelData != 0);
+    }
+  }
+
+
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginDicomInstance**  target;
+    const void*                   buffer;
+    uint32_t                      size;
+    const char*                   transferSyntax;
+  } _OrthancPluginCreateDicomInstance;
+
+  /**
+   * @brief Parse a DICOM instance.
+   *
+   * This function parses a memory buffer that contains a DICOM
+   * file. The function returns a new pointer to a data structure that
+   * is managed by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM instance.
+   * @param size The size of the memory buffer.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size)
+  {
+    OrthancPluginDicomInstance* target = NULL;
+
+    _OrthancPluginCreateDicomInstance params;
+    params.target = &target;
+    params.buffer = buffer;
+    params.size = size;
+
+    if (context->InvokeService(context, _OrthancPluginService_CreateDicomInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+  typedef struct
+  {
+    OrthancPluginDicomInstance*   dicom;
+  } _OrthancPluginFreeDicomInstance;
+
+  /**
+   * @brief Free a DICOM instance.
+   *
+   * This function frees a DICOM instance that was parsed using
+   * OrthancPluginCreateDicomInstance().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param dicom The DICOM instance.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeDicomInstance(
+    OrthancPluginContext*        context, 
+    OrthancPluginDicomInstance*  dicom)
+  {
+    _OrthancPluginFreeDicomInstance params;
+    params.dicom = dicom;
+
+    context->InvokeService(context, _OrthancPluginService_FreeDicomInstance, &params);
+  }
+
+
+  typedef struct
+  {
+    uint32_t*                             targetUint32;
+    OrthancPluginMemoryBuffer*            targetBuffer;
+    OrthancPluginImage**                  targetImage;
+    char**                                targetStringToFree;
+    const OrthancPluginDicomInstance*     instance;
+    uint32_t                              frameIndex;
+    OrthancPluginDicomToJsonFormat        format;
+    OrthancPluginDicomToJsonFlags         flags;
+    uint32_t                              maxStringLength;
+    OrthancPluginDicomWebBinaryCallback2  dicomWebCallback;
+    void*                                 dicomWebPayload;
+  } _OrthancPluginAccessDicomInstance2;
+
+  /**
+   * @brief Get the number of frames in a DICOM instance.
+   *
+   * This function returns the number of frames that are part of a
+   * DICOM image managed by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @return The number of frames (will be zero in the case of an error).
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance)
+  {
+    uint32_t count;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetUint32 = &count;
+    params.instance = instance;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceFramesCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+  /**
+   * @brief Get the raw content of a frame in a DICOM instance.
+   *
+   * This function returns a memory buffer containing the raw content
+   * of a frame in a DICOM instance that is managed by the Orthanc
+   * core. This is notably useful for compressed transfer syntaxes, as
+   * it gives access to the embedded files (such as JPEG, JPEG-LS or
+   * JPEG2k). The Orthanc core transparently reassembles the fragments
+   * to extract the raw frame.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instance The instance of interest.
+   * @param frameIndex The index of the frame of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame(
+    OrthancPluginContext*             context,
+    OrthancPluginMemoryBuffer*        target,
+    const OrthancPluginDicomInstance* instance,
+    uint32_t                          frameIndex)
+  {
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetBuffer = target;
+    params.instance = instance;
+    params.frameIndex = frameIndex;
+
+    return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, &params);
+  }
+
+
+  /**
+   * @brief Decode one frame from a DICOM instance.
+   *
+   * This function decodes one frame of a DICOM image that is managed
+   * by the Orthanc core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The instance of interest.
+   * @param frameIndex The index of the frame of interest.
+   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame(
+    OrthancPluginContext*             context,
+    const OrthancPluginDicomInstance* instance,
+    uint32_t                          frameIndex)
+  {
+    OrthancPluginImage* target = NULL;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetImage = &target;
+    params.instance = instance;
+    params.frameIndex = frameIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDecodedFrame, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+  
+  /**
+   * @brief Parse and transcode a DICOM instance.
+   *
+   * This function parses a memory buffer that contains a DICOM file,
+   * then transcodes it to the given transfer syntax. The function
+   * returns a new pointer to a data structure that is managed by the
+   * Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param buffer The memory buffer containing the DICOM instance.
+   * @param size The size of the memory buffer.
+   * @param transferSyntax The transfer syntax UID for the transcoding.
+   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance(
+    OrthancPluginContext*  context,
+    const void*            buffer,
+    uint32_t               size,
+    const char*            transferSyntax)
+  {
+    OrthancPluginDicomInstance* target = NULL;
+
+    _OrthancPluginCreateDicomInstance params;
+    params.target = &target;
+    params.buffer = buffer;
+    params.size = size;
+    params.transferSyntax = transferSyntax;
+
+    if (context->InvokeService(context, _OrthancPluginService_TranscodeDicomInstance, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+  /**
+   * @brief Writes a DICOM instance to a memory buffer.
+   *
+   * This function returns a memory buffer containing the
+   * serialization of a DICOM instance that is managed by the Orthanc
+   * core.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param instance The instance of interest.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance(
+    OrthancPluginContext*             context,
+    OrthancPluginMemoryBuffer*        target,
+    const OrthancPluginDicomInstance* instance)
+  {
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetBuffer = target;
+    params.instance = instance;
+
+    return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, &params);
+  }
+  
+
+  /**
+   * @brief Format a DICOM memory buffer as a JSON string.
+   *
+   * This function takes as DICOM instance managed by the Orthanc
+   * core, and outputs a JSON string representing the tags of this
+   * DICOM file.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param format The output format.
+   * @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
+   * string. This string must be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   * @see OrthancPluginDicomBufferToJson
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson(
+    OrthancPluginContext*              context,
+    const OrthancPluginDicomInstance*  instance,
+    OrthancPluginDicomToJsonFormat     format,
+    OrthancPluginDicomToJsonFlags      flags, 
+    uint32_t                           maxStringLength)
+  {
+    char* result = NULL;
+
+    _OrthancPluginAccessDicomInstance2 params;
+    memset(&params, 0, sizeof(params));
+    params.targetStringToFree = &result;
+    params.instance = instance;
+    params.format = format;
+    params.flags = flags;
+    params.maxStringLength = maxStringLength;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb JSON.
+   *
+   * This function converts a DICOM instance that is managed by the
+   * Orthanc core, into its DICOMweb JSON representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the JSON document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson(
+    OrthancPluginContext*                 context,
+    const OrthancPluginDicomInstance*     instance,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginAccessDicomInstance2 params;
+    params.targetStringToFree = &target;
+    params.instance = instance;
+    params.dicomWebCallback = callback;
+    params.dicomWebPayload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+  
+
+  /**
+   * @brief Convert a DICOM instance to DICOMweb XML.
+   *
+   * This function converts a DICOM instance that is managed by the
+   * Orthanc core, into its DICOMweb XML representation.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param instance The DICOM instance of interest.
+   * @param callback Callback to set the value of the binary tags.
+   * @param payload User payload.
+   * @return The NULL value in case of error, or the XML document. This string must
+   * be freed by OrthancPluginFreeString().
+   * @ingroup DicomInstance
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml(
+    OrthancPluginContext*                 context,
+    const OrthancPluginDicomInstance*     instance,
+    OrthancPluginDicomWebBinaryCallback2  callback,
+    void*                                 payload)
+  {
+    char* target = NULL;
+    
+    _OrthancPluginAccessDicomInstance2 params;
+    params.targetStringToFree = &target;
+    params.instance = instance;
+    params.dicomWebCallback = callback;
+    params.dicomWebPayload = payload;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  /**
+   * @brief Signature of a callback function to transcode a DICOM instance.
+   * @param transcoded Target memory buffer. It must be allocated by the
+   * plugin using OrthancPluginCreateMemoryBuffer().
+   * @param buffer Memory buffer containing the source DICOM instance.
+   * @param size Size of the source memory buffer.
+   * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the
+   * result of the transcoding. The plugin must choose by itself the 
+   * transfer syntax that will be used for the resulting DICOM image.
+   * @param countSyntaxes The number of transfer syntaxes that are contained
+   * in the "allowedSyntaxes" array.
+   * @param allowNewSopInstanceUid Whether the transcoding plugin can select
+   * a transfer syntax that will change the SOP instance UID (or, in other 
+   * terms, whether the plugin can transcode using lossy compression).
+   * @return 0 if success (i.e. image successfully transcoded and stored into
+   * "transcoded"), or the error code if failure.
+   * @ingroup Callbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) (
+    OrthancPluginMemoryBuffer* transcoded /* out */,
+    const void*                buffer,
+    uint64_t                   size,
+    const char* const*         allowedSyntaxes,
+    uint32_t                   countSyntaxes,
+    uint8_t                    allowNewSopInstanceUid);
+
+
+  typedef struct
+  {
+    OrthancPluginTranscoderCallback callback;
+  } _OrthancPluginTranscoderCallback;
+
+  /**
+   * @brief Register a callback to handle the transcoding of DICOM images.
+   *
+   * This function registers a custom callback to transcode DICOM
+   * images, extending the built-in transcoder of Orthanc that uses
+   * DCMTK. The exact behavior is affected by the configuration option
+   * "BuiltinDecoderTranscoderOrder" 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 OrthancPluginRegisterTranscoderCallback(
+    OrthancPluginContext*            context,
+    OrthancPluginTranscoderCallback  callback)
+  {
+    _OrthancPluginTranscoderCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  target;
+    uint32_t                    size;
+  } _OrthancPluginCreateMemoryBuffer;
+
+  /**
+   * @brief Create a memory buffer.
+   *
+   * This function creates a memory buffer that is managed by the
+   * Orthanc core. The main use case of this function is for plugins
+   * that act as DICOM transcoders.
+   * 
+   * Your plugin should never call "free()" on the resulting memory
+   * buffer, as the C library that is used by the plugin is in general
+   * not the same as the one used by the Orthanc core.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param size Size of the memory buffer to be created.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  target,
+    uint32_t                    size)
+  {
+    _OrthancPluginCreateMemoryBuffer params;
+    params.target = target;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, &params);
+  }
+  
+
+#ifdef  __cplusplus
+}
+#endif
+
+
+/** @} */
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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/OrthancServer/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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(const 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";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Basic/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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(PluginTest SHARED Plugin.c)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Basic/Plugin.c	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,554 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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.h>
+#include <stdio.h>
+
+static OrthancPluginContext* context = NULL;
+
+static OrthancPluginErrorCode customError;
+
+
+OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
+{
+  char buffer[1024];
+  uint32_t i;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    /**
+     * NB: Calling "OrthancPluginSendMethodNotAllowed(context, output,
+     * "GET");" is preferable. This is a sample to demonstrate
+     * "OrthancPluginSetHttpErrorDetails()". 
+     **/
+    OrthancPluginSetHttpErrorDetails(context, output, "This Callback1() can only be used by a GET call", 1 /* log */);
+    return OrthancPluginErrorCode_ParameterOutOfRange;
+  }
+  
+  sprintf(buffer, "Callback on URL [%s] with body [%s]\n", url, (const char*) request->body);
+  OrthancPluginLogWarning(context, buffer);
+
+  OrthancPluginSetCookie(context, output, "hello", "world");
+  OrthancPluginAnswerBuffer(context, output, buffer, strlen(buffer), "text/plain");
+
+  OrthancPluginLogWarning(context, "");    
+
+  for (i = 0; i < request->groupsCount; i++)
+  {
+    sprintf(buffer, "  REGEX GROUP %d = [%s]", i, request->groups[i]);
+    OrthancPluginLogWarning(context, buffer);    
+  }
+
+  OrthancPluginLogWarning(context, "");    
+
+  for (i = 0; i < request->getCount; i++)
+  {
+    sprintf(buffer, "  GET [%s] = [%s]", request->getKeys[i], request->getValues[i]);
+    OrthancPluginLogWarning(context, buffer);    
+  }
+
+  OrthancPluginLogWarning(context, "");
+
+  for (i = 0; i < request->headersCount; i++)
+  {
+    sprintf(buffer, "  HEADERS [%s] = [%s]", request->headersKeys[i], request->headersValues[i]);
+    OrthancPluginLogWarning(context, buffer);    
+  }
+
+  OrthancPluginLogWarning(context, "");
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
+{
+  /* Answer with a sample 16bpp image. */
+
+  uint16_t buffer[256 * 256];
+  uint32_t x, y, value;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    value = 0;
+    for (y = 0; y < 256; y++)
+    {
+      for (x = 0; x < 256; x++, value++)
+      {
+        buffer[value] = value;
+      }
+    }
+
+    OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale16,
+                                           256, 256, sizeof(uint16_t) * 256, buffer);
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    OrthancPluginMemoryBuffer dicom;
+    if (!OrthancPluginGetDicomForInstance(context, &dicom, request->groups[0]))
+    {
+      /* No error, forward the DICOM file */
+      OrthancPluginAnswerBuffer(context, output, dicom.data, dicom.size, "application/dicom");
+
+      /* Free memory */
+      OrthancPluginFreeMemoryBuffer(context, &dicom);
+    }
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
+{
+  /* Answer with a sample 8bpp image. */
+
+  uint8_t  buffer[256 * 256];
+  uint32_t x, y, value;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    value = 0;
+    for (y = 0; y < 256; y++)
+    {
+      for (x = 0; x < 256; x++, value++)
+      {
+        buffer[value] = x;
+      }
+    }
+
+    OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale8,
+                                           256, 256, 256, buffer);
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
+{
+  /**
+   * Demonstration the difference between the
+   * "OrthancPluginRestApiXXX()" and the
+   * "OrthancPluginRestApiXXXAfterPlugins()" mechanisms to forward
+   * REST calls.
+   *
+   * # curl http://localhost:8042/forward/built-in/system
+   * # curl http://localhost:8042/forward/plugins/system
+   * # curl http://localhost:8042/forward/built-in/plugin/image
+   *   => FAILURE (because the "/plugin/image" URI is implemented by this plugin)
+   * # curl http://localhost:8042/forward/plugins/plugin/image  => SUCCESS
+   **/
+
+  OrthancPluginMemoryBuffer tmp;
+  int isBuiltIn, error;
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return OrthancPluginErrorCode_Success;
+  }
+
+  isBuiltIn = strcmp("plugins", request->groups[0]);
+ 
+  if (isBuiltIn)
+  {
+    error = OrthancPluginRestApiGet(context, &tmp, request->groups[1]);
+  }
+  else
+  {
+    error = OrthancPluginRestApiGetAfterPlugins(context, &tmp, request->groups[1]);
+  }
+
+  if (error)
+  {
+    return OrthancPluginErrorCode_InternalError;
+  }
+  else
+  {
+    OrthancPluginAnswerBuffer(context, output, tmp.data, tmp.size, "application/octet-stream");
+    OrthancPluginFreeMemoryBuffer(context, &tmp);
+    return OrthancPluginErrorCode_Success;
+  }
+}
+
+
+OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output,
+                                           const char* url,
+                                           const OrthancPluginHttpRequest* request)
+{
+  const char* pathLocator = "\"Path\" : \"";
+  char info[1024];
+  char *id, *eos;
+  OrthancPluginMemoryBuffer tmp;
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+  }
+  else
+  {
+    /* Make POST request to create a new DICOM instance */
+    sprintf(info, "{\"PatientName\":\"Test\"}");
+    OrthancPluginRestApiPost(context, &tmp, "/tools/create-dicom", info, strlen(info));
+
+    /**
+     * Recover the ID of the created instance is constructed by a
+     * quick-and-dirty parsing of a JSON string.
+     **/
+    id = strstr((char*) tmp.data, pathLocator) + strlen(pathLocator);
+    eos = strchr(id, '\"');
+    eos[0] = '\0';
+
+    /* Delete the newly created DICOM instance. */
+    OrthancPluginRestApiDelete(context, id);
+    OrthancPluginFreeMemoryBuffer(context, &tmp);
+
+    /* Set some cookie */
+    OrthancPluginSetCookie(context, output, "hello", "world");
+
+    /* Set some HTTP header */
+    OrthancPluginSetHttpHeader(context, output, "Cache-Control", "max-age=0, no-cache");
+    
+    OrthancPluginAnswerBuffer(context, output, "OK\n", 3, "text/plain");
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+void DicomWebBinaryCallback(
+  OrthancPluginDicomWebNode*          node,
+  OrthancPluginDicomWebSetBinaryNode  setter,
+  uint32_t                            levelDepth,
+  const uint16_t*                     levelTagGroup,
+  const uint16_t*                     levelTagElement,
+  const uint32_t*                     levelIndex,
+  uint16_t                            tagGroup,
+  uint16_t                            tagElement,
+  OrthancPluginValueRepresentation    vr)
+{
+  setter(node, OrthancPluginDicomWebBinaryMode_BulkDataUri, "HelloURI");
+}
+
+
+OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance,
+                                        const char* instanceId)
+{
+  char buffer[256];
+  FILE* fp;
+  char* json;
+  static int first = 1;
+
+  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);  
+
+  fp = fopen("PluginReceivedInstance.dcm", "wb");
+  fwrite(OrthancPluginGetInstanceData(context, instance),
+         OrthancPluginGetInstanceSize(context, instance), 1, fp);
+  fclose(fp);
+
+  json = OrthancPluginGetInstanceSimplifiedJson(context, instance);
+  if (first)
+  {
+    printf("[%s]\n", json);
+  }
+  OrthancPluginFreeString(context, json);
+
+  if (OrthancPluginHasInstanceMetadata(context, instance, "ReceptionDate"))
+  {
+    printf("Received on [%s]\n", OrthancPluginGetInstanceMetadata(context, instance, "ReceptionDate"));
+  }
+  else
+  {
+    OrthancPluginLogError(context, "Instance has no reception date, should never happen!");
+  }
+
+  json = OrthancPluginEncodeDicomWebXml(context,
+                                        OrthancPluginGetInstanceData(context, instance),
+                                        OrthancPluginGetInstanceSize(context, instance),
+                                        DicomWebBinaryCallback);
+  if (first)
+  {
+    printf("[%s]\n", json);
+    first = 0;    /* Only print the first DICOM instance */
+  }
+  OrthancPluginFreeString(context, json);
+  
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
+                                        OrthancPluginResourceType resourceType,
+                                        const char* resourceId)
+{
+  char info[1024];
+
+  OrthancPluginMemoryBuffer tmp;
+  memset(&tmp, 0, sizeof(tmp));
+
+  sprintf(info, "Change %d on resource %s of type %d", changeType,
+          (resourceId == NULL ? "<none>" : resourceId), resourceType);
+  OrthancPluginLogWarning(context, info);
+
+  switch (changeType)
+  {
+    case OrthancPluginChangeType_NewInstance:
+    {
+      sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId);
+      if (OrthancPluginRestApiGet(context, &tmp, info) == 0)
+      {
+        sprintf(info, "  Instance %s comes from the anonymization of instance", resourceId);
+        strncat(info, (const char*) tmp.data, tmp.size);
+        OrthancPluginLogWarning(context, info);
+        OrthancPluginFreeMemoryBuffer(context, &tmp);
+      }
+
+      break;
+    }
+
+    case OrthancPluginChangeType_OrthancStarted:
+    {
+      OrthancPluginSetMetricsValue(context, "sample_started", 1, OrthancPluginMetricsType_Default); 
+
+      /* Make REST requests to the built-in Orthanc API */
+      OrthancPluginRestApiGet(context, &tmp, "/changes");
+      OrthancPluginFreeMemoryBuffer(context, &tmp);
+      OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1");
+      OrthancPluginFreeMemoryBuffer(context, &tmp);
+
+      /* Play with PUT by defining a new target modality. */
+      sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]");
+      OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info));
+
+      break;
+    }
+
+    case OrthancPluginChangeType_OrthancStopped:
+      OrthancPluginLogWarning(context, "Orthanc has stopped");
+      break;
+
+    default:
+      break;
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+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 */
+  }
+}
+
+
+static void RefreshMetrics()
+{
+  static unsigned int count = 0;
+  OrthancPluginSetMetricsValue(context, "sample_counter", 
+                               (float) (count++), OrthancPluginMetricsType_Default); 
+}
+
+
+static int32_t FilterIncomingDicomInstance(const OrthancPluginDicomInstance* instance)
+{
+  char buf[1024];
+  char* s;
+  int32_t hasPixelData;
+
+  s = OrthancPluginGetInstanceTransferSyntaxUid(context, instance);
+  sprintf(buf, "Incoming transfer syntax: %s", s);
+  OrthancPluginFreeString(context, s);
+  OrthancPluginLogWarning(context, buf);
+
+  hasPixelData = OrthancPluginHasInstancePixelData(context, instance);
+  sprintf(buf, "Incoming has pixel data: %d", hasPixelData);
+  OrthancPluginLogWarning(context, buf);
+
+  /* Reject all instances without pixel data */
+  return hasPixelData;
+}
+
+
+ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+{
+  char info[1024], *s;
+  int counter, i;
+  OrthancPluginDictionaryEntry entry;
+
+  context = c;
+  OrthancPluginLogWarning(context, "Sample plugin is initializing");
+
+  /* Check the version of the Orthanc core */
+  if (OrthancPluginCheckVersion(c) == 0)
+  {
+    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;
+  }
+
+  /* Print some information about Orthanc */
+  sprintf(info, "The version of Orthanc is '%s'", context->orthancVersion);
+  OrthancPluginLogWarning(context, info);
+
+  s = OrthancPluginGetOrthancPath(context);
+  sprintf(info, "  Path to Orthanc: %s", s);
+  OrthancPluginLogWarning(context, info);
+  OrthancPluginFreeString(context, s);
+
+  s = OrthancPluginGetOrthancDirectory(context);
+  sprintf(info, "  Directory of Orthanc: %s", s);
+  OrthancPluginLogWarning(context, info);
+  OrthancPluginFreeString(context, s);
+
+  s = OrthancPluginGetConfiguration(context);
+  sprintf(info, "  Content of the configuration file:\n");
+  OrthancPluginLogWarning(context, info);
+  OrthancPluginLogWarning(context, s);
+  OrthancPluginFreeString(context, s);
+
+  /* Print the command-line arguments of Orthanc */
+  counter = OrthancPluginGetCommandLineArgumentsCount(context);
+  for (i = 0; i < counter; i++)
+  {
+    s = OrthancPluginGetCommandLineArgument(context, i);
+    sprintf(info, "  Command-line argument %d: \"%s\"", i, s);
+    OrthancPluginLogWarning(context, info);
+    OrthancPluginFreeString(context, s);    
+  }
+
+  /* Register the callbacks */
+  OrthancPluginRegisterRestCallback(context, "/(plu.*)/hello", Callback1);
+  OrthancPluginRegisterRestCallback(context, "/plu.*/image", Callback2);
+  OrthancPluginRegisterRestCallback(context, "/plugin/instances/([^/]+)/info", Callback3);
+  OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/preview", Callback4);
+  OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5);
+  OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5);
+  OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
+
+  OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
+  OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+  OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterIncomingHttpRequest);
+  OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetrics);
+  OrthancPluginRegisterIncomingDicomInstanceFilter(context, FilterIncomingDicomInstance);
+    
+  
+  /* Declare several properties of the plugin */
+  OrthancPluginSetRootUri(context, "/plugin/hello");
+  OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
+  OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');");
+
+  customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world");
+  
+  OrthancPluginRegisterDictionaryTag(context, 0x0014, 0x1020, OrthancPluginValueRepresentation_DA,
+                                     "ValidationExpiryDate", 1, 1);
+
+  OrthancPluginLookupDictionary(context, &entry, "ValidationExpiryDate");
+  OrthancPluginLookupDictionary(context, &entry, "0010-0010");
+
+  return 0;
+}
+
+
+ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+{
+  OrthancPluginLogWarning(context, "Sample plugin is finalizing");
+}
+
+
+ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+{
+  return "sample";
+}
+
+
+ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+{
+  return "1.0";
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/DicomDatasetReader.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/DicomDatasetReader.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/DicomPath.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,116 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#include <boost/lexical_cast.hpp>
+
+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::Prefix& DicomPath::GetPrefixItem(size_t depth)
+  {
+    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);
+  }
+
+
+  std::string DicomPath::Format() const
+  {
+    std::string s;
+      
+    for (size_t i = 0; i < GetPrefixLength(); i++)
+    {
+      s += (GetPrefixTag(i).FormatHexadecimal() + " / " +
+            boost::lexical_cast<std::string>(i) + " / ");
+    }
+
+    return s + GetFinalTag().FormatHexadecimal();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/DicomPath.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,118 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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;
+
+    Prefix& GetPrefixItem(size_t depth);
+
+  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;
+    }
+
+    void SetPrefixIndex(size_t depth,
+                        size_t value)
+    {
+      GetPrefixItem(depth).second = value;
+    }
+    
+    const DicomTag& GetFinalTag() const
+    {
+      return finalTag_;
+    }
+
+    void SetFinalTag(const DicomTag& tag)
+    {
+      finalTag_ = tag;
+    }
+
+    std::string Format() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/DicomTag.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,119 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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);
+    }
+  }
+
+
+  std::string DicomTag::FormatHexadecimal() const
+  {
+    char buf[16];
+    sprintf(buf, "(%04x,%04x)", group_, element_);
+    return buf;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/DicomTag.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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>
+#include <string>
+
+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);
+    }
+
+    std::string FormatHexadecimal() const;
+  };
+
+
+  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/OrthancServer/Plugins/Samples/Common/ExportedSymbols.list	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,7 @@
+# This is the list of the symbols that must be exported by Orthanc
+# plugins, if targeting OS X
+
+_OrthancPluginInitialize
+_OrthancPluginFinalize
+_OrthancPluginGetName
+_OrthancPluginGetVersion
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/FullOrthancDataset.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/FullOrthancDataset.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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;
+
+    FullOrthancDataset* Clone() const
+    {
+      return new FullOrthancDataset(this->root_);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/IDicomDataset.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/IOrthancConnection.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/IOrthancConnection.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/OrthancHttpConnection.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/OrthancHttpConnection.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/OrthancPluginConnection.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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;
+
+    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;
+
+    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;
+
+    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;
+
+    if (!::OrthancPlugins::RestApiDelete(uri, false))
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginConnection.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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
+  {
+  public:
+    virtual void RestApiGet(std::string& result,
+                            const std::string& uri);
+
+    virtual void RestApiPost(std::string& result,
+                             const std::string& uri,
+                             const std::string& body);
+
+    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/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,3395 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <boost/algorithm/string/predicate.hpp>
+#include <boost/move/unique_ptr.hpp>
+#include <boost/thread.hpp>
+#include <json/reader.h>
+#include <json/writer.h>
+
+
+#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0)
+static const OrthancPluginErrorCode OrthancPluginErrorCode_NullPointer = OrthancPluginErrorCode_Plugin;
+#endif
+
+
+namespace OrthancPlugins
+{
+  static OrthancPluginContext* globalContext_ = NULL;
+
+
+  void SetGlobalContext(OrthancPluginContext* context)
+  {
+    if (context == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer);
+    }
+    else if (globalContext_ == NULL)
+    {
+      globalContext_ = context;
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+  }
+
+
+  bool HasGlobalContext()
+  {
+    return globalContext_ != NULL;
+  }
+
+
+  OrthancPluginContext* GetGlobalContext()
+  {
+    if (globalContext_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
+    }
+    else
+    {
+      return globalContext_;
+    }
+  }
+
+
+  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()
+  {
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  MemoryBuffer::MemoryBuffer(const void* buffer,
+                             size_t size)
+  {
+    uint32_t s = static_cast<uint32_t>(size);
+    if (static_cast<size_t>(s) != size)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+    else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
+             OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
+    }
+    else
+    {
+      memcpy(buffer_.data, buffer, size);
+    }
+  }
+#endif
+
+
+  void MemoryBuffer::Clear()
+  {
+    if (buffer_.data != NULL)
+    {
+      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &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;
+  }
+
+
+  void MemoryBuffer::Swap(MemoryBuffer& other)
+  {
+    std::swap(buffer_.data, other.buffer_.data);
+    std::swap(buffer_.size, other.buffer_.size);
+  }
+
+
+  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))
+    {
+      LogError("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(GetGlobalContext(), &buffer_, uri.c_str()));
+    }
+    else
+    {
+      return CheckHttp(OrthancPluginRestApiGet(GetGlobalContext(), &buffer_, uri.c_str()));
+    }
+  }
+
+  bool MemoryBuffer::RestApiGet(const std::string& uri,
+                                const std::map<std::string, std::string>& httpHeaders,
+                                bool applyPlugins)
+  {
+    Clear();
+
+    std::vector<const char*> headersKeys;
+    std::vector<const char*> headersValues;
+    
+    for (std::map<std::string, std::string>::const_iterator
+           it = httpHeaders.begin(); it != httpHeaders.end(); it++)
+    {
+      headersKeys.push_back(it->first.c_str());
+      headersValues.push_back(it->second.c_str());
+    }
+
+    return CheckHttp(OrthancPluginRestApiGet2(
+                       GetGlobalContext(), &buffer_, uri.c_str(), httpHeaders.size(),
+                       (headersKeys.empty() ? NULL : &headersKeys[0]),
+                       (headersValues.empty() ? NULL : &headersValues[0]), applyPlugins));
+  }
+
+  bool MemoryBuffer::RestApiPost(const std::string& uri,
+                                 const void* body,
+                                 size_t bodySize,
+                                 bool applyPlugins)
+  {
+    Clear();
+    
+    // Cast for compatibility with Orthanc SDK <= 1.5.6
+    const char* b = reinterpret_cast<const char*>(body);
+
+    if (applyPlugins)
+    {
+      return CheckHttp(OrthancPluginRestApiPostAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize));
+    }
+    else
+    {
+      return CheckHttp(OrthancPluginRestApiPost(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize));
+    }
+  }
+
+
+  bool MemoryBuffer::RestApiPut(const std::string& uri,
+                                const void* body,
+                                size_t bodySize,
+                                bool applyPlugins)
+  {
+    Clear();
+
+    // Cast for compatibility with Orthanc SDK <= 1.5.6
+    const char* b = reinterpret_cast<const char*>(body);
+
+    if (applyPlugins)
+    {
+      return CheckHttp(OrthancPluginRestApiPutAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize));
+    }
+    else
+    {
+      return CheckHttp(OrthancPluginRestApiPut(GetGlobalContext(), &buffer_, uri.c_str(), b, 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(GetGlobalContext(), &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(GetGlobalContext(), &buffer_, s.c_str(), pixelData.GetObject(), flags));
+  }
+
+
+  void MemoryBuffer::ReadFile(const std::string& path)
+  {
+    Clear();
+    Check(OrthancPluginReadFile(GetGlobalContext(), &buffer_, path.c_str()));
+  }
+
+
+  void MemoryBuffer::GetDicomQuery(const OrthancPluginWorklistQuery* query)
+  {
+    Clear();
+    Check(OrthancPluginWorklistGetDicomQuery(GetGlobalContext(), &buffer_, query));
+  }
+
+
+  void OrthancString::Assign(char* str)
+  {
+    Clear();
+
+    if (str != NULL)
+    {
+      str_ = str;
+    }
+  }
+
+
+  void OrthancString::Clear()
+  {
+    if (str_ != NULL)
+    {
+      OrthancPluginFreeString(GetGlobalContext(), 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)
+    {
+      LogError("Cannot convert an empty memory buffer to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    Json::Reader reader;
+    if (!reader.parse(str_, target))
+    {
+      LogError("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;
+    str.Assign(OrthancPluginDicomBufferToJson
+               (GetGlobalContext(), 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(GetGlobalContext(), &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(GetGlobalContext(), &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(GetGlobalContext(), &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(GetGlobalContext(), &buffer_, instanceId.c_str()));
+  }
+
+
+  bool HttpDelete(const std::string& url,
+                  const std::string& username,
+                  const std::string& password)
+  {
+    OrthancPluginErrorCode error = OrthancPluginHttpDelete
+      (GetGlobalContext(), 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);
+    }
+  }
+
+
+  void LogError(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogError(GetGlobalContext(), message.c_str());
+    }
+  }
+
+
+  void LogWarning(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
+    }
+  }
+
+
+  void LogInfo(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
+    }
+  }
+
+
+  void OrthancConfiguration::LoadConfiguration()
+  {
+    OrthancString str;
+    str.Assign(OrthancPluginGetConfiguration(GetGlobalContext()));
+
+    if (str.GetContent() == NULL)
+    {
+      LogError("Cannot access the Orthanc configuration");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    str.ToJson(configuration_);
+
+    if (configuration_.type() != Json::objectValue)
+    {
+      LogError("Unable to read the Orthanc configuration");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+    
+
+  OrthancConfiguration::OrthancConfiguration()
+  {
+    LoadConfiguration();
+  }
+
+
+  OrthancConfiguration::OrthancConfiguration(bool loadConfiguration)
+  {
+    if (loadConfiguration)
+    {
+      LoadConfiguration();
+    }
+    else
+    {
+      configuration_ = Json::objectValue;
+    }
+  }
+
+
+  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.path_ = GetPath(key);
+
+    if (!configuration_.isMember(key))
+    {
+      target.configuration_ = Json::objectValue;
+    }
+    else
+    {
+      if (configuration_[key].type() != Json::objectValue)
+      {
+        LogError("The configuration section \"" + target.path_ +
+                 "\" is not an associative array as expected");
+
+        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)
+    {
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a string as expected");
+
+      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:
+        LogError("The configuration option \"" + GetPath(key) +
+                 "\" is not an integer as expected");
+
+        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)
+    {
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a positive integer as expected");
+
+      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)
+    {
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a Boolean as expected");
+
+      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:
+        LogError("The configuration option \"" + GetPath(key) +
+                 "\" is not an integer as expected");
+
+        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;
+    }
+
+    LogError("The configuration option \"" + GetPath(key) +
+             "\" is not a list of strings as expected");
+
+    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)
+    {
+      LogError("The configuration option \"" + GetPath(key) +
+               "\" is not a string as expected");
+
+      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
+      {
+        LogError("The configuration option \"" + GetPath(key) +
+                 "\" is not a dictionary mapping strings to strings");
+
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+      }
+    }
+  }
+
+
+  void OrthancImage::Clear()
+  {
+    if (image_ != NULL)
+    {
+      OrthancPluginFreeImage(GetGlobalContext(), image_);
+      image_ = NULL;
+    }
+  }
+
+
+  void OrthancImage::CheckImageAvailable() const
+  {
+    if (image_ == NULL)
+    {
+      LogError("Trying to access a NULL image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancImage::OrthancImage() :
+    image_(NULL)
+  {
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginImage*  image) :
+    image_(image)
+  {
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginPixelFormat  format,
+                             uint32_t                  width,
+                             uint32_t                  height)
+  {
+    image_ = OrthancPluginCreateImage(GetGlobalContext(), format, width, height);
+
+    if (image_ == NULL)
+    {
+      LogError("Cannot create an image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginPixelFormat  format,
+                             uint32_t                  width,
+                             uint32_t                  height,
+                             uint32_t                  pitch,
+                             void*                     buffer)
+  {
+    image_ = OrthancPluginCreateImageAccessor
+      (GetGlobalContext(), format, width, height, pitch, buffer);
+
+    if (image_ == NULL)
+    {
+      LogError("Cannot create an image accessor");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+
+  void OrthancImage::UncompressPngImage(const void* data,
+                                        size_t size)
+  {
+    Clear();
+
+    image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Png);
+
+    if (image_ == NULL)
+    {
+      LogError("Cannot uncompress a PNG image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  void OrthancImage::UncompressJpegImage(const void* data,
+                                         size_t size)
+  {
+    Clear();
+    image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg);
+    if (image_ == NULL)
+    {
+      LogError("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(GetGlobalContext(), data, size, frame);
+    if (image_ == NULL)
+    {
+      LogError("Cannot uncompress a DICOM image");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancPluginPixelFormat OrthancImage::GetPixelFormat() const
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImagePixelFormat(GetGlobalContext(), image_);
+  }
+
+
+  unsigned int OrthancImage::GetWidth() const
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageWidth(GetGlobalContext(), image_);
+  }
+
+
+  unsigned int OrthancImage::GetHeight() const
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageHeight(GetGlobalContext(), image_);
+  }
+
+
+  unsigned int OrthancImage::GetPitch() const
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImagePitch(GetGlobalContext(), image_);
+  }
+
+
+  void* OrthancImage::GetBuffer() const
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageBuffer(GetGlobalContext(), image_);
+  }
+
+
+  void OrthancImage::CompressPngImage(MemoryBuffer& target) const
+  {
+    CheckImageAvailable();
+
+    OrthancPlugins::MemoryBuffer answer;
+    OrthancPluginCompressPngImage(GetGlobalContext(), *answer, GetPixelFormat(),
+                                  GetWidth(), GetHeight(), GetPitch(), GetBuffer());
+
+    target.Swap(answer);
+  }
+
+
+  void OrthancImage::CompressJpegImage(MemoryBuffer& target,
+                                       uint8_t quality) const
+  {
+    CheckImageAvailable();
+
+    OrthancPlugins::MemoryBuffer answer;
+    OrthancPluginCompressJpegImage(GetGlobalContext(), *answer, GetPixelFormat(),
+                                   GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
+
+    target.Swap(answer);
+  }
+
+
+  void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output) const
+  {
+    CheckImageAvailable();
+    OrthancPluginCompressAndAnswerPngImage(GetGlobalContext(), output, GetPixelFormat(),
+                                           GetWidth(), GetHeight(), GetPitch(), GetBuffer());
+  }
+
+
+  void OrthancImage::AnswerJpegImage(OrthancPluginRestOutput* output,
+                                     uint8_t quality) const
+  {
+    CheckImageAvailable();
+    OrthancPluginCompressAndAnswerJpegImage(GetGlobalContext(), output, GetPixelFormat(),
+                                            GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
+  }
+
+
+  OrthancPluginImage* OrthancImage::Release()
+  {
+    CheckImageAvailable();
+    OrthancPluginImage* tmp = image_;
+    image_ = NULL;
+    return tmp;
+  }
+
+
+#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
+  FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) :
+    matcher_(NULL),
+    worklist_(worklist)
+  {
+    if (worklist_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
+    }
+  }
+
+
+  void FindMatcher::SetupDicom(const void*  query,
+                               uint32_t     size)
+  {
+    worklist_ = NULL;
+
+    matcher_ = OrthancPluginCreateFindMatcher(GetGlobalContext(), query, size);
+    if (matcher_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+  }
+
+
+  FindMatcher::~FindMatcher()
+  {
+    // The "worklist_" field
+
+    if (matcher_ != NULL)
+    {
+      OrthancPluginFreeFindMatcher(GetGlobalContext(), matcher_);
+    }
+  }
+
+
+
+  bool FindMatcher::IsMatch(const void*  dicom,
+                            uint32_t     size) const
+  {
+    int32_t result;
+
+    if (matcher_ != NULL)
+    {
+      result = OrthancPluginFindMatcherIsMatch(GetGlobalContext(), matcher_, dicom, size);
+    }
+    else if (worklist_ != NULL)
+    {
+      result = OrthancPluginWorklistIsMatch(GetGlobalContext(), 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 */
+
+  void AnswerJson(const Json::Value& value,
+                  OrthancPluginRestOutput* output
+    )
+  {
+    Json::StyledWriter writer;
+    std::string bodyString = writer.write(value);
+
+    OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json");
+  }
+
+  void AnswerString(const std::string& answer,
+                    const char* mimeType,
+                    OrthancPluginRestOutput* output
+    )
+  {
+    OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType);
+  }
+
+  void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput *output)
+  {
+    OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError);
+  }
+
+  void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods)
+  {
+    OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods);
+  }
+
+  bool RestApiGetString(std::string& result,
+                        const std::string& uri,
+                        bool applyPlugins)
+  {
+    MemoryBuffer answer;
+    if (!answer.RestApiGet(uri, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToString(result);
+      return true;
+    }
+  }
+
+  bool RestApiGetString(std::string& result,
+                        const std::string& uri,
+                        const std::map<std::string, std::string>& httpHeaders,
+                        bool applyPlugins)
+  {
+    MemoryBuffer answer;
+    if (!answer.RestApiGet(uri, httpHeaders, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToString(result);
+      return true;
+    }
+  }
+
+
+
+  bool RestApiGet(Json::Value& result,
+                  const std::string& uri,
+                  bool applyPlugins)
+  {
+    MemoryBuffer answer;
+
+    if (!answer.RestApiGet(uri, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty())
+      {
+        answer.ToJson(result);
+      }
+      return true;
+    }
+  }
+
+
+  bool RestApiPost(std::string& result,
+                   const std::string& uri,
+                   const void* body,
+                   size_t bodySize,
+                   bool applyPlugins)
+  {
+    MemoryBuffer answer;
+
+    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty())
+      {
+        result.assign(answer.GetData(), answer.GetSize());
+      }
+      return true;
+    }
+  }
+
+
+  bool RestApiPost(Json::Value& result,
+                   const std::string& uri,
+                   const void* body,
+                   size_t bodySize,
+                   bool applyPlugins)
+  {
+    MemoryBuffer answer;
+
+    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty())
+      {
+        answer.ToJson(result);
+      }
+      return true;
+    }
+  }
+
+
+  bool RestApiPost(Json::Value& result,
+                   const std::string& uri,
+                   const Json::Value& body,
+                   bool applyPlugins)
+  {
+    Json::FastWriter writer;
+    return RestApiPost(result, uri, writer.write(body), applyPlugins);
+  }
+
+
+  bool RestApiPut(Json::Value& result,
+                  const std::string& uri,
+                  const void* body,
+                  size_t bodySize,
+                  bool applyPlugins)
+  {
+    MemoryBuffer answer;
+
+    if (!answer.RestApiPut(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      if (!answer.IsEmpty()) // i.e, on a PUT to metadata/..., orthanc returns an empty response
+      {
+        answer.ToJson(result);
+      }
+      return true;
+    }
+  }
+
+
+  bool RestApiPut(Json::Value& result,
+                  const std::string& uri,
+                  const Json::Value& body,
+                  bool applyPlugins)
+  {
+    Json::FastWriter writer;
+    return RestApiPut(result, uri, writer.write(body), applyPlugins);
+  }
+
+
+  bool RestApiDelete(const std::string& uri,
+                     bool applyPlugins)
+  {
+    OrthancPluginErrorCode error;
+
+    if (applyPlugins)
+    {
+      error = OrthancPluginRestApiDeleteAfterPlugins(GetGlobalContext(), uri.c_str());
+    }
+    else
+    {
+      error = OrthancPluginRestApiDelete(GetGlobalContext(), 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(unsigned int major,
+                                   unsigned int minor,
+                                   unsigned int revision)
+  {
+    LogError("Your version of the Orthanc core (" +
+             std::string(GetGlobalContext()->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)");
+  }
+
+
+  bool CheckMinimalOrthancVersion(unsigned int major,
+                                  unsigned int minor,
+                                  unsigned int revision)
+  {
+    if (!HasGlobalContext())
+    {
+      LogError("Bad Orthanc context in the plugin");
+      return false;
+    }
+
+    if (!strcmp(GetGlobalContext()->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
+      (GetGlobalContext()->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 ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
+  const char* AutodetectMimeType(const std::string& path)
+  {
+    const char* mime = OrthancPluginAutodetectMimeType(GetGlobalContext(), path.c_str());
+
+    if (mime == NULL)
+    {
+      // Should never happen, just for safety
+      return "application/octet-stream";
+    }
+    else
+    {
+      return mime;
+    }
+  }
+#endif
+
+
+#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
+    {
+      LogError("Inexistent peer: " + name);
+      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
+    }
+  }
+
+
+  OrthancPeers::OrthancPeers() :
+    peers_(NULL),
+    timeout_(0)
+  {
+    peers_ = OrthancPluginGetPeers(GetGlobalContext());
+
+    if (peers_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+    }
+
+    uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_);
+
+    for (uint32_t i = 0; i < count; i++)
+    {
+      const char* name = OrthancPluginGetPeerName(GetGlobalContext(), peers_, i);
+      if (name == NULL)
+      {
+        OrthancPluginFreePeers(GetGlobalContext(), peers_);
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+      }
+
+      index_[name] = i;
+    }
+  }
+
+
+  OrthancPeers::~OrthancPeers()
+  {
+    if (peers_ != NULL)
+    {
+      OrthancPluginFreePeers(GetGlobalContext(), 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(GetGlobalContext(), 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(GetGlobalContext(), 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(GetGlobalContext(), 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);
+    }
+
+    OrthancPlugins::MemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+      (GetGlobalContext(), *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.Swap(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;
+
+    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;
+
+    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;
+
+    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;
+
+    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);
+    }
+
+    OrthancPlugins::MemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+      (GetGlobalContext(), *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.Swap(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);
+    }
+
+    OrthancPlugins::MemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+      (GetGlobalContext(), *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)
+    {
+      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);
+    }
+
+    OrthancPlugins::MemoryBuffer answer;
+    uint16_t status;
+    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
+      (GetGlobalContext(), *answer, NULL, &status, peers_,
+       static_cast<uint32_t>(index), OrthancPluginHttpMethod_Delete, uri.c_str(),
+       0, NULL, NULL, NULL, 0, timeout_);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      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
+
+
+
+
+
+  /******************************************************************
+   ** JOBS
+   ******************************************************************/
+
+#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(OrthancJob* job)
+  {
+    if (job == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+    }
+
+    OrthancPluginJob* orthanc = OrthancPluginCreateJob(
+      GetGlobalContext(), 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(OrthancJob* job,
+                                 int priority)
+  {
+    if (job == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+    }
+
+    OrthancPluginJob* orthanc = Create(job);
+
+    char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority);
+
+    if (id == NULL)
+    {
+      LogError("Plugin cannot submit job");
+      OrthancPluginFreeJob(GetGlobalContext(), orthanc);
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
+    }
+    else
+    {
+      std::string tmp(id);
+      tmp.assign(id);
+      OrthancPluginFreeString(GetGlobalContext(), id);
+
+      return tmp;
+    }
+  }
+
+
+  void OrthancJob::SubmitAndWait(Json::Value& result,
+                                 OrthancJob* job /* takes ownership */,
+                                 int priority)
+  {
+    std::string id = Submit(job, priority);
+
+    for (;;)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+
+      Json::Value status;
+      if (!RestApiGet(status, "/jobs/" + id, false) ||
+          !status.isMember("State") ||
+          status["State"].type() != Json::stringValue)
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem);        
+      }
+
+      const std::string state = status["State"].asString();
+      if (state == "Success")
+      {
+        if (status.isMember("Content"))
+        {
+          result = status["Content"];
+        }
+        else
+        {
+          result = Json::objectValue;
+        }
+
+        return;
+      }
+      else if (state == "Running")
+      {
+        continue;
+      }
+      else if (!status.isMember("ErrorCode") ||
+               status["ErrorCode"].type() != Json::intValue)
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError);
+      }
+      else
+      {
+        if (!status.isMember("ErrorDescription") ||
+            status["ErrorDescription"].type() != Json::stringValue)
+        {
+          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());
+        }
+        else
+        {
+#if HAS_ORTHANC_EXCEPTION == 1
+          throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()),
+                                          status["ErrorDescription"].asString());
+#else
+          LogError("Exception while executing the job: " + status["ErrorDescription"].asString());
+          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());          
+#endif
+        }
+      }
+    }
+  }
+
+
+  void OrthancJob::SubmitFromRestApiPost(OrthancPluginRestOutput* output,
+                                         const Json::Value& body,
+                                         OrthancJob* job)
+  {
+    static const char* KEY_SYNCHRONOUS = "Synchronous";
+    static const char* KEY_ASYNCHRONOUS = "Asynchronous";
+    static const char* KEY_PRIORITY = "Priority";
+
+    boost::movelib::unique_ptr<OrthancJob> protection(job);
+  
+    if (body.type() != Json::objectValue)
+    {
+#if HAS_ORTHANC_EXCEPTION == 1
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                      "Expected a JSON object in the body");
+#else
+      LogError("Expected a JSON object in the body");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+#endif
+    }
+
+    bool synchronous = true;
+  
+    if (body.isMember(KEY_SYNCHRONOUS))
+    {
+      if (body[KEY_SYNCHRONOUS].type() != Json::booleanValue)
+      {
+#if HAS_ORTHANC_EXCEPTION == 1
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                        "Option \"" + std::string(KEY_SYNCHRONOUS) +
+                                        "\" must be Boolean");
+#else
+        LogError("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean");
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+#endif
+      }
+      else
+      {
+        synchronous = body[KEY_SYNCHRONOUS].asBool();
+      }
+    }
+
+    if (body.isMember(KEY_ASYNCHRONOUS))
+    {
+      if (body[KEY_ASYNCHRONOUS].type() != Json::booleanValue)
+      {
+#if HAS_ORTHANC_EXCEPTION == 1
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                        "Option \"" + std::string(KEY_ASYNCHRONOUS) +
+                                        "\" must be Boolean");
+#else
+        LogError("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean");
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+#endif
+      }
+      else
+      {
+        synchronous = !body[KEY_ASYNCHRONOUS].asBool();
+      }
+    }
+
+    int priority = 0;
+
+    if (body.isMember(KEY_PRIORITY))
+    {
+      if (body[KEY_PRIORITY].type() != Json::booleanValue)
+      {
+#if HAS_ORTHANC_EXCEPTION == 1
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
+                                        "Option \"" + std::string(KEY_PRIORITY) +
+                                        "\" must be an integer");
+#else
+        LogError("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer");
+        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+#endif
+      }
+      else
+      {
+        priority = !body[KEY_PRIORITY].asInt();
+      }
+    }
+  
+    Json::Value result;
+
+    if (synchronous)
+    {
+      OrthancPlugins::OrthancJob::SubmitAndWait(result, protection.release(), priority);
+    }
+    else
+    {
+      std::string id = OrthancPlugins::OrthancJob::Submit(protection.release(), priority);
+
+      result = Json::objectValue;
+      result["ID"] = id;
+      result["Path"] = "/jobs/" + id;
+    }
+
+    std::string s = result.toStyledString();
+    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(),
+                              s.size(), "application/json");
+  }
+
+#endif
+
+
+
+
+  /******************************************************************
+   ** METRICS
+   ******************************************************************/
+
+#if HAS_ORTHANC_PLUGIN_METRICS == 1
+  MetricsTimer::MetricsTimer(const char* name) :
+    name_(name)
+  {
+    start_ = boost::posix_time::microsec_clock::universal_time();
+  }
+  
+  MetricsTimer::~MetricsTimer()
+  {
+    const boost::posix_time::ptime stop = boost::posix_time::microsec_clock::universal_time();
+    const boost::posix_time::time_duration diff = stop - start_;
+    OrthancPluginSetMetricsValue(GetGlobalContext(), name_.c_str(), static_cast<float>(diff.total_milliseconds()),
+                                 OrthancPluginMetricsType_Timer);
+  }
+#endif
+
+
+
+
+  /******************************************************************
+   ** HTTP CLIENT
+   ******************************************************************/
+
+#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
+  class HttpClient::RequestBodyWrapper : public boost::noncopyable
+  {
+  private:
+    static RequestBodyWrapper& GetObject(void* body)
+    {
+      assert(body != NULL);
+      return *reinterpret_cast<RequestBodyWrapper*>(body);
+    }
+
+    IRequestBody&  body_;
+    bool           done_;
+    std::string    chunk_;
+
+  public:
+    RequestBodyWrapper(IRequestBody& body) :
+      body_(body),
+      done_(false)
+    {
+    }      
+
+    static uint8_t IsDone(void* body)
+    {
+      return GetObject(body).done_;
+    }
+    
+    static const void* GetChunkData(void* body)
+    {
+      return GetObject(body).chunk_.c_str();
+    }
+    
+    static uint32_t GetChunkSize(void* body)
+    {
+      return static_cast<uint32_t>(GetObject(body).chunk_.size());
+    }
+
+    static OrthancPluginErrorCode Next(void* body)
+    {
+      RequestBodyWrapper& that = GetObject(body);
+        
+      if (that.done_)
+      {
+        return OrthancPluginErrorCode_BadSequenceOfCalls;
+      }
+      else
+      {
+        try
+        {
+          that.done_ = !that.body_.ReadNextChunk(that.chunk_);
+          return OrthancPluginErrorCode_Success;
+        }
+        catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+        {
+          return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+        }
+        catch (...)
+        {
+          return OrthancPluginErrorCode_InternalError;
+        }
+      }
+    }    
+  };
+
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+  static OrthancPluginErrorCode AnswerAddHeaderCallback(void* answer,
+                                                        const char* key,
+                                                        const char* value)
+  {
+    assert(answer != NULL && key != NULL && value != NULL);
+
+    try
+    {
+      reinterpret_cast<HttpClient::IAnswer*>(answer)->AddHeader(key, value);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+  static OrthancPluginErrorCode AnswerAddChunkCallback(void* answer,
+                                                       const void* data,
+                                                       uint32_t size)
+  {
+    assert(answer != NULL);
+
+    try
+    {
+      reinterpret_cast<HttpClient::IAnswer*>(answer)->AddChunk(data, size);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif
+
+
+  HttpClient::HttpClient() :
+    httpStatus_(0),
+    method_(OrthancPluginHttpMethod_Get),
+    timeout_(0),
+    pkcs11_(false),
+    chunkedBody_(NULL),
+    allowChunkedTransfers_(true)
+  {
+  }
+
+
+  void HttpClient::AddHeaders(const HttpHeaders& headers)
+  {
+    for (HttpHeaders::const_iterator it = headers.begin();
+         it != headers.end(); ++it)
+    {
+      headers_[it->first] = it->second;
+    }
+  }
+
+  
+  void HttpClient::SetCredentials(const std::string& username,
+                                  const std::string& password)
+  {
+    username_ = username;
+    password_ = password;
+  }
+
+  
+  void HttpClient::ClearCredentials()
+  {
+    username_.empty();
+    password_.empty();
+  }
+
+
+  void HttpClient::SetCertificate(const std::string& certificateFile,
+                                  const std::string& keyFile,
+                                  const std::string& keyPassword)
+  {
+    certificateFile_ = certificateFile;
+    certificateKeyFile_ = keyFile;
+    certificateKeyPassword_ = keyPassword;
+  }
+
+  
+  void HttpClient::ClearCertificate()
+  {
+    certificateFile_.clear();
+    certificateKeyFile_.clear();
+    certificateKeyPassword_.clear();
+  }
+
+
+  void HttpClient::ClearBody()
+  {
+    fullBody_.clear();
+    chunkedBody_ = NULL;
+  }
+
+  
+  void HttpClient::SwapBody(std::string& body)
+  {
+    fullBody_.swap(body);
+    chunkedBody_ = NULL;
+  }
+
+  
+  void HttpClient::SetBody(const std::string& body)
+  {
+    fullBody_ = body;
+    chunkedBody_ = NULL;
+  }
+
+  
+  void HttpClient::SetBody(IRequestBody& body)
+  {
+    fullBody_.clear();
+    chunkedBody_ = &body;
+  }
+
+
+  namespace
+  {
+    class HeadersWrapper : public boost::noncopyable
+    {
+    private:
+      std::vector<const char*>  headersKeys_;
+      std::vector<const char*>  headersValues_;
+
+    public:
+      HeadersWrapper(const HttpClient::HttpHeaders& headers)
+      {
+        headersKeys_.reserve(headers.size());
+        headersValues_.reserve(headers.size());
+
+        for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
+        {
+          headersKeys_.push_back(it->first.c_str());
+          headersValues_.push_back(it->second.c_str());
+        }
+      }
+
+      void AddStaticString(const char* key,
+                           const char* value)
+      {
+        headersKeys_.push_back(key);
+        headersValues_.push_back(value);
+      }
+
+      uint32_t GetCount() const
+      {
+        return headersKeys_.size();
+      }
+
+      const char* const* GetKeys() const
+      {
+        return headersKeys_.empty() ? NULL : &headersKeys_[0];
+      }
+
+      const char* const* GetValues() const
+      {
+        return headersValues_.empty() ? NULL : &headersValues_[0];
+      }
+    };
+
+
+    class MemoryRequestBody : public HttpClient::IRequestBody
+    {
+    private:
+      std::string  body_;
+      bool         done_;
+
+    public:
+      MemoryRequestBody(const std::string& body) :
+        body_(body),
+        done_(false)
+      {
+        if (body_.empty())
+        {
+          done_ = true;
+        }
+      }
+
+      virtual bool ReadNextChunk(std::string& chunk)
+      {
+        if (done_)
+        {
+          return false;
+        }
+        else
+        {
+          chunk.swap(body_);
+          done_ = true;
+          return true;
+        }
+      }
+    };
+
+
+    // This class mimics Orthanc::ChunkedBuffer
+    class ChunkedBuffer : public boost::noncopyable
+    {
+    private:
+      typedef std::list<std::string*>  Content;
+
+      Content  content_;
+      size_t   size_;
+
+    public:
+      ChunkedBuffer() :
+        size_(0)
+      {
+      }
+
+      ~ChunkedBuffer()
+      {
+        Clear();
+      }
+
+      void Clear()
+      {
+        for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+        {
+          assert(*it != NULL);
+          delete *it;
+        }
+
+        content_.clear();
+      }
+
+      void Flatten(std::string& target) const
+      {
+        target.resize(size_);
+
+        size_t pos = 0;
+
+        for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
+        {
+          assert(*it != NULL);
+          size_t s = (*it)->size();
+
+          if (s != 0)
+          {
+            memcpy(&target[pos], (*it)->c_str(), s);
+            pos += s;
+          }
+        }
+
+        assert(size_ == 0 ||
+               pos == target.size());
+      }
+
+      void AddChunk(const void* data,
+                    size_t size)
+      {
+        content_.push_back(new std::string(reinterpret_cast<const char*>(data), size));
+        size_ += size;
+      }
+
+      void AddChunk(const std::string& chunk)
+      {
+        content_.push_back(new std::string(chunk));
+        size_ += chunk.size();
+      }
+    };
+
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+    class MemoryAnswer : public HttpClient::IAnswer
+    {
+    private:
+      HttpClient::HttpHeaders  headers_;
+      ChunkedBuffer            body_;
+
+    public:
+      const HttpClient::HttpHeaders& GetHeaders() const
+      {
+        return headers_;
+      }
+
+      const ChunkedBuffer& GetBody() const
+      {
+        return body_;
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value)
+      {
+        headers_[key] = value;
+      }
+
+      virtual void AddChunk(const void* data,
+                            size_t size)
+      {
+        body_.AddChunk(data, size);
+      }
+    };
+#endif
+  }
+
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+  void HttpClient::ExecuteWithStream(uint16_t& httpStatus,
+                                     IAnswer& answer,
+                                     IRequestBody& body) const
+  {
+    HeadersWrapper h(headers_);
+
+    if (method_ == OrthancPluginHttpMethod_Post ||
+        method_ == OrthancPluginHttpMethod_Put)
+    {
+      // Automatically set the "Transfer-Encoding" header if absent
+      bool found = false;
+
+      for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
+      {
+        if (boost::iequals(it->first, "Transfer-Encoding"))
+        {
+          found = true;
+          break;
+        }
+      }
+
+      if (!found)
+      {
+        h.AddStaticString("Transfer-Encoding", "chunked");
+      }
+    }
+
+    RequestBodyWrapper request(body);
+        
+    OrthancPluginErrorCode error = OrthancPluginChunkedHttpClient(
+      GetGlobalContext(),
+      &answer,
+      AnswerAddChunkCallback,
+      AnswerAddHeaderCallback,
+      &httpStatus,
+      method_,
+      url_.c_str(),
+      h.GetCount(),
+      h.GetKeys(),
+      h.GetValues(),
+      &request,
+      RequestBodyWrapper::IsDone,
+      RequestBodyWrapper::GetChunkData,
+      RequestBodyWrapper::GetChunkSize,
+      RequestBodyWrapper::Next,
+      username_.empty() ? NULL : username_.c_str(),
+      password_.empty() ? NULL : password_.c_str(),
+      timeout_,
+      certificateFile_.empty() ? NULL : certificateFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
+      pkcs11_ ? 1 : 0);
+
+    if (error != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+    }
+  }
+#endif    
+
+
+  void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
+                                        HttpHeaders& answerHeaders,
+                                        std::string& answerBody,
+                                        const std::string& body) const
+  {
+    HeadersWrapper headers(headers_);
+
+    MemoryBuffer answerBodyBuffer, answerHeadersBuffer;
+
+    OrthancPluginErrorCode error = OrthancPluginHttpClient(
+      GetGlobalContext(),
+      *answerBodyBuffer,
+      *answerHeadersBuffer,
+      &httpStatus,
+      method_,
+      url_.c_str(),
+      headers.GetCount(),
+      headers.GetKeys(),
+      headers.GetValues(),
+      body.empty() ? NULL : body.c_str(),
+      body.size(),
+      username_.empty() ? NULL : username_.c_str(),
+      password_.empty() ? NULL : password_.c_str(),
+      timeout_,
+      certificateFile_.empty() ? NULL : certificateFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
+      certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
+      pkcs11_ ? 1 : 0);
+
+    if (error != OrthancPluginErrorCode_Success)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
+    }
+
+    Json::Value v;
+    answerHeadersBuffer.ToJson(v);
+
+    if (v.type() != Json::objectValue)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+    }
+
+    Json::Value::Members members = v.getMemberNames();
+    answerHeaders.clear();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const Json::Value& h = v[members[i]];
+      if (h.type() != Json::stringValue)
+      {
+        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+      }
+      else
+      {
+        answerHeaders[members[i]] = h.asString();
+      }
+    }
+
+    answerBodyBuffer.ToString(answerBody);
+  }
+
+
+  void HttpClient::Execute(IAnswer& answer)
+  {
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+    if (allowChunkedTransfers_)
+    {
+      if (chunkedBody_ != NULL)
+      {
+        ExecuteWithStream(httpStatus_, answer, *chunkedBody_);
+      }
+      else
+      {
+        MemoryRequestBody wrapper(fullBody_);
+        ExecuteWithStream(httpStatus_, answer, wrapper);
+      }
+
+      return;
+    }
+#endif
+    
+    // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
+    // transfers are disabled. This results in higher memory usage
+    // (all chunks from the answer body are sent at once)
+
+    HttpHeaders answerHeaders;
+    std::string answerBody;
+    Execute(answerHeaders, answerBody);
+
+    for (HttpHeaders::const_iterator it = answerHeaders.begin(); 
+         it != answerHeaders.end(); ++it)
+    {
+      answer.AddHeader(it->first, it->second);      
+    }
+
+    if (!answerBody.empty())
+    {
+      answer.AddChunk(answerBody.c_str(), answerBody.size());
+    }
+  }
+
+
+  void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
+                           std::string& answerBody /* out */)
+  {
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+    if (allowChunkedTransfers_)
+    {
+      MemoryAnswer answer;
+      Execute(answer);
+      answerHeaders = answer.GetHeaders();
+      answer.GetBody().Flatten(answerBody);
+      return;
+    }
+#endif
+    
+    // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
+    // transfers are disabled. This results in higher memory usage
+    // (all chunks from the request body are sent at once)
+
+    if (chunkedBody_ != NULL)
+    {
+      ChunkedBuffer buffer;
+      
+      std::string chunk;
+      while (chunkedBody_->ReadNextChunk(chunk))
+      {
+        buffer.AddChunk(chunk);
+      }
+
+      std::string body;
+      buffer.Flatten(body);
+
+      ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, body);
+    }
+    else
+    {
+      ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, fullBody_);
+    }
+  }
+
+
+  void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
+                           Json::Value& answerBody /* out */)
+  {
+    std::string body;
+    Execute(answerHeaders, body);
+    
+    Json::Reader reader;
+    if (!reader.parse(body, answerBody))
+    {
+      LogError("Cannot convert HTTP answer body to JSON");
+      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+    }
+  }
+
+
+  void HttpClient::Execute()
+  {
+    HttpHeaders answerHeaders;
+    std::string body;
+    Execute(answerHeaders, body);
+  }
+
+#endif  /* HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 */
+
+
+
+
+
+  /******************************************************************
+   ** CHUNKED HTTP SERVER
+   ******************************************************************/
+
+  namespace Internals
+  {
+    void NullRestCallback(OrthancPluginRestOutput* output,
+                          const char* url,
+                          const OrthancPluginHttpRequest* request)
+    {
+    }
+  
+    IChunkedRequestReader *NullChunkedRestCallback(const char* url,
+                                                   const OrthancPluginHttpRequest* request)
+    {
+      return NULL;
+    }
+
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
+
+    OrthancPluginErrorCode ChunkedRequestReaderAddChunk(
+      OrthancPluginServerChunkedRequestReader* reader,
+      const void*                              data,
+      uint32_t                                 size)
+    {
+      try
+      {
+        if (reader == NULL)
+        {
+          return OrthancPluginErrorCode_InternalError;
+        }
+
+        reinterpret_cast<IChunkedRequestReader*>(reader)->AddChunk(data, size);
+        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;
+      }
+    }
+
+    
+    OrthancPluginErrorCode ChunkedRequestReaderExecute(
+      OrthancPluginServerChunkedRequestReader* reader,
+      OrthancPluginRestOutput*                 output)
+    {
+      try
+      {
+        if (reader == NULL)
+        {
+          return OrthancPluginErrorCode_InternalError;
+        }
+
+        reinterpret_cast<IChunkedRequestReader*>(reader)->Execute(output);
+        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;
+      }
+    }
+
+    
+    void ChunkedRequestReaderFinalize(
+      OrthancPluginServerChunkedRequestReader* reader)
+    {
+      if (reader != NULL)
+      {
+        delete reinterpret_cast<IChunkedRequestReader*>(reader);
+      }
+    }
+
+#else
+    
+    OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
+                                                    const char* url,
+                                                    const OrthancPluginHttpRequest* request,
+                                                    RestCallback         GetHandler,
+                                                    ChunkedRestCallback  PostHandler,
+                                                    RestCallback         DeleteHandler,
+                                                    ChunkedRestCallback  PutHandler)
+    {
+      try
+      {
+        std::string allowed;
+
+        if (GetHandler != Internals::NullRestCallback)
+        {
+          allowed += "GET";
+        }
+
+        if (PostHandler != Internals::NullChunkedRestCallback)
+        {
+          if (!allowed.empty())
+          {
+            allowed += ",";
+          }
+        
+          allowed += "POST";
+        }
+
+        if (DeleteHandler != Internals::NullRestCallback)
+        {
+          if (!allowed.empty())
+          {
+            allowed += ",";
+          }
+        
+          allowed += "DELETE";
+        }
+
+        if (PutHandler != Internals::NullChunkedRestCallback)
+        {
+          if (!allowed.empty())
+          {
+            allowed += ",";
+          }
+        
+          allowed += "PUT";
+        }
+      
+        switch (request->method)
+        {
+          case OrthancPluginHttpMethod_Get:
+            if (GetHandler == Internals::NullRestCallback)
+            {
+              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
+            }
+            else
+            {
+              GetHandler(output, url, request);
+            }
+
+            break;
+
+          case OrthancPluginHttpMethod_Post:
+            if (PostHandler == Internals::NullChunkedRestCallback)
+            {
+              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
+            }
+            else
+            {
+              boost::movelib::unique_ptr<IChunkedRequestReader> reader(PostHandler(url, request));
+              if (reader.get() == NULL)
+              {
+                ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+              }
+              else
+              {
+                reader->AddChunk(request->body, request->bodySize);
+                reader->Execute(output);
+              }
+            }
+
+            break;
+
+          case OrthancPluginHttpMethod_Delete:
+            if (DeleteHandler == Internals::NullRestCallback)
+            {
+              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
+            }
+            else
+            {
+              DeleteHandler(output, url, request);
+            }
+
+            break;
+
+          case OrthancPluginHttpMethod_Put:
+            if (PutHandler == Internals::NullChunkedRestCallback)
+            {
+              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
+            }
+            else
+            {
+              boost::movelib::unique_ptr<IChunkedRequestReader> reader(PutHandler(url, request));
+              if (reader.get() == NULL)
+              {
+                ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+              }
+              else
+              {
+                reader->AddChunk(request->body, request->bodySize);
+                reader->Execute(output);
+              }
+            }
+
+            break;
+
+          default:
+            ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
+        }
+
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+      {
+#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
+        if (HasGlobalContext() &&
+            e.HasDetails())
+        {
+          // The "false" instructs Orthanc not to log the detailed
+          // error message. This is to avoid duplicating the details,
+          // because "OrthancException" already does it on construction.
+          OrthancPluginSetHttpErrorDetails
+            (GetGlobalContext(), output, e.GetDetails(), false);
+        }
+#endif
+
+        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return OrthancPluginErrorCode_BadFileFormat;
+      }
+      catch (...)
+      {
+        return OrthancPluginErrorCode_Plugin;
+      }
+    }
+#endif
+  }
+
+
+#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+  OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup(
+    OrthancPluginStorageCommitmentFailureReason* target,
+    void* rawHandler,
+    const char* sopClassUid,
+    const char* sopInstanceUid)
+  {
+    assert(target != NULL &&
+           rawHandler != NULL);
+      
+    try
+    {
+      IStorageCommitmentScpHandler& handler = *reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
+      *target = handler.Lookup(sopClassUid, sopInstanceUid);
+      return OrthancPluginErrorCode_Success;
+    }
+    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+    {
+      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+    }
+    catch (...)
+    {
+      return OrthancPluginErrorCode_Plugin;
+    }
+  }
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+  void IStorageCommitmentScpHandler::Destructor(void* rawHandler)
+  {
+    assert(rawHandler != NULL);
+    delete reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
+  DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) :
+    toFree_(false),
+    instance_(instance)
+  {
+  }
+#else
+  DicomInstance::DicomInstance(OrthancPluginDicomInstance* instance) :
+    toFree_(false),
+    instance_(instance)
+  {
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  DicomInstance::DicomInstance(const void* buffer,
+                               size_t size) :
+    toFree_(true),
+    instance_(OrthancPluginCreateDicomInstance(GetGlobalContext(), buffer, size))
+  {
+    if (instance_ == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer);
+    }
+  }
+#endif
+
+
+  DicomInstance::~DicomInstance()
+  {
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    if (toFree_ &&
+        instance_ != NULL)
+    {
+      OrthancPluginFreeDicomInstance(
+        GetGlobalContext(), const_cast<OrthancPluginDicomInstance*>(instance_));
+    }
+#endif
+  }
+
+  
+  std::string DicomInstance::GetRemoteAet() const
+  {
+    const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_);
+    if (s == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+    }
+    else
+    {
+      return std::string(s);
+    }
+  }
+
+
+  void DicomInstance::GetJson(Json::Value& target) const
+  {
+    OrthancString s;
+    s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_));
+    s.ToJson(target);
+  }
+  
+
+  void DicomInstance::GetSimplifiedJson(Json::Value& target) const
+  {
+    OrthancString s;
+    s.Assign(OrthancPluginGetInstanceSimplifiedJson(GetGlobalContext(), instance_));
+    s.ToJson(target);
+  }
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
+  std::string DicomInstance::GetTransferSyntaxUid() const
+  {
+    OrthancString s;
+    s.Assign(OrthancPluginGetInstanceTransferSyntaxUid(GetGlobalContext(), instance_));
+
+    std::string result;
+    s.ToString(result);
+    return result;
+  }
+#endif
+
+  
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
+  bool DicomInstance::HasPixelData() const
+  {
+    int32_t result = OrthancPluginHasInstancePixelData(GetGlobalContext(), instance_);
+    if (result < 0)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+    }
+    else
+    {
+      return (result != 0);
+    }
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)  
+  void DicomInstance::GetRawFrame(std::string& target,
+                                  unsigned int frameIndex) const
+  {
+    MemoryBuffer buffer;
+    OrthancPluginErrorCode code = OrthancPluginGetInstanceRawFrame(
+      GetGlobalContext(), *buffer, instance_, frameIndex);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      buffer.ToString(target);
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)  
+  OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const
+  {
+    OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame(
+      GetGlobalContext(), instance_, frameIndex);
+
+    if (image == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+    }
+    else
+    {
+      return new OrthancImage(image);
+    }
+  }
+#endif  
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  void DicomInstance::Serialize(std::string& target) const
+  {
+    MemoryBuffer buffer;
+    OrthancPluginErrorCode code = OrthancPluginSerializeDicomInstance(
+      GetGlobalContext(), *buffer, instance_);
+
+    if (code == OrthancPluginErrorCode_Success)
+    {
+      buffer.ToString(target);
+    }
+    else
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
+    }
+  }
+#endif
+  
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+  DicomInstance* DicomInstance::Transcode(const void* buffer,
+                                          size_t size,
+                                          const std::string& transferSyntax)
+  {
+    OrthancPluginDicomInstance* instance = OrthancPluginTranscodeDicomInstance(
+      GetGlobalContext(), buffer, size, transferSyntax.c_str());
+
+    if (instance == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
+    }
+    else
+    {
+      boost::movelib::unique_ptr<DicomInstance> result(new DicomInstance(instance));
+      result->toFree_ = true;
+      return result.release();
+    }
+  }
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1240 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <boost/date_time/posix_time/posix_time.hpp>
+#include <json/value.h>
+#include <vector>
+#include <list>
+#include <set>
+#include <map>
+
+
+
+/**
+ * The definition of ORTHANC_PLUGINS_VERSION_IS_ABOVE below is for
+ * backward compatibility with Orthanc SDK <= 1.3.0.
+ * 
+ *   $ hg diff -r Orthanc-1.3.0:Orthanc-1.3.1 ../../../Plugins/Include/orthanc/OrthancCPlugin.h
+ *
+ **/
+#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 !defined(ORTHANC_FRAMEWORK_VERSION_IS_ABOVE)
+#define ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(major, minor, revision)      \
+  (ORTHANC_VERSION_MAJOR > major ||                                     \
+   (ORTHANC_VERSION_MAJOR == major &&                                   \
+    (ORTHANC_VERSION_MINOR > minor ||                                   \
+     (ORTHANC_VERSION_MINOR == minor &&                                 \
+      ORTHANC_VERSION_REVISION >= 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
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
+#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  1
+#else
+#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  0
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4)
+#  define HAS_ORTHANC_PLUGIN_METRICS  1
+#else
+#  define HAS_ORTHANC_PLUGIN_METRICS  0
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 1, 0)
+#  define HAS_ORTHANC_PLUGIN_HTTP_CLIENT  1
+#else
+#  define HAS_ORTHANC_PLUGIN_HTTP_CLIENT  0
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
+#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT  1
+#else
+#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT  0
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
+#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  1
+#else
+#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  0
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0)
+#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  1
+#else
+#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  0
+#endif
+
+
+
+namespace OrthancPlugins
+{
+  typedef void (*RestCallback) (OrthancPluginRestOutput* output,
+                                const char* url,
+                                const OrthancPluginHttpRequest* request);
+
+  void SetGlobalContext(OrthancPluginContext* context);
+
+  bool HasGlobalContext();
+
+  OrthancPluginContext* GetGlobalContext();
+
+  
+  class OrthancImage;
+
+
+  class MemoryBuffer : public boost::noncopyable
+  {
+  private:
+    OrthancPluginMemoryBuffer  buffer_;
+
+    void Check(OrthancPluginErrorCode code);
+
+    bool CheckHttp(OrthancPluginErrorCode code);
+
+  public:
+    MemoryBuffer();
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    // This constructor makes a copy of the given buffer in the memory
+    // handled by the Orthanc core
+    MemoryBuffer(const void* buffer,
+                 size_t size);
+#endif
+
+    ~MemoryBuffer()
+    {
+      Clear();
+    }
+
+    OrthancPluginMemoryBuffer* operator*()
+    {
+      return &buffer_;
+    }
+
+    // This transfers ownership from "other" to "this"
+    void Assign(OrthancPluginMemoryBuffer& other);
+
+    void Swap(MemoryBuffer& 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;
+    }
+
+    bool IsEmpty() const
+    {
+      return GetSize() == 0 || GetData() == NULL;
+    }
+
+    void Clear();
+
+    void ToString(std::string& target) const;
+
+    void ToJson(Json::Value& target) const;
+
+    bool RestApiGet(const std::string& uri,
+                    bool applyPlugins);
+
+    bool RestApiGet(const std::string& uri,
+                    const std::map<std::string, std::string>& httpHeaders,
+                    bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const void* body,
+                     size_t bodySize,
+                     bool applyPlugins);
+
+    bool RestApiPut(const std::string& uri,
+                    const void* 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:
+    char*   str_;
+
+    void Clear();
+
+  public:
+    OrthancString() :
+      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:
+    Json::Value  configuration_;  // Necessarily a Json::objectValue
+    std::string  path_;
+
+    std::string GetPath(const std::string& key) const;
+
+    void LoadConfiguration();
+    
+  public:
+    OrthancConfiguration();
+
+    explicit OrthancConfiguration(bool load);
+
+    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:
+    OrthancPluginImage*    image_;
+
+    void Clear();
+
+    void CheckImageAvailable() const;
+
+  public:
+    OrthancImage();
+
+    explicit OrthancImage(OrthancPluginImage* image);
+
+    OrthancImage(OrthancPluginPixelFormat  format,
+                 uint32_t                  width,
+                 uint32_t                  height);
+
+    OrthancImage(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() const;
+
+    unsigned int GetWidth() const;
+
+    unsigned int GetHeight() const;
+
+    unsigned int GetPitch() const;
+    
+    void* GetBuffer() const;
+
+    const OrthancPluginImage* GetObject() const
+    {
+      return image_;
+    }
+
+    void CompressPngImage(MemoryBuffer& target) const;
+
+    void CompressJpegImage(MemoryBuffer& target,
+                           uint8_t quality) const;
+
+    void AnswerPngImage(OrthancPluginRestOutput* output) const;
+
+    void AnswerJpegImage(OrthancPluginRestOutput* output,
+                         uint8_t quality) const;
+    
+    void* GetWriteableBuffer();
+
+    OrthancPluginImage* Release();
+  };
+
+
+#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
+  class FindMatcher : public boost::noncopyable
+  {
+  private:
+    OrthancPluginFindMatcher*          matcher_;
+    const OrthancPluginWorklistQuery*  worklist_;
+
+    void SetupDicom(const void*            query,
+                    uint32_t               size);
+
+  public:
+    explicit FindMatcher(const OrthancPluginWorklistQuery*  worklist);
+
+    FindMatcher(const void*  query,
+                uint32_t     size)
+    {
+      SetupDicom(query, size);
+    }
+
+    explicit FindMatcher(const MemoryBuffer&  dicom)
+    {
+      SetupDicom(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,
+                  const std::string& uri,
+                  bool applyPlugins);
+
+  bool RestApiGetString(std::string& result,
+                        const std::string& uri,
+                        bool applyPlugins);
+
+  bool RestApiGetString(std::string& result,
+                        const std::string& uri,
+                        const std::map<std::string, std::string>& httpHeaders,
+                        bool applyPlugins);
+
+  bool RestApiPost(std::string& result,
+                   const std::string& uri,
+                   const void* body,
+                   size_t bodySize,
+                   bool applyPlugins);
+
+  bool RestApiPost(Json::Value& result,
+                   const std::string& uri,
+                   const void* body,
+                   size_t bodySize,
+                   bool applyPlugins);
+
+  bool RestApiPost(Json::Value& result,
+                   const std::string& uri,
+                   const Json::Value& body,
+                   bool applyPlugins);
+
+  inline bool RestApiPost(Json::Value& result,
+                          const std::string& uri,
+                          const std::string& body,
+                          bool applyPlugins)
+  {
+    return RestApiPost(result, uri, body.empty() ? NULL : body.c_str(),
+                       body.size(), applyPlugins);
+  }
+
+  inline bool RestApiPost(Json::Value& result,
+                          const std::string& uri,
+                          const MemoryBuffer& body,
+                          bool applyPlugins)
+  {
+    return RestApiPost(result, uri, body.GetData(),
+                       body.GetSize(), applyPlugins);
+  }
+
+  bool RestApiPut(Json::Value& result,
+                  const std::string& uri,
+                  const void* body,
+                  size_t bodySize,
+                  bool applyPlugins);
+
+  bool RestApiPut(Json::Value& result,
+                  const std::string& uri,
+                  const Json::Value& body,
+                  bool applyPlugins);
+
+  inline bool RestApiPut(Json::Value& result,
+                         const std::string& uri,
+                         const std::string& body,
+                         bool applyPlugins)
+  {
+    return RestApiPut(result, uri, body.empty() ? NULL : body.c_str(),
+                      body.size(), applyPlugins);
+  }
+
+  bool RestApiDelete(const std::string& uri,
+                     bool applyPlugins);
+
+  bool HttpDelete(const std::string& url,
+                  const std::string& username,
+                  const std::string& password);
+
+  void AnswerJson(const Json::Value& value,
+                  OrthancPluginRestOutput* output);
+
+  void AnswerString(const std::string& answer,
+                    const char* mimeType,
+                    OrthancPluginRestOutput* output);
+
+  void AnswerHttpError(uint16_t httpError,
+                       OrthancPluginRestOutput* output);
+
+  void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods);
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
+  const char* AutodetectMimeType(const std::string& path);
+#endif
+
+  void LogError(const std::string& message);
+
+  void LogWarning(const std::string& message);
+
+  void LogInfo(const std::string& message);
+
+  void ReportMinimalOrthancVersion(unsigned int major,
+                                   unsigned int minor,
+                                   unsigned int revision);
+  
+  bool CheckMinimalOrthancVersion(unsigned int major,
+                                  unsigned int minor,
+                                  unsigned int revision);
+
+
+  namespace Internals
+  {
+    template <RestCallback Callback>
+    static OrthancPluginErrorCode Protect(OrthancPluginRestOutput* output,
+                                          const char* url,
+                                          const OrthancPluginHttpRequest* request)
+    {
+      try
+      {
+        Callback(output, url, request);
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
+      {
+#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
+        if (HasGlobalContext() &&
+            e.HasDetails())
+        {
+          // The "false" instructs Orthanc not to log the detailed
+          // error message. This is to avoid duplicating the details,
+          // because "OrthancException" already does it on construction.
+          OrthancPluginSetHttpErrorDetails
+            (GetGlobalContext(), output, e.GetDetails(), false);
+        }
+#endif
+
+        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return OrthancPluginErrorCode_BadFileFormat;
+      }
+      catch (...)
+      {
+        return OrthancPluginErrorCode_Plugin;
+      }
+    }
+  }
+
+  
+  template <RestCallback Callback>
+  void RegisterRestCallback(const std::string& uri,
+                            bool isThreadSafe)
+  {
+    if (isThreadSafe)
+    {
+      OrthancPluginRegisterRestCallbackNoLock
+        (GetGlobalContext(), uri.c_str(), Internals::Protect<Callback>);
+    }
+    else
+    {
+      OrthancPluginRegisterRestCallback
+        (GetGlobalContext(), 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;
+
+    OrthancPluginPeers   *peers_;
+    Index                 index_;
+    uint32_t              timeout_;
+
+    size_t GetPeerIndex(const std::string& name) const;
+
+  public:
+    OrthancPeers();
+
+    ~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(OrthancJob* job /* takes ownership */);
+
+    static std::string Submit(OrthancJob* job /* takes ownership */,
+                              int priority);
+
+    static void SubmitAndWait(Json::Value& result,
+                              OrthancJob* job /* takes ownership */,
+                              int priority);
+
+    // Submit a job from a POST on the REST API with the same
+    // conventions as in the Orthanc core (according to the
+    // "Synchronous" and "Priority" options)
+    static void SubmitFromRestApiPost(OrthancPluginRestOutput* output,
+                                      const Json::Value& body,
+                                      OrthancJob* job);
+  };
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_METRICS == 1
+  inline void SetMetricsValue(char* name,
+                              float value)
+  {
+    OrthancPluginSetMetricsValue(GetGlobalContext(), name,
+                                 value, OrthancPluginMetricsType_Default);
+  }
+
+  class MetricsTimer : public boost::noncopyable
+  {
+  private:
+    std::string               name_;
+    boost::posix_time::ptime  start_;
+
+  public:
+    explicit MetricsTimer(const char* name);
+
+    ~MetricsTimer();
+  };
+#endif
+
+
+#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
+  class HttpClient : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::string, std::string>  HttpHeaders;
+
+    class IRequestBody : public boost::noncopyable
+    {
+    public:
+      virtual ~IRequestBody()
+      {
+      }
+
+      virtual bool ReadNextChunk(std::string& chunk) = 0;
+    };
+
+
+    class IAnswer : public boost::noncopyable
+    {
+    public:
+      virtual ~IAnswer()
+      {
+      }
+
+      virtual void AddHeader(const std::string& key,
+                             const std::string& value) = 0;
+
+      virtual void AddChunk(const void* data,
+                            size_t size) = 0;
+    };
+
+
+  private:
+    class RequestBodyWrapper;
+
+    uint16_t                 httpStatus_;
+    OrthancPluginHttpMethod  method_;
+    std::string              url_;
+    HttpHeaders              headers_;
+    std::string              username_;
+    std::string              password_;
+    uint32_t                 timeout_;
+    std::string              certificateFile_;
+    std::string              certificateKeyFile_;
+    std::string              certificateKeyPassword_;
+    bool                     pkcs11_;
+    std::string              fullBody_;
+    IRequestBody*            chunkedBody_;
+    bool                     allowChunkedTransfers_;
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+    void ExecuteWithStream(uint16_t& httpStatus,  // out
+                           IAnswer& answer,       // out
+                           IRequestBody& body) const;
+#endif
+
+    void ExecuteWithoutStream(uint16_t& httpStatus,        // out
+                              HttpHeaders& answerHeaders,  // out
+                              std::string& answerBody,     // out
+                              const std::string& body) const;
+    
+  public:
+    HttpClient();
+
+    uint16_t GetHttpStatus() const
+    {
+      return httpStatus_;
+    }
+
+    void SetMethod(OrthancPluginHttpMethod method)
+    {
+      method_ = method;
+    }
+
+    const std::string& GetUrl() const
+    {
+      return url_;
+    }
+
+    void SetUrl(const std::string& url)
+    {
+      url_ = url;
+    }
+
+    void SetHeaders(const HttpHeaders& headers)
+    {
+      headers_ = headers;
+    }
+
+    void AddHeader(const std::string& key,
+                   const std::string& value)
+    {
+      headers_[key] = value;
+    }
+
+    void AddHeaders(const HttpHeaders& headers);
+
+    void SetCredentials(const std::string& username,
+                        const std::string& password);
+
+    void ClearCredentials();
+
+    void SetTimeout(unsigned int timeout)  // 0 for default timeout
+    {
+      timeout_ = timeout;
+    }
+
+    void SetCertificate(const std::string& certificateFile,
+                        const std::string& keyFile,
+                        const std::string& keyPassword);
+
+    void ClearCertificate();
+
+    void SetPkcs11(bool pkcs11)
+    {
+      pkcs11_ = pkcs11;
+    }
+
+    void ClearBody();
+
+    void SwapBody(std::string& body);
+
+    void SetBody(const std::string& body);
+
+    void SetBody(IRequestBody& body);
+
+    // This function can be used to disable chunked transfers if the
+    // remote server is Orthanc with a version <= 1.5.6.
+    void SetChunkedTransfersAllowed(bool allow)
+    {
+      allowChunkedTransfers_ = allow;
+    }
+
+    bool IsChunkedTransfersAllowed() const
+    {
+      return allowChunkedTransfers_;
+    }
+
+    void Execute(IAnswer& answer);
+
+    void Execute(HttpHeaders& answerHeaders /* out */,
+                 std::string& answerBody /* out */);
+
+    void Execute(HttpHeaders& answerHeaders /* out */,
+                 Json::Value& answerBody /* out */);
+
+    void Execute();
+  };
+#endif
+
+
+
+  class IChunkedRequestReader : public boost::noncopyable
+  {
+  public:
+    virtual ~IChunkedRequestReader()
+    {
+    }
+
+    virtual void AddChunk(const void* data,
+                          size_t size) = 0;
+
+    virtual void Execute(OrthancPluginRestOutput* output) = 0;
+  };
+
+
+  typedef IChunkedRequestReader* (*ChunkedRestCallback) (const char* url,
+                                                         const OrthancPluginHttpRequest* request);
+
+
+  namespace Internals
+  {
+    void NullRestCallback(OrthancPluginRestOutput* output,
+                          const char* url,
+                          const OrthancPluginHttpRequest* request);
+  
+    IChunkedRequestReader *NullChunkedRestCallback(const char* url,
+                                                   const OrthancPluginHttpRequest* request);
+
+
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
+    template <ChunkedRestCallback Callback>
+    static OrthancPluginErrorCode ChunkedProtect(OrthancPluginServerChunkedRequestReader** reader,
+                                                const char* url,
+                                                const OrthancPluginHttpRequest* request)
+    {
+      try
+      {
+        if (reader == NULL)
+        {
+          return OrthancPluginErrorCode_InternalError;
+        }
+        else
+        {
+          *reader = reinterpret_cast<OrthancPluginServerChunkedRequestReader*>(Callback(url, request));
+          if (*reader == NULL)
+          {
+            return OrthancPluginErrorCode_Plugin;
+          }
+          else
+          {
+            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;
+      }
+    }
+
+    OrthancPluginErrorCode ChunkedRequestReaderAddChunk(
+      OrthancPluginServerChunkedRequestReader* reader,
+      const void*                              data,
+      uint32_t                                 size);
+
+    OrthancPluginErrorCode ChunkedRequestReaderExecute(
+      OrthancPluginServerChunkedRequestReader* reader,
+      OrthancPluginRestOutput*                 output);
+
+    void ChunkedRequestReaderFinalize(
+      OrthancPluginServerChunkedRequestReader* reader);
+
+#else  
+
+    OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
+                                                    const char* url,
+                                                    const OrthancPluginHttpRequest* request,
+                                                    RestCallback GetHandler,
+                                                    ChunkedRestCallback PostHandler,
+                                                    RestCallback DeleteHandler,
+                                                    ChunkedRestCallback PutHandler);
+
+    template<
+      RestCallback         GetHandler,
+      ChunkedRestCallback  PostHandler,
+      RestCallback         DeleteHandler,
+      ChunkedRestCallback  PutHandler
+      >
+    inline OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
+                                                           const char* url,
+                                                           const OrthancPluginHttpRequest* request)
+    {
+      return ChunkedRestCompatibility(output, url, request, GetHandler,
+                                      PostHandler, DeleteHandler, PutHandler);
+    }
+#endif
+  }
+
+
+
+  // NB: We use a templated class instead of a templated function, because
+  // default values are only available in functions since C++11
+  template<
+    RestCallback         GetHandler    = Internals::NullRestCallback,
+    ChunkedRestCallback  PostHandler   = Internals::NullChunkedRestCallback,
+    RestCallback         DeleteHandler = Internals::NullRestCallback,
+    ChunkedRestCallback  PutHandler    = Internals::NullChunkedRestCallback
+    >
+  class ChunkedRestRegistration : public boost::noncopyable
+  {
+  public:
+    static void Apply(const std::string& uri)
+    {
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
+      OrthancPluginRegisterChunkedRestCallback(
+        GetGlobalContext(), uri.c_str(),
+        GetHandler == Internals::NullRestCallback         ? NULL : Internals::Protect<GetHandler>,
+        PostHandler == Internals::NullChunkedRestCallback ? NULL : Internals::ChunkedProtect<PostHandler>,
+        DeleteHandler == Internals::NullRestCallback      ? NULL : Internals::Protect<DeleteHandler>,
+        PutHandler == Internals::NullChunkedRestCallback  ? NULL : Internals::ChunkedProtect<PutHandler>,
+        Internals::ChunkedRequestReaderAddChunk,
+        Internals::ChunkedRequestReaderExecute,
+        Internals::ChunkedRequestReaderFinalize);
+#else
+      OrthancPluginRegisterRestCallbackNoLock(
+        GetGlobalContext(), uri.c_str(), 
+        Internals::ChunkedRestCompatibility<GetHandler, PostHandler, DeleteHandler, PutHandler>);
+#endif
+    }
+  };
+
+  
+
+#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
+  class IStorageCommitmentScpHandler : public boost::noncopyable
+  {
+  public:
+    virtual ~IStorageCommitmentScpHandler()
+    {
+    }
+    
+    virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                               const std::string& sopInstanceUid) = 0;
+    
+    static OrthancPluginErrorCode Lookup(OrthancPluginStorageCommitmentFailureReason* target,
+                                         void* rawHandler,
+                                         const char* sopClassUid,
+                                         const char* sopInstanceUid);
+
+    static void Destructor(void* rawHandler);
+  };
+#endif
+
+
+  class DicomInstance : public boost::noncopyable
+  {
+  private:
+    bool toFree_;
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
+    const OrthancPluginDicomInstance*  instance_;
+#else
+    OrthancPluginDicomInstance*  instance_;
+#endif
+    
+  public:
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
+    DicomInstance(const OrthancPluginDicomInstance* instance);
+#else
+    DicomInstance(OrthancPluginDicomInstance* instance);
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    DicomInstance(const void* buffer,
+                  size_t size);
+#endif
+
+    ~DicomInstance();
+
+    std::string GetRemoteAet() const;
+
+    const void* GetBuffer() const
+    {
+      return OrthancPluginGetInstanceData(GetGlobalContext(), instance_);
+    }
+
+    size_t GetSize() const
+    {
+      return static_cast<size_t>(OrthancPluginGetInstanceSize(GetGlobalContext(), instance_));
+    }
+
+    void GetJson(Json::Value& target) const;
+
+    void GetSimplifiedJson(Json::Value& target) const;
+
+    OrthancPluginInstanceOrigin GetOrigin() const
+    {
+      return OrthancPluginGetInstanceOrigin(GetGlobalContext(), instance_);
+    }
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
+    std::string GetTransferSyntaxUid() const;
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
+    bool HasPixelData() const;
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    unsigned int GetFramesCount() const
+    {
+      return OrthancPluginGetInstanceFramesCount(GetGlobalContext(), instance_);
+    }
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    void GetRawFrame(std::string& target,
+                     unsigned int frameIndex) const;
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    OrthancImage* GetDecodedFrame(unsigned int frameIndex) const;
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    void Serialize(std::string& target) const;
+#endif
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
+    static DicomInstance* Transcode(const void* buffer,
+                                    size_t size,
+                                    const std::string& transferSyntax);
+#endif
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginException.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <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
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPlugins.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,37 @@
+set(ORTHANC_ROOT ${SAMPLES_ROOT}/../..)
+include(CheckIncludeFiles)
+include(CheckIncludeFileCXX)
+include(CheckLibraryExists)
+include(FindPythonInterp)
+include(${ORTHANC_ROOT}/Resources/CMake/AutoGeneratedCode.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
+
+
+if (CMAKE_COMPILER_IS_GNUCXX)
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic")
+endif()
+
+
+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 pthread)
+endif()
+
+include_directories(${SAMPLES_ROOT}/../Include/)
+
+if (MSVC)
+  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(${SAMPLES_ROOT}/../../Resources/ThirdParty/VisualStudio/)
+  endif()
+endif()
+
+add_definitions(-DHAS_ORTHANC_EXCEPTION=0)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/Common/SimplifiedOrthancDataset.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/SimplifiedOrthancDataset.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Plugins/Samples/Common/VersionScript.map	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,12 @@
+# This is a version-script for Orthanc plugins
+
+{
+global:
+  OrthancPluginInitialize;
+  OrthancPluginFinalize;
+  OrthancPluginGetName;
+  OrthancPluginGetVersion;
+
+local:
+  *;
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,65 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(ConnectivityChecks)
+
+SET(PLUGIN_NAME "connectivity-checks" CACHE STRING "Name of the plugin")
+SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin")
+
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../Resources/CMake/OrthancFrameworkParameters.cmake)
+include(${CMAKE_CURRENT_SOURCE_DIR}/../../../Resources/CMake/OrthancFrameworkConfiguration.cmake)
+
+include(JavaScriptLibraries.cmake)
+
+if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+  execute_process(
+    COMMAND 
+    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
+    ${PLUGIN_VERSION} ConnectivityChecks ConnectivityChecks.dll "Orthanc plugin to serve additional folders"
+    ERROR_VARIABLE Failure
+    OUTPUT_FILE ${AUTOGENERATED_DIR}/ConnectivityChecks.rc
+    )
+
+  if (Failure)
+    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
+  endif()
+
+  list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/ConnectivityChecks.rc)
+endif()  
+
+EmbedResources(
+  --framework-path=${CMAKE_CURRENT_SOURCE_DIR}/../../../Core
+  WEB_RESOURCES  ${CMAKE_CURRENT_SOURCE_DIR}/WebResources
+  LIBRARIES      ${JAVASCRIPT_LIBS_DIR}
+  )
+
+add_definitions(
+  -DHAS_ORTHANC_EXCEPTION=1
+  -DORTHANC_PLUGIN_NAME="${PLUGIN_NAME}"
+  -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}"
+  )
+
+include_directories(
+  ${ORTHANC_ROOT}/Plugins/Include/
+  )
+
+add_library(ConnectivityChecks SHARED
+  ${ADDITIONAL_RESOURCES}
+  ${AUTOGENERATED_SOURCES}
+  ${ORTHANC_CORE_SOURCES_DEPENDENCIES}
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  Plugin.cpp
+  )
+
+set_target_properties(
+  ConnectivityChecks PROPERTIES 
+  VERSION ${PLUGIN_VERSION} 
+  SOVERSION ${PLUGIN_VERSION}
+  )
+
+install(
+  TARGETS ConnectivityChecks
+  DESTINATION .
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,42 @@
+set(BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
+
+DownloadPackage(
+  "da0189f7c33bf9f652ea65401e0a3dc9"
+  "${BASE_URL}/dicom-web/bootstrap-4.3.1.zip"
+  "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1")
+
+DownloadPackage(
+  "8242afdc5bd44105d9dc9e6535315484"
+  "${BASE_URL}/dicom-web/vuejs-2.6.10.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10")
+
+DownloadPackage(
+  "3e2b4e1522661f7fcf8ad49cb933296c"
+  "${BASE_URL}/dicom-web/axios-0.19.0.tar.gz"
+  "${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0")
+
+DownloadFile(
+  "220afd743d9e9643852e31a135a9f3ae"
+  "${BASE_URL}/jquery-3.4.1.min.js")
+
+
+set(JAVASCRIPT_LIBS_DIR  ${CMAKE_CURRENT_BINARY_DIR}/javascript-libs)
+file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR})
+
+file(COPY
+  ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js
+  ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.map
+  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/js/bootstrap.min.js
+  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/js/bootstrap.min.js.map
+  ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
+  ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/jquery-3.4.1.min.js
+  DESTINATION
+  ${JAVASCRIPT_LIBS_DIR}/js
+  )
+
+file(COPY
+  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css
+  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css.map
+  DESTINATION
+  ${JAVASCRIPT_LIBS_DIR}/css
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/Plugin.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <EmbeddedResources.h>
+#include <orthanc/OrthancCPlugin.h>
+
+#include "../../../Core/OrthancException.h"
+#include "../../../Core/SystemToolbox.h"
+
+#define ROOT_URI "/connectivity-checks"
+
+
+static OrthancPluginContext* context_ = NULL;
+
+
+template <Orthanc::EmbeddedResources::DirectoryResourceId DIRECTORY>
+static OrthancPluginErrorCode ServeStaticResource(OrthancPluginRestOutput* output,
+                                                  const char* url,
+                                                  const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    return OrthancPluginErrorCode_Success;
+  }
+
+  std::string path = "/" + std::string(request->groups[0]);
+  std::string mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path));
+
+  try
+  {
+    std::string s;
+    Orthanc::EmbeddedResources::GetDirectoryResource(s, DIRECTORY, path.c_str());
+
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context_, output, resource, s.size(), mime.c_str());
+  }
+  catch (Orthanc::OrthancException&)
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context_, s.c_str());
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+
+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[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;
+    }
+
+    /* Register the callbacks */
+    OrthancPluginSetDescription(context_, "Utilities to check connectivity to DICOM modalities, DICOMweb servers and Orthanc peers.");
+    OrthancPluginSetRootUri(context_, ROOT_URI "/app/index.html");
+    OrthancPluginRegisterRestCallback(context_, ROOT_URI "/libs/(.*)", ServeStaticResource<Orthanc::EmbeddedResources::LIBRARIES>);
+    OrthancPluginRegisterRestCallback(context_, ROOT_URI "/app/(.*)", ServeStaticResource<Orthanc::EmbeddedResources::WEB_RESOURCES>);
+ 
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return ORTHANC_PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/app.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,145 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+new Vue({
+  el: '#app',
+  data: {
+    dicomNodes: {},
+    peers: [],
+    canTestPeers: false,
+    dicomWebServers: []
+  },
+  methods: {
+    toggle: function (todo) {
+      todo.done = !todo.done
+    },
+
+    testDicomModalities: function () {
+      console.log('testing DICOM modalities');
+      axios
+        .get('../../modalities?expand')
+        .then(response => {
+          this.dicomNodes = response.data;
+          for (let alias of Object.keys(this.dicomNodes)) {
+            this.dicomNodes[alias]['alias'] = alias;
+            this.dicomNodes[alias]['status'] = 'testing';
+            axios
+              .post('../../modalities/' + alias + '/echo')
+              .then(response => {
+                this.dicomNodes[alias]['status'] = 'ok';
+                this.$forceUpdate();
+              })
+              .catch(response => {
+                this.dicomNodes[alias]['status'] = 'ko';
+                this.$forceUpdate();
+              })
+                }
+        })
+    },
+
+    testOrthancPeers: function () {
+      console.log('testing Orthanc peers');
+      axios
+        .get('../../peers?expand')
+        .then(response => {
+          this.peers = response.data;
+          for (let alias of Object.keys(this.peers)) {
+            this.peers[alias]['alias'] = alias;
+
+            if (this.canTestPeers) {
+              this.peers[alias]['status'] = 'testing';
+              axios
+                .get('../../peers/' + alias + '/system') // introduced in ApiVersion 5 only !
+                .then(response => {
+                  this.peers[alias]['status'] = 'ok';
+                  this.$forceUpdate();
+                })
+                .catch(response => {
+                  this.peers[alias]['status'] = 'ko';
+                  this.$forceUpdate();
+                })
+                  }
+            else {
+              this.peers[alias]['status'] = 'unknown';
+              this.$forceUpdate();
+            }
+          }
+        })
+    },
+
+    testDicomWebServers: function () {
+      console.log('testing Dicom-web servers');
+      axios
+        .get('../../dicom-web/servers?expand')
+        .then(response => {
+          this.dicomWebServers = response.data;
+          for (let alias of Object.keys(this.dicomWebServers)) {
+            this.dicomWebServers[alias]['alias'] = alias;
+            this.dicomWebServers[alias]['status'] = 'testing';
+
+            // perform a dummy qido-rs to test the connectivity
+            axios
+              .post('../../dicom-web/servers/' + alias + '/qido', {
+                'Uri' : '/studies',
+                'Arguments' : {
+                  '00100010' : 'CONNECTIVITY^CHECKS'
+                }
+              })
+              .then(response => {
+                this.dicomWebServers[alias]['status'] = 'ok';
+                this.$forceUpdate();
+              })
+              .catch(response => {
+                this.dicomWebServers[alias]['status'] = 'ko';
+                this.$forceUpdate();
+              })
+                }
+        })
+    },
+
+  },
+  computed: {
+  },
+  mounted() {
+    axios
+      .get('../../system')
+      .then(response => {
+        this.canTestPeers = response.data.ApiVersion >= 5;
+        this.testDicomModalities();
+        if (this.canTestPeers) {
+          this.testOrthancPeers();
+        }
+        this.testDicomWebServers();
+      })
+  }
+})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/index.html	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+
+    <link rel="stylesheet" href="../libs/css/bootstrap.min.css">
+
+    <title>Orthanc Connectivity checks</title>
+    <link rel="stylesheet" href="style.css" type="text/css">
+  </head>
+
+  <body>
+    <div id="app" class="container-fluid">
+      <h2>DICOM nodes</h2>
+      <table class="table">
+        <thead>
+          <tr>
+            <th scope="col">Alias</th>
+            <th scope="col">AET</th>
+            <th scope="col">Host</th>
+            <th scope="col">Port</th>
+            <th scope="col">Status</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="node in dicomNodes">
+            <th scope="row">{{node.alias}}</th>
+            <td>{{node.AET}}</td>
+            <td>{{node.Host}}</td>
+            <td>{{node.Port}}</td>
+            <td v-if="node.status=='ok'" class="connected">Connected</td>
+            <td v-if="node.status=='ko'" class="disconnected">Disconnected</td>
+            <td v-if="node.status=='testing'">
+              <div class="spinner-border" role="status">
+                <span class="sr-only">Testing...</span>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <h2>Orthanc peers</h2>
+      <table class="table" v-if="canTestPeers">
+        <thead>
+          <tr>
+            <th scope="col">Alias</th>
+            <th scope="col">Url</th>
+            <th scope="col">Status</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="node in peers">
+            <th scope="row">{{node.alias}}</th>
+            <td>{{node.Url}}</td>
+            <td v-if="node.status=='ok'" class="connected">Connected</td>
+            <td v-if="node.status=='ko'" class="disconnected">Disconnected</td>
+            <td v-if="node.status=='unknown'" class="unknown">
+              Can not test the peers connectivity with this version of Orthanc
+            </td>
+            <td v-if="node.status=='testing'">
+              <div class="spinner-border" role="status">
+                <span class="sr-only">Testing...</span>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      <h2>DicomWeb servers</h2>
+      <table class="table">
+        <thead>
+          <tr>
+            <th scope="col">Alias</th>
+            <th scope="col">Url</th>
+            <th scope="col">Status</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="node in dicomWebServers">
+            <th scope="row">{{node.alias}}</th>
+            <td>{{node.Url}}</td>
+            <td v-if="node.status=='ok'" class="connected">Connected</td>
+            <td v-if="node.status=='ko'" class="disconnected">Disconnected</td>
+            <td v-if="node.status=='testing'">
+              <div class="spinner-border" role="status">
+                <span class="sr-only">Testing...</span>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <script src="../libs/js/jquery-3.4.1.min.js" type="text/javascript"></script>
+    <script src="../libs/js/bootstrap.min.js" type="text/javascript"></script>
+    <script src="../libs/js/axios.min.js" type="text/javascript"></script>
+    <script src="../libs/js/vue.min.js" type="text/javascript"></script>
+    <script src="app.js" type="text/javascript"></script>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ConnectivityChecks/WebResources/style.css	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,13 @@
+.connected {
+    background-color: darkgreen;
+    color: white;
+}
+
+.disconnected {
+  background-color: darkred;
+    color: white;
+}
+
+.unknown {
+  background-color: gold;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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/OrthancServer/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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 "../../../Core/Compatibility.h"
+#include "../Common/OrthancPluginCppWrapper.h"
+
+#include <boost/filesystem.hpp>
+#include <json/value.h>
+#include <json/reader.h>
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+
+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;
+  dicom.ReadFile(path);
+
+  if (matcher.IsMatch(dicom))
+  {
+    // This DICOM file matches the worklist query, add it to the answers
+    OrthancPluginErrorCode code = OrthancPluginWorklistAddAnswer
+      (OrthancPlugins::GetGlobalContext(), answers, query, dicom.GetData(), dicom.GetSize());
+
+    if (code != OrthancPluginErrorCode_Success)
+    {
+      OrthancPlugins::LogError("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;
+  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("Received worklist query from remote modality " +
+                          std::string(issuerAet) + ":\n" + json.toStyledString());
+
+  if (!filterIssuerAet_)
+  {
+    return new OrthancPlugins::FindMatcher(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;
+    modified.CreateDicom(json, OrthancPluginCreateDicomFlags_None);
+    
+    return new OrthancPlugins::FindMatcher(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::unique_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("Worklist matched: " + it->path().string());
+              matchedWorklistCount++;
+            }
+          }
+        }
+      }
+
+      std::ostringstream message;
+      message << "Worklist C-Find: parsed " << parsedFilesCount << " files, found " << matchedWorklistCount << " match(es)";
+      OrthancPlugins::LogInfo(message.str());
+
+    }
+    catch (fs::filesystem_error&)
+    {
+      OrthancPlugins::LogError("Inexistent folder while scanning for worklists: " + source.string());
+      return OrthancPluginErrorCode_DirectoryExpected;
+    }
+
+    // Uncomment the following line if too many answers are to be returned
+    // OrthancPluginMarkWorklistAnswersIncomplete(OrthancPlugins::GetGlobalContext(), answers);
+
+    return OrthancPluginErrorCode_Success;
+  }
+  catch (OrthancPlugins::PluginException& e)
+  {
+    return e.GetErrorCode();
+  }
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    OrthancPlugins::SetGlobalContext(c);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPlugins::LogWarning("Sample worklist plugin is initializing");
+    OrthancPluginSetDescription(c, "Serve DICOM modality worklists from a folder with Orthanc.");
+
+    OrthancPlugins::OrthancConfiguration configuration;
+
+    OrthancPlugins::OrthancConfiguration worklists;
+    configuration.GetSection(worklists, "Worklists");
+
+    bool enabled = worklists.GetBooleanValue("Enable", false);
+    if (enabled)
+    {
+      if (worklists.LookupStringValue(folder_, "Database"))
+      {
+        OrthancPlugins::LogWarning("The database of worklists will be read from folder: " + folder_);
+        OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback);
+      }
+      else
+      {
+        OrthancPlugins::LogError("The configuration option \"Worklists.Database\" must contain a path");
+        return -1;
+      }
+
+      filterIssuerAet_ = worklists.GetBooleanValue("FilterIssuerAet", false);
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("Worklist server is disabled by the configuration file");
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPlugins::LogWarning("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/OrthancServer/Plugins/Samples/ModalityWorklists/README	Wed Jun 10 20:30:34 2020 +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/OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py	Wed Jun 10 20:30:34 2020 +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 sorted(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 OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist1.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist10.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist2.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist3.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist4.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist5.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist6.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist7.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist8.wl has changed
Binary file OrthancServer/Plugins/Samples/ModalityWorklists/WorklistsDatabase/wklist9.wl has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,3 @@
+More contributed samples of plugins can be found and added in
+the "OrthancContributed" repository on GitHub:
+https://github.com/jodogne/OrthancContributed/tree/master/Plugins
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ServeFolders/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(ServeFolders)
+
+SET(SERVE_FOLDERS_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(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
+include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
+
+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}")
+
+set_target_properties(ServeFolders PROPERTIES 
+  VERSION ${SERVE_FOLDERS_VERSION} 
+  SOVERSION ${SERVE_FOLDERS_VERSION})
+
+install(
+  TARGETS ServeFolders
+  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/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,465 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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 <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 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 void SetHttpHeaders(OrthancPluginRestOutput* output)
+{
+  if (!allowCache_)
+  {
+    // http://stackoverflow.com/a/2068407/881731
+    OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+    OrthancPluginSetHttpHeader(context, output, "Cache-Control", "no-cache, no-store, must-revalidate");
+    OrthancPluginSetHttpHeader(context, output, "Pragma", "no-cache");
+    OrthancPluginSetHttpHeader(context, output, "Expires", "0");
+  }
+}
+
+
+static void RegisterDefaultExtensions()
+{
+  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 std::string GetMimeType(const std::string& path)
+{
+  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())
+  {
+    return found->second;
+  }
+  else
+  {
+    OrthancPlugins::LogWarning("ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
+    return "application/octet-stream";
+  }
+}
+
+
+static bool LookupFolder(std::string& folder,
+                         OrthancPluginRestOutput* output,
+                         const OrthancPluginHttpRequest* request)
+{
+  const std::string uri = request->groups[0];
+
+  std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
+  if (found == folders_.end())
+  {
+    OrthancPlugins::LogError("Unknown URI in plugin server-folders: " + uri);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
+    return false;
+  }
+  else
+  {
+    folder = found->second;
+    return true;
+  }
+}
+
+
+static void Answer(OrthancPluginRestOutput* output,
+                   const char* content,
+                   size_t size,
+                   const std::string& mime)
+{
+  if (generateETag_)
+  {
+    OrthancPlugins::OrthancString md5;
+    md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), content, size));
+
+    std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
+    OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
+  }
+
+  SetHttpHeaders(output);
+  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), 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(OrthancPlugins::GetGlobalContext(), output, "GET");
+    return;
+  }
+
+  std::string folder;
+
+  if (LookupFolder(folder, output, request))
+  {
+    const fs::path item(request->groups[1]);
+    const fs::path parent((fs::path(folder) / item).parent_path());
+
+    if (item.filename().string() == "index.html" &&
+        fs::is_directory(parent) &&
+        !fs::is_regular_file(fs::path(folder) / item))
+    {
+      // On-the-fly generation of an "index.html" 
+      std::string s;
+      s += "<html>\n";
+      s += "  <body>\n";
+      s += "    <ul>\n";
+
+      fs::directory_iterator end;
+
+      for (fs::directory_iterator it(parent) ; it != end; ++it)
+      {
+        if (fs::is_directory(it->status()))
+        {
+          std::string f = it->path().filename().string();
+          s += "      <li><a href=\"" + f + "/index.html\">" + f + "/</a></li>\n";
+        }
+      }
+
+      for (fs::directory_iterator it(parent) ; 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 f = it->path().filename().string();
+          s += "      <li><a href=\"" + f + "\">" + f + "</a></li>\n";
+        }
+      }
+
+      s += "    </ul>\n";
+      s += "  </body>\n";
+      s += "</html>\n";
+
+      Answer(output, s.c_str(), s.size(), "text/html");
+    }
+    else
+    {
+      std::string path = folder + "/" + item.string();
+      std::string mime = GetMimeType(path);
+
+      OrthancPlugins::MemoryBuffer content;
+
+      try
+      {
+        content.ReadFile(path);
+      }
+      catch (...)
+      {
+        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(OrthancPlugins::GetGlobalContext(),
+                                 output, "Last-Modified", t.c_str());
+
+      Answer(output, content.GetData(), content.GetSize(), mime);
+    }
+  }
+}
+
+
+void ListServedFolders(OrthancPluginRestOutput* output,
+                       const char* url,
+                       const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET");
+    return;
+  }
+
+  std::string s = "<html><body><h1>Additional folders served by Orthanc</h1>\n";
+
+  if (folders_.empty())
+  {
+    s += "<p>Empty section <tt>ServeFolders</tt> in your configuration file: No additional folder is served.</p>\n";
+  }
+  else
+  {
+    s += "<ul>\n";
+    for (std::map<std::string, std::string>::const_iterator
+           it = folders_.begin(); it != folders_.end(); ++it)
+    {
+      // The URI is relative to INDEX_URI ("/app/plugin-serve-folders.html")
+      s += "<li><a href=\"../" + it->first + "/index.html\">" + it->first + "</li>\n";
+    }
+    
+    s += "</ul>\n";
+  }
+
+  s += "</body></html>\n";
+
+  Answer(output, s.c_str(), s.size(), "text/html");
+}
+
+
+static void ConfigureFolders(const Json::Value& folders)
+{
+  if (folders.type() != Json::objectValue)
+  {
+    OrthancPlugins::LogError("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("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("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("Trying to 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>(regex.c_str(), true);
+    }
+  }
+}
+
+
+static void ConfigureExtensions(const Json::Value& extensions)
+{
+  if (extensions.type() != Json::objectValue)
+  {
+    OrthancPlugins::LogError("The list of extensions is badly formatted (must be a JSON object)");
+    ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
+  }
+
+  Json::Value::Members members = extensions.getMemberNames();
+
+  for (Json::Value::Members::const_iterator 
+         it = members.begin(); it != members.end(); ++it)
+  {
+    if (extensions[*it].type() != Json::stringValue)
+    {
+      OrthancPlugins::LogError("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("ServeFolders: Removing MIME type for file extension \"." +
+                                 name + "\"");
+    }
+    else
+    {
+      OrthancPlugins::LogWarning("ServeFolders: Associating file extension \"." + name + 
+                                 "\" with MIME type \"" + mime + "\"");
+    }
+  }  
+}
+
+
+static void ReadConfiguration()
+{
+  OrthancPlugins::OrthancConfiguration configuration;
+
+  {
+    OrthancPlugins::OrthancConfiguration globalConfiguration;
+    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("ServeFolders: Requesting the HTTP client to " +
+                                 std::string(tmp ? "enable" : "disable") + 
+                                 " its caching mechanism");
+    }
+
+    if (configuration.LookupBooleanValue(tmp, "GenerateETag"))
+    {
+      generateETag_ = tmp;
+      OrthancPlugins::LogWarning("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("ServeFolders: Empty configuration file: "
+                               "No additional folder will be served!");
+  }
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
+  {
+    OrthancPlugins::SetGlobalContext(context);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(context) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    RegisterDefaultExtensions();
+    OrthancPluginSetDescription(context, "Serve additional folders with the HTTP server of Orthanc.");
+    OrthancPluginSetRootUri(context, INDEX_URI);
+    OrthancPlugins::RegisterRestCallback<ListServedFolders>(INDEX_URI, true);
+
+    try
+    {
+      ReadConfiguration();
+    }
+    catch (OrthancPlugins::PluginException& e)
+    {
+      OrthancPlugins::LogError("Error while initializing the ServeFolders plugin: " + 
+                               std::string(e.What(context)));
+    }
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "serve-folders";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return SERVE_FOLDERS_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/ServeFolders/README	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,11 @@
+ServeFolders plugin
+===================
+
+This sample plugin enables Orthanc to serve additional folders using
+its embedded Web server.
+
+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/serve-folders.html
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/StorageArea/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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(PluginTest SHARED Plugin.cpp)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/StorageArea/Plugin.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,164 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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.h>
+#include <stdio.h>
+#include <string>
+
+static OrthancPluginContext* context = NULL;
+
+
+static std::string GetPath(const char* uuid)
+{
+  return "plugin_" + std::string(uuid);
+}
+
+
+static OrthancPluginErrorCode StorageCreate(const char* uuid,
+                                            const void* content,
+                                            int64_t size,
+                                            OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid);
+
+  FILE* fp = fopen(path.c_str(), "wb");
+  if (!fp)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  bool ok = fwrite(content, size, 1, fp) == 1;
+  fclose(fp);
+
+  return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_StorageAreaPlugin;
+}
+
+
+static OrthancPluginErrorCode StorageRead(void** content,
+                                          int64_t* size,
+                                          const char* uuid,
+                                          OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid);
+
+  FILE* fp = fopen(path.c_str(), "rb");
+  if (!fp)
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  if (fseek(fp, 0, SEEK_END) < 0)
+  {
+    fclose(fp);
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  *size = ftell(fp);
+
+  if (fseek(fp, 0, SEEK_SET) < 0)
+  {
+    fclose(fp);
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+
+  bool ok = true;
+
+  if (*size == 0)
+  {
+    *content = NULL;
+  }
+  else
+  {
+    *content = malloc(*size);
+    if (*content == NULL ||
+        fread(*content, *size, 1, fp) != 1)
+    {
+      ok = false;
+    }
+  }
+
+  fclose(fp);
+
+  return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_StorageAreaPlugin;
+}
+
+
+static OrthancPluginErrorCode StorageRemove(const char* uuid,
+                                            OrthancPluginContentType type)
+{
+  std::string path = GetPath(uuid);
+
+  if (remove(path.c_str()) == 0)
+  {
+    return OrthancPluginErrorCode_Success;
+  }
+  else
+  {
+    return OrthancPluginErrorCode_StorageAreaPlugin;
+  }
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    context = c;
+    OrthancPluginLogWarning(context, "Storage plugin is initializing");
+
+    /* 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",
+              c->orthancVersion,
+              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      OrthancPluginLogError(context, info);
+      return -1;
+    }
+
+    OrthancPluginRegisterStorageArea(context, StorageCreate, StorageRead, StorageRemove);
+
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+    OrthancPluginLogWarning(context, "Storage plugin is finalizing");
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "storage";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return "1.0";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,37 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(StorageCommitmentScp)
+
+SET(PLUGIN_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(StorageCommitmentScp SHARED 
+  Plugin.cpp
+  ../Common/OrthancPluginCppWrapper.cpp
+  ${JSONCPP_SOURCES}
+  ${BOOST_SOURCES}
+  )
+
+message("Setting the version of the plugin to ${PLUGIN_VERSION}")
+add_definitions(
+  -DPLUGIN_VERSION="${PLUGIN_VERSION}"
+  )
+
+set_target_properties(StorageCommitmentScp PROPERTIES 
+  VERSION ${PLUGIN_VERSION} 
+  SOVERSION ${PLUGIN_VERSION})
+
+install(
+  TARGETS StorageCommitmentScp
+  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/OrthancServer/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,116 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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 <json/value.h>
+#include <json/reader.h>
+
+
+
+class StorageCommitmentSample : public OrthancPlugins::IStorageCommitmentScpHandler
+{
+private:
+  int count_;
+  
+public:
+  StorageCommitmentSample() : count_(0)
+  {
+  }
+  
+  virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                             const std::string& sopInstanceUid)
+  {
+    printf("?? [%s] [%s]\n", sopClassUid.c_str(), sopInstanceUid.c_str());
+    if (count_++ % 2 == 0)
+      return OrthancPluginStorageCommitmentFailureReason_Success;
+    else
+      return OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance;
+  }
+};
+
+
+static OrthancPluginErrorCode StorageCommitmentScp(void**              handler /* out */,
+                                                   const char*         jobId,
+                                                   const char*         transactionUid,
+                                                   const char* const*  sopClassUids,
+                                                   const char* const*  sopInstanceUids,
+                                                   uint32_t            countInstances,
+                                                   const char*         remoteAet,
+                                                   const char*         calledAet)
+{
+  /*std::string s;
+    OrthancPlugins::RestApiPost(s, "/jobs/" + std::string(jobId) + "/pause", NULL, 0, false);*/
+  
+  printf("[%s] [%s] [%s] [%s]\n", jobId, transactionUid, remoteAet, calledAet);
+
+  for (uint32_t i = 0; i < countInstances; i++)
+  {
+    printf("++ [%s] [%s]\n", sopClassUids[i], sopInstanceUids[i]);
+  }
+
+  *handler = new StorageCommitmentSample;
+  return OrthancPluginErrorCode_Success;
+}
+
+
+extern "C"
+{
+  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
+  {
+    OrthancPlugins::SetGlobalContext(c);
+
+    /* Check the version of the Orthanc core */
+    if (OrthancPluginCheckVersion(c) == 0)
+    {
+      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
+                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
+      return -1;
+    }
+
+    OrthancPluginSetDescription(c, "Sample storage commitment SCP plugin.");
+
+    OrthancPluginRegisterStorageCommitmentScpCallback(
+      c, StorageCommitmentScp,
+      OrthancPlugins::IStorageCommitmentScpHandler::Destructor,
+      OrthancPlugins::IStorageCommitmentScpHandler::Lookup);
+    
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return "storage-commitment-scp";
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,15 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(WebSkeleton)
+
+SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
+SET(RESOURCES_ROOT ${CMAKE_SOURCE_DIR}/StaticResources)
+
+set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
+include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
+
+include(Framework/Framework.cmake)
+
+add_library(WebSkeleton SHARED 
+  ${AUTOGENERATED_SOURCES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Configuration.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,28 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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
+
+#define ORTHANC_PLUGIN_NAME  "web-skeleton"
+
+#define ORTHANC_PLUGIN_VERSION "1.0"
+
+#define ORTHANC_PLUGIN_WEB_ROOT  "/plugin-web-skeleton/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,393 @@
+# Orthanc - A Lightweight, RESTful DICOM Store
+# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+# Department, University Hospital of Liege, Belgium
+# Copyright (C) 2017-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 sys
+import os
+import os.path
+import pprint
+import re
+
+UPCASE_CHECK = True
+ARGS = []
+for i in range(len(sys.argv)):
+    if not sys.argv[i].startswith('--'):
+        ARGS.append(sys.argv[i])
+    elif sys.argv[i].lower() == '--no-upcase-check':
+        UPCASE_CHECK = False
+
+if len(ARGS) < 2 or len(ARGS) % 2 != 0:
+    print ('Usage:')
+    print ('python %s [--no-upcase-check] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
+    exit(-1)
+
+TARGET_BASE_FILENAME = ARGS[1]
+SOURCES = ARGS[2:]
+
+try:
+    # Make sure the destination directory exists
+    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
+except:
+    pass
+
+
+#####################################################################
+## Read each resource file
+#####################################################################
+
+def CheckNoUpcase(s):
+    global UPCASE_CHECK
+    if (UPCASE_CHECK and
+        re.search('[A-Z]', s) != None):
+        raise Exception("Path in a directory with an upcase letter: %s" % s)
+
+resources = {}
+
+counter = 0
+i = 0
+while i < len(SOURCES):
+    resourceName = SOURCES[i].upper()
+    pathName = SOURCES[i + 1]
+
+    if not os.path.exists(pathName):
+        raise Exception("Non existing path: %s" % pathName)
+
+    if resourceName in resources:
+        raise Exception("Twice the same resource: " + resourceName)
+    
+    if os.path.isdir(pathName):
+        # The resource is a directory: Recursively explore its files
+        content = {}
+        for root, dirs, files in os.walk(pathName):
+            dirs.sort()
+            files.sort()
+            base = os.path.relpath(root, pathName)
+            for f in files:
+                if f.find('~') == -1:  # Ignore Emacs backup files
+                    if base == '.':
+                        r = f
+                    else:
+                        r = os.path.join(base, f)
+
+                    CheckNoUpcase(r)
+                    r = '/' + r.replace('\\', '/')
+                    if r in content:
+                        raise Exception("Twice the same filename (check case): " + r)
+
+                    content[r] = {
+                        'Filename' : os.path.join(root, f),
+                        'Index' : counter
+                        }
+                    counter += 1
+
+        resources[resourceName] = {
+            'Type' : 'Directory',
+            'Files' : content
+            }
+
+    elif os.path.isfile(pathName):
+        resources[resourceName] = {
+            'Type' : 'File',
+            'Index' : counter,
+            'Filename' : pathName
+            }
+        counter += 1
+
+    else:
+        raise Exception("Not a regular file, nor a directory: " + pathName)
+
+    i += 2
+
+#pprint.pprint(resources)
+
+
+#####################################################################
+## Write .h header
+#####################################################################
+
+header = open(TARGET_BASE_FILENAME + '.h', 'w')
+
+header.write("""
+#pragma once
+
+#include <string>
+#include <list>
+
+namespace Orthanc
+{
+  namespace EmbeddedResources
+  {
+    enum FileResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    enum DirectoryResourceId
+    {
+""")
+
+isFirst = True
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        if isFirst:
+            isFirst = False
+        else:    
+            header.write(',\n')
+        header.write('      %s' % name)
+
+header.write("""
+    };
+
+    const void* GetFileResourceBuffer(FileResourceId id);
+    size_t GetFileResourceSize(FileResourceId id);
+    void GetFileResource(std::string& result, FileResourceId id);
+
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
+
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id);
+  }
+}
+""")
+header.close()
+
+
+
+#####################################################################
+## Write the resource content in the .cpp source
+#####################################################################
+
+PYTHON_MAJOR_VERSION = sys.version_info[0]
+
+def WriteResource(cpp, item):
+    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
+
+    f = open(item['Filename'], "rb")
+    content = f.read()
+    f.close()
+
+    # http://stackoverflow.com/a/1035360
+    pos = 0
+    for b in content:
+        if PYTHON_MAJOR_VERSION == 2:
+            c = ord(b[0])
+        else:
+            c = b
+
+        if pos > 0:
+            cpp.write(', ')
+
+        if (pos % 16) == 0:
+            cpp.write('\n    ')
+
+        if c < 0:
+            raise Exception("Internal error")
+
+        cpp.write("0x%02x" % c)
+        pos += 1
+
+    # Zero-size array are disallowed, so we put one single void character in it.
+    if pos == 0:
+        cpp.write('  0')
+
+    cpp.write('  };\n')
+    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
+
+
+cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
+
+print os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+cpp.write("""
+#include "%s.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <stdexcept>
+
+namespace Orthanc
+{
+  namespace EmbeddedResources
+  {
+""" % (os.path.basename(TARGET_BASE_FILENAME)))
+
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        WriteResource(cpp, resources[name])
+    else:
+        for f in resources[name]['Files']:
+            WriteResource(cpp, resources[name]['Files'][f])
+
+
+
+#####################################################################
+## Write the accessors to the file resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetFileResourceBuffer(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+
+    size_t GetFileResourceSize(FileResourceId id)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'File':
+        cpp.write('      case %s:\n' % name)
+        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
+
+cpp.write("""
+      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+#####################################################################
+## Write the accessors to the directory resources in .cpp
+#####################################################################
+
+cpp.write("""
+    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+
+    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
+    {
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        isFirst = True
+        for path in resources[name]['Files']:
+            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
+            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
+        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+
+#####################################################################
+## List the resources in a directory
+#####################################################################
+
+cpp.write("""
+    void ListResources(std::list<std::string>& result, DirectoryResourceId id)
+    {
+      result.clear();
+
+      switch (id)
+      {
+""")
+
+for name in resources:
+    if resources[name]['Type'] == 'Directory':
+        cpp.write('      case %s:\n' % name)
+        for path in sorted(resources[name]['Files']):
+            cpp.write('        result.push_back("%s");\n' % path)
+        cpp.write('        break;\n\n')
+
+cpp.write("""      default:
+        throw std::runtime_error("Unknown resource");
+      }
+    }
+""")
+
+
+
+
+#####################################################################
+## Write the convenience wrappers in .cpp
+#####################################################################
+
+cpp.write("""
+    void GetFileResource(std::string& result, FileResourceId id)
+    {
+      size_t size = GetFileResourceSize(id);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetFileResourceBuffer(id), size);
+    }
+
+    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
+    {
+      size_t size = GetDirectoryResourceSize(id, path);
+      result.resize(size);
+      if (size > 0)
+        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
+    }
+  }
+}
+""")
+cpp.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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/>.
+
+
+if (STANDALONE_BUILD)
+  add_definitions(-DORTHANC_PLUGIN_STANDALONE=1)
+
+  set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
+  set(AUTOGENERATED_SOURCES "${AUTOGENERATED_DIR}/EmbeddedResources.cpp")
+
+  file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
+  include_directories(${AUTOGENERATED_DIR})
+
+  set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
+  add_custom_command(
+    OUTPUT
+    "${AUTOGENERATED_DIR}/EmbeddedResources.h"
+    "${AUTOGENERATED_DIR}/EmbeddedResources.cpp"
+    COMMAND 
+    python
+    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
+    "${AUTOGENERATED_DIR}/EmbeddedResources"
+    STATIC_RESOURCES
+    ${RESOURCES_ROOT}
+    DEPENDS
+    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
+    ${STATIC_RESOURCES}
+    )
+
+else()
+  add_definitions(
+    -DORTHANC_PLUGIN_STANDALONE=0
+    -DORTHANC_PLUGIN_RESOURCES_ROOT="${RESOURCES_ROOT}"
+    )
+endif()
+
+
+list(APPEND AUTOGENERATED_SOURCES
+  "${CMAKE_CURRENT_SOURCE_DIR}/Framework/Plugin.cpp"
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,272 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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 "../Configuration.h"
+
+#include <orthanc/OrthancCPlugin.h>
+#include <string>
+#include <stdexcept>
+#include <algorithm>
+#include <sys/stat.h>
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+// This is an auto-generated file for standalone builds
+#include <EmbeddedResources.h>
+#endif
+
+static OrthancPluginContext* context = NULL;
+
+
+static const char* GetMimeType(const std::string& path)
+{
+  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 == ".json")
+  {
+    return "application/json";
+  }
+  else if (extension == ".xml")
+  {
+    return "application/xml";
+  }
+  else if (extension == ".wasm")
+  {
+    return "application/wasm";
+  }
+  else if (extension == ".png")
+  {
+    return "image/png";
+  }
+  else if (extension == ".jpg" || extension == ".jpeg")
+  {
+    return "image/jpeg";
+  }
+  else
+  {
+    std::string s = "Unknown MIME type for extension: " + extension;
+    OrthancPluginLogWarning(context, s.c_str());
+    return "application/octet-stream";
+  }
+}
+
+
+static bool ReadFile(std::string& content,
+                     const std::string& path)
+{
+  struct stat s;
+  if (stat(path.c_str(), &s) != 0 ||
+      !(s.st_mode & S_IFREG))
+  {
+    // Either the path does not exist, or it is not a regular file
+    return false;
+  }
+
+  FILE* fp = fopen(path.c_str(), "rb");
+  if (fp == NULL)
+  {
+    return false;
+  }
+
+  long size;
+
+  if (fseek(fp, 0, SEEK_END) == -1 ||
+      (size = ftell(fp)) < 0)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  content.resize(size);
+      
+  if (fseek(fp, 0, SEEK_SET) == -1)
+  {
+    fclose(fp);
+    return false;
+  }
+
+  bool ok = true;
+
+  if (size > 0 &&
+      fread(&content[0], size, 1, fp) != 1)
+  {
+    ok = false;
+  }
+
+  fclose(fp);
+
+  return ok;
+}
+
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+static OrthancPluginErrorCode ServeStaticResource(OrthancPluginRestOutput* output,
+                                                  const char* url,
+                                                  const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return OrthancPluginErrorCode_Success;
+  }
+
+  std::string path = "/" + std::string(request->groups[0]);
+  const char* mime = GetMimeType(path);
+
+  try
+  {
+    std::string s;
+    Orthanc::EmbeddedResources::GetDirectoryResource
+      (s, Orthanc::EmbeddedResources::STATIC_RESOURCES, path.c_str());
+
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
+  }
+  catch (std::runtime_error&)
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context, s.c_str());
+    OrthancPluginSendHttpStatusCode(context, output, 404);
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+#endif
+
+
+#if ORTHANC_PLUGIN_STANDALONE == 0
+static OrthancPluginErrorCode ServeFolder(OrthancPluginRestOutput* output,
+                                          const char* url,
+                                          const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+    return OrthancPluginErrorCode_Success;
+  }
+
+  std::string path = ORTHANC_PLUGIN_RESOURCES_ROOT "/" + std::string(request->groups[0]);
+  const char* mime = GetMimeType(path);
+
+  std::string s;
+  if (ReadFile(s, path))
+  {
+    const char* resource = s.size() ? s.c_str() : NULL;
+    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
+  }
+  else
+  {
+    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
+    OrthancPluginLogError(context, s.c_str());
+    OrthancPluginSendHttpStatusCode(context, output, 404);
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+#endif
+
+
+static OrthancPluginErrorCode RedirectRoot(OrthancPluginRestOutput* output,
+                                           const char* url,
+                                           const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    OrthancPluginRedirect(context, output, ORTHANC_PLUGIN_WEB_ROOT "index.html");
+  }
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+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[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;
+    }
+
+    /* Register the callbacks */
+
+#if ORTHANC_PLUGIN_STANDALONE == 1
+    OrthancPluginLogInfo(context, "Serving static resources (standalone build)");
+    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeStaticResource);
+#else
+    OrthancPluginLogInfo(context, "Serving resources from folder: " ORTHANC_PLUGIN_RESOURCES_ROOT);
+    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeFolder);
+#endif
+
+    OrthancPluginRegisterRestCallback(context, "/", RedirectRoot);
+ 
+    return 0;
+  }
+
+
+  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
+  {
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
+  {
+    return ORTHANC_PLUGIN_NAME;
+  }
+
+
+  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
+  {
+    return ORTHANC_PLUGIN_VERSION;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/NOTES.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,7 @@
+This is a sample Orthanc plugin that serves static resources (HTML,
+JavaScript, CSS, images...).
+
+The resources to serve must be stored in the folder "StaticResources".
+
+The folder "Framework" contains a reusable framework for any plugin
+whose sole objective is to serve static resources.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Plugins/Samples/WebSkeleton/StaticResources/index.html	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,16 @@
+<!doctype html>
+
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>Web Skeleton</title>
+  </head>
+
+  <body>
+    <h1>Web Skeleton</h1>
+    <p>
+      This is a sample skeleton for Orthanc showing how to create a
+      plugin that serves static HTML resources.
+    </p>
+  </body>
+</html>
--- a/OrthancServer/PrecompiledHeadersServer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
--- a/OrthancServer/PrecompiledHeadersServer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/PrecompiledHeaders.h"
-
-#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
-
-#include <boost/thread.hpp>
-
-#endif
--- a/OrthancServer/QueryRetrieveHandler.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,171 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "QueryRetrieveHandler.h"
-
-#include "OrthancConfiguration.h"
-
-#include "../Core/DicomNetworking/DicomControlUserConnection.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/Logging.h"
-#include "LuaScripting.h"
-#include "ServerContext.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);
-    }
-  }
-
-
-  void QueryRetrieveHandler::Invalidate()
-  {
-    done_ = false;
-    answers_.Clear();
-  }
-
-
-  void QueryRetrieveHandler::Run()
-  {
-    if (!done_)
-    {
-      // Firstly, fix the content of the query for specific manufacturers
-      DicomMap fixed;
-      fixed.Assign(query_);
-
-      // Secondly, possibly fix the query with the user-provider Lua callback
-      FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); 
-
-      {
-        DicomAssociationParameters params(localAet_, modality_);
-        DicomControlUserConnection connection(params);
-        connection.Find(answers_, level_, fixed, findNormalized_);
-      }
-
-      done_ = true;
-    }
-  }
-
-
-  QueryRetrieveHandler::QueryRetrieveHandler(ServerContext& context) : 
-    context_(context),
-    localAet_(context.GetDefaultLocalApplicationEntityTitle()),
-    done_(false),
-    level_(ResourceType_Study),
-    answers_(false),
-    findNormalized_(true)
-  {
-  }
-
-
-  void QueryRetrieveHandler::SetModality(const std::string& symbolicName)
-  {
-    Invalidate();
-    modalityName_ = symbolicName;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      lock.GetConfiguration().GetDicomModalityUsingSymbolicName(modality_, symbolicName);
-    }
-  }
-
-
-  void QueryRetrieveHandler::SetLevel(ResourceType level)
-  {
-    Invalidate();
-    level_ = level;
-  }
-
-
-  void QueryRetrieveHandler::SetQuery(const DicomTag& tag,
-                                      const std::string& value)
-  {
-    Invalidate();
-    query_.SetValue(tag, value, false);
-  }
-
-
-  void QueryRetrieveHandler::CopyStringTag(const DicomMap& from,
-                                           const DicomTag& tag)
-  {
-    const DicomValue* value = from.TestAndGetValue(tag);
-
-    if (value == NULL ||
-        value->IsNull() ||
-        value->IsBinary())
-    {
-      throw OrthancException(ErrorCode_InexistentTag);
-    }
-    else
-    {
-      SetQuery(tag, value->GetContent());
-    }
-  }
-
-
-  size_t QueryRetrieveHandler::GetAnswersCount()
-  {
-    Run();
-    return answers_.GetSize();
-  }
-
-
-  void QueryRetrieveHandler::GetAnswer(DicomMap& target,
-                                       size_t i)
-  {
-    Run();
-    answers_.GetAnswer(i).ExtractDicomSummary(target);
-  }
-
-  
-  void QueryRetrieveHandler::SetFindNormalized(bool normalized)
-  {
-    Invalidate();
-    findNormalized_ = normalized;
-  }
-}
--- a/OrthancServer/QueryRetrieveHandler.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomNetworking/DicomFindAnswers.h"
-#include "../Core/DicomNetworking/RemoteModalityParameters.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class QueryRetrieveHandler : public IDynamicObject
-  {
-  private:
-    ServerContext&             context_;
-    std::string                localAet_;
-    bool                       done_;
-    RemoteModalityParameters   modality_;
-    ResourceType               level_;
-    DicomMap                   query_;
-    DicomFindAnswers           answers_;
-    std::string                modalityName_;
-    bool                       findNormalized_;
-
-    void Invalidate();
-
-  public:
-    QueryRetrieveHandler(ServerContext& context);
-
-    void SetModality(const std::string& symbolicName);
-
-    const RemoteModalityParameters& GetRemoteModality() const
-    {
-      return modality_;
-    }
-
-    const std::string& GetLocalAet() const
-    {
-      return localAet_;
-    }
-
-    const std::string& GetModalitySymbolicName() const
-    {
-      return modalityName_;
-    }
-
-    void SetLevel(ResourceType level);
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void SetQuery(const DicomTag& tag,
-                  const std::string& value);
-
-    const DicomMap& GetQuery() const
-    {
-      return query_;
-    }
-
-    void CopyStringTag(const DicomMap& from,
-                       const DicomTag& tag);
-
-    void Run();
-
-    size_t GetAnswersCount();
-
-    void GetAnswer(DicomMap& target,
-                   size_t i);
-
-    bool IsFindNormalized() const
-    {
-      return findNormalized_;
-    }
-
-    void SetFindNormalized(bool normalized);
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Configuration.json	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,576 @@
+{
+  /**
+   * General configuration of Orthanc
+   **/
+
+  // The logical name of this instance of Orthanc. This one is
+  // 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). 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.
+  "IndexDirectory" : "OrthancStorage",
+
+  // Path to the directory where Orthanc stores its large temporary
+  // files. The content of this folder can be safely deleted if
+  // Orthanc once stopped. The folder must exist. The corresponding
+  // filesystem must be properly sized, given that for instance a ZIP
+  // archive of DICOM images created by a job can weight several GBs,
+  // and that there might be up to "min(JobsHistorySize,
+  // MediaArchiveSize)" archives to be stored simultaneously. If not
+  // set, Orthanc will use the default temporary folder of the
+  // operating system (such as "/tmp/" on UNIX-like systems, or
+  // "C:/Temp" on Microsoft Windows).
+  // "TemporaryDirectory" : "/tmp/Orthanc/",
+
+  // Enable the transparent compression of the DICOM instances
+  "StorageCompression" : false,
+
+  // Maximum size of the storage in MB (a value of "0" indicates no
+  // limit on the storage size)
+  "MaximumStorageSize" : 0,
+
+  // Maximum number of patients that can be stored at a given time
+  // in the storage (a value of "0" indicates no limit on the number
+  // of patients)
+  "MaximumPatientCount" : 0,
+  
+  // List of paths to the custom Lua scripts that are to be loaded
+  // into this instance of Orthanc
+  "LuaScripts" : [
+  ],
+
+  // List of paths to the plugins that are to be loaded into this
+  // 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. 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,
+
+
+  /**
+   * Configuration of the HTTP server
+   **/
+
+  // Enable the HTTP server. If this parameter is set to "false",
+  // Orthanc acts as a pure DICOM server. The REST API and Orthanc
+  // Explorer will not be available.
+  "HttpServerEnabled" : true,
+
+  // HTTP port for the REST services and for the GUI
+  "HttpPort" : 8042,
+
+  // When the following option is "true", if an error is encountered
+  // while calling the REST API, a JSON message describing the error
+  // is put in the HTTP answer. This feature can be disabled if the
+  // HTTP client does not properly handles such answers.
+  "HttpDescribeErrors" : true,
+
+  // Enable HTTP compression to improve network bandwidth utilization,
+  // at the expense of more computations on the server. Orthanc
+  // supports the "gzip" and "deflate" HTTP encodings.
+  "HttpCompressionEnabled" : true,
+
+
+
+  /**
+   * Configuration of the DICOM server
+   **/
+
+  // Enable the DICOM server. If this parameter is set to "false",
+  // Orthanc acts as a pure REST server. It will not be possible to
+  // receive files or to do query/retrieve through the DICOM protocol.
+  "DicomServerEnabled" : true,
+
+  // The DICOM Application Entity Title (cannot be longer than 16
+  // characters)
+  "DicomAet" : "ORTHANC",
+
+  // 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, 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", "Chinese", "JapaneseKanji", "Korean", and
+  // "SimplifiedChinese".
+  "DefaultEncoding" : "Latin1",
+
+  // The transfer syntaxes that are accepted by Orthanc C-Store SCP
+  "DeflatedTransferSyntaxAccepted"     : true,
+  "JpegTransferSyntaxAccepted"         : true,
+  "Jpeg2000TransferSyntaxAccepted"     : true,
+  "JpegLosslessTransferSyntaxAccepted" : true,
+  "JpipTransferSyntaxAccepted"         : true,
+  "Mpeg2TransferSyntaxAccepted"        : true,
+  "RleTransferSyntaxAccepted"          : true,
+  "Mpeg4TransferSyntaxAccepted"        : true,  // New in Orthanc 1.6.0
+
+  // 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,
+
+
+
+  /**
+   * Security-related options for the HTTP server
+   **/
+
+  // Whether remote hosts can connect to the HTTP server
+  "RemoteAccessAllowed" : false,
+
+  // Whether or not SSL is enabled
+  "SslEnabled" : false,
+
+  // 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 (using HTTP
+  // basic access authentication). Starting with Orthanc 1.5.8, if
+  // "AuthenticationEnabled" is not explicitly set, authentication is
+  // enabled iff. remote access is allowed (i.e. the default value of
+  // "AuthenticationEnabled" equals that of "RemoteAccessAllowed").
+  /**
+     "AuthenticationEnabled" : false,
+   **/
+
+  // The list of the registered users. Because Orthanc uses HTTP
+  // Basic Authentication, the passwords are stored as plain text.
+  "RegisteredUsers" : {
+    // "alice" : "alicePassword"
+  },
+
+
+
+  /**
+   * Network topology
+   **/
+
+  // The list of the known DICOM modalities
+  "DicomModalities" : {
+    /**
+     * Uncommenting the following line would enable Orthanc to
+     * connect to an instance of the "storescp" open-source DICOM
+     * store (shipped in the DCMTK distribution), as started by the
+     * command line "storescp 2000". The first parameter is the
+     * AET of the remote modality (cannot be longer than 16 
+     * characters), the second one is the remote network address,
+     * and the third one is the TCP port number corresponding
+     * to the DICOM protocol on the remote modality (usually 104).
+     **/
+    // "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),
+     * - "Vitrea",
+     * - "GE" (Enterprise Archive, MRI consoles and Advantage Workstation
+     *   from GE Healthcare).
+     *
+     * This parameter is case-sensitive.
+     **/
+    // "vitrea" : [ "VITREA", "192.168.1.1", 104, "Vitrea" ]
+
+    /**
+     * By default, the Orthanc SCP accepts all DICOM commands (C-ECHO,
+     * C-STORE, C-FIND, C-MOVE, C-GET and storage commitment) issued by the
+     * registered remote SCU modalities. Starting with Orthanc 1.5.0,
+     * it is possible to specify which DICOM commands are allowed,
+     * separately for each remote modality, using the syntax
+     * below.
+     *
+     * The "AllowEcho" (resp.  "AllowStore") option only has an effect
+     * respectively if global option "DicomAlwaysAllowEcho"
+     * (resp. "DicomAlwaysAllowStore") is set to "false".
+     *
+     * Starting with Orthanc 1.7.0, "AllowTranscoding" can be used to
+     * disable the transcoding to uncompressed transfer syntaxes if
+     * the remote modality doesn't support compressed transfer
+     * syntaxes. This option only has an effect if global option
+     * "EnableTranscoding" is set to "true".
+     **/
+    //"untrusted" : {
+    //  "AET" : "ORTHANC",
+    //  "Port" : 104,
+    //  "Host" : "127.0.0.1",
+    //  "Manufacturer" : "Generic",
+    //  "AllowEcho" : false,
+    //  "AllowFind" : false,
+    //  "AllowGet"  : false,
+    //  "AllowMove" : false,
+    //  "AllowStore" : true,
+    //  "AllowStorageCommitment" : false,  // new in 1.6.0
+    //  "AllowTranscoding" : true          // new in 1.7.0
+    //}
+  },
+
+  // Whether to store the DICOM modalities in the Orthanc database
+  // instead of in this configuration file (new in Orthanc 1.5.0)
+  "DicomModalitiesInDatabase" : false,
+
+  // 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" : {
+    /**
+     * Each line gives the base URL of an Orthanc peer, possibly
+     * followed by the username/password pair (if the password
+     * protection is enabled on the peer).
+     **/
+    // "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 one 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
+    // }
+  },
+
+  // Whether to store the Orthanc peers in the Orthanc database
+  // instead of in this configuration file (new in Orthanc 1.5.0)
+  "OrthancPeersInDatabase" : false,
+
+  // Parameters of the HTTP proxy to be used by Orthanc. If set to the
+  // empty string, no HTTP proxy is used. For instance:
+  //   "HttpProxy" : "192.168.0.1:3128"
+  //   "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" : 60,
+
+  // 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,
+
+  // Path to the CA (certification authority) certificates to validate
+  // peers in HTTPS requests. From curl documentation ("--cacert"
+  // option): "Tells curl to use the specified certificate file to
+  // verify the peers. The file may contain multiple CA
+  // certificates. The certificate(s) must be in PEM format." On
+  // Debian-based systems, this option can be set to
+  // "/etc/ssl/certs/ca-certificates.crt"
+  "HttpsCACertificates" : "",
+
+
+
+  /**
+   * Advanced options
+   **/
+
+  // Dictionary of symbolic names for the user-defined metadata. Each
+  // 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 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
+  // patient, a study or a series is considered as stable.
+  "StableAge" : 60,
+
+  // By default, Orthanc compares AET (Application Entity Titles) in a
+  // case-insensitive way. Setting this option to "true" will enable
+  // case-sensitive matching.
+  "StrictAetComparison" : false,
+
+  // When the following option is "true", the MD5 of the DICOM files
+  // will be computed and stored in the Orthanc database. This
+  // information can be used to detect disk corruption, at the price
+  // of a small performance overhead.
+  "StoreMD5ForAttachments" : true,
+
+  // The maximum number of results for a single C-FIND request at the
+  // Patient, Study or Series level. Setting this option to "0" means
+  // no limit.
+  "LimitFindResults" : 0,
+
+  // The maximum number of results for a single C-FIND request at the
+  // Instance level. Setting this option to "0" means no limit.
+  "LimitFindInstances" : 0,
+
+  // The maximum number of active jobs in the Orthanc scheduler. When
+  // this limit is reached, the addition of new jobs is blocked until
+  // some job finishes.
+  "LimitJobs" : 10,
+
+  // 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 (persistent HTTP
+  // connections). Setting this option to "true" prevents Orthanc
+  // issue #32 ("HttpServer does not support multiple HTTP requests in
+  // the same TCP stream"), but can possibly slow down HTTP clients
+  // that do not support persistent connections. The default behavior
+  // used to be "false" in Orthanc <= 1.5.1. Setting this option to
+  // "false" is also recommended if Orthanc is compiled against
+  // Mongoose.
+  "KeepAlive" : true,
+
+  // Enable or disable Nagle's algorithm. Only taken into
+  // consideration if Orthanc is compiled to use CivetWeb. Experiments
+  // show that best performance can be obtained by setting both
+  // "KeepAlive" and "TcpNoDelay" to "true". Beware however of
+  // caveats: https://eklitzke.org/the-caveats-of-tcp-nodelay
+  "TcpNoDelay" : true,
+
+  // Number of threads that are used by the embedded HTTP server.
+  "HttpThreadsCount" : 50,
+
+  // If this option is set to "false", Orthanc will run in index-only
+  // mode. The DICOM files will not be stored on the drive. Note that
+  // this option might prevent the upgrade to newer versions of Orthanc.
+  "StoreDicom" : true,
+
+  // 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. This option is only used in Lua scripts.
+  "DicomAssociationCloseDelay" : 5,
+
+  // Maximum number of query/retrieve DICOM requests that are
+  // maintained by Orthanc. The least recently used requests get
+  // deleted as new requests are issued.
+  "QueryRetrieveSize" : 100,
+
+  // When handling a C-Find SCP request, setting this flag to "true"
+  // 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,
+
+  // 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 "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" (asynchronous mode), each incoming C-Move request results
+  // in the creation of a new background job. Up to Orthanc 1.3.2, the
+  // implicit behavior was to use synchronous C-Move ("true"). Between
+  // Orthanc 1.4.0 and 1.4.2, the default behavior was set to
+  // asynchronous C-Move ("false"). Since Orthanc 1.5.0, the default
+  // behavior is back to synchronous C-Move ("true", which ensures
+  // backward compatibility with Orthanc <= 1.3.2).
+  "SynchronousCMove" : true,
+
+  // 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". Since Orthanc 1.5.0, a value of "0"
+  // indicates to keep no job in memory (i.e. jobs are removed from
+  // the history as soon as they are completed), which prevents the
+  // use of some features of Orthanc (typically, synchronous mode in
+  // REST API) and should be avoided for non-developers.
+  "JobsHistorySize" : 10,
+
+  // Whether to save the jobs into the Orthanc database. If this
+  // option is set to "true", the pending/running/completed jobs are
+  // automatically reloaded from the database if Orthanc is stopped
+  // then restarted (except if the "--no-jobs" command-line argument
+  // is specified). This option should be set to "false" if multiple
+  // Orthanc servers are using the same database (e.g. if PostgreSQL
+  // or MariaDB/MySQL is used).
+  "SaveJobs" : true,
+
+  // 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,
+
+  // Maximum number of ZIP/media archives that are maintained by
+  // Orthanc, as a response to the asynchronous creation of archives.
+  // The least recently used archives get deleted as new archives are
+  // generated. This option was introduced in Orthanc 1.5.0, and has
+  // no effect on the synchronous generation of archives.
+  "MediaArchiveSize" : 1,
+
+  // Performance setting to specify how Orthanc accesses the storage
+  // area during C-FIND. Three modes are available: (1) "Always"
+  // allows Orthanc to read the storage area as soon as it needs an
+  // information that is not present in its database (slowest mode),
+  // (2) "Never" prevents Orthanc from accessing the storage area, and
+  // makes it uses exclusively its database (fastest mode), and (3)
+  // "Answers" allows Orthanc to read the storage area to generate its
+  // answers, but not to filter the DICOM resources (balance between
+  // the two modes). By default, the mode is "Always", which
+  // corresponds to the behavior of Orthanc <= 1.5.0.
+  "StorageAccessOnFind" : "Always",
+
+  // Whether Orthanc monitors its metrics (new in Orthanc 1.5.4). If
+  // set to "true", the metrics can be retrieved at
+  // "/tools/metrics-prometheus" formetted using the Prometheus
+  // text-based exposition format.
+  "MetricsEnabled" : true,
+
+  // Whether calls to URI "/tools/execute-script" is enabled. Starting
+  // with Orthanc 1.5.8, this URI is disabled by default for security.
+  "ExecuteLuaEnabled" : false,
+
+  // Set the timeout for HTTP requests, in seconds. This corresponds
+  // to option "request_timeout_ms" of Mongoose/Civetweb. It will set
+  // the socket options "SO_RCVTIMEO" and "SO_SNDTIMEO" to the
+  // specified value.
+  "HttpRequestTimeout" : 30,
+
+  // Set the default private creator that is used by Orthanc when it
+  // looks for a private tag in its dictionary (cf. "Dictionary"
+  // option), or when it creates/modifies a DICOM file (new in Orthanc 1.6.0).
+  "DefaultPrivateCreator" : "",
+
+  // Maximum number of storage commitment reports (i.e. received from
+  // remote modalities) to be kept in memory (new in Orthanc 1.6.0).
+  "StorageCommitmentReportsSize" : 100,
+
+  // Whether Orthanc transcodes DICOM files to an uncompressed
+  // transfer syntax over the DICOM protocol, if the remote modality
+  // does not support compressed transfer syntaxes (new in Orthanc 1.7.0).
+  "TranscodeDicomProtocol" : true,
+
+  // If some plugin to decode/transcode DICOM instances is installed,
+  // this option specifies whether the built-in decoder/transcoder of
+  // Orthanc (that uses DCMTK) is applied before or after the plugins,
+  // or is not applied at all (new in Orthanc 1.7.0). The allowed
+  // values for this option are "After" (default value, corresponding
+  // to the behavior of Orthanc <= 1.6.1), "Before", or "Disabled".
+  "BuiltinDecoderTranscoderOrder" : "After",
+
+  // If this option is set, Orthanc will transparently transcode any
+  // incoming DICOM instance to the given transfer syntax before
+  // storing it into its database. Beware that this might result in
+  // high CPU usage (if transcoding to some compressed transfer
+  // syntax), or in higher disk consumption (if transcoding to an
+  // uncompressed syntax). Also, beware that transcoding to a transfer
+  // syntax with lossy compression (notably JPEG) will change the
+  // "SOPInstanceUID" DICOM tag, and thus the Orthanc identifier at
+  // the instance level, which might break external workflow.
+  /**
+     "IngestTranscoding" : "1.2.840.10008.1.2",
+  **/
+
+  // The compression level that is used when transcoding to one of the
+  // lossy/JPEG transfer syntaxes (integer between 1 and 100).
+  "DicomLossyTranscodingQuality" : 90
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/DicomConformanceStatement.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,64 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# In addition, as a special exception, the copyright holders of this
+# program give permission to link the code of its release with the
+# OpenSSL project's "OpenSSL" library (or with modified versions of it
+# that use the same license as the "OpenSSL" library), and distribute
+# the linked executables. You must obey the GNU General Public License
+# in all respects for all of the code used other than "OpenSSL". If you
+# modify file(s) with this exception, you may extend this exception to
+# your version of the file(s), but you are not obligated to do so. If
+# you do not wish to do so, delete this exception statement from your
+# version. If you delete this exception statement from all source files
+# in the program, then also delete it here.
+# 
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# 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 injects the UID information into the DICOM conformance
+# statement of Orthanc
+
+import re
+
+# Read the conformance statement of Orthanc
+with open('DicomConformanceStatement.txt', 'r') as f:
+    statements = f.readlines()
+
+# Create an index of all the DICOM UIDs that are known to DCMTK
+uids = {}
+with open('/usr/include/dcmtk/dcmdata/dcuid.h', 'r') as dcmtk:
+    for l in dcmtk.readlines():
+        m = re.match(r'#define UID_(.+?)\s*"(.+?)"', l)
+        if m != None:
+            uids[m.group(1)] = m.group(2)
+
+# Loop over the lines of the statement, looking for the "|" separator
+with open('/tmp/DicomConformanceStatement.txt', 'w') as f:
+    for l in statements:
+        m = re.match(r'(\s*)(.*?)(\s*)\|.*$', l)
+        if m != None:
+            name = m.group(2)
+            uid = uids[name]
+            f.write('%s%s%s| %s\n' % (m.group(1), name, m.group(3), uid))
+
+        else:
+            # No "|" in this line, just output it
+            f.write(l)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/DicomConformanceStatement.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,276 @@
+======================================
+DICOM Conformance Statement of Orthanc
+======================================
+
+
+---------------------
+Echo SCP Conformance
+---------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Echo:
+
+  VerificationSOPClass      | 1.2.840.10008.1.1
+
+
+---------------------
+Store SCP Conformance
+---------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Store:
+
+  AmbulatoryECGWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.1.3
+  ArterialPulseWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.5.1
+  AutorefractionMeasurementsStorage                        | 1.2.840.10008.5.1.4.1.1.78.2
+  BasicStructuredDisplayStorage                            | 1.2.840.10008.5.1.4.1.1.131
+  BasicTextSRStorage                                       | 1.2.840.10008.5.1.4.1.1.88.11
+  BasicVoiceAudioWaveformStorage                           | 1.2.840.10008.5.1.4.1.1.9.4.1
+  BlendingSoftcopyPresentationStateStorage                 | 1.2.840.10008.5.1.4.1.1.11.4
+  BreastTomosynthesisImageStorage                          | 1.2.840.10008.5.1.4.1.1.13.1.3
+  CardiacElectrophysiologyWaveformStorage                  | 1.2.840.10008.5.1.4.1.1.9.3.1
+  ChestCADSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.65
+  ColonCADSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.69
+  ColorSoftcopyPresentationStateStorage                    | 1.2.840.10008.5.1.4.1.1.11.2
+  ComprehensiveSRStorage                                   | 1.2.840.10008.5.1.4.1.1.88.33
+  ComputedRadiographyImageStorage                          | 1.2.840.10008.5.1.4.1.1.1
+  CTImageStorage                                           | 1.2.840.10008.5.1.4.1.1.2
+  DeformableSpatialRegistrationStorage                     | 1.2.840.10008.5.1.4.1.1.66.3
+  DigitalIntraOralXRayImageStorageForPresentation          | 1.2.840.10008.5.1.4.1.1.1.3
+  DigitalIntraOralXRayImageStorageForProcessing            | 1.2.840.10008.5.1.4.1.1.1.3.1
+  DigitalMammographyXRayImageStorageForPresentation        | 1.2.840.10008.5.1.4.1.1.1.2
+  DigitalMammographyXRayImageStorageForProcessing          | 1.2.840.10008.5.1.4.1.1.1.2.1
+  DigitalXRayImageStorageForPresentation                   | 1.2.840.10008.5.1.4.1.1.1.1
+  DigitalXRayImageStorageForProcessing                     | 1.2.840.10008.5.1.4.1.1.1.1.1
+  EncapsulatedCDAStorage                                   | 1.2.840.10008.5.1.4.1.1.104.2
+  EncapsulatedPDFStorage                                   | 1.2.840.10008.5.1.4.1.1.104.1
+  EnhancedCTImageStorage                                   | 1.2.840.10008.5.1.4.1.1.2.1
+  EnhancedMRColorImageStorage                              | 1.2.840.10008.5.1.4.1.1.4.3
+  EnhancedMRImageStorage                                   | 1.2.840.10008.5.1.4.1.1.4.1
+  EnhancedPETImageStorage                                  | 1.2.840.10008.5.1.4.1.1.130
+  EnhancedSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.22
+  EnhancedUSVolumeStorage                                  | 1.2.840.10008.5.1.4.1.1.6.2
+  EnhancedXAImageStorage                                   | 1.2.840.10008.5.1.4.1.1.12.1.1
+  EnhancedXRFImageStorage                                  | 1.2.840.10008.5.1.4.1.1.12.2.1
+  GeneralAudioWaveformStorage                              | 1.2.840.10008.5.1.4.1.1.9.4.2
+  GeneralECGWaveformStorage                                | 1.2.840.10008.5.1.4.1.1.9.1.2
+  GenericImplantTemplateStorage                            | 1.2.840.10008.5.1.4.43.1
+  GrayscaleSoftcopyPresentationStateStorage                | 1.2.840.10008.5.1.4.1.1.11.1
+  HemodynamicWaveformStorage                               | 1.2.840.10008.5.1.4.1.1.9.2.1
+  ImplantAssemblyTemplateStorage                           | 1.2.840.10008.5.1.4.44.1
+  ImplantationPlanSRDocumentStorage                        | 1.2.840.10008.5.1.4.1.1.88.70
+  ImplantTemplateGroupStorage                              | 1.2.840.10008.5.1.4.45.1
+  IntraocularLensCalculationsStorage                       | 1.2.840.10008.5.1.4.1.1.78.8
+  KeratometryMeasurementsStorage                           | 1.2.840.10008.5.1.4.1.1.78.3
+  KeyObjectSelectionDocumentStorage                        | 1.2.840.10008.5.1.4.1.1.88.59
+  LensometryMeasurementsStorage                            | 1.2.840.10008.5.1.4.1.1.78.1
+  MacularGridThicknessAndVolumeReportStorage               | 1.2.840.10008.5.1.4.1.1.79.1
+  MammographyCADSRStorage                                  | 1.2.840.10008.5.1.4.1.1.88.50
+  MRImageStorage                                           | 1.2.840.10008.5.1.4.1.1.4
+  MRSpectroscopyStorage                                    | 1.2.840.10008.5.1.4.1.1.4.2
+  MultiframeGrayscaleByteSecondaryCaptureImageStorage      | 1.2.840.10008.5.1.4.1.1.7.2
+  MultiframeGrayscaleWordSecondaryCaptureImageStorage      | 1.2.840.10008.5.1.4.1.1.7.3
+  MultiframeSingleBitSecondaryCaptureImageStorage          | 1.2.840.10008.5.1.4.1.1.7.1
+  MultiframeTrueColorSecondaryCaptureImageStorage          | 1.2.840.10008.5.1.4.1.1.7.4
+  NuclearMedicineImageStorage                              | 1.2.840.10008.5.1.4.1.1.20
+  OphthalmicAxialMeasurementsStorage                       | 1.2.840.10008.5.1.4.1.1.78.7
+  OphthalmicPhotography16BitImageStorage                   | 1.2.840.10008.5.1.4.1.1.77.1.5.2
+  OphthalmicPhotography8BitImageStorage                    | 1.2.840.10008.5.1.4.1.1.77.1.5.1
+  OphthalmicTomographyImageStorage                         | 1.2.840.10008.5.1.4.1.1.77.1.5.4
+  OphthalmicVisualFieldStaticPerimetryMeasurementsStorage  | 1.2.840.10008.5.1.4.1.1.80.1
+  PositronEmissionTomographyImageStorage                   | 1.2.840.10008.5.1.4.1.1.128
+  ProcedureLogStorage                                      | 1.2.840.10008.5.1.4.1.1.88.40
+  PseudoColorSoftcopyPresentationStateStorage              | 1.2.840.10008.5.1.4.1.1.11.3
+  RawDataStorage                                           | 1.2.840.10008.5.1.4.1.1.66
+  RealWorldValueMappingStorage                             | 1.2.840.10008.5.1.4.1.1.67
+  RespiratoryWaveformStorage                               | 1.2.840.10008.5.1.4.1.1.9.6.1
+  RTBeamsTreatmentRecordStorage                            | 1.2.840.10008.5.1.4.1.1.481.4
+  RTBrachyTreatmentRecordStorage                           | 1.2.840.10008.5.1.4.1.1.481.6
+  RTDoseStorage                                            | 1.2.840.10008.5.1.4.1.1.481.2
+  RTImageStorage                                           | 1.2.840.10008.5.1.4.1.1.481.1
+  RTIonBeamsTreatmentRecordStorage                         | 1.2.840.10008.5.1.4.1.1.481.9
+  RTIonPlanStorage                                         | 1.2.840.10008.5.1.4.1.1.481.8
+  RTPlanStorage                                            | 1.2.840.10008.5.1.4.1.1.481.5
+  RTStructureSetStorage                                    | 1.2.840.10008.5.1.4.1.1.481.3
+  RTTreatmentSummaryRecordStorage                          | 1.2.840.10008.5.1.4.1.1.481.7
+  SecondaryCaptureImageStorage                             | 1.2.840.10008.5.1.4.1.1.7
+  SegmentationStorage                                      | 1.2.840.10008.5.1.4.1.1.66.4
+  SpatialFiducialsStorage                                  | 1.2.840.10008.5.1.4.1.1.66.2
+  SpatialRegistrationStorage                               | 1.2.840.10008.5.1.4.1.1.66.1
+  SpectaclePrescriptionReportStorage                       | 1.2.840.10008.5.1.4.1.1.78.6
+  StereometricRelationshipStorage                          | 1.2.840.10008.5.1.4.1.1.77.1.5.3
+  SubjectiveRefractionMeasurementsStorage                  | 1.2.840.10008.5.1.4.1.1.78.4
+  SurfaceSegmentationStorage                               | 1.2.840.10008.5.1.4.1.1.66.5
+  TwelveLeadECGWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.1.1
+  UltrasoundImageStorage                                   | 1.2.840.10008.5.1.4.1.1.6.1
+  UltrasoundMultiframeImageStorage                         | 1.2.840.10008.5.1.4.1.1.3.1
+  VideoEndoscopicImageStorage                              | 1.2.840.10008.5.1.4.1.1.77.1.1.1
+  VideoMicroscopicImageStorage                             | 1.2.840.10008.5.1.4.1.1.77.1.2.1
+  VideoPhotographicImageStorage                            | 1.2.840.10008.5.1.4.1.1.77.1.4.1
+  VisualAcuityMeasurementsStorage                          | 1.2.840.10008.5.1.4.1.1.78.5
+  VLEndoscopicImageStorage                                 | 1.2.840.10008.5.1.4.1.1.77.1.1
+  VLMicroscopicImageStorage                                | 1.2.840.10008.5.1.4.1.1.77.1.2
+  VLPhotographicImageStorage                               | 1.2.840.10008.5.1.4.1.1.77.1.4
+  VLSlideCoordinatesMicroscopicImageStorage                | 1.2.840.10008.5.1.4.1.1.77.1.3
+  VLWholeSlideMicroscopyImageStorage                       | 1.2.840.10008.5.1.4.1.1.77.1.6
+  XAXRFGrayscaleSoftcopyPresentationStateStorage           | 1.2.840.10008.5.1.4.1.1.11.5
+  XRay3DAngiographicImageStorage                           | 1.2.840.10008.5.1.4.1.1.13.1.1
+  XRay3DCraniofacialImageStorage                           | 1.2.840.10008.5.1.4.1.1.13.1.2
+  XRayAngiographicImageStorage                             | 1.2.840.10008.5.1.4.1.1.12.1
+  XRayRadiationDoseSRStorage                               | 1.2.840.10008.5.1.4.1.1.88.67
+  XRayRadiofluoroscopicImageStorage                        | 1.2.840.10008.5.1.4.1.1.12.2
+
+  RETIRED_HardcopyColorImageStorage                        | 1.2.840.10008.5.1.1.30
+  RETIRED_HardcopyGrayscaleImageStorage                    | 1.2.840.10008.5.1.1.29
+  RETIRED_NuclearMedicineImageStorage                      | 1.2.840.10008.5.1.4.1.1.5
+  RETIRED_StandaloneCurveStorage                           | 1.2.840.10008.5.1.4.1.1.9
+  RETIRED_StandaloneModalityLUTStorage                     | 1.2.840.10008.5.1.4.1.1.10
+  RETIRED_StandaloneOverlayStorage                         | 1.2.840.10008.5.1.4.1.1.8
+  RETIRED_StandalonePETCurveStorage                        | 1.2.840.10008.5.1.4.1.1.129
+  RETIRED_StandaloneVOILUTStorage                          | 1.2.840.10008.5.1.4.1.1.11
+  RETIRED_StoredPrintStorage                               | 1.2.840.10008.5.1.1.27
+  RETIRED_UltrasoundImageStorage                           | 1.2.840.10008.5.1.4.1.1.6
+  RETIRED_UltrasoundMultiframeImageStorage                 | 1.2.840.10008.5.1.4.1.1.3
+  RETIRED_VLImageStorage                                   | 1.2.840.10008.5.1.4.1.1.77.1
+  RETIRED_VLMultiFrameImageStorage                         | 1.2.840.10008.5.1.4.1.1.77.2
+  RETIRED_XRayAngiographicBiPlaneImageStorage              | 1.2.840.10008.5.1.4.1.1.12.3
+
+  DRAFT_SRAudioStorage                                     | 1.2.840.10008.5.1.4.1.1.88.2
+  DRAFT_SRComprehensiveStorage                             | 1.2.840.10008.5.1.4.1.1.88.4
+  DRAFT_SRDetailStorage                                    | 1.2.840.10008.5.1.4.1.1.88.3
+  DRAFT_SRTextStorage                                      | 1.2.840.10008.5.1.4.1.1.88.1
+  DRAFT_WaveformStorage                                    | 1.2.840.10008.5.1.4.1.1.9.1
+  DRAFT_RTBeamsDeliveryInstructionStorage                  | 1.2.840.10008.5.1.4.34.1
+
+
+--------------------
+Find SCP Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Find:
+
+  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
+
+
+--------------------
+Move SCP Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Move:
+
+  MOVEPatientRootQueryRetrieveInformationModel   | 1.2.840.10008.5.1.4.1.2.1.2
+  MOVEStudyRootQueryRetrieveInformationModel     | 1.2.840.10008.5.1.4.1.2.2.2
+
+
+-------------------
+Get SCP Conformance
+-------------------
+
+Orthanc supports the following SOP Classes as an SCP for C-Get:
+
+  GETPatientRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.1.3
+  GETStudyRootQueryRetrieveInformationModel      | 1.2.840.10008.5.1.4.1.2.2.3
+
+
+---------------------
+Echo SCU Conformance
+---------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Echo:
+
+  VerificationSOPClass      | 1.2.840.10008.1.1
+
+
+---------------------
+Store SCU Conformance
+---------------------
+
+All the SOP Classes that are listed in the "Store SCP Conformance"
+(see above) section are available as an SCU for C-Store.
+
+
+--------------------
+Find SCU Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Find:
+
+  FINDPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.1
+  FINDStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.1
+
+
+--------------------
+Move SCU Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Move:
+
+  MOVEPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.2
+  MOVEStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.2
+
+
+-----------------
+Transfer Syntaxes
+-----------------
+
+Orthanc will accept and negotiate presentation contexts for all of the
+abovementioned supported SOP Classes using any of the following
+transfer syntaxes:
+
+  LittleEndianImplicitTransferSyntax                                    | 1.2.840.10008.1.2
+  LittleEndianExplicitTransferSyntax                                    | 1.2.840.10008.1.2.1
+  BigEndianExplicitTransferSyntax                                       | 1.2.840.10008.1.2.2
+  DeflatedExplicitVRLittleEndianTransferSyntax                          | 1.2.840.10008.1.2.1.99
+  JPEGProcess1TransferSyntax                                            | 1.2.840.10008.1.2.4.50
+  JPEGProcess2_4TransferSyntax                                          | 1.2.840.10008.1.2.4.51
+  JPEGProcess3_5TransferSyntax                                          | 1.2.840.10008.1.2.4.52
+  JPEGProcess6_8TransferSyntax                                          | 1.2.840.10008.1.2.4.53
+  JPEGProcess7_9TransferSyntax                                          | 1.2.840.10008.1.2.4.54
+  JPEGProcess10_12TransferSyntax                                        | 1.2.840.10008.1.2.4.55
+  JPEGProcess11_13TransferSyntax                                        | 1.2.840.10008.1.2.4.56
+  JPEGProcess14TransferSyntax                                           | 1.2.840.10008.1.2.4.57
+  JPEGProcess15TransferSyntax                                           | 1.2.840.10008.1.2.4.58
+  JPEGProcess16_18TransferSyntax                                        | 1.2.840.10008.1.2.4.59
+  JPEGProcess17_19TransferSyntax                                        | 1.2.840.10008.1.2.4.60
+  JPEGProcess20_22TransferSyntax                                        | 1.2.840.10008.1.2.4.61
+  JPEGProcess21_23TransferSyntax                                        | 1.2.840.10008.1.2.4.62
+  JPEGProcess24_26TransferSyntax                                        | 1.2.840.10008.1.2.4.63
+  JPEGProcess25_27TransferSyntax                                        | 1.2.840.10008.1.2.4.64
+  JPEGProcess28TransferSyntax                                           | 1.2.840.10008.1.2.4.65
+  JPEGProcess29TransferSyntax                                           | 1.2.840.10008.1.2.4.66
+  JPEGProcess14SV1TransferSyntax                                        | 1.2.840.10008.1.2.4.70
+  JPEGLSLosslessTransferSyntax                                          | 1.2.840.10008.1.2.4.80
+  JPEGLSLossyTransferSyntax                                             | 1.2.840.10008.1.2.4.81
+  JPEG2000LosslessOnlyTransferSyntax                                    | 1.2.840.10008.1.2.4.90
+  JPEG2000TransferSyntax                                                | 1.2.840.10008.1.2.4.91
+  JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax | 1.2.840.10008.1.2.4.92
+  JPEG2000Part2MulticomponentImageCompressionTransferSyntax             | 1.2.840.10008.1.2.4.93
+  JPIPReferencedTransferSyntax                                          | 1.2.840.10008.1.2.4.94
+  JPIPReferencedDeflateTransferSyntax                                   | 1.2.840.10008.1.2.4.95
+  MPEG2MainProfileAtMainLevelTransferSyntax                             | 1.2.840.10008.1.2.4.100
+  MPEG2MainProfileAtHighLevelTransferSyntax                             | 1.2.840.10008.1.2.4.101
+  RLELosslessTransferSyntax                                             | 1.2.840.10008.1.2.5
+
+It is possible to disable a subset of these transfer syntaxes thanks to the
+"*TransferSyntaxAccepted" options in the Orthanc configuration file.
+
+When possible, Orthanc will prefer the
+LittleEndianImplicitTransferSyntax transfer syntax
+(1.2.840.10008.1.2).
+
+Orthanc does not support extended negotiation.
+
+
+--------------------
+Implementation notes
+--------------------
+
+The information above about the SCP support is readily extracted from
+the function "Orthanc::Internals::AcceptAssociation()" from file
+"Core/DicomNetworking/Internals/CommandDispatcher.cpp".
+
+The information above about the SCU support is derived from the
+classes "Orthanc::DicomControlUserConnection" and
+"Orthanc::DicomStoreUserConnection" from file
+"Core/DicomNetworking/DicomControlUserConnection.cpp" and
+"Core/DicomNetworking/DicomStoreUserConnection.cpp".
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/ErrorCodes.json	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,576 @@
+[
+  /** Generic error codes **/
+
+  {
+    "Code": -1, 
+    "Name": "InternalError", 
+    "Description": "Internal error"
+  }, 
+  {
+    "Code": 0, 
+    "HttpStatus": 200, 
+    "Name": "Success", 
+    "Description": "Success"
+  }, 
+  {
+    "Code": 1, 
+    "Name": "Plugin", 
+    "Description": "Error encountered within the plugin engine"
+  },
+  {
+    "Code": 2, 
+    "Name": "NotImplemented", 
+    "Description": "Not implemented yet"
+  }, 
+  {
+    "Code": 3, 
+    "HttpStatus": 400, 
+    "Name": "ParameterOutOfRange", 
+    "Description": "Parameter out of range",
+    "SQLite": true
+  }, 
+  {
+    "Code": 4, 
+    "Name": "NotEnoughMemory", 
+    "Description": "The server hosting Orthanc is running out of memory"
+  }, 
+  {
+    "Code": 5, 
+    "HttpStatus": 400, 
+    "Name": "BadParameterType", 
+    "Description": "Bad type for a parameter",
+    "SQLite": true
+  }, 
+  {
+    "Code": 6, 
+    "Name": "BadSequenceOfCalls", 
+    "Description": "Bad sequence of calls"
+  }, 
+  {
+    "Code": 7, 
+    "HttpStatus": 404, 
+    "Name": "InexistentItem", 
+    "Description": "Accessing an inexistent item"
+  }, 
+  {
+    "Code": 8, 
+    "HttpStatus": 400, 
+    "Name": "BadRequest", 
+    "Description": "Bad request"
+  }, 
+  {
+    "Code": 9, 
+    "Name": "NetworkProtocol", 
+    "Description": "Error in the network protocol"
+  }, 
+  {
+    "Code": 10, 
+    "Name": "SystemCommand", 
+    "Description": "Error while calling a system command"
+  }, 
+  {
+    "Code": 11, 
+    "Name": "Database", 
+    "Description": "Error with the database engine"
+  }, 
+  {
+    "Code": 12, 
+    "HttpStatus": 400, 
+    "Name": "UriSyntax", 
+    "Description": "Badly formatted URI"
+  }, 
+  {
+    "Code": 13, 
+    "HttpStatus": 404, 
+    "Name": "InexistentFile", 
+    "Description": "Inexistent file"
+  }, 
+  {
+    "Code": 14, 
+    "Name": "CannotWriteFile", 
+    "Description": "Cannot write to file"
+  }, 
+  {
+    "Code": 15, 
+    "HttpStatus": 400, 
+    "Name": "BadFileFormat", 
+    "Description": "Bad file format"
+  }, 
+  {
+    "Code": 16, 
+    "Name": "Timeout", 
+    "Description": "Timeout"
+  }, 
+  {
+    "Code": 17, 
+    "HttpStatus": 404, 
+    "Name": "UnknownResource", 
+    "Description": "Unknown resource"
+  }, 
+  {
+    "Code": 18, 
+    "Name": "IncompatibleDatabaseVersion", 
+    "Description": "Incompatible version of the database"
+  }, 
+  {
+    "Code": 19, 
+    "Name": "FullStorage", 
+    "Description": "The file storage is full"
+  }, 
+  {
+    "Code": 20, 
+    "Name": "CorruptedFile", 
+    "Description": "Corrupted file (e.g. inconsistent MD5 hash)"
+  }, 
+  {
+    "Code": 21, 
+    "HttpStatus": 404, 
+    "Name": "InexistentTag", 
+    "Description": "Inexistent tag"
+  }, 
+  {
+    "Code": 22, 
+    "Name": "ReadOnly", 
+    "Description": "Cannot modify a read-only data structure"
+  }, 
+  {
+    "Code": 23, 
+    "Name": "IncompatibleImageFormat", 
+    "Description": "Incompatible format of the images"
+  }, 
+  {
+    "Code": 24, 
+    "Name": "IncompatibleImageSize", 
+    "Description": "Incompatible size of the images"
+  }, 
+  {
+    "Code": 25, 
+    "Name": "SharedLibrary", 
+    "Description": "Error while using a shared library (plugin)"
+  }, 
+  {
+    "Code": 26, 
+    "Name": "UnknownPluginService", 
+    "Description": "Plugin invoking an unknown service"
+  }, 
+  {
+    "Code": 27, 
+    "Name": "UnknownDicomTag", 
+    "Description": "Unknown DICOM tag"
+  }, 
+  {
+    "Code": 28, 
+    "HttpStatus": 400, 
+    "Name": "BadJson", 
+    "Description": "Cannot parse a JSON document"
+  }, 
+  {
+    "Code": 29, 
+    "HttpStatus": 401, 
+    "Name": "Unauthorized", 
+    "Description": "Bad credentials were provided to an HTTP request"
+  }, 
+  {
+    "Code": 30, 
+    "Name": "BadFont", 
+    "Description": "Badly formatted font file"
+  },
+  {
+    "Code": 31, 
+    "Name": "DatabasePlugin", 
+    "Description": "The plugin implementing a custom database back-end does not fulfill the proper interface"
+  }, 
+  {
+    "Code": 32, 
+    "Name": "StorageAreaPlugin", 
+    "Description": "Error in the plugin implementing a custom storage area"
+  },
+  {
+    "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"
+  }, 
+  {
+    "Code": 38, 
+    "Name": "BadGeometry", 
+    "Description": "Geometry error encountered in Stone"
+  }, 
+  {
+    "Code": 39, 
+    "Name": "SslInitialization", 
+    "Description": "Cannot initialize SSL encryption, check out your certificates"
+  }, 
+
+
+
+  /** SQLite **/
+
+
+  {
+    "Code": 1000, 
+    "Name": "SQLiteNotOpened",
+    "Description": "SQLite: The database is not opened",
+    "SQLite": true
+  },
+  {
+    "Code": 1001, 
+    "Name": "SQLiteAlreadyOpened", 
+    "Description": "SQLite: Connection is already open",
+    "SQLite": true
+  },
+  {
+    "Code": 1002, 
+    "Name": "SQLiteCannotOpen", 
+    "Description": "SQLite: Unable to open the database",
+    "SQLite": true
+  },
+  {
+    "Code": 1003, 
+    "Name": "SQLiteStatementAlreadyUsed", 
+    "Description": "SQLite: This cached statement is already being referred to",
+    "SQLite": true
+  },
+  {
+    "Code": 1004, 
+    "Name": "SQLiteExecute", 
+    "Description": "SQLite: Cannot execute a command",
+    "SQLite": true
+  },
+  {
+    "Code": 1005, 
+    "Name": "SQLiteRollbackWithoutTransaction", 
+    "Description": "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)",
+    "SQLite": true
+  },
+  {
+    "Code": 1006, 
+    "Name": "SQLiteCommitWithoutTransaction", 
+    "Description": "SQLite: Committing a nonexistent transaction",
+    "SQLite": true
+  },
+  {
+    "Code": 1007, 
+    "Name": "SQLiteRegisterFunction", 
+    "Description": "SQLite: Unable to register a function",
+    "SQLite": true
+  },
+  {
+    "Code": 1008, 
+    "Name": "SQLiteFlush", 
+    "Description": "SQLite: Unable to flush the database",
+    "SQLite": true
+  },
+  {
+    "Code": 1009, 
+    "Name": "SQLiteCannotRun", 
+    "Description": "SQLite: Cannot run a cached statement",
+    "SQLite": true
+  },
+  {
+    "Code": 1010, 
+    "Name": "SQLiteCannotStep", 
+    "Description": "SQLite: Cannot step over a cached statement",
+    "SQLite": true
+  },
+  {
+    "Code": 1011, 
+    "Name": "SQLiteBindOutOfRange", 
+    "Description": "SQLite: Bing a value while out of range (serious error)",
+    "SQLite": true
+  },
+  {
+    "Code": 1012, 
+    "Name": "SQLitePrepareStatement", 
+    "Description": "SQLite: Cannot prepare a cached statement",
+    "SQLite": true
+  },
+  {
+    "Code": 1013, 
+    "Name": "SQLiteTransactionAlreadyStarted", 
+    "Description": "SQLite: Beginning the same transaction twice",
+    "SQLite": true
+  },
+  {
+    "Code": 1014, 
+    "Name": "SQLiteTransactionCommit", 
+    "Description": "SQLite: Failure when committing the transaction",
+    "SQLite": true
+  },
+  {
+    "Code": 1015, 
+    "Name": "SQLiteTransactionBegin", 
+    "Description": "SQLite: Cannot start a transaction",
+    "SQLite": true
+  },
+
+
+
+  /** Specific error codes **/
+
+  
+  {
+    "Code": 2000, 
+    "Name": "DirectoryOverFile", 
+    "Description": "The directory to be created is already occupied by a regular file"
+  },
+  {
+    "Code": 2001, 
+    "Name": "FileStorageCannotWrite", 
+    "Description": "Unable to create a subdirectory or a file in the file storage"
+  },
+  {
+    "Code": 2002, 
+    "Name": "DirectoryExpected", 
+    "Description": "The specified path does not point to a directory"
+  },
+  {
+    "Code": 2003, 
+    "Name": "HttpPortInUse", 
+    "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 privileged or already in use"
+  },
+  {
+    "Code": 2005, 
+    "Name": "BadHttpStatusInRest", 
+    "Description": "This HTTP status is not allowed in a REST API"
+  },
+  {
+    "Code": 2006, 
+    "Name": "RegularFileExpected", 
+    "Description": "The specified path does not point to a regular file"
+  },
+  {
+    "Code": 2007, 
+    "Name": "PathToExecutable", 
+    "Description": "Unable to get the path to the executable"
+  },
+  {
+    "Code": 2008, 
+    "Name": "MakeDirectory", 
+    "Description": "Cannot create a directory"
+  },
+  {
+    "Code": 2009, 
+    "Name": "BadApplicationEntityTitle", 
+    "Description": "An application entity title (AET) cannot be empty or be longer than 16 characters"
+  },
+  {
+    "Code": 2010, 
+    "Name": "NoCFindHandler", 
+    "Description": "No request handler factory for DICOM C-FIND SCP"
+  },
+  {
+    "Code": 2011, 
+    "Name": "NoCMoveHandler", 
+    "Description": "No request handler factory for DICOM C-MOVE SCP"
+  },
+  {
+    "Code": 2012, 
+    "Name": "NoCStoreHandler", 
+    "Description": "No request handler factory for DICOM C-STORE SCP"
+  },
+  {
+    "Code": 2013, 
+    "Name": "NoApplicationEntityFilter", 
+    "Description": "No application entity filter"
+  },
+  {
+    "Code": 2014, 
+    "Name": "NoSopClassOrInstance", 
+    "Description": "DicomUserConnection: Unable to find the SOP class and instance"
+  },
+  {
+    "Code": 2015, 
+    "Name": "NoPresentationContext", 
+    "Description": "DicomUserConnection: No acceptable presentation context for modality"
+  },
+  {
+    "Code": 2016, 
+    "Name": "DicomFindUnavailable", 
+    "Description": "DicomUserConnection: The C-FIND command is not supported by the remote SCP"
+  },
+  {
+    "Code": 2017, 
+    "Name": "DicomMoveUnavailable", 
+    "Description": "DicomUserConnection: The C-MOVE command is not supported by the remote SCP"
+  },
+  {
+    "Code": 2018, 
+    "Name": "CannotStoreInstance", 
+    "Description": "Cannot store an instance"
+  },
+  {
+    "Code": 2019,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomNotString", 
+    "Description": "Only string values are supported when creating DICOM instances"
+  },
+  {
+    "Code": 2020,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomOverrideTag", 
+    "Description": "Trying to override a value inherited from a parent module"
+  },
+  {
+    "Code": 2021,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomUseContent", 
+    "Description": "Use \\\"Content\\\" to inject an image into a new DICOM instance"
+  },
+  {
+    "Code": 2022,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomNoPayload", 
+    "Description": "No payload is present for one instance in the series"
+  },
+  {
+    "Code": 2023,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomUseDataUriScheme", 
+    "Description": "The payload of the DICOM instance must be specified according to Data URI scheme"
+  },
+  {
+    "Code": 2024,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomBadParent", 
+    "Description": "Trying to attach a new DICOM instance to an inexistent resource"
+  },
+  {
+    "Code": 2025,
+    "HttpStatus": 400, 
+    "Name": "CreateDicomParentIsInstance", 
+    "Description": "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)"
+  },
+  {
+    "Code": 2026, 
+    "Name": "CreateDicomParentEncoding", 
+    "Description": "Unable to get the encoding of the parent resource"
+  },
+  {
+    "Code": 2027, 
+    "Name": "UnknownModality", 
+    "Description": "Unknown modality"
+  },
+  {
+    "Code": 2028, 
+    "Name": "BadJobOrdering", 
+    "Description": "Bad ordering of filters in a job"
+  },
+  {
+    "Code": 2029, 
+    "Name": "JsonToLuaTable", 
+    "Description": "Cannot convert the given JSON object to a Lua table"
+  },
+  {
+    "Code": 2030, 
+    "Name": "CannotCreateLua", 
+    "Description": "Cannot create the Lua context"
+  },
+  {
+    "Code": 2031, 
+    "Name": "CannotExecuteLua", 
+    "Description": "Cannot execute a Lua command"
+  },
+  {
+    "Code": 2032, 
+    "Name": "LuaAlreadyExecuted", 
+    "Description": "Arguments cannot be pushed after the Lua function is executed"
+  },
+  {
+    "Code": 2033, 
+    "Name": "LuaBadOutput", 
+    "Description": "The Lua function does not give the expected number of outputs"
+  },
+  {
+    "Code": 2034, 
+    "Name": "NotLuaPredicate", 
+    "Description": "The Lua function is not a predicate (only true/false outputs allowed)"
+  },
+  {
+    "Code": 2035, 
+    "Name": "LuaReturnsNoString", 
+    "Description": "The Lua function does not return a string"
+  },
+  {
+    "Code": 2036,
+    "Name": "StorageAreaAlreadyRegistered",
+    "Description": "Another plugin has already registered a custom storage area"
+  },
+  {
+    "Code": 2037,
+    "Name": "DatabaseBackendAlreadyRegistered",
+    "Description": "Another plugin has already registered a custom database back-end"
+  },
+  {
+    "Code": 2038,
+    "Name": "DatabaseNotInitialized",
+    "Description": "Plugin trying to call the database during its initialization"
+  },
+  { 
+    "Code": 2039,
+    "Name": "SslDisabled",
+    "Description": "Orthanc has been built without SSL support"
+  },
+  {
+    "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"
+  },
+  {
+    "Code": 2043, 
+    "Name": "NoStorageCommitmentHandler", 
+    "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)"
+  },
+  {
+    "Code": 2044,
+    "Name": "NoCGetHandler", 
+    "Description": "No request handler factory for DICOM C-GET SCP"
+  }
+
+
+
+  /** HTTP-related error codes **/
+
+  {
+    "Code": 3000,
+    "HttpStatus": 415, 
+    "Name": "UnsupportedMediaType",
+    "Description": "Unsupported media type"
+  }
+]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/GenerateAnonymizationProfile.py	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "LookupIdentifierQuery.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+#include "../ServerToolbox.h"
+#include "SetOfResources.h"
+
+#include <cassert>
+
+
+
+namespace Orthanc
+{
+  LookupIdentifierQuery::SingleConstraint::
+  SingleConstraint(const DicomTag& tag,
+                   IdentifierConstraintType type,
+                   const std::string& value) : 
+    tag_(tag),
+    type_(type),
+    value_(ServerToolbox::NormalizeIdentifier(value))
+  {
+  }
+
+
+  LookupIdentifierQuery::RangeConstraint::
+  RangeConstraint(const DicomTag& tag,
+                  const std::string& start,
+                  const std::string& end) : 
+    tag_(tag),
+    start_(ServerToolbox::NormalizeIdentifier(start)),
+    end_(ServerToolbox::NormalizeIdentifier(end))
+  {
+  }
+
+
+  LookupIdentifierQuery::Disjunction::~Disjunction()
+  {
+    for (size_t i = 0; i < singleConstraints_.size(); i++)
+    {
+      delete singleConstraints_[i];
+    }
+
+    for (size_t i = 0; i < rangeConstraints_.size(); i++)
+    {
+      delete rangeConstraints_[i];
+    }
+  }
+
+
+  void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag,
+                                               IdentifierConstraintType type,
+                                               const std::string& 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 (Disjunctions::iterator it = disjunctions_.begin();
+         it != disjunctions_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
+  {
+    return ServerToolbox::IsIdentifier(tag, level_);
+  }
+
+
+  void LookupIdentifierQuery::AddConstraint(DicomTag tag,
+                                            IdentifierConstraintType type,
+                                            const std::string& value)
+  {
+    assert(IsIdentifier(tag));
+    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()
+  {
+    disjunctions_.push_back(new Disjunction);
+    return *disjunctions_.back();
+  }
+
+
+  void LookupIdentifierQuery::Apply(std::list<std::string>& result,
+                                    IDatabaseWrapper& database)
+  {
+    SetOfResources resources(database, level_);
+    Apply(resources, database);
+
+    resources.Flatten(result);
+  }
+
+
+  void LookupIdentifierQuery::Apply(SetOfResources& result,
+                                    IDatabaseWrapper& database)
+  {
+    for (size_t i = 0; i < disjunctions_.size(); i++)
+    {
+      std::list<int64_t> a;
+
+      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
+      {
+        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
+        std::list<int64_t> b;
+        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);
+      }
+
+      result.Intersect(a);
+    }
+  }
+
+
+  void LookupIdentifierQuery::Print(std::ostream& s) const
+  {
+    s << "Constraint: " << std::endl;
+    for (Disjunctions::const_iterator
+           it = disjunctions_.begin(); it != disjunctions_.end(); ++it)
+    {
+      if (it == disjunctions_.begin())
+        s << "   ";
+      else
+        s << "OR ";
+
+      for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++)
+      {
+        const SingleConstraint& c = (*it)->GetSingleConstraint(j);
+        s << FromDcmtkBridge::GetTagName(c.GetTag(), "");
+
+        switch (c.GetType())
+        {
+          case IdentifierConstraintType_Equal: s << " == "; break;
+          case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break;
+          case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break;
+          case IdentifierConstraintType_Wildcard: s << " ~= "; break;
+          default:
+            s << " ? ";
+        }
+
+        s << c.GetValue() << std::endl;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,207 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../IDatabaseWrapper.h"
+
+#include "SetOfResources.h"
+
+#include <vector>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  /**
+   * Primitive for wildcard matching, as defined in DICOM:
+   * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4
+   * 
+   * "Any occurrence of an "*" or a "?", then "*" shall match any
+   * sequence of characters (including a zero length value) and "?"
+   * shall match any single character. This matching is case
+   * sensitive, except for Attributes with an PN Value
+   * Representation (e.g., Patient Name (0010,0010))."
+   * 
+   * Pay attention to the fact that "*" (resp. "?") generally
+   * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The
+   * values "%", "_", "\" should in the user request should
+   * respectively be escaped as "\%", "\_" and "\\".
+   *
+   * This matching must be case sensitive: The special case of PN VR
+   * is taken into consideration by normalizing the query string in
+   * method "NormalizeIdentifier()".
+   **/
+
+  class LookupIdentifierQuery : public boost::noncopyable
+  {
+    // This class encodes a conjunction ("AND") of disjunctions. Each
+    // disjunction represents an "OR" of several constraints.
+
+  public:
+    class SingleConstraint
+    {
+    private:
+      DicomTag                  tag_;
+      IdentifierConstraintType  type_;
+      std::string               value_;
+
+    public:
+      SingleConstraint(const DicomTag& tag,
+                       IdentifierConstraintType type,
+                       const std::string& value);
+
+      const DicomTag& GetTag() const
+      {
+        return tag_;
+      }
+
+      IdentifierConstraintType GetType() const
+      {
+        return type_;
+      }
+      
+      const std::string& GetValue() const
+      {
+        return value_;
+      }
+    };
+
+
+    class RangeConstraint
+    {
+    private:
+      DicomTag     tag_;
+      std::string  start_;
+      std::string  end_;
+
+    public:
+      RangeConstraint(const DicomTag& tag,
+                      const std::string& start,
+                      const std::string& 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<SingleConstraint*>  singleConstraints_;
+      std::vector<RangeConstraint*>   rangeConstraints_;
+
+    public:
+      ~Disjunction();
+
+      void Add(const DicomTag& tag,
+               IdentifierConstraintType type,
+               const std::string& value);
+
+      void AddRange(const DicomTag& tag,
+                    const std::string& start,
+                    const std::string& end);
+
+      size_t GetSingleConstraintsCount() const
+      {
+        return singleConstraints_.size();
+      }
+
+      const SingleConstraint&  GetSingleConstraint(size_t i) const
+      {
+        return *singleConstraints_[i];
+      }
+
+      size_t GetRangeConstraintsCount() const
+      {
+        return rangeConstraints_.size();
+      }
+
+      const RangeConstraint&  GetRangeConstraint(size_t i) const
+      {
+        return *rangeConstraints_[i];
+      }
+    };
+
+
+  private:
+    typedef std::vector<Disjunction*>  Disjunctions;
+
+    ResourceType  level_;
+    Disjunctions  disjunctions_;
+
+  public:
+    LookupIdentifierQuery(ResourceType level) : level_(level)
+    {
+    }
+
+    ~LookupIdentifierQuery();
+
+    bool IsIdentifier(const DicomTag& tag);
+
+    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
+    {
+      return level_;
+    }
+
+    // The database must be locked
+    void Apply(std::list<std::string>& result,
+               IDatabaseWrapper& database);
+
+    void Apply(SetOfResources& result,
+               IDatabaseWrapper& database);
+
+    void Print(std::ostream& s) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,479 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "LookupResource.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/FileStorage/StorageAccessor.h"
+#include "../ServerToolbox.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+
+
+namespace Orthanc
+{
+  static bool DoesDicomMapMatch(const DicomMap& dicom,
+                                const DicomTag& tag,
+                                const IFindConstraint& constraint)
+  {
+    const DicomValue* value = dicom.TestAndGetValue(tag);
+
+    return (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary() &&
+            constraint.Match(value->GetContent()));
+  }
+
+  
+  LookupResource::Level::Level(ResourceType level) : level_(level)
+  {
+    const DicomTag* tags = NULL;
+    size_t size;
+    
+    ServerToolbox::LoadIdentifiers(tags, size, level);
+    
+    for (size_t i = 0; i < size; i++)
+    {
+      identifiers_.insert(tags[i]);
+    }
+    
+    DicomMap::LoadMainDicomTags(tags, size, level);
+    
+    for (size_t i = 0; i < size; i++)
+    {
+      if (identifiers_.find(tags[i]) == identifiers_.end())
+      {
+        mainTags_.insert(tags[i]);
+      }
+    }    
+  }
+
+  LookupResource::Level::~Level()
+  {
+    for (Constraints::iterator it = mainTagsConstraints_.begin();
+         it != mainTagsConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    for (Constraints::iterator it = identifiersConstraints_.begin();
+         it != identifiersConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+  bool LookupResource::Level::Add(const DicomTag& tag,
+                                  std::auto_ptr<IFindConstraint>& constraint)
+  {
+    if (identifiers_.find(tag) != identifiers_.end())
+    {
+      if (level_ == ResourceType_Patient)
+      {
+        // The filters on the patient level must be cloned to the study level
+        identifiersConstraints_[tag] = constraint->Clone();
+      }
+      else
+      {
+        identifiersConstraints_[tag] = constraint.release();
+      }
+
+      return true;
+    }
+    else if (mainTags_.find(tag) != mainTags_.end())
+    {
+      if (level_ == ResourceType_Patient)
+      {
+        // The filters on the patient level must be cloned to the study level
+        mainTagsConstraints_[tag] = constraint->Clone();
+      }
+      else
+      {
+        mainTagsConstraints_[tag] = constraint.release();
+      }
+
+      return true;
+    }
+    else
+    {
+      // This is not a main DICOM tag
+      return false;
+    }
+  }
+
+
+  bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
+  {
+    for (Constraints::const_iterator it = identifiersConstraints_.begin();
+         it != identifiersConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    for (Constraints::const_iterator it = mainTagsConstraints_.begin();
+         it != mainTagsConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+  
+
+  LookupResource::LookupResource(ResourceType level) : level_(level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
+        break;
+
+      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_Study:
+        levels_[ResourceType_Study] = new Level(ResourceType_Study);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  LookupResource::~LookupResource()
+  {
+    for (Levels::iterator it = levels_.begin();
+         it != levels_.end(); ++it)
+    {
+      delete it->second;
+    }
+
+    for (Constraints::iterator it = unoptimizedConstraints_.begin();
+         it != unoptimizedConstraints_.end(); ++it)
+    {
+      delete it->second;
+    }    
+  }
+
+
+
+  bool LookupResource::AddInternal(ResourceType level,
+                                   const DicomTag& tag,
+                                   std::auto_ptr<IFindConstraint>& constraint)
+  {
+    Levels::iterator it = levels_.find(level);
+    if (it != levels_.end())
+    {
+      if (it->second->Add(tag, constraint))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void LookupResource::Add(const DicomTag& tag,
+                           IFindConstraint* constraint)
+  {
+    std::auto_ptr<IFindConstraint> c(constraint);
+
+    if (!AddInternal(ResourceType_Patient, tag, c) &&
+        !AddInternal(ResourceType_Study, tag, c) &&
+        !AddInternal(ResourceType_Series, tag, c) &&
+        !AddInternal(ResourceType_Instance, tag, c))
+    {
+      unoptimizedConstraints_[tag] = c.release();
+    }
+  }
+
+
+  static bool Match(const DicomMap& tags,
+                    const DicomTag& tag,
+                    const IFindConstraint& constraint)
+  {
+    const DicomValue* value = tags.TestAndGetValue(tag);
+
+    if (value == NULL ||
+        value->IsNull() ||
+        value->IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      return constraint.Match(value->GetContent());
+    }
+  }
+
+
+  void LookupResource::Level::Apply(SetOfResources& candidates,
+                                    IDatabaseWrapper& database) const
+  {
+    // First, use the indexed identifiers
+    LookupIdentifierQuery query(level_);
+
+    for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
+         it != identifiersConstraints_.end(); ++it)
+    {
+      it->second->Setup(query, it->first);
+    }
+
+    query.Apply(candidates, database);
+
+    /*{
+      query.Print(std::cout);
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      printf("=> %d\n", source.size());
+      }*/
+
+    // Secondly, filter using the main DICOM tags
+    if (!identifiersConstraints_.empty() ||
+        !mainTagsConstraints_.empty())
+    {
+      std::list<int64_t>  source;
+      candidates.Flatten(source);
+      candidates.Clear();
+
+      std::list<int64_t>  filtered;
+      for (std::list<int64_t>::const_iterator candidate = source.begin(); 
+           candidate != source.end(); ++candidate)
+      {
+        DicomMap tags;
+        database.GetMainDicomTags(tags, *candidate);
+
+        bool match = true;
+
+        // Re-apply the identifier constraints, as their "Setup"
+        // method is less restrictive than their "Match" method
+        for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
+             match && it != identifiersConstraints_.end(); ++it)
+        {
+          if (!Match(tags, it->first, *it->second))
+          {
+            match = false;
+          }
+        }
+
+        for (Constraints::const_iterator it = mainTagsConstraints_.begin(); 
+             match && it != mainTagsConstraints_.end(); ++it)
+        {
+          if (!Match(tags, it->first, *it->second))
+          {
+            match = false;
+          }
+        }
+
+        if (match)
+        {
+          filtered.push_back(*candidate);
+        }
+      }
+      
+      candidates.Intersect(filtered);
+    }
+  }
+
+
+
+  bool LookupResource::IsMatch(const DicomMap& dicom) const
+  {
+    for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
+    {
+      if (!it->second->IsMatch(dicom))
+      {
+        return false;
+      }
+    }
+
+    for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); 
+         it != unoptimizedConstraints_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  void LookupResource::ApplyLevel(SetOfResources& candidates,
+                                  ResourceType level,
+                                  IDatabaseWrapper& database) const
+  {
+    Levels::const_iterator it = levels_.find(level);
+    if (it != levels_.end())
+    {
+      it->second->Apply(candidates, database);
+    }
+
+    if (level == ResourceType_Study &&
+        modalitiesInStudy_.get() != NULL)
+    {
+      // There is a constraint on the "ModalitiesInStudy" DICOM
+      // extension. Check out whether one child series has one of the
+      // allowed modalities
+      std::list<int64_t> allStudies, matchingStudies;
+      candidates.Flatten(allStudies);
+ 
+      for (std::list<int64_t>::const_iterator
+             study = allStudies.begin(); study != allStudies.end(); ++study)
+      {
+        std::list<int64_t> childrenSeries;
+        database.GetChildrenInternalId(childrenSeries, *study);
+
+        for (std::list<int64_t>::const_iterator
+               series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
+        {
+          DicomMap tags;
+          database.GetMainDicomTags(tags, *series);
+
+          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+          if (value != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            if (modalitiesInStudy_->Match(value->GetContent()))
+            {
+              matchingStudies.push_back(*study);
+              break;
+            }
+          }
+        }
+      }
+
+      candidates.Intersect(matchingStudies);
+    }
+  }
+
+
+  void LookupResource::FindCandidates(std::list<int64_t>& result,
+                                      IDatabaseWrapper& database) const
+  {
+    ResourceType startingLevel;
+    if (level_ == ResourceType_Patient)
+    {
+      startingLevel = ResourceType_Patient;
+    }
+    else
+    {
+      startingLevel = ResourceType_Study;
+    }
+
+    SetOfResources candidates(database, startingLevel);
+
+    switch (level_)
+    {
+      case ResourceType_Patient:
+        ApplyLevel(candidates, ResourceType_Patient, database);
+        break;
+
+      case ResourceType_Study:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        break;
+
+      case ResourceType_Series:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Series, database);
+        break;
+
+      case ResourceType_Instance:
+        ApplyLevel(candidates, ResourceType_Study, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Series, database);
+        candidates.GoDown();
+        ApplyLevel(candidates, ResourceType_Instance, database);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    candidates.Flatten(result);
+  }
+
+
+  void LookupResource::SetModalitiesInStudy(const std::string& modalities)
+  {
+    modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
+    
+    std::vector<std::string> items;
+    Toolbox::TokenizeString(items, modalities, '\\');
+    
+    for (size_t i = 0; i < items.size(); i++)
+    {
+      modalitiesInStudy_->AddAllowedValue(items[i]);
+    }
+  }
+
+
+  void LookupResource::AddDicomConstraint(const DicomTag& tag,
+                                          const std::string& dicomQuery,
+                                          bool caseSensitive)
+  {
+    // 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 
+    {
+      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Graveyard/DatabaseOptimizations/LookupResource.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ListConstraint.h"
+#include "SetOfResources.h"
+
+#include <memory>
+
+namespace Orthanc
+{
+  class LookupResource : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
+    
+    class Level
+    {
+    private:
+      ResourceType        level_;
+      std::set<DicomTag>  identifiers_;
+      std::set<DicomTag>  mainTags_;
+      Constraints         identifiersConstraints_;
+      Constraints         mainTagsConstraints_;
+
+    public:
+      Level(ResourceType level);
+
+      ~Level();
+
+      bool Add(const DicomTag& tag,
+               std::auto_ptr<IFindConstraint>& constraint);
+
+      void Apply(SetOfResources& candidates,
+                 IDatabaseWrapper& database) const;
+
+      bool IsMatch(const DicomMap& dicom) const;
+    };
+
+    typedef std::map<ResourceType, Level*>  Levels;
+
+    ResourceType                    level_;
+    Levels                          levels_;
+    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
+    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
+
+    bool AddInternal(ResourceType level,
+                     const DicomTag& tag,
+                     std::auto_ptr<IFindConstraint>& constraint);
+
+    void ApplyLevel(SetOfResources& candidates,
+                    ResourceType level,
+                    IDatabaseWrapper& database) const;
+
+  public:
+    LookupResource(ResourceType level);
+
+    ~LookupResource();
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetModalitiesInStudy(const std::string& modalities); 
+
+    void Add(const DicomTag& tag,
+             IFindConstraint* constraint);   // Takes ownership
+
+    void AddDicomConstraint(const DicomTag& tag,
+                            const std::string& dicomQuery,
+                            bool caseSensitive);
+
+    void FindCandidates(std::list<int64_t>& result,
+                        IDatabaseWrapper& database) const;
+
+    bool HasOnlyMainDicomTags() const
+    {
+      return unoptimizedConstraints_.empty();
+    }
+
+    bool IsMatch(const DicomMap& dicom) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Graveyard/DatabasePluginSample/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +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/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Resources/Graveyard/DatabasePluginSample/Database.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Resources/Graveyard/DatabasePluginSample/Plugin.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/OrthancServer/Resources/Graveyard/SetupAnonymization2011.cpp	Wed Jun 10 20:30:34 2020 +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/OrthancServer/Resources/ImplementationNotes/JobsEngineStates.dot	Wed Jun 10 20:30:34 2020 +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 OrthancServer/Resources/ImplementationNotes/JobsEngineStates.pdf has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/OldBuildInstructions.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,159 @@
+This file contains old build instructions that are not tested anymore.
+
+
+Debian Squeeze (6.x)
+--------------------
+
+# sudo apt-get install build-essential unzip cmake mercurial \
+       	       	       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
+       	       	       libgoogle-glog-dev libpng-dev libgtest-dev \
+       	       	       libsqlite3-dev libssl-dev zlib1g-dev
+
+# cmake -DALLOW_DOWNLOADS=ON \
+  	-DUSE_SYSTEM_BOOST=OFF \
+	-DUSE_SYSTEM_DCMTK=OFF \
+	-DUSE_SYSTEM_MONGOOSE=OFF \
+	-DUSE_SYSTEM_JSONCPP=OFF \
+	-DUSE_SYSTEM_PUGIXML=OFF \
+        -DENABLE_JPEG=OFF \
+        -DENABLE_JPEG_LOSSLESS=OFF \
+	~/Orthanc 
+
+
+Debian Wheezy (7.x)
+-------------------
+
+# sudo apt-get install 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 \
+       	       	       libboost-all-dev libwrap0-dev libjsoncpp-dev
+
+# cmake -DALLOW_DOWNLOADS=ON \
+        -DUSE_SYSTEM_GOOGLE_LOG=OFF \
+	-DUSE_SYSTEM_MONGOOSE=OFF \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
+        -DENABLE_JPEG=OFF \
+        -DENABLE_JPEG_LOSSLESS=OFF \
+	~/Orthanc
+
+
+Ubuntu 12.04.5 LTS
+------------------
+
+# sudo apt-get install build-essential unzip cmake mercurial \
+                       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
+                       libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \
+                       zlib1g-dev libdcmtk2-dev libboost1.48-all-dev libwrap0-dev \
+                       libcharls-dev
+
+# cmake "-DDCMTK_LIBRARIES=boost_locale;CharLS;dcmjpls;wrap;oflog" \
+        -DALLOW_DOWNLOADS=ON \
+        -DUSE_SYSTEM_CIVETWEB=OFF \
+        -DUSE_SYSTEM_JSONCPP=OFF \
+        -DUSE_SYSTEM_PUGIXML=OFF \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+        -DCMAKE_BUILD_TYPE=Release \
+        ~/Orthanc
+
+
+Ubuntu 12.10
+------------
+
+# 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 \
+       	       	       libsqlite3-dev libssl-dev zlib1g-dev \
+       	       	       libdcmtk2-dev libboost-all-dev libwrap0-dev libcharls-dev
+
+With JPEG:
+
+# cmake "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog" \
+        -DALLOW_DOWNLOADS=ON \
+	-DUSE_SYSTEM_MONGOOSE=OFF \
+	-DUSE_SYSTEM_JSONCPP=OFF \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
+	~/Orthanc
+
+
+Without JPEG:
+
+# cmake "-DDCMTK_LIBRARIES=wrap;oflog" \
+        -DALLOW_DOWNLOADS=ON \
+	-DUSE_SYSTEM_MONGOOSE=OFF \
+	-DUSE_SYSTEM_JSONCPP=OFF \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
+        -DENABLE_JPEG=OFF \
+        -DENABLE_JPEG_LOSSLESS=OFF \
+	~/Orthanc
+
+
+Ubuntu 13.10
+------------
+
+# 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 \
+       	       	       libsqlite3-dev libssl-dev zlib1g-dev \
+       	       	       libdcmtk2-dev libboost-all-dev libwrap0-dev libjsoncpp-dev
+
+# cmake "-DDCMTK_LIBRARIES=wrap;oflog" \
+        -DALLOW_DOWNLOADS=ON \
+	-DUSE_SYSTEM_MONGOOSE=OFF \
+        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
+	-DUSE_SYSTEM_PUGIXML=OFF \
+        -DENABLE_JPEG=OFF \
+        -DENABLE_JPEG_LOSSLESS=OFF \
+	~/Orthanc
+
+
+Fedora 19
+---------
+
+# sudo yum install unzip make automake gcc gcc-c++ python cmake \
+                   boost-devel curl-devel dcmtk-devel glog-devel \
+                   gtest-devel libpng-devel libsqlite3x-devel libuuid-devel \
+                   mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel
+
+# cmake  "-DDCMTK_LIBRARIES=CharLS" \
+         -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \
+         ~/Orthanc
+       
+Note: Have also a look at the official package:
+http://pkgs.fedoraproject.org/cgit/orthanc.git/tree/?h=f18
+
+
+CentOS 6
+--------
+
+# yum install unzip make automake gcc gcc-c++ python cmake curl-devel \
+              libpng-devel sqlite-devel libuuid-devel openssl-devel \
+              lua-devel mercurial patch tar
+
+Using static linking with Civetweb (tested with Orthanc 1.5.7):
+
+# cmake -DSTATIC_BUILD=ON \
+        -DSTANDALONE_BUILD=ON \
+        -DUSE_LEGACY_JSONCPP=ON \
+        -DUSE_LEGACY_LIBICU=ON \
+        -DBOOST_LOCALE_BACKEND=icu \
+        -DCMAKE_BUILD_TYPE=Debug \
+        ~/Orthanc
+
+Using Mongoose (untested):
+
+# cmake -DALLOW_DOWNLOADS=ON \
+        -DUSE_SYSTEM_JSONCPP=OFF \
+        -DUSE_SYSTEM_CIVETWEB=OFF \
+        -DUSE_SYSTEM_PUGIXML=OFF \
+        -DUSE_SYSTEM_SQLITE=OFF \
+        -DUSE_SYSTEM_BOOST=OFF \
+        -DUSE_SYSTEM_DCMTK=OFF \
+        -DUSE_SYSTEM_GOOGLE_TEST=OFF \
+        -DUSE_SYSTEM_LIBJPEG=OFF \
+        -DCMAKE_BUILD_TYPE=Release \
+        ~/Orthanc
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Orthanc.doxygen	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2313 @@
+# 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 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 (\" \").
+
+#---------------------------------------------------------------------------
+# 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.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# 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.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "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.
+
+PROJECT_LOGO           = @CMAKE_SOURCE_DIR@/Resources/OrthancLogoDocumentation.png
+
+# 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 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.
+# 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 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 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 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
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# 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
+
+# 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 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 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-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 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 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 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.
+# 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.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 8
+
+# 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.
+
+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.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# 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. 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.
+# 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,
+# 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 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 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);
+# 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 (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 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
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# 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).
+#
+# 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 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 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
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# 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
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE 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.
+# 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.
+# 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.
+# 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. 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 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 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 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 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 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 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 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
+# 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 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 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 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 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 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
+# 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        = YES
+
+# 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 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 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.
+# 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.
+# 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 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 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 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 default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# 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 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.
+# 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 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 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. 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.
+#
+# 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 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
+#---------------------------------------------------------------------------
+
+# 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 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 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 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
+
+# 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 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 standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# 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. 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 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 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.
+
+EXCLUDE                =
+
+# 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/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# 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).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# 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 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.
+#
+# 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 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 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 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
+#---------------------------------------------------------------------------
+
+# 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,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# 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.
+# 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.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# 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 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 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
+#---------------------------------------------------------------------------
+
+# 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
+
+# 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 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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).
+# 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 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 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 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.
+# 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 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.
+# 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.
+# 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.
+# 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.
+# 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 (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
+
+# 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"
+
+# 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
+
+# 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 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 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
+
+# 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               =
+
+# 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           =
+
+# 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
+
+# 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     =
+
+# 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 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.
+# 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.
+# 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 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 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 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_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. 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  =
+
+# 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, 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. 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
+
+# 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. 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 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.
+# 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.
+# 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. 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 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.
+# 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 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 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     =
+
+# 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 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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.
+#
+# 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.
+# 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.
+# 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 (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 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. 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.
+#
+# 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           =
+
+# 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 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# 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 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. 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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.
+# 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 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 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. 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
+#---------------------------------------------------------------------------
+
+# 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. 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. 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
+
+# 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
+#---------------------------------------------------------------------------
+
+# 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.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# 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 related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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.
+# 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.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# 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 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.
+# 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 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.
+# 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.
+# 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 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.
+# 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 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 options related to external references
+#---------------------------------------------------------------------------
+
+# 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. 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. 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 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.
+# 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').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# 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
+# 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            =
+
+# 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 (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 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
+
+# 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 (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 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 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 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 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 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 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 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 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 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 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 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 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.
+# 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.
+# 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 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).
+# 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).
+
+MSCFILE_DIRS           =
+
+# 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
+# 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).
+# 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.
+# 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 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 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 OrthancServer/Resources/OrthancLogo.png has changed
Binary file OrthancServer/Resources/OrthancLogoDocumentation.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/OrthancPlugin.doxygen	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2314 @@
+# 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 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 (\" \").
+
+#---------------------------------------------------------------------------
+# 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.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# 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         = @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.
+
+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.
+
+PROJECT_LOGO           = @CMAKE_SOURCE_DIR@/Resources/OrthancLogoDocumentation.png
+
+# 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 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.
+# 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 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 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 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
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# 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
+
+# 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 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 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-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 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 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 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.
+# 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.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 8
+
+# 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.
+
+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.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# 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. 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.
+# 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,
+# 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 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 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);
+# 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 (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 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
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# 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).
+#
+# 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 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 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
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# 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
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE 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.
+# 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.
+# 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.
+# 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. 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 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 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 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 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 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 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 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
+# 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 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 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 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 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 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
+# 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        = YES
+
+# 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 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 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.
+# 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.
+# 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 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 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 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 default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# 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 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.
+# 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 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 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. 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.
+#
+# 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 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
+#---------------------------------------------------------------------------
+
+# 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 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 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 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
+
+# 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 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 standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# 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
+
+# This tag can be used to specify the character encoding of the source files
+# 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 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 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.
+
+EXCLUDE                =
+
+# 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/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# 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       =
+
+# 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.
+# 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 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.
+#
+# 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 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 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 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
+#---------------------------------------------------------------------------
+
+# 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,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# 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.
+# 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.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# 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 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 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
+#---------------------------------------------------------------------------
+
+# 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
+
+# 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 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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).
+# 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 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 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 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.
+# 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 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.
+# 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.
+# 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.
+# 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.
+# 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 (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
+
+# 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"
+
+# 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
+
+# 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 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 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
+
+# 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               =
+
+# 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           =
+
+# 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
+
+# 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     =
+
+# 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 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.
+# 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.
+# 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 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 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 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_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. 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  =
+
+# 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, 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. 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
+
+# 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. 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 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.
+# 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.
+# 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. 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 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.
+# 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 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 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     =
+
+# 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 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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.
+#
+# 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.
+# 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.
+# 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 (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 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. 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.
+#
+# 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           =
+
+# 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 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.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# 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 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. 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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.
+# 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 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 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. 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
+#---------------------------------------------------------------------------
+
+# 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. 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. 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
+
+# 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
+#---------------------------------------------------------------------------
+
+# 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.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# 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 related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# 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
+#---------------------------------------------------------------------------
+
+# 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.
+# 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.
+# 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.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# 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 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.
+# 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 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.
+# 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.
+# 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 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.
+# 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 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 options related to external references
+#---------------------------------------------------------------------------
+
+# 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. 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. 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 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.
+# 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').
+# The default file (with absolute path) is: /usr/bin/perl.
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# 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
+# 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            =
+
+# 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 (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 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
+
+# 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 (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 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 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 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 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 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 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 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 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 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 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 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 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.
+# 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.
+# 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 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).
+# 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).
+
+MSCFILE_DIRS           =
+
+# 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
+# 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).
+# 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.
+# 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 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 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/OrthancServer/Resources/Samples/CppHelpers/Logging/ILogger.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,168 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <boost/algorithm/string.hpp>
+#include <boost/thread.hpp>
+
+namespace OrthancHelpers
+{
+
+
+  inline std::string ShortenId(const std::string& orthancUuid)
+  {
+    size_t firstHyphenPos = orthancUuid.find_first_of('-');
+    if (firstHyphenPos == std::string::npos)
+    {
+      return orthancUuid;
+    }
+    else
+    {
+      return orthancUuid.substr(0, firstHyphenPos);
+    }
+  }
+
+
+  // Interface for loggers providing the same interface
+  // in Orthanc framework or in an Orthanc plugins.
+  // Furthermore, compared to the LOG and VLOG macros,
+  // these loggers will provide "contexts".
+  class ILogger
+  {
+  public:
+    virtual ~ILogger() {}
+    virtual void Trace(const char* message) = 0;
+    virtual void Trace(const std::string& message) = 0;
+    virtual void Info(const char* message) = 0;
+    virtual void Info(const std::string& message) = 0;
+    virtual void Warning(const char* message) = 0;
+    virtual void Warning(const std::string& message) = 0;
+    virtual void Error(const char* message) = 0;
+    virtual void Error(const std::string& message) = 0;
+
+    virtual void EnterContext(const char* message, bool forceLogContextChange = false) = 0;
+    virtual void EnterContext(const std::string& message, bool forceLogContextChange = false) = 0;
+    virtual void LeaveContext(bool forceLogContextChange = false) = 0;
+  };
+
+
+  // Implements ILogger by providing contexts.  Contexts defines
+  // the "call-stack" of the logs and are prepended to the log.
+  // check LogContext class for more details
+  class BaseLogger : public ILogger
+  {
+#if ORTHANC_ENABLE_THREADS == 1
+    boost::thread_specific_ptr<std::vector<std::string>> contexts_;
+#else
+    std::auto_ptr<std::vector<std::string>> contexts_;
+#endif
+    bool logContextChanges_;
+
+  public:
+
+    BaseLogger()
+      : logContextChanges_(false)
+    {
+    }
+
+    void EnableLogContextChanges(bool enable)
+    {
+      logContextChanges_ = enable;
+    }
+
+    virtual void EnterContext(const char* message, bool forceLogContextChange = false)
+    {
+      EnterContext(std::string(message), forceLogContextChange);
+    }
+
+    virtual void EnterContext(const std::string& message, bool forceLogContextChange = false)
+    {
+      if (!contexts_.get())
+      {
+        contexts_.reset(new std::vector<std::string>());
+      }
+      contexts_->push_back(message);
+
+      if (logContextChanges_ || forceLogContextChange)
+      {
+        Info(".. entering");
+      }
+    }
+
+    virtual void LeaveContext(bool forceLogContextChange = false)
+    {
+      if (logContextChanges_ || forceLogContextChange)
+      {
+        Info(".. leaving");
+      }
+
+      contexts_->pop_back();
+      if (contexts_->size() == 0)
+      {
+        contexts_.reset(NULL);
+      }
+    }
+
+  protected:
+
+    virtual std::string GetContext()
+    {
+      if (contexts_.get() != NULL && contexts_->size() > 0)
+      {
+        return "|" + boost::algorithm::join(*contexts_, " | ") + "|";
+      }
+      else
+      {
+        return std::string("|");
+      }
+    }
+  };
+
+
+  /* RAII to set a Log context.
+  * Example:
+  * ILogger* logger = new OrthancPluginLogger(..);
+  * {
+  *   LogContext logContext(logger, "A");
+  *   {
+  *     LogContext nestedLogContext(logger, "B");
+  *     logger->Error("out of memory");
+  *   }
+  * }
+  * will produce:
+  * |A | B| out of memory
+  *
+  * furthermore, if LogContextChanges are enabled in the BaseLogger,
+  * you'll get;
+  * |A| .. entering
+  * |A | B| .. entering
+  * |A | B| out of memory
+  * |A | B| .. leaving
+  * |A| .. leaving
+  */
+  class LogContext
+  {
+    ILogger* logger_;
+    bool     forceLogContextChange_;
+  public:
+    LogContext(ILogger* logger, const char* context, bool forceLogContextChange = false) :
+      logger_(logger),
+      forceLogContextChange_(forceLogContextChange)
+    {
+      logger_->EnterContext(context, forceLogContextChange_);
+    }
+
+    LogContext(ILogger* logger, const std::string& context, bool forceLogContextChange = false) :
+      logger_(logger),
+      forceLogContextChange_(forceLogContextChange)
+    {
+      logger_->EnterContext(context, forceLogContextChange_);
+    }
+
+    ~LogContext()
+    {
+      logger_->LeaveContext(forceLogContextChange_);
+    }
+
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/NullLogger.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "ILogger.h"
+
+namespace OrthancHelpers
+{
+  // a logger ... that does not log.
+  // Instead of writing:
+  // if (logger != NULL)
+  // {
+  //   logger->Info("hello")   ;
+  // }
+  // you should create a NullLogger:
+  // logger = new NullLogger();
+  // ...
+  // logger->Info("hello");
+  class NullLogger : public BaseLogger {
+  public:
+    NullLogger() {}
+
+    virtual void Trace(const char* message) {}
+    virtual void Trace(const std::string& message) {}
+    virtual void Info(const char* message) {}
+    virtual void Info(const std::string& message) {}
+    virtual void Warning(const char* message) {}
+    virtual void Warning(const std::string& message) {}
+    virtual void Error(const char* message) {}
+    virtual void Error(const std::string& message) {}
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,46 @@
+#include "OrthancLogger.h"
+#include "Logging.h"
+
+namespace OrthancHelpers
+{
+
+  void OrthancLogger::Trace(const char *message)
+  {
+    VLOG(1) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Trace(const std::string& message)
+  {
+    VLOG(1) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Info(const char *message)
+  {
+    LOG(INFO) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Info(const std::string& message)
+  {
+    LOG(INFO) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Warning(const char *message)
+  {
+    LOG(WARNING) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Warning(const std::string& message)
+  {
+    LOG(WARNING) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Error(const char *message)
+  {
+    LOG(ERROR) << GetContext() << " " << message;
+  }
+
+  void OrthancLogger::Error(const std::string& message)
+  {
+    LOG(ERROR) << GetContext() << " " << message;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancLogger.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "ILogger.h"
+
+namespace OrthancHelpers
+{
+
+  class OrthancLogger : public BaseLogger
+  {
+  public:
+    virtual void Trace(const char *message);
+    virtual void Trace(const std::string &message);
+    virtual void Info(const char *message);
+    virtual void Info(const std::string &message);
+    virtual void Warning(const char *message);
+    virtual void Warning(const std::string &message);
+    virtual void Error(const char *message);
+    virtual void Error(const std::string &message);
+  };
+} // namespace OrthancHelpers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,55 @@
+#include "OrthancPluginLogger.h"
+
+namespace OrthancHelpers
+{
+
+  OrthancPluginLogger::OrthancPluginLogger(OrthancPluginContext *context)
+    : pluginContext_(context),
+      hasAlreadyLoggedTraceWarning_(false)
+  {
+  }
+
+  void OrthancPluginLogger::Trace(const char *message)
+  {
+    Trace(std::string(message));
+  }
+
+  void OrthancPluginLogger::Trace(const std::string &message)
+  {
+    if (!hasAlreadyLoggedTraceWarning_)
+    {
+      Warning("Trying to log 'TRACE' level information in a plugin is not possible.  These logs won't appear.");
+      hasAlreadyLoggedTraceWarning_ = true;
+    }
+  }
+
+  void OrthancPluginLogger::Info(const char *message)
+  {
+    Info(std::string(message));
+  }
+
+  void OrthancPluginLogger::Info(const std::string &message)
+  {
+    OrthancPluginLogInfo(pluginContext_, (GetContext() + " " + message).c_str());
+  }
+
+  void OrthancPluginLogger::Warning(const char *message)
+  {
+    Warning(std::string(message));
+  }
+
+  void OrthancPluginLogger::Warning(const std::string &message)
+  {
+    OrthancPluginLogWarning(pluginContext_, (GetContext() + " " + message).c_str());
+  }
+
+  void OrthancPluginLogger::Error(const char *message)
+  {
+    Error(std::string(message));
+  }
+
+  void OrthancPluginLogger::Error(const std::string &message)
+  {
+    OrthancPluginLogError(pluginContext_, (GetContext() + " " + message).c_str());
+  }
+} // namespace OrthancHelpers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,26 @@
+#pragma once
+
+#include "ILogger.h"
+#include <orthanc/OrthancCPlugin.h>
+
+namespace OrthancHelpers
+{
+
+  class OrthancPluginLogger : public BaseLogger
+  {
+    OrthancPluginContext *pluginContext_;
+    bool hasAlreadyLoggedTraceWarning_;
+
+  public:
+    OrthancPluginLogger(OrthancPluginContext *context);
+
+    virtual void Trace(const char *message);
+    virtual void Trace(const std::string &message);
+    virtual void Info(const char *message);
+    virtual void Info(const std::string &message);
+    virtual void Warning(const char *message);
+    virtual void Warning(const std::string &message);
+    virtual void Error(const char *message);
+    virtual void Error(const std::string &message);
+  };
+} // namespace OrthancHelpers
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/CppHelpers/README.md	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,3 @@
+This folder contains a bunch of helpers that are used in multiple Orthanc side projects (either plugins, standalone executables using the Orthanc framework or orthanc-stone based applications)
+
+When writing plugins, you may also want to check the Plugins/Samples/Common folder for other helpers.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,108 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 sys
+import os.path
+import httplib2
+import base64
+
+if len(sys.argv) != 4 and len(sys.argv) != 6:
+    print("""
+Sample script to recursively import in Orthanc all the DICOM files
+that are stored in some path. Please make sure that Orthanc is running
+before starting this script. The files are uploaded through the REST
+API.
+
+Usage: %s [hostname] [HTTP port] [path]
+Usage: %s [hostname] [HTTP port] [path] [username] [password]
+For instance: %s 127.0.0.1 8042 .
+""" % (sys.argv[0], sys.argv[0], sys.argv[0]))
+    exit(-1)
+
+URL = 'http://%s:%d/instances' % (sys.argv[1], int(sys.argv[2]))
+
+success_count = 0
+total_file_count = 0
+
+
+# This function will upload a single file to Orthanc through the REST API
+def UploadFile(path):
+    global success_count
+    global total_file_count
+
+    f = open(path, "rb")
+    content = f.read()
+    f.close()
+    total_file_count += 1
+
+    try:
+        sys.stdout.write("Importing %s" % path)
+
+        h = httplib2.Http()
+
+        headers = { 'content-type' : 'application/dicom' }
+
+        if len(sys.argv) == 6:
+            username = sys.argv[4]
+            password = sys.argv[5]
+
+            # h.add_credentials(username, password)
+
+            # This is a custom reimplementation of the
+            # "Http.add_credentials()" method for Basic HTTP Access
+            # Authentication (for some weird reason, this method does
+            # not always work)
+            # http://en.wikipedia.org/wiki/Basic_access_authentication
+            creds_str = username + ':' + password
+            creds_str_bytes = creds_str.encode("ascii")
+            creds_str_bytes_b64 = b'Basic ' + base64.b64encode(creds_str_bytes)
+            headers['authorization'] = creds_str_bytes_b64.decode("ascii")
+
+        resp, content = h.request(URL, 'POST', 
+                                  body = content,
+                                  headers = headers)
+
+        if resp.status == 200:
+            sys.stdout.write(" => success\n")
+            success_count += 1
+        else:
+            sys.stdout.write(" => failure (Is it a DICOM file? Is there a password?)\n")
+
+    except:
+        type, value, traceback = sys.exc_info()
+        sys.stderr.write(str(value))
+        sys.stdout.write(" => unable to connect (Is Orthanc running? Is there a password?)\n")
+
+
+if os.path.isfile(sys.argv[3]):
+    # Upload a single file
+    UploadFile(sys.argv[3])
+else:
+    # Recursively upload a directory
+    for root, dirs, files in os.walk(sys.argv[3]):
+        for f in files:
+            UploadFile(os.path.join(root, f))
+
+if success_count == total_file_count:
+    print("\nSummary: all %d DICOM file(s) have been imported successfully" % success_count)
+else:
+    print("\nSummary: %d out of %d files have been imported successfully as DICOM instances" % (success_count, total_file_count))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,38 @@
+-- This sample shows how to use Orthanc to compress on-the-fly any
+-- incoming DICOM file, as a JPEG2k file.
+
+function OnStoredInstance(instanceId, tags, metadata, origin)
+   -- Do not compress twice the same file
+   if origin['RequestOrigin'] ~= 'Lua' then
+
+      -- Retrieve the incoming DICOM instance from Orthanc
+      local dicom = RestApiGet('/instances/' .. instanceId .. '/file')
+
+      -- Write the DICOM content to some temporary file
+      local uncompressed = instanceId .. '-uncompressed.dcm'
+      local target = assert(io.open(uncompressed, 'wb'))
+      target:write(dicom)
+      target:close()
+
+      -- Compress to JPEG2000 using gdcm
+      local compressed = instanceId .. '-compressed.dcm'
+      os.execute('gdcmconv -U --j2k ' .. uncompressed .. ' ' .. compressed)
+
+      -- Generate a new SOPInstanceUID for the JPEG2000 file, as
+      -- gdcmconv does not do this by itself
+      os.execute('dcmodify --no-backup -gin ' .. compressed)
+
+      -- Read the JPEG2000 file
+      local source = assert(io.open(compressed, 'rb'))
+      local jpeg2k = source:read("*all")
+      source:close()
+
+      -- Upload the JPEG2000 file and remove the uncompressed file
+      RestApiPost('/instances', jpeg2k)
+      RestApiDelete('/instances/' .. instanceId)
+
+      -- Remove the temporary DICOM files
+      os.remove(uncompressed)
+      os.remove(compressed)
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/Autorouting.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,3 @@
+function OnStoredInstance(instanceId, tags, metadata)
+   Delete(SendToModality(instanceId, 'sample'))
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/AutoroutingConditional.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,16 @@
+function OnStoredInstance(instanceId, tags, metadata, origin)
+   -- The "origin" is only available since Orthanc 0.9.4
+   PrintRecursive(origin)
+
+   -- Extract the value of the "PatientName" DICOM tag
+   local patientName = string.lower(tags['PatientName'])
+
+   if string.find(patientName, 'david') ~= nil then
+      -- Only send patients whose name contains "David"
+      Delete(SendToModality(instanceId, 'sample'))
+
+   else
+      -- Delete the patients that are not called "David"
+      Delete(instanceId)
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/AutoroutingModification.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,31 @@
+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 = {}
+      replace['StationName'] = 'My Medical Device'
+      replace['0031-1020'] = 'Some private tag'
+
+      -- The tags to be removed
+      local remove = { 'MilitaryRank' }
+
+      -- Modify the instance
+      local command = {}
+      command['Replace'] = replace
+      command['Remove'] = remove
+      local modifiedFile = RestApiPost('/instances/' .. instanceId .. '/modify', DumpJson(command, true))
+
+      -- 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/CallDcm2Xml.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,8 @@
+function OnStoredInstance(instanceId, tags, metadata)
+   -- Assume Latin1 encoding in dcm2xml
+   local args = {}
+   table.insert(args, '+Ca')
+   table.insert(args, 'latin-1')
+
+   Delete(CallSystem(instanceId, 'dcm2xml', args))
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/CallImageJ.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,35 @@
+-- This sample shows how to invoke an ImageJ script on every DICOM
+-- image received by Orthanc. The ImageJ script is generated by the
+-- "Initialize()" function at the startup of Orthanc. Whenever a new
+-- instance is received, its DICOM file is stored into a temporary
+-- file, and a system call to ImageJ is triggered.
+
+SCRIPT = 'ImageJScript.txt'
+
+function Initialize()
+   local target = assert(io.open(SCRIPT, 'w'))
+
+   -- This is a sample ImageJ script that display the size of the DICOM image
+   target:write('if (getArgument=="") exit ("No argument!");\n')
+   target:write('open(getArgument);\n')
+   target:write('print(getTitle + ": " + getWidth + "x" + getHeight);\n')
+
+   target:close()
+end
+
+function OnStoredInstance(instanceId)
+   -- Retrieve the DICOM instance from Orthanc
+   local dicom = RestApiGet('/instances/' .. instanceId .. '/file')
+
+   -- Write the DICOM content to some temporary file
+   local path = instanceId .. '.dcm'
+   local target = assert(io.open(path, 'wb'))
+   target:write(dicom)
+   target:close()
+
+   -- Call ImageJ
+   os.execute('imagej -b ' .. SCRIPT .. ' ' .. path)
+
+   -- Remove the temporary DICOM file
+   os.remove(path)
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/CallWebService.js	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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 file is a simple echo Web service implemented using
+ * "node.js". Whenever it receives a POST HTTP query, it echoes its
+ * body both to stdout and to the client. Credentials are checked.
+ **/
+
+
+// Parameters of the ECHO server 
+var port = 8000;
+var username = 'alice';
+var password = 'alicePassword';
+
+
+var http = require('http');
+var authorization = 'Basic ' + new Buffer(username + ':' + password).toString('base64')
+
+var server = http.createServer(function(req, response) {
+  // Check the credentials
+  if (req.headers.authorization != authorization)
+  {
+    console.log('Bad credentials, access not allowed');
+    response.writeHead(401);
+    response.end();
+    return;
+  }
+
+  switch (req.method)
+  {
+  case 'POST':
+    {
+      var body = '';
+
+      req.on('data', function (data) {
+        response.write(data);
+        body += data;
+      });
+
+      req.on('end', function () {
+        console.log('Message received: ' + body);
+        response.end();
+      });
+
+      break;
+    }
+
+  default:
+    console.log('Method ' + req.method + ' is not supported by this ECHO Web service');
+    response.writeHead(405, {'Allow': 'POST'});
+    response.end();
+  }
+});
+
+server.listen(port);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/CallWebService.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,26 @@
+-- This sample shows how to call a remote Web service whenever an
+-- instance is received by Orthanc. For this sample to work, you have
+-- to start the "CallWebService.js" script next to this file using
+-- NodeJs.
+
+-- Download and install the JSON module for Lua by Jeffrey Friedl
+-- http://regex.info/blog/lua/json
+
+-- NOTE : Replace "load" by "loadstring" for Lua <= 5.1
+JSON = (load(HttpGet('http://regex.info/code/JSON.lua'))) ()
+
+SetHttpCredentials('alice', 'alicePassword')
+
+function OnStoredInstance(instanceId, tags, metadata)
+   -- Build the POST body
+   local info = {}
+   info['InstanceID'] = instanceId
+   info['PatientName'] = tags['PatientName']
+   info['PatientID'] = tags['PatientID']
+
+   -- Send the POST request
+   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)
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/IncomingFindRequestFilter.lua	Wed Jun 10 20:30:34 2020 +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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/ModifyInstanceWithSequence.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,28 @@
+-- Answer to:
+-- https://groups.google.com/d/msg/orthanc-users/0ymHe1cDBCQ/YfD0NoOTn0wJ
+-- Applicable starting with Orthanc 0.9.5
+
+function OnStoredInstance(instanceId, tags, metadata, origin)
+   -- Do not modify twice the same file
+   if origin['RequestOrigin'] ~= 'Lua' then
+      local replace = {}
+      replace['0010,1002'] = {}
+      replace['0010,1002'][1] = {}
+      replace['0010,1002'][1]['PatientID'] = 'Hello'
+      replace['0010,1002'][2] = {}
+      replace['0010,1002'][2]['PatientID'] = 'World'
+
+      local request = {}
+      request['Replace'] = replace
+
+      -- Create the modified instance
+      local modified = RestApiPost('/instances/' .. instanceId .. '/modify',
+                                   DumpJson(request, true))
+
+      -- Upload the modified instance to the Orthanc store
+      RestApiPost('/instances/', modified)
+
+      -- Delete the original instance
+      RestApiDelete('/instances/' .. instanceId)
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/OnStableStudy.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,46 @@
+function Initialize()
+   print('Number of stored studies at initialization: ' ..
+            table.getn(ParseJson(RestApiGet('/studies'))))
+end
+
+
+function Finalize()
+   print('Number of stored studies at finalization: ' ..
+            table.getn(ParseJson(RestApiGet('/studies'))))
+end
+
+
+function OnStoredInstance(instanceId, tags, metadata)
+   patient = ParseJson(RestApiGet('/instances/' .. instanceId .. '/patient'))
+   print('Received an instance for patient: ' .. 
+            patient['MainDicomTags']['PatientID'] .. ' - ' .. 
+            patient['MainDicomTags']['PatientName'])
+end
+
+
+function OnStableStudy(studyId, tags, metadata)
+   if (metadata['ModifiedFrom'] == nil and
+       metadata['AnonymizedFrom'] == nil) then
+
+      print('This study is now stable: ' .. studyId)
+      
+      -- The tags to be replaced
+      local replace = {}
+      replace['StudyDescription'] = 'Modified study'
+      replace['StationName'] = 'My Medical Device'
+      replace['0031-1020'] = 'Some private tag'
+
+      -- The tags to be removed
+      local remove = { 'MilitaryRank' }
+
+      -- The modification command
+      local command = {}
+      command['Remove'] = remove
+      command['Replace'] = replace
+
+      -- Modify the entire study in one single call
+      local m = RestApiPost('/studies/' .. studyId .. '/modify',
+                            DumpJson(command, true))
+      print('Modified study: ' .. m)
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/ParseDoseReport.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,66 @@
+-- Sample Lua script that demonstrates how to extract DICOM tags
+-- related to dose reports. In this example, the value of the DLP
+-- (Dose Length Product), the value of the mean CTDI volume (Computed
+-- Tomography Dose Index), and the number of "IntervalsRejected" are
+-- extracted and printed. Furthermore, these values are saved as
+-- metadata that is attached to their parent DICOM instance for
+-- further processing by external software.
+
+
+function ExploreContentSequence(instanceId, tags)
+   if tags then
+      for key, value in pairs(tags) do
+         -- Recursive exploration
+         ExploreContentSequence(instanceId, value['ContentSequence'])
+
+         local concept = value['ConceptNameCodeSequence'] 
+         local measure = value['MeasuredValueSequence']
+         if concept and measure then
+
+            local value = measure[1]['NumericValue']
+            local code = concept[1]['CodeValue']
+
+            if type(value) == 'string' and type(code) == 'string' then
+               -- If the field contains the DLP, stores it as a metadata.
+               -- "DLP" is associated with CodeValue 113838.
+               -- ftp://medical.nema.org/medical/dicom/final/sup127_ft.pdf
+               if code == '113838' then
+                  print('DLP = ' .. value)
+                  RestApiPut('/instances/' .. instanceId .. '/metadata/2001', value)
+               end
+
+               -- Extract the mean CTDI volume
+               if code == '113830' then
+                  print('CTDI = ' .. value)
+                  RestApiPut('/instances/' .. instanceId .. '/metadata/2002', value)
+               end
+
+               -- Other values can be extracted here
+            end
+         end
+      end
+   end
+end
+
+
+function StoreTagToMetadata(instanceId, tags, name, metadata)
+   if tags then
+      for key, value in pairs(tags) do
+         if type(value) ~= 'string' then
+            -- Recursive exploration
+            StoreTagToMetadata(instanceId, value, name, metadata)
+         elseif key == name then
+            print(name .. ' = ' .. value)
+            if metadata then
+               RestApiPut('/instances/' .. instanceId .. '/metadata/' .. metadata, value)
+            end
+         end
+      end
+   end
+end
+
+
+function OnStoredInstance(instanceId, tags)
+   StoreTagToMetadata(instanceId, tags, 'IntervalsRejected', 2000)
+   ExploreContentSequence(instanceId, tags['ContentSequence'])
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/TransferSyntaxDisable.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,37 @@
+function IsDeflatedTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpegTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpeg2000TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsJpipTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsMpeg2TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsMpeg4TransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsRleTransferSyntaxAccepted(aet, ip)
+   return false
+end
+
+function IsUnknownSopClassAccepted(aet, ip)
+   return false
+end
+
+print('All special transfer syntaxes are now disallowed')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/TransferSyntaxEnable.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,37 @@
+function IsDeflatedTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpegTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpeg2000TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsJpipTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsMpeg2TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsMpeg4TransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsRleTransferSyntaxAccepted(aet, ip)
+   return true
+end
+
+function IsUnknownSopClassAccepted(aet, ip)
+   return true
+end
+
+print('All special transfer syntaxes are now accepted')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Lua/WriteToDisk.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,35 @@
+TARGET = '/tmp/lua'
+
+function ToAscii(s)
+   -- http://www.lua.org/manual/5.1/manual.html#pdf-string.gsub
+   -- https://groups.google.com/d/msg/orthanc-users/qMLgkEmwwPI/6jRpCrlgBwAJ
+   return s:gsub('[^a-zA-Z0-9-/-: ]', '_')
+end
+
+function OnStableSeries(seriesId, tags, metadata)
+   print('This series is now stable, writing its instances on the disk: ' .. seriesId)
+
+   local instances = ParseJson(RestApiGet('/series/' .. seriesId)) ['Instances']
+   local patient = ParseJson(RestApiGet('/series/' .. seriesId .. '/patient')) ['MainDicomTags']
+   local study = ParseJson(RestApiGet('/series/' .. seriesId .. '/study')) ['MainDicomTags']
+   local series = ParseJson(RestApiGet('/series/' .. seriesId)) ['MainDicomTags']
+
+   for i, instance in pairs(instances) do
+      local path = ToAscii(TARGET .. '/' .. 
+                              patient['PatientID'] .. ' - ' .. patient['PatientName'] .. '/' ..
+                              study['StudyDate'] .. ' - ' .. study['StudyDescription'] .. '/' ..
+                              series['SeriesDescription'])
+
+      -- Retrieve the DICOM file from Orthanc
+      local dicom = RestApiGet('/instances/' .. instance .. '/file')
+
+      -- Create the subdirectory (CAUTION: For Linux demo only, this is insecure!)
+      -- http://stackoverflow.com/a/16029744/881731
+      os.execute('mkdir -p "' .. path .. '"')
+
+      -- Write to the file
+      local target = assert(io.open(path .. '/' .. instance .. '.dcm', 'wb'))
+      target:write(dicom)
+      target:close()
+   end
+end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/AnonymizeAllPatients.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,46 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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/>.
+
+
+
+URL = 'http://127.0.0.1:8042'
+
+#
+# This sample code will anonymize all the patients that are stored in
+# Orthanc.
+#
+
+import sys
+import RestToolbox
+
+# Loop over the patients
+for patient in RestToolbox.DoGet('%s/patients' % URL):
+
+    # Ignore patients whose name starts with "Anonymized", as it is
+    # the result of a previous anonymization
+    infos = RestToolbox.DoGet('%s/patients/%s' % (URL, patient))
+    name = infos['MainDicomTags']['PatientName'].lower()
+    if not name.startswith('anonymized'):
+
+        # Trigger the anonymization
+        RestToolbox.DoPost('%s/patients/%s/anonymize' % (URL, patient),
+                           { 'Keep' : [ 'SeriesDescription',
+                                        'StudyDescription' ] })
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/ArchiveAllPatients.py	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/ArchiveStudiesInTimeRange.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,85 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 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://127.0.0.1:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0])
+    exit(-1)
+
+def CheckIsDate(date):
+    if len(date) != 8 or not date.isdigit():
+        print '"%s" is not a valid date!\n' % date
+        exit(-1)
+
+
+if len(sys.argv) != 5:
+    PrintHelp()
+
+URL = sys.argv[1]
+START = sys.argv[2]
+END = sys.argv[3]
+TARGET = sys.argv[4]
+
+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
+    study = RestToolbox.DoGet('%s/studies/%s' % (URL, studyId))['MainDicomTags']
+
+    # Retrieve the DICOM tags of the parent patient of this study
+    patient = RestToolbox.DoGet('%s/studies/%s/patient' % (URL, studyId))['MainDicomTags']
+
+    # Check that the StudyDate tag lies within the given range
+    studyDate = study['StudyDate'][:8]
+    if studyDate >= START and studyDate <= END:
+        # Create a filename
+        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').translate(None, r"'\/:*?\"<>|!=").strip()
+
+        # Download the ZIP archive of the study
+        print('Downloading %s' % filename)
+        zipContent = RestToolbox.DoGet('%s/studies/%s/archive' % (URL, studyId))
+
+        # Write the ZIP archive at the proper location
+        with open(os.path.join(TARGET, filename), 'wb') as f:
+            f.write(zipContent)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/AutoClassify.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,129 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 argparse
+import time
+import os
+import os.path
+import sys
+import RestToolbox
+
+parser = argparse.ArgumentParser(
+    description = 'Automated classification of DICOM files from Orthanc.',
+    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
+
+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')
+parser.add_argument('--target', default = 'OrthancFiles',
+                    help = 'The target directory where to store the DICOM files')
+parser.add_argument('--all', action = 'store_true',
+                    help = 'Replay the entire history on startup (disabled by default)')
+parser.set_defaults(all = False)
+parser.add_argument('--remove', action = 'store_true',
+                    help = 'Remove DICOM files from Orthanc once classified (disabled by default)')
+parser.set_defaults(remove = False)
+
+
+def FixPath(p):
+    return p.encode('ascii', errors = 'replace').translate(None, r"'\/:*?\"<>|!=").strip()
+
+def GetTag(resource, tag):
+    if ('MainDicomTags' in resource and
+        tag in resource['MainDicomTags']):
+        return resource['MainDicomTags'][tag]
+    else:
+        return 'No' + tag
+
+def ClassifyInstance(instanceId):
+    # Extract the patient, study, series and instance information
+    instance = RestToolbox.DoGet('%s/instances/%s' % (URL, instanceId))
+    series = RestToolbox.DoGet('%s/series/%s' % (URL, instance['ParentSeries']))
+    study = RestToolbox.DoGet('%s/studies/%s' % (URL, series['ParentStudy']))
+    patient = RestToolbox.DoGet('%s/patients/%s' % (URL, study['ParentPatient']))
+
+    # Construct a target path
+    a = '%s - %s' % (GetTag(patient, 'PatientID'),
+                     GetTag(patient, 'PatientName'))
+    b = GetTag(study, 'StudyDescription')
+    c = '%s - %s' % (GetTag(series, 'Modality'),
+                     GetTag(series, 'SeriesDescription'))
+    d = '%s.dcm' % GetTag(instance, 'SOPInstanceUID')
+    
+    p = os.path.join(args.target, FixPath(a), FixPath(b), FixPath(c))
+    f = os.path.join(p, FixPath(d))
+
+    # Copy the DICOM file to the target path
+    print('Writing new DICOM file: %s' % f)
+    
+    try:
+        os.makedirs(p)
+    except:
+        # Already existing directory, ignore the error
+        pass
+    
+    dcm = RestToolbox.DoGet('%s/instances/%s/file' % (URL, instanceId))
+    with open(f, 'wb') as g:
+        g.write(dcm)
+
+
+# Parse the arguments
+args = parser.parse_args()
+URL = 'http://%s:%d' % (args.host, args.port)
+print('Connecting to Orthanc on address: %s' % URL)
+
+# Compute the starting point for the changes loop
+if args.all:
+    current = 0
+else:
+    current = RestToolbox.DoGet(URL + '/changes?last')['Last']
+
+# Polling loop using the 'changes' API of Orthanc, waiting for the
+# incoming of new DICOM files
+while True:
+    r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+    for change in r['Changes']:
+        # We are only interested in the arrival of new instances
+        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'])
+
+    current = r['Last']
+
+    if r['Done']:
+        print('Everything has been processed: Waiting...')
+        time.sleep(1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/ChangesLoop.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,87 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 time
+import sys
+import RestToolbox
+
+
+##
+## Print help message
+##
+
+if len(sys.argv) != 3:
+    print("""
+Sample script that continuously monitors the arrival of new DICOM
+images into Orthanc (through the Changes API).
+
+Usage: %s [hostname] [HTTP port]
+For instance: %s 127.0.0.1 8042
+""" % (sys.argv[0], sys.argv[0]))
+    exit(-1)
+
+URL = 'http://%s:%d' % (sys.argv[1], int(sys.argv[2]))
+
+
+
+##
+## The following function is called each time a new instance is
+## received.
+##
+
+def NewInstanceReceived(path):
+    global URL
+    patientName = RestToolbox.DoGet(URL + path + '/content/PatientName')
+    
+    # Remove the possible trailing characters due to DICOM padding
+    patientName = patientName.strip()
+
+    print('New instance received for patient "%s": "%s"' % (patientName, path))
+
+
+
+##
+## Main loop that listens to the changes API.
+## 
+
+current = 0
+while True:
+    r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+    for change in r['Changes']:
+        # We are only interested in the arrival of new instances
+        if change['ChangeType'] == 'NewInstance':
+            # Call the callback function
+            path = change['Path']
+            NewInstanceReceived(path)
+
+            # Delete the instance once it has been discovered
+            RestToolbox.DoDelete(URL + path)
+
+    current = r['Last']
+
+    if r['Done']:
+        print('Everything has been processed: Waiting...')
+        time.sleep(1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/ContinuousPatientAnonymization.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,100 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 time
+import sys
+import RestToolbox
+import md5
+
+
+##
+## Print help message
+##
+
+if len(sys.argv) != 3:
+    print("""
+Sample script that anonymizes patients in real-time. A patient gets
+anonymized as soon as she gets stable (i.e. when no DICOM instance has
+been received for this patient for a sufficient amount of time - cf.
+the configuration option "StableAge").
+
+Usage: %s [hostname] [HTTP port]
+For instance: %s 127.0.0.1 8042
+""" % (sys.argv[0], sys.argv[0]))
+    exit(-1)
+
+URL = 'http://%s:%d' % (sys.argv[1], int(sys.argv[2]))
+
+
+
+##
+## The following function is called whenever a patient gets stable
+##
+
+COUNT = 1
+
+def AnonymizePatient(path):
+    global URL
+    global COUNT
+
+    patient = RestToolbox.DoGet(URL + path)
+    patientID = patient['MainDicomTags']['PatientID']
+
+    # Ignore anonymized patients
+    if not 'AnonymizedFrom' in patient:
+        print('Patient with ID "%s" is stabilized: anonymizing it...' % (patientID))
+        
+        # The PatientID after anonymization is taken as the 8 first
+        # characters from the MD5 hash of the original PatientID
+        anonymizedID = md5.new(patientID).hexdigest()[:8]
+        anonymizedName = 'Anonymized patient %d' % COUNT
+        COUNT += 1
+
+        RestToolbox.DoPost(URL + path + '/anonymize',
+                           { 'Replace' : { 'PatientID' : anonymizedID,
+                                           'PatientName' : anonymizedName } })
+
+        # Delete the source patient after the anonymization
+        RestToolbox.DoDelete(URL + change['Path'])
+
+
+
+##
+## Main loop that listens to the changes API.
+## 
+
+current = 0
+while True:
+    r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+    for change in r['Changes']:
+        if change['ChangeType'] == 'StablePatient':
+            AnonymizePatient(change['Path'])
+
+    current = r['Last']
+
+    if r['Done']:
+        print('Everything has been processed: Waiting...')
+        time.sleep(1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/DeleteAllStudies.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,42 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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('Delete all the imaging studies that are stored in Orthanc\n')
+    print('Usage: %s <URL>\n' % sys.argv[0])
+    print('Example: %s http://127.0.0.1:8042/\n' % sys.argv[0])
+    exit(-1)
+
+if len(sys.argv) != 2:
+    PrintHelp()
+
+URL = sys.argv[1]
+
+for study in RestToolbox.DoGet('%s/studies' % URL):
+    print('Removing study: %s' % study)
+    RestToolbox.DoDelete('%s/studies/%s' % (URL, study))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/DownloadAnonymized.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,49 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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/>.
+
+
+
+URL = 'http://127.0.0.1:8042'
+
+#
+# This sample code will download a ZIP file for each patient that has
+# been anonymized in Orthanc.
+#
+
+import os
+import os.path
+import sys
+import RestToolbox
+
+# Loop over the patients
+for patient in RestToolbox.DoGet('%s/patients' % URL):
+
+    # Ignore patients whose name starts with "Anonymized", as it is
+    # the result of a previous anonymization
+    infos = RestToolbox.DoGet('%s/patients/%s' % (URL, patient))
+    name = infos['MainDicomTags']['PatientName'].lower()
+    if name.startswith('anonymized'):
+
+        # Trigger the download
+        print('Downloading %s' % name)
+        zipContent = RestToolbox.DoGet('%s/patients/%s/archive' % (URL, patient))
+        with open(os.path.join('/tmp', name + '.zip'), 'wb') as f:
+            f.write(zipContent)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/HighPerformanceAutoRouting.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,163 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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/>.
+
+
+
+URL = 'http://127.0.0.1:8042'
+TARGET = 'sample'
+
+
+#
+# This sample code shows how to setup a simple, high-performance DICOM
+# auto-routing. All the DICOM instances that arrive inside Orthanc
+# will be sent to a remote modality. A producer-consumer pattern is
+# used. The target modality is specified by the TARGET variable above:
+# It must match an entry in the Orthanc configuration file inside the
+# "DicomModalities" section.
+#
+# NOTE: This sample only works with Orthanc >= 0.5.2. Make sure that
+# Orthanc was built with "-DCMAKE_BUILD_TYPE=Release" to get the best
+# performance.
+#
+
+import Queue
+import sys
+import time
+import threading
+
+import RestToolbox
+
+
+#
+# Queue that is shared between the producer and the consumer
+# threads. It holds the instances that are still to be sent.
+#
+
+queue = Queue.Queue()
+
+
+#
+# The producer thread. It monitors the arrival of new instances into
+# Orthanc, and pushes their ID into the shared queue. This code is
+# based upon the "ChangesLoop.py" sample code.
+#
+
+def Producer(queue):
+    current = 0
+
+    while True:
+        r = RestToolbox.DoGet(URL + '/changes', {
+            'since' : current,
+            'limit' : 4   # Retrieve at most 4 changes at once
+            })
+
+        for change in r['Changes']:
+            # We are only interested in the arrival of new instances
+            if change['ChangeType'] == 'NewInstance':
+                queue.put(change['ID'])
+
+        current = r['Last']
+
+        if r['Done']:
+            time.sleep(1)
+
+
+#
+# The consumer thread. It continuously reads the instances from the
+# queue, and send them to the remote modality. Each time a packet of
+# instances is sent, a single DICOM connexion is used, hence improving
+# the performance.
+#
+
+def Consumer(queue):
+    TIMEOUT = 0.1
+    
+    while True:
+        instances = []
+
+        while True:
+            try:
+                # Block for a while, waiting for the arrival of a new
+                # instance
+                instance = queue.get(True, TIMEOUT)
+
+                # A new instance has arrived: Record its ID
+                instances.append(instance)
+                queue.task_done()
+
+            except Queue.Empty:
+                # Timeout: No more data was received
+                break
+
+        if len(instances) > 0:
+            print('Sending a packet of %d instances' % len(instances))
+            start = time.time()
+
+            # Send all the instances with a single DICOM connexion
+            RestToolbox.DoPost('%s/modalities/sample/store' % URL, instances)
+
+            # Remove all the instances from Orthanc
+            for instance in instances:
+                RestToolbox.DoDelete('%s/instances/%s' % (URL, instance))
+
+            # Clear the log of the exported instances (to prevent the
+            # SQLite database from growing indefinitely). More simply,
+            # you could also set the "LogExportedResources" option to
+            # "false" in the configuration file since Orthanc 0.8.3.
+            RestToolbox.DoDelete('%s/exports' % URL)
+
+            end = time.time()
+            print('The packet of %d instances has been sent in %d seconds' % (len(instances), end - start))
+
+
+#
+# Thread to display the progress
+#
+
+def PrintProgress(queue):
+    while True:
+        print('Current queue size: %d' % (queue.qsize()))
+        time.sleep(1)
+
+
+#
+# Start the various threads
+#
+
+progress = threading.Thread(None, PrintProgress, None, (queue, ))
+progress.daemon = True
+progress.start()
+
+producer = threading.Thread(None, Producer, None, (queue, ))
+producer.daemon = True
+producer.start()
+
+consumer = threading.Thread(None, Consumer, None, (queue, ))
+consumer.daemon = True
+consumer.start()
+
+
+#
+# Active waiting for Ctrl-C
+#
+
+while True:
+    time.sleep(0.1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/ManualModification.py	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/Replicate.py	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,110 @@
+#!/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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 base64
+import httplib2
+import json
+import re
+import sys
+
+URL_REGEX = re.compile('(http|https)://((.+?):(.+?)@|)(.*)')
+
+
+if len(sys.argv) != 3:
+    print("""
+Script to copy the content of one Orthanc server to another Orthanc
+server through their REST API.
+
+Usage: %s [SourceURI] [TargetURI]
+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)
+
+
+
+def CreateHeaders(parsedUrl):
+    headers = { }
+    username = parsedUrl.group(3)
+    password = parsedUrl.group(4)
+
+    if username != None and password != None:
+        # This is a custom reimplementation of the
+        # "Http.add_credentials()" method for Basic HTTP Access
+        # Authentication (for some weird reason, this method does not
+        # always work)
+        # http://en.wikipedia.org/wiki/Basic_access_authentication
+        headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password)
+
+    return headers
+
+
+def GetBaseUrl(parsedUrl):
+    return '%s://%s' % (parsedUrl.group(1), parsedUrl.group(5))
+
+
+def DoGetString(url):
+    global URL_REGEX
+    parsedUrl = URL_REGEX.match(url)
+    headers = CreateHeaders(parsedUrl)
+
+    h = httplib2.Http()
+    resp, content = h.request(GetBaseUrl(parsedUrl), 'GET', headers = headers)
+
+    if resp.status == 200:
+        return content
+    else:
+        raise Exception('Unable to contact Orthanc at: ' + url)
+    
+
+def DoPostDicom(url, body):
+    global URL_REGEX
+    parsedUrl = URL_REGEX.match(url)
+    headers = CreateHeaders(parsedUrl)
+    headers['content-type'] = 'application/dicom'
+
+    h = httplib2.Http()
+    resp, content = h.request(GetBaseUrl(parsedUrl), 'POST',
+                              body = body,
+                              headers = headers)
+
+    if resp.status != 200:
+        raise Exception('Unable to contact Orthanc at: ' + url)
+    
+
+def _DecodeJson(s):
+    if (sys.version_info >= (3, 0)):
+        return json.loads(s.decode())
+    else:
+        return json.loads(s)
+
+
+def DoGetJson(url):
+    return _DecodeJson(DoGetString(url))
+
+
+SOURCE = sys.argv[1]
+TARGET = sys.argv[2]
+
+for study in DoGetJson('%s/studies' % SOURCE):
+    print('Sending study %s...' % study)
+    for instance in DoGetJson('%s/studies/%s/instances' % (SOURCE, study)):
+        dicom = DoGetString('%s/instances/%s/file' % (SOURCE, instance['ID']))
+        DoPostDicom('%s/instances' % TARGET, dicom)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Python/RestToolbox.py	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+#
+# This program is free software: you can redistribute it and/or
+# modify it under the terms of the 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 httplib2
+import json
+import sys
+
+if (sys.version_info >= (3, 0)):
+    from urllib.parse import urlencode
+else:
+    from urllib import urlencode
+
+
+_credentials = None
+
+
+def _DecodeJson(s):
+    try:
+        if (sys.version_info >= (3, 0)):
+            return json.loads(s.decode())
+        else:
+            return json.loads(s)
+    except:
+        return s
+
+
+def SetCredentials(username, password):
+    global _credentials
+    _credentials = (username, password)
+
+def _SetupCredentials(h):
+    global _credentials
+    if _credentials != None:
+        h.add_credentials(_credentials[0], _credentials[1])
+
+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(_ComputeGetUri(uri, data), 'GET')
+    if not (resp.status in [ 200 ]):
+        raise Exception(resp.status)
+    elif not interpretAsJson:
+        return content.decode()
+    else:
+        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)
+
+    if isinstance(data, str):
+        body = data
+        if len(contentType) != 0:
+            headers = { 'content-type' : contentType }
+        else:
+            headers = { 'content-type' : 'text/plain' }
+    else:
+        body = json.dumps(data)
+        headers = { 'content-type' : 'application/json' }
+    
+    resp, content = h.request(
+        uri, method,
+        body = body,
+        headers = headers)
+
+    if not (resp.status in [ 200, 302 ]):
+        raise Exception(resp.status)
+    else:
+        return _DecodeJson(content)
+
+
+def DoDelete(uri):
+    h = httplib2.Http()
+    _SetupCredentials(h)
+    resp, content = h.request(uri, 'DELETE')
+
+    if not (resp.status in [ 200 ]):
+        raise Exception(resp.status)
+    else:
+        return _DecodeJson(content)
+
+
+def DoPut(uri, data = {}, contentType = ''):
+    return _DoPutOrPost(uri, 'PUT', data, contentType)
+
+
+def DoPost(uri, data = {}, contentType = ''):
+    return _DoPutOrPost(uri, 'POST', data, contentType)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,7 @@
+More contributed samples and documentation can be found and added in
+the "OrthancContributed" repository on GitHub:
+https://github.com/jodogne/OrthancContributed
+
+The integration tests of Orthanc provide many samples about the
+features of the REST API of Orthanc:
+https://hg.orthanc-server.com/orthanc-tests/file/tip/Tests/Tests.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Tools/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,36 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(OrthancTools)
+
+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(pthread dl)
+endif()
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkParameters.cmake)
+
+set(STATIC_BUILD ON)
+set(ALLOW_DOWNLOADS ON)
+
+include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkConfiguration.cmake)
+
+add_library(CommonLibraries
+  ${BOOST_SOURCES}
+  ${JSONCPP_SOURCES}
+  ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/Logging.cpp
+  ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
+  ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
+  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
+  )
+
+add_executable(RecoverCompressedFile
+  RecoverCompressedFile.cpp
+  ${ORTHANC_ROOT}/Core/Compression/DeflateBaseCompressor.cpp
+  ${ORTHANC_ROOT}/Core/Compression/ZlibCompressor.cpp
+  ${ZLIB_SOURCES}
+  )
+
+target_link_libraries(RecoverCompressedFile CommonLibraries)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/Tools/RecoverCompressedFile.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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 "../../../Core/Compression/ZlibCompressor.h"
+#include "../../../Core/SystemToolbox.h"
+#include "../../../Core/OrthancException.h"
+
+#include <stdio.h>
+
+int main(int argc, const char* argv[])
+{
+  if (argc != 2 && argc != 3)
+  {
+    fprintf(stderr, "Maintenance tool to recover a DICOM file that was compressed by Orthanc.\n\n");
+    fprintf(stderr, "Usage: %s <input> [output]\n", argv[0]);
+    fprintf(stderr, "If \"output\" is not given, the data will be output to stdout\n");
+    return -1;
+  }
+
+  try
+  {
+    fprintf(stderr, "Reading the file into memory...\n");
+    fflush(stderr);
+
+    std::string content;
+    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.empty() ? NULL : content.c_str(), 
+                          content.size());
+
+    fprintf(stderr, "Writing the uncompressed data...\n");
+    fflush(stderr);
+
+    if (argc == 3)
+    {
+      Orthanc::SystemToolbox::WriteFile(uncompressed, argv[2]);
+    }
+    else
+    {
+      if (uncompressed.size() > 0)
+      {
+        fwrite(&uncompressed[0], uncompressed.size(), 1, stdout);
+      }
+    }
+
+    fprintf(stderr, "Done!\n");
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    fprintf(stderr, "Error: %s\n", e.What());
+    return -1;
+  }
+
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer.js	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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/>.
+ **/
+
+
+
+/**
+ * Parameters of the HTTP server.
+ **/
+
+var orthanc = { 
+  host: 'localhost',
+  port: 8042 
+};
+
+var port = 8000;
+
+
+
+/**
+ * The Web application.
+ **/
+
+var http = require('http');
+var querystring = require('querystring');
+var toolbox = require('./NodeToolbox.js');
+
+var server = http.createServer(function(req, response) {
+  switch (req.method)
+  {
+  case 'GET':
+    {
+      if (req.url == '/') {
+        toolbox.Redirect('/index.html', response);
+      }
+      else if (req.url == '/index.html') {
+        toolbox.ServeFile('DrawingDicomizer/index.html', response);
+      }
+      else if (req.url == '/drawing.js') {
+        toolbox.ServeFile('DrawingDicomizer/drawing.js', response);
+      }
+      else if (req.url == '/orthanc.js') {
+        toolbox.ServeFile('DrawingDicomizer/orthanc.js', response);
+      }
+      else if (req.url == '/jquery.js') {
+        toolbox.ServeFile('../../../OrthancExplorer/libs/jquery.min.js', response);
+      }
+      else if (req.url.startsWith('/orthanc')) {
+        toolbox.ForwardGetRequest(orthanc, req.url.substr(8), response);
+      }
+      else {
+        toolbox.NotFound(response);
+      }
+
+      break;
+    }
+
+  case 'POST':
+    {
+      var body = '';
+
+      req.on('data', function (data) {
+        body += data;
+      });
+
+      req.on('end', function () {
+        if (req.url == '/orthanc/tools/create-dicom') {
+          body = JSON.stringify(querystring.parse(body));
+          toolbox.ForwardPostRequest(orthanc, '/tools/create-dicom', body, response);
+        }
+        else {
+          toolbox.NotFound(response);
+        }
+      });
+
+      break;
+    }
+
+  default:
+    toolbox.NotFound(response);
+  }
+});
+
+
+console.log('The demo is running at http://localhost:' + port + '/');
+server.listen(port);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/drawing.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2010 William Malone (www.williammalone.com)
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ **/
+
+
+/**
+ * This code comes from the blog entry "Create a Drawing App with
+ * HTML5 Canvas and JavaScript" by William Malone. It is the "simple
+ * demo" of a pure HTML5 drawing application.
+ *
+ * http://www.williammalone.com/articles/create-html5-canvas-javascript-drawing-app/
+ *
+ * To keep this sample code as simple as possible, we do not implement
+ * hacks for the canvas of Microsoft Internet Explorer.
+ **/
+
+
+if ($.browser.msie) {
+  alert('Please use Mozilla Firefox or Google Chrome. Microsoft Internet Explorer is not supported.');
+}
+
+
+var context;
+var clickX = new Array();
+var clickY = new Array();
+var clickDrag = new Array();
+var paint;
+
+
+function addClick(x, y, dragging)
+{
+  clickX.push(x);
+  clickY.push(y);
+  clickDrag.push(dragging);
+}
+
+
+function Redraw() 
+{
+  context.fillStyle = '#ffffff';
+  context.fillRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
+  
+  context.strokeStyle = '#df4b26';
+  context.lineJoin = 'round';
+  context.lineWidth = 5;
+  
+  for (var i=0; i < clickX.length; i++) {		
+    context.beginPath();
+    if (clickDrag[i] && i) {
+      context.moveTo(clickX[i - 1], clickY[i - 1]);
+    } else {
+      context.moveTo(clickX[i] - 1, clickY[i]);
+    }
+    context.lineTo(clickX[i], clickY[i]);
+    context.closePath();
+    context.stroke();
+  }
+}
+
+
+$(document).ready(function() {
+  context = document.getElementById('canvas').getContext('2d');
+  Redraw();
+
+  $('#canvas').mousedown(function(e) {
+    var mouseX = e.pageX - this.offsetLeft;
+    var mouseY = e.pageY - this.offsetTop;
+    
+    paint = true;
+    addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop);
+    Redraw();
+  });
+
+  $('#canvas').mousemove(function(e) {
+    if(paint) {
+      addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop, true);
+      Redraw();
+    }
+  });
+
+  $('#canvas').mouseup(function(e) {
+    paint = false;
+  });
+
+  $('#canvas').mouseleave(function(e) {
+    paint = false;
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/index.html	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,33 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>HTML5 Drawing Dicomizer</title>
+    <style media="screen" type="text/css">
+      canvas {
+        border: 1px inset brown;
+      }
+    </style>
+    <script src="jquery.js"></script>
+    <script src="drawing.js"></script>
+    <script src="orthanc.js"></script>
+  </head>
+  <body>
+    <canvas id="canvas" width="490" height="220"></canvas>
+    <p>
+      Patient ID: <input type="text" id="patientID"></input>
+    </p>
+    <p>
+      Patient Name: <input type="text" id="patientName" value="HELLO^WORLD"></input>
+    </p>
+    <p>
+      Study Description: <input type="text" id="studyDescription" value="My Study"></input>
+    </p>
+    <p>
+      Series Description: <input type="text" id="seriesDescription" value="My Series"></input>
+    </p>
+    <p>
+      <button id="submit">Submit</button>
+    </p>
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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/>.
+ **/
+
+
+function guid4Block() {
+  return Math.floor((1 + Math.random()) * 0x10000)
+    .toString(16)
+    .substring(1);
+}
+ 
+function guid() {
+  return (guid4Block() + guid4Block() + '-' + guid4Block() + '-' + guid4Block() + '-' +
+          guid4Block() + '-' + guid4Block() + guid4Block() + guid4Block());
+}
+
+
+$(document).ready(function() {
+  $('#patientID').val(guid());
+
+  $('#submit').click(function(event) {
+    var png = context.canvas.toDataURL();
+
+    $.ajax({
+      type: 'POST',
+      url: '/orthanc/tools/create-dicom',
+      dataType: 'text',
+      data: { 
+        PatientID: $('#patientID').val(),
+        PatientName: $('#patientName').val(),
+        StudyDescription: $('#studyDescription').val(),
+        SeriesDescription: $('#seriesDescription').val(),
+        PixelData: png,
+        Modality: 'RX'
+      },
+      success : function(msg) {
+        alert('Your drawing has been DICOM-ized!\n\n' + msg);
+      },
+      error : function() {
+        alert('Error while DICOM-izing the drawing');
+      }
+    });
+
+    return false;
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/WebApplications/NodeToolbox.js	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,119 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the 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/>.
+ **/
+
+
+var fs = require('fs');
+var http = require('http');
+
+
+function ForwardGetRequest(orthanc, path, res) {
+  var opts = orthanc;
+  opts.path = path;
+  opts.method = 'GET';
+
+  http.get(opts, function(response) {
+    if (response.statusCode == 200) {
+      response.setEncoding('utf-8');
+      response.on('data', function(chunk) {
+        res.write(chunk);
+      });
+      response.on('end', function() {
+        res.end();
+      });
+    } else {
+      console.log('Got error on GET forwarding: ' + 
+                  response.statusCode + ' (' + path + ')');
+      res.writeHead(response.statusCode);
+      res.end();
+    }
+  }).on('error', function(e) {
+    console.log('Unable to contact Orthanc: ' + e.message);
+    res.writeHead(503);  // Service Unavailable
+    res.end();
+  });
+}
+
+
+function ForwardPostRequest(orthanc, path, body, res) {
+  var opts = orthanc;
+  opts.path = path;
+  opts.method = 'POST';
+  opts.headers = {
+    'Content-Length': body.length
+  }
+
+  var req = http.request(opts, function(response) {
+    if (response.statusCode == 200) {
+      response.setEncoding('utf-8');
+      response.on('data', function(chunk) {
+        res.write(chunk);
+      });
+      response.on('end', function() {
+        res.end();
+      });
+    } else {
+      console.log('Got error on POST forwarding: ' + 
+                  response.statusCode + ' (' + path + ')');
+      res.writeHead(response.statusCode);
+      res.end();
+    }
+  }).on('error', function(e) {
+    console.log('Unable to contact Orthanc: ' + e.message);
+    res.writeHead(503);  // Service Unavailable
+    res.end();
+  });
+
+  req.write(body);
+  req.end();
+}
+
+
+function ServeFile(filename, res) {
+  fs.readFile(filename, function(r, c) {
+    res.end(c.toString());
+  });
+}
+
+
+function NotFound(res) {
+  res.writeHead(404, {'Content-Type': 'text/plain'});
+  res.end();
+}
+
+
+function Redirect(path, res) {
+  res.writeHead(301, {
+    'Content-Type': 'text/plain',
+    'Location': path
+  });
+  res.end();
+}
+
+
+String.prototype.startsWith = function(prefix) {
+  return this.indexOf(prefix) === 0;
+}
+
+
+module.exports.ForwardGetRequest = ForwardGetRequest;
+module.exports.ForwardPostRequest = ForwardPostRequest;
+module.exports.NotFound = NotFound;
+module.exports.Redirect = Redirect;
+module.exports.ServeFile = ServeFile;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Samples/WebApplications/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,34 @@
+===================
+GENERAL INFORMATION
+===================
+
+This folder contains sample Web applications.
+
+These Web applications make use of NodeJs (http://nodejs.org/). To run
+the applications, you therefore need to install NodeJs on your
+computer. NodeJs acts here as a lightweight, cross-platform Web server
+that statically serves the HTML/JavaScript files and that dynamically
+serves the Orthanc REST API as a reverse proxy (to avoid cross-domain
+problems with AJAX).
+
+Once NodeJs is installed, start Orthanc with default parameters
+(i.e. HTTP port set to 8042), start NodeJs with the sample application
+you are interested in (e.g. "node DrawingDicomizer.js"). Then, open
+http://localhost:8000/ with a standard Web browser to try the sample
+application.
+
+
+
+=======================================
+DRAWING DICOMIZER (DrawingDicomizer.js)
+=======================================
+
+This sample shows how to convert the content of a HTML5 canvas as a
+DICOM file, using a single AJAX request to Orthanc.
+
+Internally, the content of the HTML5 canvas is serialized through the
+standard "toDataURL()" method of the canvas object. This returns a
+string containing the PNG image encoded using the Data URI Scheme
+(http://en.wikipedia.org/wiki/Data_URI_scheme). Such a string is then
+sent to Orthanc using the '/tools/create-dicom' REST call, that
+transparently decompresses the PNG image into a DICOM image.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 2.8)
+
+project(Orthanc)
+
+set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../../../Resources/CMake)
+
+include(${ORTHANC_ROOT}/OrthancFrameworkParameters.cmake)
+set(ENABLE_WEB_CLIENT ON)
+include(${ORTHANC_ROOT}/OrthancFrameworkConfiguration.cmake)
+
+include_directories(${ORTHANC_ROOT})
+
+add_executable(Sample
+  main.cpp
+  ${ORTHANC_CORE_SOURCES}
+  ${AUTOGENERATED_SOURCES}
+  )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Testing/Issue32/Cpp/main.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,86 @@
+#include <Core/HttpClient.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+#include <Core/SystemToolbox.h>
+
+#include <iostream>
+#include <boost/thread.hpp>
+
+static void Worker(bool *done)
+{
+  LOG(WARNING) << "One thread has started";
+
+  Orthanc::HttpClient client;
+  //client.SetUrl("http://localhost:8042/studies");
+  //client.SetUrl("http://localhost:8042/tools/default-encoding");
+  client.SetUrl("http://localhost:8042/system");
+  //client.SetUrl("http://localhost:8042/");
+  //client.SetCredentials("orthanc", "orthanc");
+  client.SetRedirectionFollowed(false);
+  
+  while (!(*done))
+  {
+    try
+    {
+#if 0
+      Json::Value v;
+      if (!client.Apply(v) ||
+          v.type() != Json::objectValue)
+      {
+        printf("ERROR\n");
+      }
+#else
+      std::string s;
+      if (!client.Apply(s) ||
+          s.empty())
+      {
+        printf("ERROR\n");
+      }
+#endif
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      printf("EXCEPTION: %s", e.What());
+    }
+  }
+
+  LOG(WARNING) << "One thread has stopped";
+}
+
+int main()
+{
+  Orthanc::Logging::Initialize();
+  //Orthanc::Logging::EnableInfoLevel(true);
+  Orthanc::HttpClient::GlobalInitialize();
+
+  {
+    bool done = false;
+
+    std::vector<boost::thread*> threads;
+
+    for (size_t i = 0; i < 100; i++)
+    {
+      threads.push_back(new boost::thread(Worker, &done));
+    }
+
+    LOG(WARNING) << "STARTED";
+    Orthanc::SystemToolbox::ServerBarrier();
+    LOG(WARNING) << "STOPPING";
+
+    done = true;
+
+    for (size_t i = 0; i < threads.size(); i++)
+    {
+      if (threads[i]->joinable())
+      {
+        threads[i]->join();
+      }
+
+      delete threads[i];
+    }
+  }
+  
+  Orthanc::HttpClient::GlobalFinalize();
+  printf("OK\n");
+  return 0;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Testing/Issue32/Java/README.txt	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2 @@
+$ sudo apt-get install maven
+$ mvn test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Testing/Issue32/Java/pom.xml	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>io.osimis</groupId>
+  <artifactId>issue32</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>issue32</name>
+  <!-- FIXME change it to the project's website -->
+  <url>http://www.example.com</url>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+      <version>4.5.3</version>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.11</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Testing/Issue32/Java/src/test/java/io/osimis/AppTest.java	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,66 @@
+package io.osimis;
+
+import java.io.IOException;
+import java.util.Base64;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class AppTest 
+{
+  @Test
+  public void testKeepAlive()
+  {
+    PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
+
+    CloseableHttpClient client = HttpClients
+      .custom()
+      .setConnectionManager(cm)
+      .setRetryHandler((HttpRequestRetryHandler) (exception, executionCount, context) -> {
+          System.out.println("ERROR");
+          assertTrue(false);
+          return false;
+        }).build();
+
+    HttpRequestBase request = new HttpGet("http://localhost:8042/system");
+
+    // Low-level handling of HTTP basic authentication (for integration tests)
+    request.addHeader("Authorization", "Basic " +
+                      Base64.getEncoder().encodeToString("alice:orthanctest".getBytes()));
+
+    // The following call works
+    //HttpRequestBase request = new HttpGet("https://api.ipify.org?format=json");
+    
+    for (int i = 0; i < 5; i++) {
+      System.out.println("================================");
+      try (CloseableHttpResponse httpResponse = client.execute(request)) {
+        String responseContent = null;
+
+        HttpEntity entity = httpResponse.getEntity();
+        if (entity != null) {
+          responseContent = EntityUtils.toString(entity);
+        }
+
+        System.out.println(httpResponse.getStatusLine().getStatusCode());
+        System.out.println(responseContent);
+
+        EntityUtils.consume(entity);
+        httpResponse.close();
+      } catch (IOException e) {
+        System.out.println("Request error " + e);
+      }
+    }
+      
+    assertTrue(true);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Resources/Toolbox.lua	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,124 @@
+--[[ PrintRecursive(struct, [limit], [indent])   Recursively print arbitrary data. 
+Set limit (default 100) to stanch infinite loops.
+Indents tables as [KEY] VALUE, nested tables as [KEY] [KEY]...[KEY] VALUE
+Set indent ("") to prefix each line:    Mytable [KEY] [KEY]...[KEY] VALUE
+Source: https://gist.github.com/stuby/5445834#file-rprint-lua
+--]]
+
+function PrintRecursive(s, l, i) -- recursive Print (structure, limit, indent)
+   l = (l) or 100;  -- default item limit
+   i = i or "";     -- indent string
+   if (l<1) then print "ERROR: Item limit reached."; return l-1 end;
+   local ts = type(s);
+   if (ts ~= "table") then print (i,ts,s); return l-1 end
+   print (i,ts);           -- print "table"
+   for k,v in pairs(s) do  -- print "[KEY] VALUE"
+      l = PrintRecursive(v, l, i.."\t["..tostring(k).."]");
+      if (l < 0) then break end
+   end
+   return l
+end	
+
+
+
+
+function _InitializeJob()
+   _job = {}
+end
+
+
+function _AccessJob()
+   return _job
+end
+
+
+function SendToModality(resourceId, modality, localAet)
+   if resourceId == nil then
+      error('Cannot send a nonexistent resource')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'store-scu', 
+                   Resource = resourceId,
+                   Modality = modality,
+                   LocalAet = localAet
+                })
+   return resourceId
+end
+
+
+function SendToPeer(resourceId, peer)
+   if resourceId == nil then
+      error('Cannot send a nonexistent resource')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'store-peer', 
+                   Resource = resourceId,
+                   Peer = peer
+                })
+   return resourceId
+end
+
+
+function Delete(resourceId)
+   if resourceId == nil then
+      error('Cannot delete a nonexistent resource')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'delete', 
+                   Resource = resourceId
+                })
+   return nil  -- Forbid chaining
+end
+
+
+function ModifyResource(resourceId, replacements, removals, removePrivateTags)
+   if resourceId == nil then
+      error('Cannot modify a nonexistent resource')
+   end
+
+   if resourceId == '' then
+      error('Cannot modify twice an resource');
+   end
+
+   table.insert(_job, { 
+                   Operation = 'modify', 
+                   Resource = resourceId,
+                   Replace = replacements, 
+                   Remove = removals,
+                   RemovePrivateTags = removePrivateTags 
+                })
+
+   return ''  -- Chain with another operation
+end
+
+
+function ModifyInstance(resourceId, replacements, removals, removePrivateTags)
+   return ModifyResource(resourceId, replacements, removals, removePrivateTags)
+end
+
+
+-- This function is only applicable to individual instances
+function CallSystem(resourceId, command, args)
+   if resourceId == nil then
+      error('Cannot execute a system call on a nonexistent resource')
+   end
+
+   if command == nil then
+      error('No command was specified for system call')
+   end
+
+   table.insert(_job, { 
+                   Operation = 'call-system', 
+                   Resource = resourceId,
+                   Command = command,
+                   Arguments = args
+                })
+
+   return resourceId
+end
+
+
+print('Lua toolbox installed')
--- a/OrthancServer/Search/DatabaseConstraint.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,245 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DatabaseConstraint.h"
-
-#include "../../Core/OrthancException.h"
-
-
-namespace Orthanc
-{
-  namespace Plugins
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    OrthancPluginResourceType Convert(ResourceType type)
-    {
-      switch (type)
-      {
-        case ResourceType_Patient:
-          return OrthancPluginResourceType_Patient;
-
-        case ResourceType_Study:
-          return OrthancPluginResourceType_Study;
-
-        case ResourceType_Series:
-          return OrthancPluginResourceType_Series;
-
-        case ResourceType_Instance:
-          return OrthancPluginResourceType_Instance;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif
-
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    ResourceType Convert(OrthancPluginResourceType type)
-    {
-      switch (type)
-      {
-        case OrthancPluginResourceType_Patient:
-          return ResourceType_Patient;
-
-        case OrthancPluginResourceType_Study:
-          return ResourceType_Study;
-
-        case OrthancPluginResourceType_Series:
-          return ResourceType_Series;
-
-        case OrthancPluginResourceType_Instance:
-          return ResourceType_Instance;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif
-
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    OrthancPluginConstraintType Convert(ConstraintType constraint)
-    {
-      switch (constraint)
-      {
-        case ConstraintType_Equal:
-          return OrthancPluginConstraintType_Equal;
-
-        case ConstraintType_GreaterOrEqual:
-          return OrthancPluginConstraintType_GreaterOrEqual;
-
-        case ConstraintType_SmallerOrEqual:
-          return OrthancPluginConstraintType_SmallerOrEqual;
-
-        case ConstraintType_Wildcard:
-          return OrthancPluginConstraintType_Wildcard;
-
-        case ConstraintType_List:
-          return OrthancPluginConstraintType_List;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif    
-
-    
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    ConstraintType Convert(OrthancPluginConstraintType constraint)
-    {
-      switch (constraint)
-      {
-        case OrthancPluginConstraintType_Equal:
-          return ConstraintType_Equal;
-
-        case OrthancPluginConstraintType_GreaterOrEqual:
-          return ConstraintType_GreaterOrEqual;
-
-        case OrthancPluginConstraintType_SmallerOrEqual:
-          return ConstraintType_SmallerOrEqual;
-
-        case OrthancPluginConstraintType_Wildcard:
-          return ConstraintType_Wildcard;
-
-        case OrthancPluginConstraintType_List:
-          return ConstraintType_List;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-#endif
-  }
-
-  DatabaseConstraint::DatabaseConstraint(ResourceType level,
-                                         const DicomTag& tag,
-                                         bool isIdentifier,
-                                         ConstraintType type,
-                                         const std::vector<std::string>& values,
-                                         bool caseSensitive,
-                                         bool mandatory) :
-    level_(level),
-    tag_(tag),
-    isIdentifier_(isIdentifier),
-    constraintType_(type),
-    values_(values),
-    caseSensitive_(caseSensitive),
-    mandatory_(mandatory)
-  {
-    if (type != ConstraintType_List &&
-        values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }      
-
-    
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-  DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) :
-    level_(Plugins::Convert(constraint.level)),
-    tag_(constraint.tagGroup, constraint.tagElement),
-    isIdentifier_(constraint.isIdentifierTag),
-    constraintType_(Plugins::Convert(constraint.type)),
-    caseSensitive_(constraint.isCaseSensitive),
-    mandatory_(constraint.isMandatory)
-  {
-    if (constraintType_ != ConstraintType_List &&
-        constraint.valuesCount != 1)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    values_.resize(constraint.valuesCount);
-
-    for (uint32_t i = 0; i < constraint.valuesCount; i++)
-    {
-      assert(constraint.values[i] != NULL);
-      values_[i].assign(constraint.values[i]);
-    }
-  }
-#endif
-    
-
-  const std::string& DatabaseConstraint::GetValue(size_t index) const
-  {
-    if (index >= values_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return values_[index];
-    }
-  }
-
-
-  const std::string& DatabaseConstraint::GetSingleValue() const
-  {
-    if (values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return values_[0];
-    }
-  }
-
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-  void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
-                                            std::vector<const char*>& tmpValues) const
-  {
-    memset(&constraint, 0, sizeof(constraint));
-    
-    tmpValues.resize(values_.size());
-
-    for (size_t i = 0; i < values_.size(); i++)
-    {
-      tmpValues[i] = values_[i].c_str();
-    }
-
-    constraint.level = Plugins::Convert(level_);
-    constraint.tagGroup = tag_.GetGroup();
-    constraint.tagElement = tag_.GetElement();
-    constraint.isIdentifierTag = isIdentifier_;
-    constraint.isCaseSensitive = caseSensitive_;
-    constraint.isMandatory = mandatory_;
-    constraint.type = Plugins::Convert(constraintType_);
-    constraint.valuesCount = values_.size();
-    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
-  }
-#endif    
-}
--- a/OrthancServer/Search/DatabaseConstraint.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,144 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../ServerEnumerations.h"
-
-#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-#  include <orthanc/OrthancCDatabasePlugin.h>
-#  if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)      // Macro introduced in 1.3.1
-#    if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2)
-#      undef  ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT
-#      define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1
-#    endif
-#  endif
-#endif
-
-namespace Orthanc
-{
-  namespace Plugins
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    OrthancPluginResourceType Convert(ResourceType type);
-#endif
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    ResourceType Convert(OrthancPluginResourceType type);
-#endif
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    OrthancPluginConstraintType Convert(ConstraintType constraint);
-#endif
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    ConstraintType Convert(OrthancPluginConstraintType constraint);
-#endif
-  }
-
-
-  // This class is also used by the "orthanc-databases" project
-  class DatabaseConstraint
-  {
-  private:
-    ResourceType              level_;
-    DicomTag                  tag_;
-    bool                      isIdentifier_;
-    ConstraintType            constraintType_;
-    std::vector<std::string>  values_;
-    bool                      caseSensitive_;
-    bool                      mandatory_;
-
-  public:
-    DatabaseConstraint(ResourceType level,
-                       const DicomTag& tag,
-                       bool isIdentifier,
-                       ConstraintType type,
-                       const std::vector<std::string>& values,
-                       bool caseSensitive,
-                       bool mandatory);
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint);
-#endif
-    
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    const DicomTag& GetTag() const
-    {
-      return tag_;
-    }
-
-    bool IsIdentifier() const
-    {
-      return isIdentifier_;
-    }
-
-    ConstraintType GetConstraintType() const
-    {
-      return constraintType_;
-    }
-
-    size_t GetValuesCount() const
-    {
-      return values_.size();
-    }
-
-    const std::string& GetValue(size_t index) const;
-
-    const std::string& GetSingleValue() const;
-
-    bool IsCaseSensitive() const
-    {
-      return caseSensitive_;
-    }
-
-    bool IsMandatory() const
-    {
-      return mandatory_;
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-
-#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
-    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
-                          std::vector<const char*>& tmpValues) const;
-#endif    
-  };
-}
--- a/OrthancServer/Search/DatabaseLookup.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,316 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DatabaseLookup.h"
-
-#include "../ServerToolbox.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/DicomParsing/ToDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
-
-namespace Orthanc
-{
-  DatabaseLookup::~DatabaseLookup()
-  {
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      delete constraints_[i];
-    }
-  }
-
-
-  const DicomTagConstraint& DatabaseLookup::GetConstraint(size_t index) const
-  {
-    if (index >= constraints_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      assert(constraints_[index] != NULL);
-      return *constraints_[index];
-    }
-  }
-
-
-  void DatabaseLookup::AddConstraint(DicomTagConstraint* constraint)
-  {
-    if (constraint == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else
-    {
-      constraints_.push_back(constraint);
-    }
-  }
-
-
-  bool DatabaseLookup::IsMatch(const DicomMap& value) const
-  {
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      if (!constraints_[i]->IsMatch(value))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  bool DatabaseLookup::IsMatch(DcmItem& item,
-                               Encoding encoding,
-                               bool hasCodeExtensions) const
-  {
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-
-      const bool isOptionalConstraint = !constraints_[i]->IsMandatory();
-      const DcmTagKey tag = ToDcmtkBridge::Convert(constraints_[i]->GetTag());
-
-      DcmElement* element = NULL;
-      if (!item.findAndGetElement(tag, element).good())
-      {
-        return isOptionalConstraint;
-      }
-
-      if (element == NULL)
-      {
-        return false;
-      }
-
-      std::set<DicomTag> ignoreTagLength;
-      std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
-                                        (*element, DicomToJsonFlags_None, 
-                                         0, encoding, hasCodeExtensions, ignoreTagLength));
-
-      // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code
-      if (value.get() == NULL ||
-          value->IsNull())
-      {
-        return isOptionalConstraint;        
-      }
-      else if (value->IsBinary() ||
-               !constraints_[i]->IsMatch(value->GetContent()))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag,
-                                                  ValueRepresentation vr,
-                                                  const std::string& dicomQuery,
-                                                  bool caseSensitive,
-                                                  bool mandatoryTag)
-  {
-    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);
-
-      if (!lower.empty())
-      {
-        AddConstraint(new DicomTagConstraint
-                      (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag));
-      }
-
-      if (!upper.empty())
-      {
-        AddConstraint(new DicomTagConstraint
-                      (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag));
-      }
-    }
-    else if (tag == DICOM_TAG_MODALITIES_IN_STUDY ||
-             dicomQuery.find('\\') != std::string::npos)
-    {
-      DicomTag fixedTag(tag);
-
-      if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
-      {
-        // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-        // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-        fixedTag = DICOM_TAG_MODALITY;
-      }
-
-      std::unique_ptr<DicomTagConstraint> constraint
-        (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag));
-
-      std::vector<std::string> items;
-      Toolbox::TokenizeString(items, dicomQuery, '\\');
-
-      for (size_t i = 0; i < items.size(); i++)
-      {
-        constraint->AddValue(items[i]);
-      }
-
-      AddConstraint(constraint.release());
-    }
-    else if (
-      /**
-       * New test in Orthanc 1.6.0: Wild card matching is only allowed
-       * for a subset of value representations: AE, CS, LO, LT, PN,
-       * SH, ST, UC, UR, UT.
-       * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part04/sect_C.2.2.2.4.html
-       **/
-      (vr == ValueRepresentation_ApplicationEntity ||    // AE
-       vr == ValueRepresentation_CodeString ||           // CS
-       vr == ValueRepresentation_LongString ||           // LO
-       vr == ValueRepresentation_LongText ||             // LT
-       vr == ValueRepresentation_PersonName ||           // PN
-       vr == ValueRepresentation_ShortString ||          // SH
-       vr == ValueRepresentation_ShortText ||            // ST
-       vr == ValueRepresentation_UnlimitedCharacters ||  // UC
-       vr == ValueRepresentation_UniversalResource ||    // UR
-       vr == ValueRepresentation_UnlimitedText           // UT
-        ) &&
-      (dicomQuery.find('*') != std::string::npos ||
-       dicomQuery.find('?') != std::string::npos))
-    {
-      AddConstraint(new DicomTagConstraint
-                    (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag));
-    }
-    else
-    {
-      AddConstraint(new DicomTagConstraint
-                    (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag));
-    }
-  }
-
-
-  void DatabaseLookup::AddDicomConstraint(const DicomTag& tag,
-                                          const std::string& dicomQuery,
-                                          bool caseSensitivePN,
-                                          bool mandatoryTag)
-  {
-    ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
-
-    if (vr == ValueRepresentation_Sequence)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    /**
-     * DICOM specifies that searches must always be case sensitive,
-     * except for tags with a PN value representation. For PN, Orthanc
-     * uses the configuration option "CaseSensitivePN" to decide
-     * whether matching is case-sensitive or case-insensitive.
-     *
-     * 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
-     **/
-    
-    if (vr == ValueRepresentation_PersonName)
-    {
-      AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag);
-    }
-    else
-    {
-      AddDicomConstraintInternal(tag, vr, dicomQuery, true /* case sensitive */, mandatoryTag);
-    }
-  }
-
-
-  void DatabaseLookup::AddRestConstraint(const DicomTag& tag,
-                                         const std::string& dicomQuery,
-                                         bool caseSensitive,
-                                         bool mandatoryTag)
-  {
-    AddDicomConstraintInternal(tag, FromDcmtkBridge::LookupValueRepresentation(tag),
-                               dicomQuery, caseSensitive, mandatoryTag);
-  }
-
-
-  bool DatabaseLookup::HasOnlyMainDicomTags() const
-  {
-    std::set<DicomTag> mainTags;
-    DicomMap::GetMainDicomTags(mainTags);
-
-    for (size_t i = 0; i < constraints_.size(); i++)
-    {
-      assert(constraints_[i] != NULL);
-      
-      if (mainTags.find(constraints_[i]->GetTag()) == mainTags.end())
-      {
-        // This is not a main DICOM tag
-        return false;
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Search/DatabaseLookup.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomTagConstraint.h"
-
-class DcmItem;
-
-namespace Orthanc
-{
-  class DatabaseLookup : public boost::noncopyable
-  {
-  private:
-    std::vector<DicomTagConstraint*>  constraints_;
-
-    void AddDicomConstraintInternal(const DicomTag& tag,
-                                    ValueRepresentation vr,
-                                    const std::string& dicomQuery,
-                                    bool caseSensitive,
-                                    bool mandatoryTag);
-
-    void AddConstraint(DicomTagConstraint* constraint);  // Takes ownership
-
-  public:
-    DatabaseLookup()
-    {
-    }
-
-    ~DatabaseLookup();
-
-    void Reserve(size_t n)
-    {
-      constraints_.reserve(n);
-    }
-
-    size_t GetConstraintsCount() const
-    {
-      return constraints_.size();
-    }
-
-    const DicomTagConstraint& GetConstraint(size_t index) const;
-
-    bool IsMatch(const DicomMap& value) const;
-
-    bool IsMatch(DcmItem& item,
-                 Encoding encoding,
-                 bool hasCodeExtensions) const;
-
-    void AddDicomConstraint(const DicomTag& tag,
-                            const std::string& dicomQuery,
-                            bool caseSensitivePN,
-                            bool mandatoryTag);
-
-    void AddRestConstraint(const DicomTag& tag,
-                           const std::string& dicomQuery,
-                           bool caseSensitive,
-                           bool mandatoryTag);
-
-    bool HasOnlyMainDicomTags() const;
-  };
-}
--- a/OrthancServer/Search/DicomTagConstraint.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,385 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomTagConstraint.h"
-
-#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0
-#  include "../ServerToolbox.h"
-#endif
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
-#include "DatabaseConstraint.h"
-
-#include <boost/regex.hpp>
-
-namespace Orthanc
-{
-  class DicomTagConstraint::NormalizedString : public boost::noncopyable
-  {
-  private:
-    const std::string&  source_;
-    bool                caseSensitive_;
-    std::string         upper_;
-
-  public:
-    NormalizedString(const std::string& source,
-                     bool caseSensitive) :
-      source_(source),
-      caseSensitive_(caseSensitive)
-    {
-      if (!caseSensitive_)
-      {
-        upper_ = Toolbox::ToUpperCaseWithAccents(source);
-      }
-    }
-
-    const std::string& GetValue() const
-    {
-      if (caseSensitive_)
-      {
-        return source_;
-      }
-      else
-      {
-        return upper_;
-      }
-    }
-  };
-
-
-  class DicomTagConstraint::RegularExpression : public boost::noncopyable
-  {
-  private:
-    boost::regex  regex_;
-
-  public:
-    RegularExpression(const std::string& source,
-                      bool caseSensitive)
-    {
-      NormalizedString normalized(source, caseSensitive);
-      regex_ = boost::regex(Toolbox::WildcardToRegularExpression(normalized.GetValue()));
-    }
-
-    const boost::regex& GetValue() const
-    {
-      return regex_;
-    }
-  };
-
-
-  void DicomTagConstraint::AssignSingleValue(const std::string& value)
-  {
-    if (constraintType_ != ConstraintType_Wildcard &&
-        (value.find('*') != std::string::npos ||
-         value.find('?') != std::string::npos))
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Wildcards are not allowed on tag " + tag_.Format());
-    }
-
-    if (constraintType_ == ConstraintType_Equal ||
-        constraintType_ == ConstraintType_SmallerOrEqual ||
-        constraintType_ == ConstraintType_GreaterOrEqual ||
-        constraintType_ == ConstraintType_Wildcard)
-    {
-      values_.clear();
-      values_.insert(value);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
-                                         ConstraintType type,
-                                         const std::string& value,
-                                         bool caseSensitive,
-                                         bool mandatory) :
-    tag_(tag),
-    constraintType_(type),
-    caseSensitive_(caseSensitive),
-    mandatory_(mandatory)
-  {
-    AssignSingleValue(value);
-  }
-
-
-  DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
-                                         ConstraintType type,
-                                         bool caseSensitive,
-                                         bool mandatory) :
-    tag_(tag),
-    constraintType_(type),
-    caseSensitive_(caseSensitive),
-    mandatory_(mandatory)
-  {
-    if (type != ConstraintType_List)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) :
-    tag_(constraint.GetTag()),
-    constraintType_(constraint.GetConstraintType()),
-    caseSensitive_(constraint.IsCaseSensitive()),
-    mandatory_(constraint.IsMandatory())
-  {
-#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0
-    assert(constraint.IsIdentifier() ==
-           ServerToolbox::IsIdentifier(constraint.GetTag(), constraint.GetLevel()));
-#endif
-    
-    if (constraint.IsIdentifier())
-    {
-      // This conversion is only available for main DICOM tags, not for identifers
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    
-    if (constraintType_ == ConstraintType_List)
-    {
-      for (size_t i = 0; i < constraint.GetValuesCount(); i++)
-      {
-        AddValue(constraint.GetValue(i));
-      }
-    }
-    else
-    {
-      AssignSingleValue(constraint.GetSingleValue());
-    }
-  }
-
-
-  void DicomTagConstraint::AddValue(const std::string& value)
-  {
-    if (constraintType_ != ConstraintType_List)
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-    else
-    {
-      values_.insert(value);
-    }
-  }
-
-
-  const std::string& DicomTagConstraint::GetValue() const
-  {
-    if (constraintType_ == ConstraintType_List)
-    {
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-    else if (values_.size() != 1)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else
-    {
-      return *values_.begin();
-    }
-  }
-
-
-  bool DicomTagConstraint::IsMatch(const std::string& value)
-  {
-    NormalizedString source(value, caseSensitive_);
-
-    switch (constraintType_)
-    {
-      case ConstraintType_Equal:
-      {
-        NormalizedString reference(GetValue(), caseSensitive_);
-        return source.GetValue() == reference.GetValue();
-      }
-
-      case ConstraintType_SmallerOrEqual:
-      {
-        NormalizedString reference(GetValue(), caseSensitive_);
-        return source.GetValue() <= reference.GetValue();
-      }
-
-      case ConstraintType_GreaterOrEqual:
-      {
-        NormalizedString reference(GetValue(), caseSensitive_);
-        return source.GetValue() >= reference.GetValue();
-      }
-
-      case ConstraintType_Wildcard:
-      {
-        if (regex_.get() == NULL)
-        {
-          regex_.reset(new RegularExpression(GetValue(), caseSensitive_));
-        }
-
-        return boost::regex_match(source.GetValue(), regex_->GetValue());
-      }
-
-      case ConstraintType_List:
-      {
-        for (std::set<std::string>::const_iterator
-               it = values_.begin(); it != values_.end(); ++it)
-        {
-          NormalizedString reference(*it, caseSensitive_);
-          if (source.GetValue() == reference.GetValue())
-          {
-            return true;
-          }
-        }
-
-        return false;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  bool DicomTagConstraint::IsMatch(const DicomMap& value)
-  {
-    const DicomValue* tmp = value.TestAndGetValue(tag_);
-
-    if (tmp == NULL ||
-        tmp->IsNull())
-    {
-      if (mandatory_)
-      {
-        return false;
-      }
-      else
-      {
-        return true;
-      }
-    }
-    else if (tmp->IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      return IsMatch(tmp->GetContent());
-    }
-  }
-
-
-  std::string DicomTagConstraint::Format() const
-  {
-    switch (constraintType_)
-    {
-      case ConstraintType_Equal:
-        return tag_.Format() + " == " + GetValue();
-
-      case ConstraintType_SmallerOrEqual:
-        return tag_.Format() + " <= " + GetValue();
-
-      case ConstraintType_GreaterOrEqual:
-        return tag_.Format() + " >= " + GetValue();
-
-      case ConstraintType_Wildcard:
-        return tag_.Format() + " ~~ " + GetValue();
-
-      case ConstraintType_List:
-      {
-        std::string s = tag_.Format() + " IN [ ";
-
-        bool first = true;
-        for (std::set<std::string>::const_iterator
-               it = values_.begin(); it != values_.end(); ++it)
-        {
-          if (first)
-          {
-            first = false;
-          }
-          else
-          {
-            s += ", ";
-          }
-
-          s += *it;
-        }
-
-        return s + "]";
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  DatabaseConstraint DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
-                                                                     DicomTagType tagType) const
-  {
-    bool isIdentifier, caseSensitive;
-    
-    switch (tagType)
-    {
-      case DicomTagType_Identifier:
-        isIdentifier = true;
-        caseSensitive = true;
-        break;
-
-      case DicomTagType_Main:
-        isIdentifier = false;
-        caseSensitive = IsCaseSensitive();
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    std::vector<std::string> values;
-    values.reserve(values_.size());
-      
-    for (std::set<std::string>::const_iterator
-           it = values_.begin(); it != values_.end(); ++it)
-    {
-      if (isIdentifier)
-      {
-        values.push_back(ServerToolbox::NormalizeIdentifier(*it));
-      }
-      else
-      {
-        values.push_back(*it);
-      }
-    }
-
-    return DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
-                              values, caseSensitive, mandatory_);
-  }  
-}
--- a/OrthancServer/Search/DicomTagConstraint.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DatabaseConstraint.h"
-
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class DicomTagConstraint : public boost::noncopyable
-  {
-  private:
-    class NormalizedString;
-    class RegularExpression;
-
-    DicomTag                tag_;
-    ConstraintType          constraintType_;
-    std::set<std::string>   values_;
-    bool                    caseSensitive_;
-    bool                    mandatory_;
-
-    boost::shared_ptr<RegularExpression>  regex_;
-
-    void AssignSingleValue(const std::string& value);
-
-  public:
-    DicomTagConstraint(const DicomTag& tag,
-                       ConstraintType type,
-                       const std::string& value,
-                       bool caseSensitive,
-                       bool mandatory);
-
-    // For list search
-    DicomTagConstraint(const DicomTag& tag,
-                       ConstraintType type,
-                       bool caseSensitive,
-                       bool mandatory);
-
-    DicomTagConstraint(const DatabaseConstraint& constraint);
-
-    const DicomTag& GetTag() const
-    {
-      return tag_;
-    }
-
-    ConstraintType GetConstraintType() const
-    {
-      return constraintType_;
-    }
-    
-    bool IsCaseSensitive() const
-    {
-      return caseSensitive_;
-    }
-
-    void SetCaseSensitive(bool caseSensitive)
-    {
-      caseSensitive_ = caseSensitive;
-    }
-
-    bool IsMandatory() const
-    {
-      return mandatory_;
-    }
-
-    void AddValue(const std::string& value);
-
-    const std::string& GetValue() const;
-
-    const std::set<std::string>& GetValues() const
-    {
-      return values_;
-    }
-
-    bool IsMatch(const std::string& value);
-
-    bool IsMatch(const DicomMap& value);
-
-    std::string Format() const;
-
-    DatabaseConstraint ConvertToDatabaseConstraint(ResourceType level,
-                                                   DicomTagType tagType) const;
-  };
-}
--- a/OrthancServer/Search/HierarchicalMatcher.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,332 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancConfiguration.h"
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-
-namespace Orthanc
-{
-  HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query)
-  {
-    bool caseSensitivePN;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
-    }
-
-    bool hasCodeExtensions;
-    Encoding encoding = query.DetectEncoding(hasCodeExtensions);
-    Setup(*query.GetDcmtkObject().getDataset(), caseSensitivePN, encoding, hasCodeExtensions);
-  }
-
-
-  HierarchicalMatcher::~HierarchicalMatcher()
-  {
-    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,
-                                  bool hasCodeExtensions)
-  {
-    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;
-      }
-
-      if (flatTags_.find(tag) != flatTags_.end() ||
-          sequences_.find(tag) != sequences_.end())
-      {
-        // A constraint already exists on this tag
-        throw OrthancException(ErrorCode_BadRequest);        
-      }
-
-      if (FromDcmtkBridge::LookupValueRepresentation(tag) == 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, hasCodeExtensions);
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_BadRequest);        
-        }
-      }
-      else
-      {
-        flatTags_.insert(tag);
-
-        std::set<DicomTag> ignoreTagLength;
-        std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
-                                          (*element, DicomToJsonFlags_None, 
-                                           0, encoding, hasCodeExtensions, ignoreTagLength));
-
-        // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code
-        if (value.get() == NULL ||
-            value->IsNull())
-        {
-          // This is an universal constraint
-        }
-        else 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.";
-          }
-        }
-        else if (value->GetContent().empty())
-        {
-          // This is an universal matcher
-        }
-        else
-        {
-          flatConstraints_.AddDicomConstraint
-            (tag, value->GetContent(), caseSensitivePN, true /* mandatory */);
-        }
-      }
-    }
-  }
-
-
-  std::string HierarchicalMatcher::Format(const std::string& prefix) const
-  {
-    std::string s;
-
-    std::set<DicomTag> tags;
-    for (size_t i = 0; i < flatConstraints_.GetConstraintsCount(); i++)
-    {
-      const DicomTagConstraint& c = flatConstraints_.GetConstraint(i);
-
-      s += c.Format() + "\n";
-      tags.insert(c.GetTag());
-    }
-    
-    // Loop over the universal constraints
-    for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
-         it != flatTags_.end(); ++it)
-    {
-      if (tags.find(*it) == tags.end())
-      {
-        s += prefix + it->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
-  {
-    bool hasCodeExtensions;
-    Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-    
-    return MatchInternal(*dicom.GetDcmtkObject().getDataset(),
-                         encoding, hasCodeExtensions);
-  }
-
-
-  bool HierarchicalMatcher::MatchInternal(DcmItem& item,
-                                          Encoding encoding,
-                                          bool hasCodeExtensions) const
-  {
-    if (!flatConstraints_.IsMatch(item, encoding, hasCodeExtensions))
-    {
-      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, hasCodeExtensions))
-          {
-            match = true;
-            break;
-          }
-        }
-
-        if (!match)
-        {
-          return false;
-        }
-      }
-    }
-
-    return true;
-  }
-
-
-  DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source,
-                                                   Encoding encoding,
-                                                   bool hasCodeExtensions) const
-  {
-    std::unique_ptr<DcmDataset> target(new DcmDataset);
-
-    for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
-         it != flatTags_.end(); ++it)
-    {
-      DcmTagKey tag = ToDcmtkBridge::Convert(*it);
-      
-      DcmElement* element = NULL;
-      if (source.findAndGetElement(tag, element).good() &&
-          element != NULL)
-      {
-        if (it->IsPrivate())
-        {
-          throw OrthancException(ErrorCode_NotImplemented,
-                                 "Not applicable to private tags: " + it->Format());
-        }
-        
-        std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, "" /* no private creator */));
-        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::unique_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, hasCodeExtensions))  // 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::unique_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding, hasCodeExtensions));
-            cloned->append(new DcmItem(*child));
-          }
-        }
-
-        target->insert(cloned.release());
-      }
-    }
-
-    return target.release();
-  }
-
-
-  ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const
-  {
-    bool hasCodeExtensions;
-    Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-    
-    std::unique_ptr<DcmDataset> dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(),
-                                                        encoding, hasCodeExtensions));
-
-    std::unique_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset));
-    result->SetEncoding(encoding);
-
-    return result.release();
-  }
-}
--- a/OrthancServer/Search/HierarchicalMatcher.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DatabaseLookup.h"
-#include "../../Core/DicomParsing/ParsedDicomFile.h"
-
-class DcmItem;
-
-namespace Orthanc
-{
-  class HierarchicalMatcher : public boost::noncopyable
-  {
-  private:
-    typedef std::map<DicomTag, HierarchicalMatcher*>  Sequences;
-
-    std::set<DicomTag>  flatTags_;
-    DatabaseLookup      flatConstraints_;
-    Sequences           sequences_;
-
-    void Setup(DcmItem& query,
-               bool caseSensitivePN,
-               Encoding encoding,
-               bool hasCodeExtensions);
-
-    HierarchicalMatcher(DcmItem& query,
-                        bool caseSensitivePN,
-                        Encoding encoding,
-                        bool hasCodeExtensions)
-    {
-      Setup(query, caseSensitivePN, encoding, hasCodeExtensions);
-    }
-
-    bool MatchInternal(DcmItem& dicom,
-                       Encoding encoding,
-                       bool hasCodeExtensions) const;
-
-    DcmDataset* ExtractInternal(DcmItem& dicom,
-                                Encoding encoding,
-                                bool hasCodeExtensions) const;
-
-  public:
-    HierarchicalMatcher(ParsedDicomFile& query);
-
-    ~HierarchicalMatcher();
-
-    std::string Format(const std::string& prefix = "") const;
-
-    bool Match(ParsedDicomFile& dicom) const;
-
-    ParsedDicomFile* Extract(ParsedDicomFile& dicom) const;
-  };
-}
--- a/OrthancServer/Search/ISqlLookupFormatter.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,343 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ISqlLookupFormatter.h"
-
-#include "../../Core/OrthancException.h"
-#include "DatabaseConstraint.h"
-
-namespace Orthanc
-{
-  static std::string FormatLevel(ResourceType level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        return "patients";
-        
-      case ResourceType_Study:
-        return "studies";
-        
-      case ResourceType_Series:
-        return "series";
-        
-      case ResourceType_Instance:
-        return "instances";
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }      
-  
-
-  static bool FormatComparison(std::string& target,
-                               ISqlLookupFormatter& formatter,
-                               const DatabaseConstraint& constraint,
-                               size_t index)
-  {
-    std::string tag = "t" + boost::lexical_cast<std::string>(index);
-
-    std::string comparison;
-    
-    switch (constraint.GetConstraintType())
-    {
-      case ConstraintType_Equal:
-      case ConstraintType_SmallerOrEqual:
-      case ConstraintType_GreaterOrEqual:
-      {
-        std::string op;
-        switch (constraint.GetConstraintType())
-        {
-          case ConstraintType_Equal:
-            op = "=";
-            break;
-          
-          case ConstraintType_SmallerOrEqual:
-            op = "<=";
-            break;
-          
-          case ConstraintType_GreaterOrEqual:
-            op = ">=";
-            break;
-          
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-
-        std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
-
-        if (constraint.IsCaseSensitive())
-        {
-          comparison = tag + ".value " + op + " " + parameter;
-        }
-        else
-        {
-          comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")";
-        }
-
-        break;
-      }
-
-      case ConstraintType_List:
-      {
-        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
-        {
-          if (!comparison.empty())
-          {
-            comparison += ", ";
-          }
-            
-          std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
-
-          if (constraint.IsCaseSensitive())
-          {
-            comparison += parameter;
-          }
-          else
-          {
-            comparison += "lower(" + parameter + ")";
-          }
-        }
-
-        if (constraint.IsCaseSensitive())
-        {
-          comparison = tag + ".value IN (" + comparison + ")";
-        }
-        else
-        {
-          comparison = "lower(" +  tag + ".value) IN (" + comparison + ")";
-        }
-            
-        break;
-      }
-
-      case ConstraintType_Wildcard:
-      {
-        const std::string value = constraint.GetSingleValue();
-
-        if (value == "*")
-        {
-          if (!constraint.IsMandatory())
-          {
-            // Universal constraint on an optional tag, ignore it
-            return false;
-          }
-        }
-        else
-        {
-          std::string escaped;
-          escaped.reserve(value.size());
-
-          for (size_t i = 0; i < value.size(); i++)
-          {
-            if (value[i] == '*')
-            {
-              escaped += "%";
-            }
-            else if (value[i] == '?')
-            {
-              escaped += "_";
-            }
-            else if (value[i] == '%')
-            {
-              escaped += "\\%";
-            }
-            else if (value[i] == '_')
-            {
-              escaped += "\\_";
-            }
-            else if (value[i] == '\\')
-            {
-              escaped += "\\\\";
-            }
-            else
-            {
-              escaped += value[i];
-            }               
-          }
-
-          std::string parameter = formatter.GenerateParameter(escaped);
-
-          if (constraint.IsCaseSensitive())
-          {
-            comparison = (tag + ".value LIKE " + parameter + " " +
-                          formatter.FormatWildcardEscape());
-          }
-          else
-          {
-            comparison = ("lower(" + tag + ".value) LIKE lower(" +
-                          parameter + ") " + formatter.FormatWildcardEscape());
-          }
-        }
-          
-        break;
-      }
-
-      default:
-        return false;
-    }
-
-    if (constraint.IsMandatory())
-    {
-      target = comparison;
-    }
-    else if (comparison.empty())
-    {
-      target = tag + ".value IS NULL";
-    }
-    else
-    {
-      target = tag + ".value IS NULL OR " + comparison;
-    }
-
-    return true;
-  }
-
-
-  static void FormatJoin(std::string& target,
-                         const DatabaseConstraint& constraint,
-                         size_t index)
-  {
-    std::string tag = "t" + boost::lexical_cast<std::string>(index);
-
-    if (constraint.IsMandatory())
-    {
-      target = " INNER JOIN ";
-    }
-    else
-    {
-      target = " LEFT JOIN ";
-    }
-
-    if (constraint.IsIdentifier())
-    {
-      target += "DicomIdentifiers ";
-    }
-    else
-    {
-      target += "MainDicomTags ";
-    }
-
-    target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) +
-               ".internalId AND " + tag + ".tagGroup = " +
-               boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) +
-               " AND " + tag + ".tagElement = " +
-               boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
-  }
-  
-
-  void ISqlLookupFormatter::Apply(std::string& sql,
-                                  ISqlLookupFormatter& formatter,
-                                  const std::vector<DatabaseConstraint>& lookup,
-                                  ResourceType queryLevel,
-                                  size_t limit)
-  {
-    assert(ResourceType_Patient < ResourceType_Study &&
-           ResourceType_Study < ResourceType_Series &&
-           ResourceType_Series < ResourceType_Instance);
-    
-    ResourceType upperLevel = queryLevel;
-    ResourceType lowerLevel = queryLevel;
-
-    for (size_t i = 0; i < lookup.size(); i++)
-    {
-      ResourceType level = lookup[i].GetLevel();
-
-      if (level < upperLevel)
-      {
-        upperLevel = level;
-      }
-
-      if (level > lowerLevel)
-      {
-        lowerLevel = level;
-      }
-    }
-    
-    assert(upperLevel <= queryLevel &&
-           queryLevel <= lowerLevel);
-
-    std::string joins, comparisons;
-
-    size_t count = 0;
-    
-    for (size_t i = 0; i < lookup.size(); i++)
-    {
-      std::string comparison;
-      
-      if (FormatComparison(comparison, formatter, lookup[i], count))
-      {
-        std::string join;
-        FormatJoin(join, lookup[i], count);
-        joins += join;
-
-        if (!comparison.empty())
-        {
-          comparisons += " AND " + comparison;
-        }
-        
-        count ++;
-      }
-    }
-
-    sql = ("SELECT " +
-           FormatLevel(queryLevel) + ".publicId, " +
-           FormatLevel(queryLevel) + ".internalId" +
-           " FROM Resources AS " + FormatLevel(queryLevel));
-
-    for (int level = queryLevel - 1; level >= upperLevel; level--)
-    {
-      sql += (" INNER JOIN Resources " +
-              FormatLevel(static_cast<ResourceType>(level)) + " ON " +
-              FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
-              FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
-    }
-      
-    for (int level = queryLevel + 1; level <= lowerLevel; level++)
-    {
-      sql += (" INNER JOIN Resources " +
-              FormatLevel(static_cast<ResourceType>(level)) + " ON " +
-              FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
-              FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
-    }
-      
-    sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " +
-            formatter.FormatResourceType(queryLevel) + comparisons);
-
-    if (limit != 0)
-    {
-      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
-    }
-  }
-}
--- a/OrthancServer/Search/ISqlLookupFormatter.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Enumerations.h"
-
-#include <boost/noncopyable.hpp>
-#include <vector>
-
-namespace Orthanc
-{
-  class DatabaseConstraint;
-  
-  // This class is also used by the "orthanc-databases" project
-  class ISqlLookupFormatter : public boost::noncopyable
-  {
-  public:
-    virtual ~ISqlLookupFormatter()
-    {
-    }
-
-    virtual std::string GenerateParameter(const std::string& value) = 0;
-
-    virtual std::string FormatResourceType(ResourceType level) = 0;
-
-    virtual std::string FormatWildcardEscape() = 0;
-
-    static void Apply(std::string& sql,
-                      ISqlLookupFormatter& formatter,
-                      const std::vector<DatabaseConstraint>& lookup,
-                      ResourceType queryLevel,
-                      size_t limit);
-  };
-}
--- a/OrthancServer/ServerContext.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1373 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ServerContext.h"
-
-#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../Core/Cache/SharedArchive.h"
-#include "../Core/DicomParsing/DcmtkTranscoder.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/FileStorage/StorageAccessor.h"
-#include "../Core/HttpServer/FilesystemHttpSender.h"
-#include "../Core/HttpServer/HttpStreamTranscoder.h"
-#include "../Core/JobsEngine/SetOfInstancesJob.h"
-#include "../Core/Logging.h"
-#include "../Core/MetricsRegistry.h"
-#include "../Plugins/Engine/OrthancPlugins.h"
-
-#include "OrthancConfiguration.h"
-#include "OrthancRestApi/OrthancRestApi.h"
-#include "Search/DatabaseLookup.h"
-#include "ServerJobs/OrthancJobUnserializer.h"
-#include "ServerToolbox.h"
-#include "StorageCommitmentReports.h"
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-
-
-
-#define ENABLE_DICOM_CACHE  1
-
-static const size_t DICOM_CACHE_SIZE = 2;
-
-/**
- * IMPORTANT: We make the assumption that the same instance of
- * FileStorage can be accessed from multiple threads. This seems OK
- * since the filesystem implements the required locking mechanisms,
- * but maybe a read-writer lock on the "FileStorage" could be
- * useful. Conversely, "ServerIndex" already implements mutex-based
- * locking.
- **/
-
-namespace Orthanc
-{
-  void ServerContext::ChangeThread(ServerContext* that,
-                                   unsigned int sleepDelay)
-  {
-    while (!that->done_)
-    {
-      std::unique_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay));
-        
-      if (obj.get() != NULL)
-      {
-        const ServerIndexChange& change = dynamic_cast<const ServerIndexChange&>(*obj.get());
-
-        boost::shared_lock<boost::shared_mutex> lock(that->listenersMutex_);
-        for (ServerListeners::iterator it = that->listeners_.begin(); 
-             it != that->listeners_.end(); ++it)
-        {
-          try
-          {
-            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()
-                       << " (code " << e.GetErrorCode() << ")";
-          }
-        }
-      }
-    }
-  }
-
-
-  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);
-    plugins_->SignalJobSubmitted(jobId);
-  }
-  
-
-  void ServerContext::SignalJobSuccess(const std::string& jobId)
-  {
-    haveJobsChanged_ = true;
-    mainLua_.SignalJobSuccess(jobId);
-    plugins_->SignalJobSuccess(jobId);
-  }
-
-  
-  void ServerContext::SignalJobFailure(const std::string& jobId)
-  {
-    haveJobsChanged_ = true;
-    mainLua_.SignalJobFailure(jobId);
-    plugins_->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(WARNING) << "Cannot unserialize the jobs engine, starting anyway: " << e.What();
-        }
-      }
-      else
-      {
-        LOG(INFO) << "The last execution of Orthanc has archived no job";
-      }
-    }
-    else
-    {
-      LOG(INFO) << "Not reloading the jobs from the last execution of Orthanc";
-    }
-
-    jobsEngine_.GetRegistry().SetObserver(*this);
-    jobsEngine_.Start();
-    isJobsEngineUnserialized_ = true;
-
-    saveJobsThread_ = boost::thread(SaveJobsThread, this, (unitTesting ? 20 : 100));
-  }
-
-
-  void ServerContext::SaveJobsEngine()
-  {
-    if (saveJobs_)
-    {
-      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,
-                               bool unitTesting,
-                               size_t maxCompletedJobs) :
-    index_(*this, database, (unitTesting ? 20 : 500)),
-    area_(area),
-    compressionEnabled_(false),
-    storeMD5_(true),
-    provider_(*this),
-    dicomCache_(provider_, DICOM_CACHE_SIZE),
-    mainLua_(*this),
-    filterLua_(*this),
-    luaListener_(*this),
-    jobsEngine_(maxCompletedJobs),
-#if ORTHANC_ENABLE_PLUGINS == 1
-    plugins_(NULL),
-#endif
-    done_(false),
-    haveJobsChanged_(false),
-    isJobsEngineUnserialized_(false),
-    metricsRegistry_(new MetricsRegistry),
-    isHttpServerSecure_(true),
-    isExecuteLuaEnabled_(false),
-    overwriteInstances_(false),
-    dcmtkTranscoder_(new DcmtkTranscoder),
-    isIngestTranscoding_(false)
-  {
-    try
-    {
-      unsigned int lossyQuality;
-
-      {
-        OrthancConfiguration::ReaderLock lock;
-
-        queryRetrieveArchive_.reset(
-          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100)));
-        mediaArchive_.reset(
-          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
-        defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
-        jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
-        saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
-        metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
-
-        // New configuration options in Orthanc 1.5.1
-        findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
-        limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
-        limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
-
-        // New configuration option in Orthanc 1.6.0
-        storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
-
-        // New options in Orthanc 1.7.0
-        transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
-        builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After"));
-        lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90);
-
-        std::string s;
-        if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding"))
-        {
-          if (LookupTransferSyntax(ingestTransferSyntax_, s))
-          {
-            isIngestTranscoding_ = true;
-            LOG(WARNING) << "Incoming DICOM instances will automatically be transcoded to "
-                         << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_);
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_ParameterOutOfRange,
-                                   "Unknown transfer syntax for ingest transcoding: " + s);
-          }
-        }
-        else
-        {
-          isIngestTranscoding_ = false;
-          LOG(INFO) << "Automated transcoding of incoming DICOM instances is disabled";
-        }
-      }
-
-      jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
-
-      listeners_.push_back(ServerListener(luaListener_, "Lua"));
-      changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
-    
-      dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetLossyQuality(lossyQuality);
-    }
-    catch (OrthancException&)
-    {
-      Stop();
-      throw;
-    }
-  }
-
-
-  
-  ServerContext::~ServerContext()
-  {
-    if (!done_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: ServerContext::Stop() should be invoked manually to avoid mess in the destruction order!";
-      Stop();
-    }
-  }
-
-
-  void ServerContext::Stop()
-  {
-    if (!done_)
-    {
-      {
-        boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
-        listeners_.clear();
-      }
-
-      done_ = true;
-
-      if (changeThread_.joinable())
-      {
-        changeThread_.join();
-      }
-
-      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!
-      jobsEngine_.Stop();
-      index_.Stop();
-    }
-  }
-
-
-  void ServerContext::SetCompressionEnabled(bool enabled)
-  {
-    if (enabled)
-      LOG(WARNING) << "Disk compression is enabled";
-    else
-      LOG(WARNING) << "Disk compression is disabled";
-
-    compressionEnabled_ = enabled;
-  }
-
-
-  void ServerContext::RemoveFile(const std::string& fileUuid,
-                                 FileContentType type)
-  {
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    accessor.Remove(fileUuid, type);
-  }
-
-
-  StoreStatus ServerContext::StoreAfterTranscoding(std::string& resultPublicId,
-                                                   DicomInstanceToStore& dicom,
-                                                   StoreInstanceMode mode)
-  {
-    bool overwrite;
-    switch (mode)
-    {
-      case StoreInstanceMode_Default:
-        overwrite = overwriteInstances_;
-        break;
-        
-      case StoreInstanceMode_OverwriteDuplicate:
-        overwrite = true;
-        break;
-        
-      case StoreInstanceMode_IgnoreDuplicate:
-        overwrite = false;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }    
-    
-    try
-    {
-      MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
-      StorageAccessor accessor(area_, GetMetricsRegistry());
-
-      resultPublicId = dicom.GetHasher().HashInstance();
-
-      Json::Value simplifiedTags;
-      ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human);
-
-      // Test if the instance must be filtered out
-      bool accepted = true;
-
-      {
-        boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
-
-        for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
-        {
-          try
-          {
-            if (!it->GetListener().FilterIncomingInstance(dicom, simplifiedTags))
-            {
-              accepted = false;
-              break;
-            }
-          }
-          catch (OrthancException& e)
-          {
-            LOG(ERROR) << "Error in the " << it->GetDescription() 
-                       << " callback while receiving an instance: " << e.What()
-                       << " (code " << e.GetErrorCode() << ")";
-            throw;
-          }
-        }
-      }
-
-      if (!accepted)
-      {
-        LOG(INFO) << "An incoming instance has been discarded by the filter";
-        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);
-
-      FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), 
-                                          FileContentType_Dicom, compression, storeMD5_);
-      FileInfo jsonInfo = accessor.Write(dicom.GetJson().toStyledString(), 
-                                         FileContentType_DicomAsJson, compression, storeMD5_);
-
-      ServerIndex::Attachments attachments;
-      attachments.push_back(dicomInfo);
-      attachments.push_back(jsonInfo);
-
-      typedef std::map<MetadataType, std::string>  InstanceMetadata;
-      InstanceMetadata  instanceMetadata;
-      StoreStatus status = index_.Store(
-        instanceMetadata, dicom, attachments, overwrite);
-
-      // Only keep the metadata for the "instance" level
-      dicom.GetMetadata().clear();
-
-      for (InstanceMetadata::const_iterator it = instanceMetadata.begin();
-           it != instanceMetadata.end(); ++it)
-      {
-        dicom.GetMetadata().insert(std::make_pair(std::make_pair(ResourceType_Instance, it->first),
-                                                  it->second));
-      }
-            
-      if (status != StoreStatus_Success)
-      {
-        accessor.Remove(dicomInfo);
-        accessor.Remove(jsonInfo);
-      }
-
-      switch (status)
-      {
-        case StoreStatus_Success:
-          LOG(INFO) << "New instance stored";
-          break;
-
-        case StoreStatus_AlreadyStored:
-          LOG(INFO) << "Already stored";
-          break;
-
-        case StoreStatus_Failure:
-          LOG(ERROR) << "Store failure";
-          break;
-
-        default:
-          // This should never happen
-          break;
-      }
-
-      if (status == StoreStatus_Success ||
-          status == StoreStatus_AlreadyStored)
-      {
-        boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
-
-        for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
-        {
-          try
-          {
-            it->GetListener().SignalStoredInstance(resultPublicId, dicom, simplifiedTags);
-          }
-          catch (OrthancException& e)
-          {
-            LOG(ERROR) << "Error in the " << it->GetDescription() 
-                       << " callback while receiving an instance: " << e.What()
-                       << " (code " << e.GetErrorCode() << ")";
-          }
-        }
-      }
-
-      return status;
-    }
-    catch (OrthancException& e)
-    {
-      if (e.GetErrorCode() == ErrorCode_InexistentTag)
-      {
-        dicom.GetSummary().LogMissingTagsForStore();
-      }
-
-      throw;
-    }
-  }
-
-
-  StoreStatus ServerContext::Store(std::string& resultPublicId,
-                                   DicomInstanceToStore& dicom,
-                                   StoreInstanceMode mode)
-  {
-    if (!isIngestTranscoding_)
-    {
-      // No automated transcoding. This was the only path in Orthanc <= 1.6.1.
-      return StoreAfterTranscoding(resultPublicId, dicom, mode);
-    }
-    else
-    {
-      // Automated transcoding of incoming DICOM files
-
-      DicomTransferSyntax sourceSyntax;
-      if (!FromDcmtkBridge::LookupOrthancTransferSyntax(
-            sourceSyntax, dicom.GetParsedDicomFile().GetDcmtkObject()) ||
-          sourceSyntax == ingestTransferSyntax_)
-      {
-        // No transcoding
-        return StoreAfterTranscoding(resultPublicId, dicom, mode);
-      }
-      else
-      {      
-        std::set<DicomTransferSyntax> syntaxes;
-        syntaxes.insert(ingestTransferSyntax_);
-
-        IDicomTranscoder::DicomImage source;
-        source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize());
-
-        IDicomTranscoder::DicomImage transcoded;
-        if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
-        {
-          std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
-
-          DicomInstanceToStore toStore;
-          toStore.SetParsedDicomFile(*tmp);
-          toStore.SetOrigin(dicom.GetOrigin());
-
-          StoreStatus ok = StoreAfterTranscoding(resultPublicId, toStore, mode);
-          assert(resultPublicId == tmp->GetHasher().HashInstance());
-
-          return ok;
-        }
-        else
-        {
-          // Cannot transcode => store the original file
-          return StoreAfterTranscoding(resultPublicId, dicom, mode);
-        }
-      }
-    }
-  }
-
-  
-  void ServerContext::AnswerAttachment(RestApiOutput& output,
-                                       const std::string& resourceId,
-                                       FileContentType content)
-  {
-    FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, resourceId, content))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    accessor.AnswerFile(output, attachment, GetFileContentMime(content));
-  }
-
-
-  void ServerContext::ChangeAttachmentCompression(const std::string& resourceId,
-                                                  FileContentType attachmentType,
-                                                  CompressionType compression)
-  {
-    LOG(INFO) << "Changing compression type for attachment "
-              << EnumerationToString(attachmentType) 
-              << " of resource " << resourceId << " to " 
-              << compression; 
-
-    FileInfo attachment;
-    if (!index_.LookupAttachment(attachment, resourceId, attachmentType))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (attachment.GetCompressionType() == compression)
-    {
-      // Nothing to do
-      return;
-    }
-
-    std::string content;
-
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    accessor.Read(content, attachment);
-
-    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
-                                       content.size(), attachmentType, compression, storeMD5_);
-
-    try
-    {
-      StoreStatus status = index_.AddAttachment(modified, resourceId);
-      if (status != StoreStatus_Success)
-      {
-        accessor.Remove(modified);
-        throw OrthancException(ErrorCode_Database);
-      }
-    }
-    catch (OrthancException&)
-    {
-      accessor.Remove(modified);
-      throw;
-    }    
-  }
-
-
-  void ServerContext::ReadDicomAsJsonInternal(std::string& result,
-                                              const std::string& instancePublicId)
-  {
-    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);
-
-      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()))
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot associate the DICOM-as-JSON summary to instance: " + instancePublicId);
-      }
-    }
-  }
-
-
-  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))
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Unable to read attachment " + EnumerationToString(content) +
-                             " of instance " + instancePublicId);
-    }
-
-    assert(attachment.GetContentType() == content);
-
-    if (uncompressIfNeeded)
-    {
-      ReadAttachment(result, attachment);
-    }
-    else
-    {
-      // Do not uncompress the content of the storage area, return the
-      // raw data
-      StorageAccessor accessor(area_, GetMetricsRegistry());
-      accessor.ReadRaw(result, attachment);
-    }
-  }
-
-
-  void ServerContext::ReadAttachment(std::string& result,
-                                     const FileInfo& attachment)
-  {
-    // This will decompress the attachment
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    accessor.Read(result, attachment);
-  }
-
-
-  IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId)
-  {
-    std::string content;
-    context_.ReadDicom(content, instancePublicId);
-    return new ParsedDicomFile(content);
-  }
-
-
-  ServerContext::DicomCacheLocker::DicomCacheLocker(ServerContext& that,
-                                                    const std::string& instancePublicId) : 
-    that_(that),
-    lock_(that_.dicomCacheMutex_)
-  {
-#if ENABLE_DICOM_CACHE == 0
-    static std::unique_ptr<IDynamicObject> p;
-    p.reset(that_.provider_.Provide(instancePublicId));
-    dicom_ = dynamic_cast<ParsedDicomFile*>(p.get());
-#else
-    dicom_ = &dynamic_cast<ParsedDicomFile&>(that_.dicomCache_.Access(instancePublicId));
-#endif
-  }
-
-
-  ServerContext::DicomCacheLocker::~DicomCacheLocker()
-  {
-  }
-
-
-  void ServerContext::SetStoreMD5ForAttachments(bool storeMD5)
-  {
-    LOG(INFO) << "Storing MD5 for attachments: " << (storeMD5 ? "yes" : "no");
-    storeMD5_ = storeMD5;
-  }
-
-
-  bool ServerContext::AddAttachment(const std::string& resourceId,
-                                    FileContentType attachmentType,
-                                    const void* data,
-                                    size_t size)
-  {
-    LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId;
-    
-    // TODO Should we use "gzip" instead?
-    CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
-
-    StorageAccessor accessor(area_, GetMetricsRegistry());
-    FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
-
-    StoreStatus status = index_.AddAttachment(attachment, resourceId);
-    if (status != StoreStatus_Success)
-    {
-      accessor.Remove(attachment);
-      return false;
-    }
-    else
-    {
-      return true;
-    }
-  }
-
-
-  bool ServerContext::DeleteResource(Json::Value& target,
-                                     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);
-  }
-
-
-  void ServerContext::SignalChange(const ServerIndexChange& change)
-  {
-    pendingChanges_.Enqueue(change.Clone());
-  }
-
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-  void ServerContext::SetPlugins(OrthancPlugins& plugins)
-  {
-    boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
-
-    plugins_ = &plugins;
-
-    // TODO REFACTOR THIS
-    listeners_.clear();
-    listeners_.push_back(ServerListener(luaListener_, "Lua"));
-    listeners_.push_back(ServerListener(plugins, "plugin"));
-  }
-
-
-  void ServerContext::ResetPlugins()
-  {
-    boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
-
-    plugins_ = NULL;
-
-    // TODO REFACTOR THIS
-    listeners_.clear();
-    listeners_.push_back(ServerListener(luaListener_, "Lua"));
-  }
-
-
-  const OrthancPlugins& ServerContext::GetPlugins() const
-  {
-    if (HasPlugins())
-    {
-      return *plugins_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-  OrthancPlugins& ServerContext::GetPlugins()
-  {
-    if (HasPlugins())
-    {
-      return *plugins_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-#endif
-
-
-  bool ServerContext::HasPlugins() const
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    return (plugins_ != NULL);
-#else
-    return false;
-#endif
-  }
-
-
-  void ServerContext::Apply(ILookupVisitor& visitor,
-                            const DatabaseLookup& lookup,
-                            ResourceType queryLevel,
-                            size_t since,
-                            size_t limit)
-  {
-    unsigned int databaseLimit = (queryLevel == ResourceType_Instance ?
-                                  limitFindInstances_ : limitFindResults_);
-      
-    std::vector<std::string> resources, instances;
-
-    {
-      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
-      GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit);
-    }
-
-    bool complete = (databaseLimit == 0 ||
-                     resources.size() <= databaseLimit);
-
-    LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
-
-    /**
-     * "resources" contains the Orthanc ID of the resource at level
-     * "queryLevel", "instances" contains one the Orthanc ID of one
-     * sample instance from this resource.
-     **/
-    assert(resources.size() == instances.size());
-
-    size_t countResults = 0;
-    size_t skipped = 0;
-
-    const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
-    
-    for (size_t i = 0; i < instances.size(); i++)
-    {
-      // Optimization in Orthanc 1.5.1 - Don't read the full JSON from
-      // the disk if only "main DICOM tags" are to be returned
-
-      std::unique_ptr<Json::Value> dicomAsJson;
-
-      bool hasOnlyMainDicomTags;
-      DicomMap dicom;
-      
-      if (findStorageAccessMode_ == FindStorageAccessMode_DatabaseOnly ||
-          findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer ||
-          lookup.HasOnlyMainDicomTags())
-      {
-        // Case (1): The main DICOM tags, as stored in the database,
-        // are sufficient to look for match
-
-        DicomMap tmp;
-        if (!GetIndex().GetAllMainDicomTags(tmp, instances[i]))
-        {
-          // The instance has been removed during the execution of the
-          // lookup, ignore it
-          continue;
-        }
-
-#if 1
-        // New in Orthanc 1.6.0: Only keep the main DICOM tags at the
-        // level of interest for the query
-        switch (queryLevel)
-        {
-          // WARNING: Don't reorder cases below, and don't add "break"
-          case ResourceType_Instance:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Instance);
-
-          case ResourceType_Series:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Series);
-
-          case ResourceType_Study:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Study);
-            
-          case ResourceType_Patient:
-            dicom.MergeMainDicomTags(tmp, ResourceType_Patient);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-
-        // Special case of the "Modality" at the study level, in order
-        // to deal with C-FIND on "ModalitiesInStudy" (0008,0061).
-        // Check out integration test "test_rest_modalities_in_study".
-        if (queryLevel == ResourceType_Study)
-        {
-          dicom.CopyTagIfExists(tmp, DICOM_TAG_MODALITY);
-        }
-#else
-        dicom.Assign(tmp);  // This emulates Orthanc <= 1.5.8
-#endif
-        
-        hasOnlyMainDicomTags = true;
-      }
-      else
-      {
-        // Case (2): Need to read the "DICOM-as-JSON" attachment from
-        // the storage area
-        dicomAsJson.reset(new Json::Value);
-        ReadDicomAsJson(*dicomAsJson, instances[i]);
-
-        dicom.FromDicomAsJson(*dicomAsJson);
-
-        // This map contains the entire JSON, i.e. more than the main DICOM tags
-        hasOnlyMainDicomTags = false;   
-      }
-      
-      if (lookup.IsMatch(dicom))
-      {
-        if (skipped < since)
-        {
-          skipped++;
-        }
-        else if (limit != 0 &&
-                 countResults >= limit)
-        {
-          // Too many results, don't mark as complete
-          complete = false;
-          break;
-        }
-        else
-        {
-          if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer ||
-               findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) &&
-              dicomAsJson.get() == NULL &&
-              isDicomAsJsonNeeded)
-          {
-            dicomAsJson.reset(new Json::Value);
-            ReadDicomAsJson(*dicomAsJson, instances[i]);
-          }
-
-          if (hasOnlyMainDicomTags)
-          {
-            // This is Case (1): The variable "dicom" only contains the main DICOM tags
-            visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get());
-          }
-          else
-          {
-            // Remove the non-main DICOM tags from "dicom" if Case (2)
-            // was used, for consistency with Case (1)
-
-            DicomMap mainDicomTags;
-            mainDicomTags.ExtractMainDicomTags(dicom);
-            visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());            
-          }
-            
-          countResults ++;
-        }
-      }
-    }
-
-    if (complete)
-    {
-      visitor.MarkAsComplete();
-    }
-
-    LOG(INFO) << "Number of matching resources: " << countResults;
-  }
-
-
-  bool ServerContext::LookupOrReconstructMetadata(std::string& target,
-                                                  const std::string& publicId,
-                                                  MetadataType metadata)
-  {
-    // This is a backwards-compatibility function, that can
-    // reconstruct metadata that were not generated by an older
-    // release of Orthanc
-
-    if (metadata == MetadataType_Instance_SopClassUid ||
-        metadata == MetadataType_Instance_TransferSyntax)
-    {
-      if (index_.LookupMetadata(target, publicId, metadata))
-      {
-        return true;
-      }
-      else
-      {
-        // These metadata are mandatory in DICOM instances, and were
-        // introduced in Orthanc 1.2.0. The fact that
-        // "LookupMetadata()" has failed indicates that this database
-        // comes from an older release of Orthanc.
-        
-        DicomTag tag(0, 0);
-      
-        switch (metadata)
-        {
-          case MetadataType_Instance_SopClassUid:
-            tag = DICOM_TAG_SOP_CLASS_UID;
-            break;
-
-          case MetadataType_Instance_TransferSyntax:
-            tag = DICOM_TAG_TRANSFER_SYNTAX_UID;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-      
-        Json::Value dicomAsJson;
-        ReadDicomAsJson(dicomAsJson, publicId);
-
-        DicomMap tags;
-        tags.FromDicomAsJson(dicomAsJson);
-
-        const DicomValue* value = tags.TestAndGetValue(tag);
-
-        if (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          target = value->GetContent();
-
-          // Store for reuse
-          index_.SetMetadata(publicId, metadata, target);
-          return true;
-        }
-        else
-        {
-          // Should never happen
-          return false;
-        }
-      }
-    }
-    else
-    {
-      // No backward
-      return index_.LookupMetadata(target, publicId, metadata);
-    }
-  }
-
-
-  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);
-    }
-  }
-
-
-  void ServerContext::SignalUpdatedModalities()
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
-    {
-      GetPlugins().SignalUpdatedModalities();
-    }
-#endif
-  }
-
-   
-  void ServerContext::SignalUpdatedPeers()
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
-    {
-      GetPlugins().SignalUpdatedPeers();
-    }
-#endif
-  }
-
-
-  IStorageCommitmentFactory::ILookupHandler*
-  ServerContext::CreateStorageCommitment(const std::string& jobId,
-                                         const std::string& transactionUid,
-                                         const std::vector<std::string>& sopClassUids,
-                                         const std::vector<std::string>& sopInstanceUids,
-                                         const std::string& remoteAet,
-                                         const std::string& calledAet)
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins())
-    {
-      return GetPlugins().CreateStorageCommitment(
-        jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet);
-    }
-#endif
-
-    return NULL;
-  }
-
-
-  ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId,
-                                                 unsigned int frameIndex)
-  {
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
-    {
-      // Use Orthanc's built-in decoder, using the cache to speed-up
-      // things on multi-frame images
-      ServerContext::DicomCacheLocker locker(*this, publicId);        
-      std::unique_ptr<ImageAccessor> decoded(
-        DicomImageDecoder::Decode(locker.GetDicom(), frameIndex));
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins() &&
-        GetPlugins().HasCustomImageDecoder())
-    {
-      // TODO: Store the raw buffer in the DicomCacheLocker
-      std::string dicomContent;
-      ReadDicom(dicomContent, publicId);
-      std::unique_ptr<ImageAccessor> decoded(
-        GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex));
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-      {
-        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
-                  << "fallback to the built-in DCMTK decoder";
-      }
-    }
-#endif
-
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-    {
-      ServerContext::DicomCacheLocker locker(*this, publicId);        
-      return DicomImageDecoder::Decode(locker.GetDicom(), frameIndex);
-    }
-    else
-    {
-      return NULL;  // Built-in decoder is disabled
-    }
-  }
-
-
-  ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom,
-                                                 unsigned int frameIndex)
-  {
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
-    {
-      std::unique_ptr<ImageAccessor> decoded(
-        DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex));
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-    }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins() &&
-        GetPlugins().HasCustomImageDecoder())
-    {
-      std::unique_ptr<ImageAccessor> decoded(
-        GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex));
-      if (decoded.get() != NULL)
-      {
-        return decoded.release();
-      }
-      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-      {
-        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
-                  << "fallback to the built-in DCMTK decoder";
-      }
-    }
-#endif
-
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-    {
-      return DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex);
-    }
-    else
-    {
-      return NULL;
-    }
-  }
-
-
-  ImageAccessor* ServerContext::DecodeDicomFrame(const void* dicom,
-                                                 size_t size,
-                                                 unsigned int frameIndex)
-  {
-    DicomInstanceToStore instance;
-    instance.SetBuffer(dicom, size);
-    return DecodeDicomFrame(instance, frameIndex);
-  }
-  
-
-  void ServerContext::StoreWithTranscoding(std::string& sopClassUid,
-                                           std::string& sopInstanceUid,
-                                           DicomStoreUserConnection& connection,
-                                           const std::string& dicom,
-                                           bool hasMoveOriginator,
-                                           const std::string& moveOriginatorAet,
-                                           uint16_t moveOriginatorId)
-  {
-    const void* data = dicom.empty() ? NULL : dicom.c_str();
-    
-    if (!transcodeDicomProtocol_ ||
-        !connection.GetParameters().GetRemoteModality().IsTranscodingAllowed())
-    {
-      connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(),
-                       hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
-    }
-    else
-    {
-      connection.Transcode(sopClassUid, sopInstanceUid, *this, data, dicom.size(),
-                           hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
-    }
-  }
-
-
-  bool ServerContext::Transcode(DicomImage& target,
-                                DicomImage& source /* in, "GetParsed()" possibly modified */,
-                                const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                bool allowNewSopInstanceUid)
-  {
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
-    {
-      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
-      {
-        return true;
-      }
-    }
-      
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (HasPlugins() &&
-        GetPlugins().HasCustomTranscoder())
-    {
-      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
-      {
-        return true;
-      }
-      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-      {
-        LOG(INFO) << "The installed transcoding plugins cannot handle an image, "
-                  << "fallback to the built-in DCMTK transcoder";
-      }
-    }
-#endif
-
-    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
-    {
-      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
-    }
-    else
-    {
-      return false;
-    }
-  }
-}
--- a/OrthancServer/ServerContext.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,496 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IServerListener.h"
-#include "LuaScripting.h"
-#include "OrthancHttpHandler.h"
-#include "ServerIndex.h"
-#include "ServerJobs/IStorageCommitmentFactory.h"
-
-#include "../Core/Cache/MemoryCache.h"
-#include "../Core/DicomParsing/IDicomTranscoder.h"
-
-
-namespace Orthanc
-{
-  class DicomInstanceToStore;
-  class IStorageArea;
-  class JobsEngine;
-  class MetricsRegistry;
-  class OrthancPlugins;
-  class ParsedDicomFile;
-  class RestApiOutput;
-  class SetOfInstancesJob;
-  class SharedArchive;
-  class SharedMessageQueue;
-  class StorageCommitmentReports;
-  
-  
-  /**
-   * This class is responsible for maintaining the storage area on the
-   * filesystem (including compression), as well as the index of the
-   * DICOM store. It implements the required locking mechanisms.
-   **/
-  class ServerContext :
-    public IStorageCommitmentFactory,
-    public IDicomTranscoder,
-    private JobsRegistry::IObserver
-  {
-  public:
-    class ILookupVisitor : public boost::noncopyable
-    {
-    public:
-      virtual ~ILookupVisitor()
-      {
-      }
-
-      virtual bool IsDicomAsJsonNeeded() const = 0;
-      
-      virtual void MarkAsComplete() = 0;
-
-      virtual void Visit(const std::string& publicId,
-                         const std::string& instanceId,
-                         const DicomMap& mainDicomTags,
-                         const Json::Value* dicomAsJson) = 0;
-    };
-    
-    
-  private:
-    class LuaServerListener : public IServerListener
-    {
-    private:
-      ServerContext& context_;
-
-    public:
-      LuaServerListener(ServerContext& context) :
-        context_(context)
-      {
-      }
-
-      virtual void SignalStoredInstance(const std::string& publicId,
-                                        const 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 Deprecated::ICachePageProvider  // TODO
-    {
-    private:
-      ServerContext& context_;
-
-    public:
-      DicomCacheProvider(ServerContext& context) : context_(context)
-      {
-      }
-      
-      virtual IDynamicObject* Provide(const std::string& id);
-    };
-
-    class ServerListener
-    {
-    private:
-      IServerListener *listener_;
-      std::string      description_;
-
-    public:
-      ServerListener(IServerListener& listener,
-                     const std::string& description) :
-        listener_(&listener),
-        description_(description)
-      {
-      }
-
-      IServerListener& GetListener()
-      {
-        return *listener_;
-      }
-
-      const std::string& GetDescription()
-      {
-        return description_;
-      }
-    };
-
-    typedef std::list<ServerListener>  ServerListeners;
-
-
-    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) ORTHANC_OVERRIDE;
-
-    virtual void SignalJobSuccess(const std::string& jobId) ORTHANC_OVERRIDE;
-
-    virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE;
-
-    ServerIndex index_;
-    IStorageArea& area_;
-
-    bool compressionEnabled_;
-    bool storeMD5_;
-    
-    DicomCacheProvider provider_;
-    boost::mutex dicomCacheMutex_;
-    Deprecated::MemoryCache dicomCache_;  // TODO
-
-    LuaScripting mainLua_;
-    LuaScripting filterLua_;
-    LuaServerListener  luaListener_;
-    std::unique_ptr<SharedArchive>  mediaArchive_;
-    
-    // The "JobsEngine" must be *after* "LuaScripting", as
-    // "LuaScripting" embeds "LuaJobManager" that registers as an
-    // observer to "SequenceOfOperationsJob", whose lifetime
-    // corresponds to that of "JobsEngine". It must also be after
-    // "mediaArchive_", as jobs might access this archive.
-    JobsEngine jobsEngine_;
-    
-#if ORTHANC_ENABLE_PLUGINS == 1
-    OrthancPlugins* plugins_;
-#endif
-
-    ServerListeners listeners_;
-    boost::shared_mutex listenersMutex_;
-
-    bool done_;
-    bool haveJobsChanged_;
-    bool isJobsEngineUnserialized_;
-    SharedMessageQueue  pendingChanges_;
-    boost::thread  changeThread_;
-    boost::thread  saveJobsThread_;
-        
-    std::unique_ptr<SharedArchive>  queryRetrieveArchive_;
-    std::string defaultLocalAet_;
-    OrthancHttpHandler  httpHandler_;
-    bool saveJobs_;
-    FindStorageAccessMode findStorageAccessMode_;
-    unsigned int limitFindInstances_;
-    unsigned int limitFindResults_;
-
-    std::unique_ptr<MetricsRegistry>  metricsRegistry_;
-    bool isHttpServerSecure_;
-    bool isExecuteLuaEnabled_;
-    bool overwriteInstances_;
-
-    std::unique_ptr<StorageCommitmentReports>  storageCommitmentReports_;
-
-    bool transcodeDicomProtocol_;
-    std::unique_ptr<IDicomTranscoder>  dcmtkTranscoder_;
-    BuiltinDecoderTranscoderOrder builtinDecoderTranscoderOrder_;
-    bool isIngestTranscoding_;
-    DicomTransferSyntax ingestTransferSyntax_;
-
-    StoreStatus StoreAfterTranscoding(std::string& resultPublicId,
-                                      DicomInstanceToStore& dicom,
-                                      StoreInstanceMode mode);
-
-  public:
-    class DicomCacheLocker : public boost::noncopyable
-    {
-    private:
-      ServerContext& that_;
-      ParsedDicomFile *dicom_;
-      boost::mutex::scoped_lock lock_;
-
-    public:
-      DicomCacheLocker(ServerContext& that,
-                       const std::string& instancePublicId);
-
-      ~DicomCacheLocker();
-
-      ParsedDicomFile& GetDicom()
-      {
-        return *dicom_;
-      }
-    };
-
-    ServerContext(IDatabaseWrapper& database,
-                  IStorageArea& area,
-                  bool unitTesting,
-                  size_t maxCompletedJobs);
-
-    ~ServerContext();
-
-    void SetupJobsEngine(bool unitTesting,
-                         bool loadJobsFromDatabase);
-
-    ServerIndex& GetIndex()
-    {
-      return index_;
-    }
-
-    void SetCompressionEnabled(bool enabled);
-
-    bool IsCompressionEnabled() const
-    {
-      return compressionEnabled_;
-    }
-
-    void RemoveFile(const std::string& fileUuid,
-                    FileContentType type);
-
-    bool AddAttachment(const std::string& resourceId,
-                       FileContentType attachmentType,
-                       const void* data,
-                       size_t size);
-
-    StoreStatus Store(std::string& resultPublicId,
-                      DicomInstanceToStore& dicom,
-                      StoreInstanceMode mode);
-
-    void AnswerAttachment(RestApiOutput& output,
-                          const std::string& resourceId,
-                          FileContentType content);
-
-    void ChangeAttachmentCompression(const std::string& resourceId,
-                                     FileContentType attachmentType,
-                                     CompressionType compression);
-
-    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 ReadAttachment(std::string& result,
-                        const std::string& instancePublicId,
-                        FileContentType content,
-                        bool uncompressIfNeeded);
-    
-    void ReadAttachment(std::string& result,
-                        const FileInfo& attachment);
-
-    void SetStoreMD5ForAttachments(bool storeMD5);
-
-    bool IsStoreMD5ForAttachments() const
-    {
-      return storeMD5_;
-    }
-
-    JobsEngine& GetJobsEngine()
-    {
-      return jobsEngine_;
-    }
-
-    bool DeleteResource(Json::Value& target,
-                        const std::string& uuid,
-                        ResourceType expectedType);
-
-    void SignalChange(const ServerIndexChange& change);
-
-    SharedArchive& GetQueryRetrieveArchive()
-    {
-      return *queryRetrieveArchive_;
-    }
-
-    SharedArchive& GetMediaArchive()
-    {
-      return *mediaArchive_;
-    }
-
-    const std::string& GetDefaultLocalApplicationEntityTitle() const
-    {
-      return defaultLocalAet_;
-    }
-
-    LuaScripting& GetLuaScripting()
-    {
-      return mainLua_;
-    }
-
-    OrthancHttpHandler& GetHttpHandler()
-    {
-      return httpHandler_;
-    }
-
-    void Stop();
-
-    void Apply(ILookupVisitor& visitor,
-               const DatabaseLookup& lookup,
-               ResourceType queryLevel,
-               size_t since,
-               size_t limit);
-
-    bool LookupOrReconstructMetadata(std::string& target,
-                                     const std::string& publicId,
-                                     MetadataType type);
-
-
-    /**
-     * Management of the plugins
-     **/
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-    void SetPlugins(OrthancPlugins& plugins);
-
-    void ResetPlugins();
-
-    const OrthancPlugins& GetPlugins() const;
-
-    OrthancPlugins& GetPlugins();
-#endif
-
-    bool HasPlugins() const;
-
-    void AddChildInstances(SetOfInstancesJob& job,
-                           const std::string& publicId);
-
-    void SignalUpdatedModalities();
-
-    void SignalUpdatedPeers();
-
-    MetricsRegistry& GetMetricsRegistry()
-    {
-      return *metricsRegistry_;
-    }
-
-    void SetHttpServerSecure(bool isSecure)
-    {
-      isHttpServerSecure_ = isSecure;
-    }
-
-    bool IsHttpServerSecure() const
-    {
-      return isHttpServerSecure_;
-    }
-
-    void SetExecuteLuaEnabled(bool enabled)
-    {
-      isExecuteLuaEnabled_ = enabled;
-    }
-
-    bool IsExecuteLuaEnabled() const
-    {
-      return isExecuteLuaEnabled_;
-    }
-
-    void SetOverwriteInstances(bool overwrite)
-    {
-      overwriteInstances_ = overwrite;
-    }
-    
-    bool IsOverwriteInstances() const
-    {
-      return overwriteInstances_;
-    }
-    
-    virtual IStorageCommitmentFactory::ILookupHandler*
-    CreateStorageCommitment(const std::string& jobId,
-                            const std::string& transactionUid,
-                            const std::vector<std::string>& sopClassUids,
-                            const std::vector<std::string>& sopInstanceUids,
-                            const std::string& remoteAet,
-                            const std::string& calledAet) ORTHANC_OVERRIDE;
-
-    StorageCommitmentReports& GetStorageCommitmentReports()
-    {
-      return *storageCommitmentReports_;
-    }
-
-    ImageAccessor* DecodeDicomFrame(const std::string& publicId,
-                                    unsigned int frameIndex);
-
-    ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom,
-                                    unsigned int frameIndex);
-
-    ImageAccessor* DecodeDicomFrame(const void* dicom,
-                                    size_t size,
-                                    unsigned int frameIndex);
-    
-    void StoreWithTranscoding(std::string& sopClassUid,
-                              std::string& sopInstanceUid,
-                              DicomStoreUserConnection& connection,
-                              const std::string& dicom,
-                              bool hasMoveOriginator,
-                              const std::string& moveOriginatorAet,
-                              uint16_t moveOriginatorId);
-
-    // This method can be used even if the global option
-    // "TranscodeDicomProtocol" is set to "false"
-    virtual bool Transcode(DicomImage& target,
-                           DicomImage& source /* in, "GetParsed()" possibly modified */,
-                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-
-    bool IsTranscodeDicomProtocol() const
-    {
-      return transcodeDicomProtocol_;
-    }
-  };
-}
--- a/OrthancServer/ServerEnumerations.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,376 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ServerEnumerations.h"
-
-#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 EnumerationDictionary<MetadataType> dictMetadataType_;
-  static EnumerationDictionary<FileContentType> dictContentType_;
-  static MimeTypes  mimeTypes_;
-
-  void InitializeServerEnumerations()
-  {
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-
-    dictMetadataType_.Clear();
-    dictContentType_.Clear();
-    
-    dictMetadataType_.Add(MetadataType_Instance_IndexInSeries, "IndexInSeries");
-    dictMetadataType_.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate");
-    dictMetadataType_.Add(MetadataType_Instance_RemoteAet, "RemoteAET");
-    dictMetadataType_.Add(MetadataType_Series_ExpectedNumberOfInstances, "ExpectedNumberOfInstances");
-    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");
-  }
-
-  void RegisterUserMetadata(int metadata,
-                            const std::string& name)
-  {
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-
-    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);
-    }
-
-    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)
-  {
-    // This function MUST return a "std::string" and not "const
-    // char*", as the result is not a static string
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-    return dictMetadataType_.Translate(type);
-  }
-
-  MetadataType StringToMetadata(const std::string& str)
-  {
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-    return dictMetadataType_.Translate(str);
-  }
-
-  void RegisterUserContentType(int contentType,
-                               const std::string& name,
-                               const std::string& mime)
-  {
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-
-    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);
-    }
-
-    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)
-  {
-    // This function MUST return a "std::string" and not "const
-    // char*", as the result is not a static string
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-    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 EnumerationToString(MimeType_Dicom);
-
-      case FileContentType_DicomAsJson:
-        return MIME_JSON_UTF8;
-
-      default:
-        return EnumerationToString(MimeType_Binary);
-    }
-  }
-
-  FileContentType StringToContentType(const std::string& str)
-  {
-    boost::mutex::scoped_lock lock(enumerationsMutex_);
-    return dictContentType_.Translate(str);
-  }
-
-
-  FindStorageAccessMode StringToFindStorageAccessMode(const std::string& value)
-  {
-    if (value == "Always")
-    {
-      return FindStorageAccessMode_DiskOnLookupAndAnswer;
-    }
-    else if (value == "Never")
-    {
-      return FindStorageAccessMode_DatabaseOnly;
-    }
-    else if (value == "Answers")
-    {
-      return FindStorageAccessMode_DiskOnAnswer;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Configuration option \"StorageAccessOnFind\" "
-                             "should be \"Always\", \"Never\" or \"Answers\": " + value);
-    }    
-  }
-
-
-  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value)
-  {
-    if (value == "Before")
-    {
-      return BuiltinDecoderTranscoderOrder_Before;
-    }
-    else if (value == "After")
-    {
-      return BuiltinDecoderTranscoderOrder_After;
-    }
-    else if (value == "Disabled")
-    {
-      return BuiltinDecoderTranscoderOrder_Disabled;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Configuration option \"BuiltinDecoderTranscoderOrder\" "
-                             "should be \"After\", \"Before\" or \"Disabled\": " + value);
-    }    
-  }
-  
-
-  std::string GetBasePath(ResourceType type,
-                          const std::string& publicId)
-  {
-    switch (type)
-    {
-      case ResourceType_Patient:
-        return "/patients/" + publicId;
-
-      case ResourceType_Study:
-        return "/studies/" + publicId;
-
-      case ResourceType_Series:
-        return "/series/" + publicId;
-
-      case ResourceType_Instance:
-        return "/instances/" + publicId;
-      
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-  const char* EnumerationToString(SeriesStatus status)
-  {
-    switch (status)
-    {
-      case SeriesStatus_Complete:
-        return "Complete";
-
-      case SeriesStatus_Missing:
-        return "Missing";
-
-      case SeriesStatus_Inconsistent:
-        return "Inconsistent";
-
-      case SeriesStatus_Unknown:
-        return "Unknown";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-  const char* EnumerationToString(StoreStatus status)
-  {
-    switch (status)
-    {
-      case StoreStatus_Success:
-        return "Success";
-
-      case StoreStatus_AlreadyStored:
-        return "AlreadyStored";
-
-      case StoreStatus_Failure:
-        return "Failure";
-
-      case StoreStatus_FilteredOut:
-        return "FilteredOut";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  const char* EnumerationToString(ChangeType type)
-  {
-    switch (type)
-    {
-      case ChangeType_CompletedSeries:
-        return "CompletedSeries";
-
-      case ChangeType_NewInstance:
-        return "NewInstance";
-
-      case ChangeType_NewPatient:
-        return "NewPatient";
-
-      case ChangeType_NewSeries:
-        return "NewSeries";
-
-      case ChangeType_NewStudy:
-        return "NewStudy";
-
-      case ChangeType_AnonymizedStudy:
-        return "AnonymizedStudy";
-
-      case ChangeType_AnonymizedSeries:
-        return "AnonymizedSeries";
-
-      case ChangeType_ModifiedStudy:
-        return "ModifiedStudy";
-
-      case ChangeType_ModifiedSeries:
-        return "ModifiedSeries";
-
-      case ChangeType_AnonymizedPatient:
-        return "AnonymizedPatient";
-
-      case ChangeType_ModifiedPatient:
-        return "ModifiedPatient";
-
-      case ChangeType_StablePatient:
-        return "StablePatient";
-
-      case ChangeType_StableStudy:
-        return "StableStudy";
-
-      case ChangeType_StableSeries:
-        return "StableSeries";
-
-      case ChangeType_Deleted:
-        return "Deleted";
-
-      case ChangeType_NewChildInstance:
-        return "NewChildInstance";
-
-      case ChangeType_UpdatedAttachment:
-        return "UpdatedAttachment";
-
-      case ChangeType_UpdatedMetadata:
-        return "UpdatedMetadata";
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-  
-  bool IsUserMetadata(MetadataType metadata)
-  {
-    return (metadata >= MetadataType_StartUser &&
-            metadata <= MetadataType_EndUser);
-  }
-}
--- a/OrthancServer/ServerEnumerations.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,220 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <map>
-
-#include "../Core/Enumerations.h"
-#include "../Core/DicomFormat/DicomTag.h"
-
-namespace Orthanc
-{
-  enum SeriesStatus
-  {
-    SeriesStatus_Complete,
-    SeriesStatus_Missing,
-    SeriesStatus_Inconsistent,
-    SeriesStatus_Unknown
-  };
-
-  enum StoreStatus
-  {
-    StoreStatus_Success,
-    StoreStatus_AlreadyStored,
-    StoreStatus_Failure,
-    StoreStatus_FilteredOut     // Removed by NewInstanceFilter
-  };
-
-  enum DicomTagType
-  {
-    DicomTagType_Identifier,   // Tag that whose value is stored and indexed in the DB
-    DicomTagType_Main,         // Tag that is stored in the DB (but not indexed)
-    DicomTagType_Generic       // Tag that is only stored in the JSON files
-  };
-
-  enum ConstraintType
-  {
-    ConstraintType_Equal,
-    ConstraintType_SmallerOrEqual,
-    ConstraintType_GreaterOrEqual,
-    ConstraintType_Wildcard,
-    ConstraintType_List
-  };
-
-  namespace Compatibility
-  {
-    enum IdentifierConstraintType
-    {
-      IdentifierConstraintType_Equal,
-      IdentifierConstraintType_SmallerOrEqual,
-      IdentifierConstraintType_GreaterOrEqual,
-      IdentifierConstraintType_Wildcard        /* Case sensitive, "*" or "?" are the only allowed wildcards */
-    };
-  }
-
-  enum FindStorageAccessMode
-  {
-    FindStorageAccessMode_DatabaseOnly,
-    FindStorageAccessMode_DiskOnAnswer,
-    FindStorageAccessMode_DiskOnLookupAndAnswer
-  };
-
-  enum StoreInstanceMode
-  {
-    StoreInstanceMode_Default,
-    StoreInstanceMode_OverwriteDuplicate,
-    StoreInstanceMode_IgnoreDuplicate
-  };
-
-
-  /**
-   * WARNING: Do not change the explicit values in the enumerations
-   * below this point. This would result in incompatible databases
-   * between versions of Orthanc!
-   **/
-
-  enum GlobalProperty
-  {
-    GlobalProperty_DatabaseSchemaVersion = 1,   // Unused in the Orthanc core as of Orthanc 0.9.5
-    GlobalProperty_FlushSleep = 2,
-    GlobalProperty_AnonymizationSequence = 3,
-    GlobalProperty_JobsRegistry = 5,
-    GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
-    GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
-    GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
-
-    // 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
-  {
-    MetadataType_Instance_IndexInSeries = 1,
-    MetadataType_Instance_ReceptionDate = 2,
-    MetadataType_Instance_RemoteAet = 3,
-    MetadataType_Series_ExpectedNumberOfInstances = 4,
-    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,
-    MetadataType_EndUser = 65535
-  };
-
-  enum ChangeType
-  {
-    ChangeType_CompletedSeries = 1,
-    ChangeType_NewInstance = 2,
-    ChangeType_NewPatient = 3,
-    ChangeType_NewSeries = 4,
-    ChangeType_NewStudy = 5,
-    ChangeType_AnonymizedStudy = 6,
-    ChangeType_AnonymizedSeries = 7,
-    ChangeType_ModifiedStudy = 8,
-    ChangeType_ModifiedSeries = 9,
-    ChangeType_AnonymizedPatient = 10,
-    ChangeType_ModifiedPatient = 11,
-    ChangeType_StablePatient = 12,
-    ChangeType_StableStudy = 13,
-    ChangeType_StableSeries = 14,
-    ChangeType_UpdatedAttachment = 15,
-    ChangeType_UpdatedMetadata = 16,
-
-    ChangeType_INTERNAL_LastLogged = 4095,
-
-    // The changes below this point are not logged into the database
-    ChangeType_Deleted = 4096,
-    ChangeType_NewChildInstance = 4097
-  };
-
-  enum BuiltinDecoderTranscoderOrder
-  {
-    BuiltinDecoderTranscoderOrder_Before,
-    BuiltinDecoderTranscoderOrder_After,
-    BuiltinDecoderTranscoderOrder_Disabled
-  };
-
-
-
-  void InitializeServerEnumerations();
-
-  void RegisterUserMetadata(int metadata,
-                            const std::string& name);
-
-  MetadataType StringToMetadata(const std::string& str);
-
-  std::string EnumerationToString(MetadataType type);
-
-  void RegisterUserContentType(int contentType,
-                               const std::string& name,
-                               const std::string& mime);
-
-  FileContentType StringToContentType(const std::string& str);
-
-  FindStorageAccessMode StringToFindStorageAccessMode(const std::string& str);
-
-  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& str);
-
-  std::string EnumerationToString(FileContentType type);
-
-  std::string GetFileContentMime(FileContentType type);
-
-  std::string GetBasePath(ResourceType type,
-                          const std::string& publicId);
-
-  const char* EnumerationToString(SeriesStatus status);
-
-  const char* EnumerationToString(StoreStatus status);
-
-  const char* EnumerationToString(ChangeType type);
-
-  bool IsUserMetadata(MetadataType type);
-}
--- a/OrthancServer/ServerIndex.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2597 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ServerIndex.h"
-
-#ifndef NOMINMAX
-#define NOMINMAX
-#endif
-
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/Logging.h"
-#include "../Core/Toolbox.h"
-
-#include "Database/ResourcesContent.h"
-#include "DicomInstanceToStore.h"
-#include "OrthancConfiguration.h"
-#include "Search/DatabaseLookup.h"
-#include "Search/DicomTagConstraint.h"
-#include "ServerContext.h"
-#include "ServerIndexChange.h"
-#include "ServerToolbox.h"
-
-#include <boost/lexical_cast.hpp>
-#include <stdio.h>
-
-static const uint64_t MEGA_BYTES = 1024 * 1024;
-
-namespace Orthanc
-{
-  static void CopyListToVector(std::vector<std::string>& target,
-                               const std::list<std::string>& source)
-  {
-    target.resize(source.size());
-
-    size_t pos = 0;
-    
-    for (std::list<std::string>::const_iterator
-           it = source.begin(); it != source.end(); ++it)
-    {
-      target[pos] = *it;
-      pos ++;
-    }      
-  }
-
-  
-  class ServerIndex::Listener : public IDatabaseListener
-  {
-  private:
-    struct FileToRemove
-    {
-    private:
-      std::string  uuid_;
-      FileContentType  type_;
-
-    public:
-      FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), 
-                                           type_(info.GetContentType())
-      {
-      }
-
-      const std::string& GetUuid() const
-      {
-        return uuid_;
-      }
-
-      FileContentType GetContentType() const 
-      {
-        return type_;
-      }
-    };
-
-    ServerContext& context_;
-    bool hasRemainingLevel_;
-    ResourceType remainingType_;
-    std::string remainingPublicId_;
-    std::list<FileToRemove> pendingFilesToRemove_;
-    std::list<ServerIndexChange> pendingChanges_;
-    uint64_t sizeOfFilesToRemove_;
-    bool insideTransaction_;
-
-    void Reset()
-    {
-      sizeOfFilesToRemove_ = 0;
-      hasRemainingLevel_ = false;
-      pendingFilesToRemove_.clear();
-      pendingChanges_.clear();
-    }
-
-  public:
-    Listener(ServerContext& context) : context_(context),
-                                       insideTransaction_(false)      
-    {
-      Reset();
-      assert(ResourceType_Patient < ResourceType_Study &&
-             ResourceType_Study < ResourceType_Series &&
-             ResourceType_Series < ResourceType_Instance);
-    }
-
-    void StartTransaction()
-    {
-      Reset();
-      insideTransaction_ = true;
-    }
-
-    void EndTransaction()
-    {
-      insideTransaction_ = false;
-    }
-
-    uint64_t GetSizeOfFilesToRemove()
-    {
-      return sizeOfFilesToRemove_;
-    }
-
-    void CommitFilesToRemove()
-    {
-      for (std::list<FileToRemove>::const_iterator 
-             it = pendingFilesToRemove_.begin();
-           it != pendingFilesToRemove_.end(); ++it)
-      {
-        try
-        {
-          context_.RemoveFile(it->GetUuid(), it->GetContentType());
-        }
-        catch (OrthancException& e)
-        {
-          LOG(ERROR) << "Unable to remove an attachment from the storage area: "
-                     << it->GetUuid() << " (type: " << EnumerationToString(it->GetContentType()) << ")";
-        }
-      }
-    }
-
-    void CommitChanges()
-    {
-      for (std::list<ServerIndexChange>::const_iterator 
-             it = pendingChanges_.begin(); 
-           it != pendingChanges_.end(); ++it)
-      {
-        context_.SignalChange(*it);
-      }
-    }
-
-    virtual void SignalRemainingAncestor(ResourceType parentType,
-                                         const std::string& publicId)
-    {
-      VLOG(1) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")";
-
-      if (hasRemainingLevel_)
-      {
-        if (parentType < remainingType_)
-        {
-          remainingType_ = parentType;
-          remainingPublicId_ = publicId;
-        }
-      }
-      else
-      {
-        hasRemainingLevel_ = true;
-        remainingType_ = parentType;
-        remainingPublicId_ = publicId;
-      }        
-    }
-
-    virtual void SignalFileDeleted(const FileInfo& info)
-    {
-      assert(Toolbox::IsUuid(info.GetUuid()));
-      pendingFilesToRemove_.push_back(FileToRemove(info));
-      sizeOfFilesToRemove_ += info.GetCompressedSize();
-    }
-
-    virtual void SignalChange(const ServerIndexChange& change)
-    {
-      VLOG(1) << "Change related to resource " << change.GetPublicId() << " of type " 
-              << EnumerationToString(change.GetResourceType()) << ": " 
-              << EnumerationToString(change.GetChangeType());
-
-      if (insideTransaction_)
-      {
-        pendingChanges_.push_back(change);
-      }
-      else
-      {
-        context_.SignalChange(change);
-      }
-    }
-
-    bool HasRemainingLevel() const
-    {
-      return hasRemainingLevel_;
-    }
-
-    ResourceType GetRemainingType() const
-    {
-      assert(HasRemainingLevel());
-      return remainingType_;
-    }
-
-    const std::string& GetRemainingPublicId() const
-    {
-      assert(HasRemainingLevel());
-      return remainingPublicId_;
-    }                                 
-  };
-
-
-  class ServerIndex::Transaction
-  {
-  private:
-    ServerIndex& index_;
-    std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_;
-    bool isCommitted_;
-
-  public:
-    Transaction(ServerIndex& index) : 
-      index_(index),
-      isCommitted_(false)
-    {
-      transaction_.reset(index_.db_.StartTransaction());
-      transaction_->Begin();
-
-      index_.listener_->StartTransaction();
-    }
-
-    ~Transaction()
-    {
-      index_.listener_->EndTransaction();
-
-      if (!isCommitted_)
-      {
-        transaction_->Rollback();
-      }
-    }
-
-    void Commit(uint64_t sizeOfAddedFiles)
-    {
-      if (!isCommitted_)
-      {
-        int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) -
-                         static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove()));
-
-        transaction_->Commit(delta);
-
-        // We can remove the files once the SQLite transaction has
-        // been successfully committed. Some files might have to be
-        // deleted because of recycling.
-        index_.listener_->CommitFilesToRemove();
-
-        // Send all the pending changes to the Orthanc plugins
-        index_.listener_->CommitChanges();
-
-        isCommitted_ = true;
-      }
-    }
-  };
-
-
-  class ServerIndex::UnstableResourcePayload
-  {
-  private:
-    ResourceType type_;
-    std::string publicId_;
-    boost::posix_time::ptime time_;
-
-  public:
-    UnstableResourcePayload() : type_(ResourceType_Instance)
-    {
-    }
-
-    UnstableResourcePayload(Orthanc::ResourceType type,
-                            const std::string& publicId) : 
-      type_(type),
-      publicId_(publicId)
-    {
-      time_ = boost::posix_time::second_clock::local_time();
-    }
-
-    unsigned int GetAge() const
-    {
-      return (boost::posix_time::second_clock::local_time() - time_).total_seconds();
-    }
-
-    ResourceType GetResourceType() const
-    {
-      return type_;
-    }
-    
-    const std::string& GetPublicId() const
-    {
-      return publicId_;
-    }
-  };
-
-
-  class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable
-  {
-  private:
-    class TagInfo
-    {
-    private:
-      ResourceType  level_;
-      DicomTagType  type_;
-
-    public:
-      TagInfo()
-      {
-      }
-
-      TagInfo(ResourceType level,
-              DicomTagType type) :
-        level_(level),
-        type_(type)
-      {
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      DicomTagType GetType() const
-      {
-        return type_;
-      }
-    };
-      
-    typedef std::map<DicomTag, TagInfo>   Registry;
-
-
-    Registry  registry_;
-      
-    void LoadTags(ResourceType level)
-    {
-      {
-        const DicomTag* tags = NULL;
-        size_t size;
-  
-        ServerToolbox::LoadIdentifiers(tags, size, level);
-  
-        for (size_t i = 0; i < size; i++)
-        {
-          if (registry_.find(tags[i]) == registry_.end())
-          {
-            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
-          }
-          else
-          {
-            // These patient-level tags are copied in the study level
-            assert(level == ResourceType_Study &&
-                   (tags[i] == DICOM_TAG_PATIENT_ID ||
-                    tags[i] == DICOM_TAG_PATIENT_NAME ||
-                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
-          }
-        }
-      }
-
-      {
-        std::set<DicomTag> tags;
-        DicomMap::GetMainDicomTags(tags, level);
-
-        for (std::set<DicomTag>::const_iterator
-               tag = tags.begin(); tag != tags.end(); ++tag)
-        {
-          if (registry_.find(*tag) == registry_.end())
-          {
-            registry_[*tag] = TagInfo(level, DicomTagType_Main);
-          }
-        }
-      }
-    }
-
-  public:
-    MainDicomTagsRegistry()
-    {
-      LoadTags(ResourceType_Patient);
-      LoadTags(ResourceType_Study);
-      LoadTags(ResourceType_Series);
-      LoadTags(ResourceType_Instance); 
-    }
-
-    void LookupTag(ResourceType& level,
-                   DicomTagType& type,
-                   const DicomTag& tag) const
-    {
-      Registry::const_iterator it = registry_.find(tag);
-
-      if (it == registry_.end())
-      {
-        // Default values
-        level = ResourceType_Instance;
-        type = DicomTagType_Generic;
-      }
-      else
-      {
-        level = it->second.GetLevel();
-        type = it->second.GetType();
-      }
-    }
-  };
-
-
-  bool ServerIndex::DeleteResource(Json::Value& target,
-                                   const std::string& uuid,
-                                   ResourceType expectedType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction t(*this);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, uuid) ||
-        expectedType != type)
-    {
-      return false;
-    }
-      
-    db_.DeleteResource(id);
-
-    if (listener_->HasRemainingLevel())
-    {
-      ResourceType type = listener_->GetRemainingType();
-      const std::string& uuid = listener_->GetRemainingPublicId();
-
-      target["RemainingAncestor"] = Json::Value(Json::objectValue);
-      target["RemainingAncestor"]["Path"] = GetBasePath(type, uuid);
-      target["RemainingAncestor"]["Type"] = EnumerationToString(type);
-      target["RemainingAncestor"]["ID"] = uuid;
-    }
-    else
-    {
-      target["RemainingAncestor"] = Json::nullValue;
-    }
-
-    t.Commit(0);
-
-    return true;
-  }
-
-
-  void ServerIndex::FlushThread(ServerIndex* that,
-                                unsigned int threadSleep)
-  {
-    // By default, wait for 10 seconds before flushing
-    unsigned int sleep = 10;
-
-    try
-    {
-      boost::mutex::scoped_lock lock(that->mutex_);
-      std::string sleepString;
-
-      if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) &&
-          Toolbox::IsInteger(sleepString))
-      {
-        sleep = boost::lexical_cast<unsigned int>(sleepString);
-      }
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-    }
-
-    LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")";
-
-    unsigned int count = 0;
-
-    while (!that->done_)
-    {
-      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
-      count++;
-      if (count < sleep)
-      {
-        continue;
-      }
-
-      Logging::Flush();
-
-      boost::mutex::scoped_lock lock(that->mutex_);
-
-      try
-      {
-        that->db_.FlushToDisk();
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
-      }
-          
-      count = 0;
-    }
-
-    LOG(INFO) << "Stopping the database flushing thread";
-  }
-
-
-  static bool ComputeExpectedNumberOfInstances(int64_t& target,
-                                               const DicomMap& dicomSummary)
-  {
-    try
-    {
-      const DicomValue* value;
-      const DicomValue* value2;
-          
-      if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary() &&
-          (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL &&
-          !value2->IsNull() &&
-          !value2->IsBinary())
-      {
-        // Patch for series with temporal positions thanks to Will Ryder
-        int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent());
-        int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
-        target = imagesInAcquisition * countTemporalPositions;
-        return (target > 0);
-      }
-
-      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL &&
-               !value->IsNull() &&
-               !value->IsBinary() &&
-               (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL &&
-               !value2->IsBinary() &&
-               !value2->IsNull())
-      {
-        // Support of Cardio-PET images
-        int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent());
-        int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
-        target = numberOfSlices * numberOfTimeSlices;
-        return (target > 0);
-      }
-
-      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL &&
-               !value->IsNull() &&
-               !value->IsBinary())
-      {
-        target = boost::lexical_cast<int64_t>(value->GetContent());
-        return (target > 0);
-      }
-    }
-    catch (OrthancException&)
-    {
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-    }
-
-    return false;
-  }
-
-
-
-
-  static bool LookupStringMetadata(std::string& result,
-                                   const std::map<MetadataType, std::string>& metadata,
-                                   MetadataType type)
-  {
-    std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
-
-    if (found == metadata.end())
-    {
-      return false;
-    }
-    else
-    {
-      result = found->second;
-      return true;
-    }
-  }
-
-
-  static bool LookupIntegerMetadata(int64_t& result,
-                                    const std::map<MetadataType, std::string>& metadata,
-                                    MetadataType type)
-  {
-    std::string s;
-    if (!LookupStringMetadata(s, metadata, type))
-    {
-      return false;
-    }
-
-    try
-    {
-      result = boost::lexical_cast<int64_t>(s);
-      return true;
-    }
-    catch (boost::bad_lexical_cast&)
-    {
-      return false;
-    }
-  }
-
-
-  void ServerIndex::LogChange(int64_t internalId,
-                              ChangeType changeType,
-                              ResourceType resourceType,
-                              const std::string& publicId)
-  {
-    ServerIndexChange change(changeType, resourceType, publicId);
-
-    if (changeType <= ChangeType_INTERNAL_LastLogged)
-    {
-      db_.LogChange(internalId, change);
-    }
-
-    assert(listener_.get() != NULL);
-    listener_->SignalChange(change);
-  }
-
-
-  uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property)
-  {
-    std::string oldValue;
-
-    if (db_.LookupGlobalProperty(oldValue, property))
-    {
-      uint64_t oldNumber;
-
-      try
-      {
-        oldNumber = boost::lexical_cast<uint64_t>(oldValue);
-        db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1));
-        return oldNumber + 1;
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-    else
-    {
-      // Initialize the sequence at "1"
-      db_.SetGlobalProperty(property, "1");
-      return 1;
-    }
-  }
-
-
-
-  ServerIndex::ServerIndex(ServerContext& context,
-                           IDatabaseWrapper& db,
-                           unsigned int threadSleep) : 
-    done_(false),
-    db_(db),
-    maximumStorageSize_(0),
-    maximumPatients_(0),
-    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
-  {
-    listener_.reset(new Listener(context));
-    db_.SetListener(*listener_);
-
-    // Initial recycling if the parameters have changed since the last
-    // execution of Orthanc
-    StandaloneRecycling();
-
-    if (db.HasFlushToDisk())
-    {
-      flushThread_ = boost::thread(FlushThread, this, threadSleep);
-    }
-
-    unstableResourcesMonitorThread_ = boost::thread
-      (UnstableResourcesMonitorThread, this, threadSleep);
-  }
-
-
-
-  ServerIndex::~ServerIndex()
-  {
-    if (!done_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: ServerIndex::Stop() should be invoked manually to avoid mess in the destruction order!";
-      Stop();
-    }
-  }
-
-
-
-  void ServerIndex::Stop()
-  {
-    if (!done_)
-    {
-      done_ = true;
-
-      if (db_.HasFlushToDisk() &&
-          flushThread_.joinable())
-      {
-        flushThread_.join();
-      }
-
-      if (unstableResourcesMonitorThread_.joinable())
-      {
-        unstableResourcesMonitorThread_.join();
-      }
-    }
-  }
-
-
-  static void SetInstanceMetadata(ResourcesContent& content,
-                                  std::map<MetadataType, std::string>& instanceMetadata,
-                                  int64_t instance,
-                                  MetadataType metadata,
-                                  const std::string& value)
-  {
-    content.AddMetadata(instance, metadata, value);
-    instanceMetadata[metadata] = value;
-  }
-
-
-  void ServerIndex::SignalNewResource(ChangeType changeType,
-                                      ResourceType level,
-                                      const std::string& publicId,
-                                      int64_t internalId)
-  {
-    ServerIndexChange change(changeType, level, publicId);
-    db_.LogChange(internalId, change);
-    
-    assert(listener_.get() != NULL);
-    listener_->SignalChange(change);
-  }
-
-  
-  StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
-                                 DicomInstanceToStore& instanceToStore,
-                                 const Attachments& attachments,
-                                 bool overwrite)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    const DicomMap& dicomSummary = instanceToStore.GetSummary();
-    const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata();
-
-    int64_t expectedInstances;
-    const bool hasExpectedInstances =
-      ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary);
-    
-    instanceMetadata.clear();
-
-    const std::string hashPatient = instanceToStore.GetHasher().HashPatient();
-    const std::string hashStudy = instanceToStore.GetHasher().HashStudy();
-    const std::string hashSeries = instanceToStore.GetHasher().HashSeries();
-    const std::string hashInstance = instanceToStore.GetHasher().HashInstance();
-
-    try
-    {
-      Transaction t(*this);
-
-      IDatabaseWrapper::CreateInstanceResult status;
-      int64_t instanceId;
-
-      // Check whether this instance is already stored
-      if (!db_.CreateInstance(status, instanceId, hashPatient,
-                              hashStudy, hashSeries, hashInstance))
-      {
-        // The instance already exists
-        
-        if (overwrite)
-        {
-          // Overwrite the old instance
-          LOG(INFO) << "Overwriting instance: " << hashInstance;
-          db_.DeleteResource(instanceId);
-
-          // Re-create the instance, now that the old one is removed
-          if (!db_.CreateInstance(status, instanceId, hashPatient,
-                                  hashStudy, hashSeries, hashInstance))
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        else
-        {
-          // Do nothing if the instance already exists and overwriting is disabled
-          db_.GetAllMetadata(instanceMetadata, instanceId);
-          return StoreStatus_AlreadyStored;
-        }
-      }
-
-
-      // Warn about the creation of new resources. The order must be
-      // from instance to patient.
-
-      // NB: In theory, could be sped up by grouping the underlying
-      // calls to "db_.LogChange()". However, this would only have an
-      // impact when new patient/study/series get created, which
-      // occurs far less often that creating new instances. The
-      // positive impact looks marginal in practice.
-      SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId);
-
-      if (status.isNewSeries_)
-      {
-        SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_);
-      }
-      
-      if (status.isNewStudy_)
-      {
-        SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_);
-      }
-      
-      if (status.isNewPatient_)
-      {
-        SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_);
-      }
-      
-      
-      // Ensure there is enough room in the storage for the new instance
-      uint64_t instanceSize = 0;
-      for (Attachments::const_iterator it = attachments.begin();
-           it != attachments.end(); ++it)
-      {
-        instanceSize += it->GetCompressedSize();
-      }
-
-      Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */);
-      
-     
-      // Attach the files to the newly created instance
-      for (Attachments::const_iterator it = attachments.begin();
-           it != attachments.end(); ++it)
-      {
-        db_.AddAttachment(instanceId, *it);
-      }
-
-      
-      {
-        ResourcesContent content;
-      
-        // Populate the tags of the newly-created resources
-
-        content.AddResource(instanceId, ResourceType_Instance, dicomSummary);
-
-        if (status.isNewSeries_)
-        {
-          content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary);
-        }
-
-        if (status.isNewStudy_)
-        {
-          content.AddResource(status.studyId_, ResourceType_Study, dicomSummary);
-        }
-
-        if (status.isNewPatient_)
-        {
-          content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary);
-        }
-
-
-        // Attach the user-specified metadata
-
-        for (MetadataMap::const_iterator 
-               it = metadata.begin(); it != metadata.end(); ++it)
-        {
-          switch (it->first.first)
-          {
-            case ResourceType_Patient:
-              content.AddMetadata(status.patientId_, it->first.second, it->second);
-              break;
-
-            case ResourceType_Study:
-              content.AddMetadata(status.studyId_, it->first.second, it->second);
-              break;
-
-            case ResourceType_Series:
-              content.AddMetadata(status.seriesId_, it->first.second, it->second);
-              break;
-
-            case ResourceType_Instance:
-              SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                  it->first.second, it->second);
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_ParameterOutOfRange);
-          }
-        }
-
-        
-        // Attach the auto-computed metadata for the patient/study/series levels
-        std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
-        content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
-        content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
-        content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
-
-        if (status.isNewSeries_ &&
-            hasExpectedInstances)
-        {
-          content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
-                              boost::lexical_cast<std::string>(expectedInstances));
-        }
-
-        
-        // Attach the auto-computed metadata for the instance level,
-        // reflecting these additions into the input metadata map
-        SetInstanceMetadata(content, instanceMetadata, instanceId,
-                            MetadataType_Instance_ReceptionDate, now);
-        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteAet,
-                            instanceToStore.GetOrigin().GetRemoteAetC());
-        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, 
-                            EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
-
-
-        {
-          std::string s;
-
-          if (instanceToStore.LookupTransferSyntax(s))
-          {
-            // New in Orthanc 1.2.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_TransferSyntax, s);
-          }
-
-          if (instanceToStore.GetOrigin().LookupRemoteIp(s))
-          {
-            // New in Orthanc 1.4.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_RemoteIp, s);
-          }
-
-          if (instanceToStore.GetOrigin().LookupCalledAet(s))
-          {
-            // New in Orthanc 1.4.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_CalledAet, s);
-          }
-
-          if (instanceToStore.GetOrigin().LookupHttpUsername(s))
-          {
-            // New in Orthanc 1.4.0
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_HttpUsername, s);
-          }
-        }
-
-        
-        const DicomValue* value;
-        if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          SetInstanceMetadata(content, instanceMetadata, instanceId,
-                              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())
-          {
-            SetInstanceMetadata(content, instanceMetadata, instanceId,
-                                MetadataType_Instance_IndexInSeries, value->GetContent());
-          }
-        }
-
-        
-        db_.SetResourcesContent(content);
-      }
-
-  
-      // Check whether the series of this new instance is now completed
-      int64_t expectedNumberOfInstances;
-      if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary))
-      {
-        SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
-        if (seriesStatus == SeriesStatus_Complete)
-        {
-          LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
-        }
-      }
-      
-
-      // Mark the parent resources of this instance as unstable
-      MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries);
-      MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy);
-      MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient);
-
-      t.Commit(instanceSize);
-
-      return StoreStatus_Success;
-    }
-    catch (OrthancException& e)
-    {
-      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
-    }
-
-    return StoreStatus_Failure;
-  }
-
-
-  void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize,
-                                        /* out */ uint64_t& uncompressedSize,
-                                        /* out */ uint64_t& countPatients, 
-                                        /* out */ uint64_t& countStudies, 
-                                        /* out */ uint64_t& countSeries, 
-                                        /* out */ uint64_t& countInstances)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    diskSize = db_.GetTotalCompressedSize();
-    uncompressedSize = db_.GetTotalUncompressedSize();
-    countPatients = db_.GetResourceCount(ResourceType_Patient);
-    countStudies = db_.GetResourceCount(ResourceType_Study);
-    countSeries = db_.GetResourceCount(ResourceType_Series);
-    countInstances = db_.GetResourceCount(ResourceType_Instance);
-  }
-
-  
-  SeriesStatus ServerIndex::GetSeriesStatus(int64_t id,
-                                            int64_t expectedNumberOfInstances)
-  {
-    std::list<std::string> values;
-    db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
-
-    std::set<int64_t> instances;
-
-    for (std::list<std::string>::const_iterator
-           it = values.begin(); it != values.end(); ++it)
-    {
-      int64_t index;
-
-      try
-      {
-        index = boost::lexical_cast<int64_t>(*it);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return SeriesStatus_Unknown;
-      }
-      
-      if (!(index > 0 && index <= expectedNumberOfInstances))
-      {
-        // Out-of-range instance index
-        return SeriesStatus_Inconsistent;
-      }
-
-      if (instances.find(index) != instances.end())
-      {
-        // Twice the same instance index
-        return SeriesStatus_Inconsistent;
-      }
-
-      instances.insert(index);
-    }
-
-    if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances)
-    {
-      return SeriesStatus_Complete;
-    }
-    else
-    {
-      return SeriesStatus_Missing;
-    }
-  }
-
-
-  void ServerIndex::MainDicomTagsToJson(Json::Value& target,
-                                        int64_t resourceId,
-                                        ResourceType resourceType)
-  {
-    DicomMap tags;
-    db_.GetMainDicomTags(tags, resourceId);
-
-    if (resourceType == ResourceType_Study)
-    {
-      DicomMap t1, t2;
-      tags.ExtractStudyInformation(t1);
-      tags.ExtractPatientInformation(t2);
-
-      target["MainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true);
-
-      target["PatientMainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true);
-    }
-    else
-    {
-      target["MainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true);
-    }
-  }
-
-  
-  bool ServerIndex::LookupResource(Json::Value& result,
-                                   const std::string& publicId,
-                                   ResourceType expectedType)
-  {
-    result = Json::objectValue;
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    std::string parent;
-    if (!db_.LookupResourceAndParent(id, type, parent, publicId) ||
-        type != expectedType)
-    {
-      return false;
-    }
-
-    // Set information about the parent resource (if it exists)
-    if (type == ResourceType_Patient)
-    {
-      if (!parent.empty())
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-    else
-    {
-      if (parent.empty())
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-
-      switch (type)
-      {
-        case ResourceType_Study:
-          result["ParentPatient"] = parent;
-          break;
-
-        case ResourceType_Series:
-          result["ParentStudy"] = parent;
-          break;
-
-        case ResourceType_Instance:
-          result["ParentSeries"] = parent;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    // List the children resources
-    std::list<std::string> children;
-    db_.GetChildrenPublicId(children, id);
-
-    if (type != ResourceType_Instance)
-    {
-      Json::Value c = Json::arrayValue;
-
-      for (std::list<std::string>::const_iterator
-             it = children.begin(); it != children.end(); ++it)
-      {
-        c.append(*it);
-      }
-
-      switch (type)
-      {
-        case ResourceType_Patient:
-          result["Studies"] = c;
-          break;
-
-        case ResourceType_Study:
-          result["Series"] = c;
-          break;
-
-        case ResourceType_Series:
-          result["Instances"] = c;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-
-    // Extract the metadata
-    std::map<MetadataType, std::string> metadata;
-    db_.GetAllMetadata(metadata, id);
-
-    // Set the resource type
-    switch (type)
-    {
-      case ResourceType_Patient:
-        result["Type"] = "Patient";
-        break;
-
-      case ResourceType_Study:
-        result["Type"] = "Study";
-        break;
-
-      case ResourceType_Series:
-      {
-        result["Type"] = "Series";
-
-        int64_t i;
-        if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
-        {
-          result["ExpectedNumberOfInstances"] = static_cast<int>(i);
-          result["Status"] = EnumerationToString(GetSeriesStatus(id, i));
-        }
-        else
-        {
-          result["ExpectedNumberOfInstances"] = Json::nullValue;
-          result["Status"] = EnumerationToString(SeriesStatus_Unknown);
-        }
-
-        break;
-      }
-
-      case ResourceType_Instance:
-      {
-        result["Type"] = "Instance";
-
-        FileInfo attachment;
-        if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
-        result["FileUuid"] = attachment.GetUuid();
-
-        int64_t i;
-        if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
-        {
-          result["IndexInSeries"] = static_cast<int>(i);
-        }
-        else
-        {
-          result["IndexInSeries"] = Json::nullValue;
-        }
-
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Record the remaining information
-    result["ID"] = publicId;
-    MainDicomTagsToJson(result, id, type);
-
-    std::string tmp;
-
-    if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
-    {
-      result["AnonymizedFrom"] = tmp;
-    }
-
-    if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
-    {
-      result["ModifiedFrom"] = tmp;
-    }
-
-    if (type == ResourceType_Patient ||
-        type == ResourceType_Study ||
-        type == ResourceType_Series)
-    {
-      result["IsStable"] = !unstableResources_.Contains(id);
-
-      if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
-      {
-        result["LastUpdate"] = tmp;
-      }
-    }
-
-    return true;
-  }
-
-
-  bool ServerIndex::LookupAttachment(FileInfo& attachment,
-                                     const std::string& instanceUuid,
-                                     FileContentType contentType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, instanceUuid))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (db_.LookupAttachment(attachment, id, contentType))
-    {
-      assert(attachment.GetContentType() == contentType);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-
-  void ServerIndex::GetAllUuids(std::list<std::string>& target,
-                                ResourceType resourceType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    db_.GetAllPublicIds(target, resourceType);
-  }
-
-
-  void ServerIndex::GetAllUuids(std::list<std::string>& target,
-                                ResourceType resourceType,
-                                size_t since,
-                                size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-      return;
-    }
-
-    boost::mutex::scoped_lock lock(mutex_);
-    db_.GetAllPublicIds(target, resourceType, since, limit);
-  }
-
-
-  template <typename T>
-  static void FormatLog(Json::Value& target,
-                        const std::list<T>& log,
-                        const std::string& name,
-                        bool done,
-                        int64_t since,
-                        bool hasLast,
-                        int64_t last)
-  {
-    Json::Value items = Json::arrayValue;
-    for (typename std::list<T>::const_iterator
-           it = log.begin(); it != log.end(); ++it)
-    {
-      Json::Value item;
-      it->Format(item);
-      items.append(item);
-    }
-
-    target = Json::objectValue;
-    target[name] = items;
-    target["Done"] = done;
-
-    if (!hasLast)
-    {
-      // Best-effort guess of the last index in the sequence
-      if (log.empty())
-      {
-        last = since;
-      }
-      else
-      {
-        last = log.back().GetSeq();
-      }
-    }
-    
-    target["Last"] = static_cast<int>(last);
-  }
-
-
-  void ServerIndex::GetChanges(Json::Value& target,
-                               int64_t since,                               
-                               unsigned int maxResults)
-  {
-    std::list<ServerIndexChange> changes;
-    bool done;
-    bool hasLast = false;
-    int64_t last = 0;
-
-    {
-      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);
-      if (changes.empty())
-      {
-        last = db_.GetLastChangeIndex();
-        hasLast = true;
-      }
-      
-      transaction.Commit(0);
-    }
-
-    FormatLog(target, changes, "Changes", done, since, hasLast, last);
-  }
-
-
-  void ServerIndex::GetLastChange(Json::Value& target)
-  {
-    std::list<ServerIndexChange> changes;
-    bool hasLast = false;
-    int64_t last = 0;
-
-    {
-      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);
-      if (changes.empty())
-      {
-        last = db_.GetLastChangeIndex();
-        hasLast = true;
-      }
-
-      transaction.Commit(0);
-    }
-
-    FormatLog(target, changes, "Changes", true, 0, hasLast, last);
-  }
-
-
-  void ServerIndex::LogExportedResource(const std::string& publicId,
-                                        const std::string& remoteModality)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_InexistentItem);
-    }
-
-    std::string patientId;
-    std::string studyInstanceUid;
-    std::string seriesInstanceUid;
-    std::string sopInstanceUid;
-
-    int64_t currentId = id;
-    ResourceType currentType = type;
-
-    // Iteratively go up inside the patient/study/series/instance hierarchy
-    bool done = false;
-    while (!done)
-    {
-      DicomMap map;
-      db_.GetMainDicomTags(map, currentId);
-
-      switch (currentType)
-      {
-        case ResourceType_Patient:
-          if (map.HasTag(DICOM_TAG_PATIENT_ID))
-          {
-            patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
-          }
-          done = true;
-          break;
-
-        case ResourceType_Study:
-          if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
-          {
-            studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
-          }
-          currentType = ResourceType_Patient;
-          break;
-
-        case ResourceType_Series:
-          if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
-          {
-            seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
-          }
-          currentType = ResourceType_Study;
-          break;
-
-        case ResourceType_Instance:
-          if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
-          {
-            sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
-          }
-          currentType = ResourceType_Series;
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      // If we have not reached the Patient level, find the parent of
-      // the current resource
-      if (!done)
-      {
-        bool ok = db_.LookupParent(currentId, currentId);
-        assert(ok);
-      }
-    }
-
-    ExportedResource resource(-1, 
-                              type,
-                              publicId,
-                              remoteModality,
-                              SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
-                              patientId,
-                              studyInstanceUid,
-                              seriesInstanceUid,
-                              sopInstanceUid);
-
-    db_.LogExportedResource(resource);
-    transaction.Commit(0);
-  }
-
-
-  void ServerIndex::GetExportedResources(Json::Value& target,
-                                         int64_t since,
-                                         unsigned int maxResults)
-  {
-    std::list<ExportedResource> exported;
-    bool done;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      db_.GetExportedResources(exported, done, since, maxResults);
-    }
-
-    FormatLog(target, exported, "Exports", done, since, false, -1);
-  }
-
-
-  void ServerIndex::GetLastExportedResource(Json::Value& target)
-  {
-    std::list<ExportedResource> exported;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      db_.GetLastExportedResource(exported);
-    }
-
-    FormatLog(target, exported, "Exports", true, 0, false, -1);
-  }
-
-
-  bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize)
-  {
-    if (maximumStorageSize_ != 0)
-    {
-      assert(maximumStorageSize_ >= instanceSize);
-      
-      if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize))
-      {
-        return true;
-      }
-    }
-
-    if (maximumPatients_ != 0)
-    {
-      uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient);
-      if (patientCount > maximumPatients_)
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  
-  void ServerIndex::Recycle(uint64_t instanceSize,
-                            const std::string& newPatientId)
-  {
-    if (!IsRecyclingNeeded(instanceSize))
-    {
-      return;
-    }
-
-    // Check whether other DICOM instances from this patient are
-    // already stored
-    int64_t patientToAvoid;
-    ResourceType type;
-    bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
-
-    if (hasPatientToAvoid && type != ResourceType_Patient)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    // Iteratively select patient to remove until there is enough
-    // space in the DICOM store
-    int64_t patientToRecycle;
-    while (true)
-    {
-      // If other instances of this patient are already in the store,
-      // we must avoid to recycle them
-      bool ok = hasPatientToAvoid ?
-        db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
-        db_.SelectPatientToRecycle(patientToRecycle);
-        
-      if (!ok)
-      {
-        throw OrthancException(ErrorCode_FullStorage);
-      }
-      
-      VLOG(1) << "Recycling one patient";
-      db_.DeleteResource(patientToRecycle);
-
-      if (!IsRecyclingNeeded(instanceSize))
-      {
-        // OK, we're done
-        break;
-      }
-    }
-  }  
-
-  void ServerIndex::SetMaximumPatientCount(unsigned int count) 
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    maximumPatients_ = count;
-
-    if (count == 0)
-    {
-      LOG(WARNING) << "No limit on the number of stored patients";
-    }
-    else
-    {
-      LOG(WARNING) << "At most " << count << " patients will be stored";
-    }
-
-    StandaloneRecycling();
-  }
-
-  void ServerIndex::SetMaximumStorageSize(uint64_t size) 
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    maximumStorageSize_ = size;
-
-    if (size == 0)
-    {
-      LOG(WARNING) << "No limit on the size of the storage area";
-    }
-    else
-    {
-      LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
-    }
-
-    StandaloneRecycling();
-  }
-
-
-  void ServerIndex::StandaloneRecycling()
-  {
-    // WARNING: No mutex here, do not include this as a public method
-    Transaction t(*this);
-    Recycle(0, "");
-    t.Commit(0);
-  }
-
-
-  bool ServerIndex::IsProtectedPatient(const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
-        type != ResourceType_Patient)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    return db_.IsProtectedPatient(id);
-  }
-     
-
-  void ServerIndex::SetProtectedPatient(const std::string& publicId,
-                                        bool isProtected)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
-        type != ResourceType_Patient)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    db_.SetProtectedPatient(id, isProtected);
-    transaction.Commit(0);
-
-    if (isProtected)
-      LOG(INFO) << "Patient " << publicId << " has been protected";
-    else
-      LOG(INFO) << "Patient " << publicId << " has been unprotected";
-  }
-
-
-  void ServerIndex::GetChildren(std::list<std::string>& result,
-                                const std::string& publicId)
-  {
-    result.clear();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t resource;
-    if (!db_.LookupResource(resource, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (type == ResourceType_Instance)
-    {
-      // An instance cannot have a child
-      throw OrthancException(ErrorCode_BadParameterType);
-    }
-
-    std::list<int64_t> tmp;
-    db_.GetChildrenInternalId(tmp, resource);
-
-    for (std::list<int64_t>::const_iterator 
-           it = tmp.begin(); it != tmp.end(); ++it)
-    {
-      result.push_back(db_.GetPublicId(*it));
-    }
-  }
-
-
-  void ServerIndex::GetChildInstances(std::list<std::string>& result,
-                                      const std::string& publicId)
-  {
-    result.clear();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t top;
-    if (!db_.LookupResource(top, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    if (type == ResourceType_Instance)
-    {
-      // The resource is already an instance: Do not go down the hierarchy
-      result.push_back(publicId);
-      return;
-    }
-
-    std::stack<int64_t> toExplore;
-    toExplore.push(top);
-
-    std::list<int64_t> tmp;
-
-    while (!toExplore.empty())
-    {
-      // Get the internal ID of the current resource
-      int64_t resource = toExplore.top();
-      toExplore.pop();
-
-      if (db_.GetResourceType(resource) == ResourceType_Instance)
-      {
-        result.push_back(db_.GetPublicId(resource));
-      }
-      else
-      {
-        // Tag all the children of this resource as to be explored
-        db_.GetChildrenInternalId(tmp, resource);
-        for (std::list<int64_t>::const_iterator 
-               it = tmp.begin(); it != tmp.end(); ++it)
-        {
-          toExplore.push(*it);
-        }
-      }
-    }
-  }
-
-
-  void ServerIndex::SetMetadata(const std::string& publicId,
-                                MetadataType type,
-                                const std::string& value)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction t(*this);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.SetMetadata(id, type, value);
-
-    if (IsUserMetadata(type))
-    {
-      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
-    }
-
-    t.Commit(0);
-  }
-
-
-  void ServerIndex::DeleteMetadata(const std::string& publicId,
-                                   MetadataType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction t(*this);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.DeleteMetadata(id, type);
-
-    if (IsUserMetadata(type))
-    {
-      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
-    }
-
-    t.Commit(0);
-  }
-
-
-  bool ServerIndex::LookupMetadata(std::string& target,
-                                   const std::string& publicId,
-                                   MetadataType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    return db_.LookupMetadata(target, id, type);
-  }
-
-
-  void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                   const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    return db_.GetAllMetadata(target, id);
-  }
-
-
-  void ServerIndex::ListAvailableAttachments(std::list<FileContentType>& target,
-                                             const std::string& publicId,
-                                             ResourceType expectedType)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId) ||
-        expectedType != type)
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.ListAvailableAttachments(target, id);
-  }
-
-
-  bool ServerIndex::LookupParent(std::string& target,
-                                 const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    ResourceType type;
-    int64_t id;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    int64_t parentId;
-    if (db_.LookupParent(parentId, id))
-    {
-      target = db_.GetPublicId(parentId);
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-
-  uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    uint64_t seq = IncrementGlobalSequenceInternal(sequence);
-    transaction.Commit(0);
-
-    return seq;
-  }
-
-
-
-  void ServerIndex::LogChange(ChangeType changeType,
-                              const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction transaction(*this);
-
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    LogChange(id, changeType, type, publicId);
-    transaction.Commit(0);
-  }
-
-
-  void ServerIndex::DeleteChanges()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction transaction(*this);
-    db_.ClearChanges();
-    transaction.Commit(0);
-  }
-
-  void ServerIndex::DeleteExportedResources()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction transaction(*this);
-    db_.ClearExportedResources();
-    transaction.Commit(0);
-  }
-
-
-  void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type,
-                                          /* out */ uint64_t& diskSize, 
-                                          /* out */ uint64_t& uncompressedSize, 
-                                          /* out */ unsigned int& countStudies, 
-                                          /* out */ unsigned int& countSeries, 
-                                          /* out */ unsigned int& countInstances, 
-                                          /* out */ uint64_t& dicomDiskSize, 
-                                          /* out */ uint64_t& dicomUncompressedSize, 
-                                          const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t top;
-    if (!db_.LookupResource(top, type, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    std::stack<int64_t> toExplore;
-    toExplore.push(top);
-
-    countInstances = 0;
-    countSeries = 0;
-    countStudies = 0;
-    diskSize = 0;
-    uncompressedSize = 0;
-    dicomDiskSize = 0;
-    dicomUncompressedSize = 0;
-
-    while (!toExplore.empty())
-    {
-      // Get the internal ID of the current resource
-      int64_t resource = toExplore.top();
-      toExplore.pop();
-
-      ResourceType thisType = db_.GetResourceType(resource);
-
-      std::list<FileContentType> f;
-      db_.ListAvailableAttachments(f, resource);
-
-      for (std::list<FileContentType>::const_iterator
-             it = f.begin(); it != f.end(); ++it)
-      {
-        FileInfo attachment;
-        if (db_.LookupAttachment(attachment, resource, *it))
-        {
-          if (attachment.GetContentType() == FileContentType_Dicom)
-          {
-            dicomDiskSize += attachment.GetCompressedSize();
-            dicomUncompressedSize += attachment.GetUncompressedSize();
-          }
-          
-          diskSize += attachment.GetCompressedSize();
-          uncompressedSize += attachment.GetUncompressedSize();
-        }
-      }
-
-      if (thisType == ResourceType_Instance)
-      {
-        countInstances++;
-      }
-      else
-      {
-        switch (thisType)
-        {
-          case ResourceType_Study:
-            countStudies++;
-            break;
-
-          case ResourceType_Series:
-            countSeries++;
-            break;
-
-          default:
-            break;
-        }
-
-        // Tag all the children of this resource as to be explored
-        std::list<int64_t> tmp;
-        db_.GetChildrenInternalId(tmp, resource);
-        for (std::list<int64_t>::const_iterator 
-               it = tmp.begin(); it != tmp.end(); ++it)
-        {
-          toExplore.push(*it);
-        }
-      }
-    }
-
-    if (countStudies == 0)
-    {
-      countStudies = 1;
-    }
-
-    if (countSeries == 0)
-    {
-      countSeries = 1;
-    }
-  }
-
-
-  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
-                                                   unsigned int threadSleep)
-  {
-    int stableAge;
-    
-    {
-      OrthancConfiguration::ReaderLock lock;
-      stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60);
-    }
-
-    if (stableAge <= 0)
-    {
-      stableAge = 60;
-    }
-
-    LOG(INFO) << "Starting the monitor for stable resources (stable age = " << stableAge << ")";
-
-    while (!that->done_)
-    {
-      // Check for stable resources each few seconds
-      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
-
-      boost::mutex::scoped_lock lock(that->mutex_);
-
-      while (!that->unstableResources_.IsEmpty() &&
-             that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
-      {
-        // This DICOM resource has not received any new instance for
-        // some time. It can be considered as stable.
-          
-        UnstableResourcePayload payload;
-        int64_t id = that->unstableResources_.RemoveOldest(payload);
-
-        // Ensure that the resource is still existing before logging the change
-        if (that->db_.IsExistingResource(id))
-        {
-          switch (payload.GetResourceType())
-          {
-            case ResourceType_Patient:
-              that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId());
-              break;
-
-            case ResourceType_Study:
-              that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId());
-              break;
-
-            case ResourceType_Series:
-              that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId());
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-
-          //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id;
-        }
-      }
-    }
-
-    LOG(INFO) << "Closing the monitor thread for stable resources";
-  }
-  
-
-  void ServerIndex::MarkAsUnstable(int64_t id,
-                                   Orthanc::ResourceType type,
-                                   const std::string& publicId)
-  {
-    // WARNING: Before calling this method, "mutex_" must be locked.
-
-    assert(type == Orthanc::ResourceType_Patient ||
-           type == Orthanc::ResourceType_Study ||
-           type == Orthanc::ResourceType_Series);
-
-    UnstableResourcePayload payload(type, publicId);
-    unstableResources_.AddOrMakeMostRecent(id, payload);
-    //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
-
-    LogChange(id, ChangeType_NewChildInstance, type, publicId);
-  }
-
-
-
-  void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result,
-                                          ResourceType level,
-                                          const DicomTag& tag,
-                                          const std::string& value)
-  {
-    assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
-           (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
-           (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
-           (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
-           (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
-    
-    result.clear();
-
-    DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
-
-    std::vector<DatabaseConstraint> query;
-    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-
-    std::list<std::string> tmp;
-    
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      db_.ApplyLookupResources(tmp, NULL, query, level, 0);
-    }
-
-    CopyListToVector(result, tmp);
-  }
-
-
-  StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
-                                         const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction t(*this);
-
-    ResourceType resourceType;
-    int64_t resourceId;
-    if (!db_.LookupResource(resourceId, resourceType, publicId))
-    {
-      return StoreStatus_Failure;  // Inexistent resource
-    }
-
-    // Remove possible previous attachment
-    db_.DeleteAttachment(resourceId, attachment.GetContentType());
-
-    // Locate the patient of the target resource
-    int64_t patientId = resourceId;
-    for (;;)
-    {
-      int64_t parent;
-      if (db_.LookupParent(parent, patientId))
-      {
-        // We have not reached the patient level yet
-        patientId = parent;
-      }
-      else
-      {
-        // We have reached the patient level
-        break;
-      }
-    }
-
-    // Possibly apply the recycling mechanism while preserving this patient
-    assert(db_.GetResourceType(patientId) == ResourceType_Patient);
-    Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId));
-
-    db_.AddAttachment(resourceId, attachment);
-
-    if (IsUserContentType(attachment.GetContentType()))
-    {
-      LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId);
-    }
-
-    t.Commit(attachment.GetCompressedSize());
-
-    return StoreStatus_Success;
-  }
-
-
-  void ServerIndex::DeleteAttachment(const std::string& publicId,
-                                     FileContentType type)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    Transaction t(*this);
-
-    ResourceType rtype;
-    int64_t id;
-    if (!db_.LookupResource(id, rtype, publicId))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    db_.DeleteAttachment(id, type);
-
-    if (IsUserContentType(type))
-    {
-      LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId);
-    }
-
-    t.Commit(0);
-  }
-
-
-  void ServerIndex::SetGlobalProperty(GlobalProperty property,
-                                      const std::string& value)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Transaction transaction(*this);
-    db_.SetGlobalProperty(property, value);
-    transaction.Commit(0);
-  }
-
-
-  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)
-  {
-    std::string value;
-
-    if (LookupGlobalProperty(value, property))
-    {
-      return value;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  bool ServerIndex::GetMainDicomTags(DicomMap& result,
-                                     const std::string& publicId,
-                                     ResourceType expectedType,
-                                     ResourceType levelOfInterest)
-  {
-    // Yes, the following test could be shortened, but we wish to make it as clear as possible
-    if (!(expectedType == ResourceType_Patient  && levelOfInterest == ResourceType_Patient) &&
-        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Patient) &&
-        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Study)   &&
-        !(expectedType == ResourceType_Series   && levelOfInterest == ResourceType_Series)  &&
-        !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance))
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    result.Clear();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t id;
-    ResourceType type;
-    if (!db_.LookupResource(id, type, publicId) ||
-        type != expectedType)
-    {
-      return false;
-    }
-
-    if (type == ResourceType_Study)
-    {
-      DicomMap tmp;
-      db_.GetMainDicomTags(tmp, id);
-
-      switch (levelOfInterest)
-      {
-        case ResourceType_Patient:
-          tmp.ExtractPatientInformation(result);
-          return true;
-
-        case ResourceType_Study:
-          tmp.ExtractStudyInformation(result);
-          return true;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-    else
-    {
-      db_.GetMainDicomTags(result, id);
-      return true;
-    }    
-  }
-
-
-  bool ServerIndex::GetAllMainDicomTags(DicomMap& result,
-                                        const std::string& instancePublicId)
-  {
-    result.Clear();
-    
-    boost::mutex::scoped_lock lock(mutex_);
-
-    // Lookup for the requested resource
-    int64_t instance;
-    ResourceType type;
-    if (!db_.LookupResource(instance, type, instancePublicId) ||
-        type != ResourceType_Instance)
-    {
-      return false;
-    }
-    else
-    {
-      DicomMap tmp;
-
-      db_.GetMainDicomTags(tmp, instance);
-      result.Merge(tmp);
-
-      int64_t series;
-      if (!db_.LookupParent(series, instance))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      tmp.Clear();
-      db_.GetMainDicomTags(tmp, series);
-      result.Merge(tmp);
-
-      int64_t study;
-      if (!db_.LookupParent(study, series))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      tmp.Clear();
-      db_.GetMainDicomTags(tmp, study);
-      result.Merge(tmp);
-
-#ifndef NDEBUG
-      {
-        // Sanity test to check that all the main DICOM tags from the
-        // patient level are copied at the study level
-        
-        int64_t patient;
-        if (!db_.LookupParent(patient, study))
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        tmp.Clear();
-        db_.GetMainDicomTags(tmp, study);
-
-        std::set<DicomTag> patientTags;
-        tmp.GetTags(patientTags);
-
-        for (std::set<DicomTag>::const_iterator
-               it = patientTags.begin(); it != patientTags.end(); ++it)
-        {
-          assert(result.HasTag(*it));
-        }
-      }
-#endif
-      
-      return true;
-    }
-  }
-
-
-  bool ServerIndex::LookupResourceType(ResourceType& type,
-                                       const std::string& publicId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    int64_t id;
-    return db_.LookupResource(id, type, publicId);
-  }
-
-
-  unsigned int ServerIndex::GetDatabaseVersion()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    return db_.GetDatabaseVersion();
-  }
-
-
-  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);
-
-      {
-        ResourcesContent content;
-        content.AddResource(patient, ResourceType_Patient, summary);
-        content.AddResource(study, ResourceType_Study, summary);
-        content.AddResource(series, ResourceType_Series, summary);
-        content.AddResource(instance, ResourceType_Instance, summary);
-        db_.SetResourcesContent(content);
-      }
-
-      {
-        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() << "]";
-    }
-  }
-
-
-  void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                                    const DatabaseLookup& source,
-                                    ResourceType queryLevel) const
-  {
-    assert(mainDicomTagsRegistry_.get() != NULL);
-
-    target.clear();
-    target.reserve(source.GetConstraintsCount());
-
-    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
-    {
-      ResourceType level;
-      DicomTagType type;
-      
-      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
-
-      if (type == DicomTagType_Identifier ||
-          type == DicomTagType_Main)
-      {
-        // Use the fact that patient-level tags are copied at the study level
-        if (level == ResourceType_Patient &&
-            queryLevel != ResourceType_Patient)
-        {
-          level = ResourceType_Study;
-        }
-        
-        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
-      }
-    }
-  }
-
-
-  void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId,
-                                         std::vector<std::string>* instancesId,
-                                         const DatabaseLookup& lookup,
-                                         ResourceType queryLevel,
-                                         size_t limit)
-  {
-    std::vector<DatabaseConstraint> normalized;
-    NormalizeLookup(normalized, lookup, queryLevel);
-
-    std::list<std::string> resourcesList, instancesList;
-    
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      if (instancesId == NULL)
-      {
-        db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit);
-      }
-      else
-      {
-        db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit);
-      }
-    }
-
-    CopyListToVector(resourcesId, resourcesList);
-
-    if (instancesId != NULL)
-    { 
-      CopyListToVector(*instancesId, instancesList);
-    }
-  }
-}
--- a/OrthancServer/ServerIndex.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Cache/LeastRecentlyUsedIndex.h"
-#include "../Core/DicomFormat/DicomMap.h"
-
-#include "Database/IDatabaseWrapper.h"
-
-#include <boost/thread.hpp>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class DatabaseLookup;
-  class DicomInstanceToStore;
-  class ParsedDicomFile;
-  class ServerContext;
-
-  class ServerIndex : public boost::noncopyable
-  {
-  public:
-    typedef std::list<FileInfo> Attachments;
-    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
-
-  private:
-    class Listener;
-    class Transaction;
-    class UnstableResourcePayload;
-    class MainDicomTagsRegistry;
-
-    bool done_;
-    boost::mutex mutex_;
-    boost::thread flushThread_;
-    boost::thread unstableResourcesMonitorThread_;
-
-    std::unique_ptr<Listener> listener_;
-    IDatabaseWrapper& db_;
-    LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
-
-    uint64_t     maximumStorageSize_;
-    unsigned int maximumPatients_;
-    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
-
-    static void FlushThread(ServerIndex* that,
-                            unsigned int threadSleep);
-
-    static void UnstableResourcesMonitorThread(ServerIndex* that,
-                                               unsigned int threadSleep);
-
-    void MainDicomTagsToJson(Json::Value& result,
-                             int64_t resourceId,
-                             ResourceType resourceType);
-
-    bool IsRecyclingNeeded(uint64_t instanceSize);
-
-    void Recycle(uint64_t instanceSize,
-                 const std::string& newPatientId);
-
-    void StandaloneRecycling();
-
-    void MarkAsUnstable(int64_t id,
-                        Orthanc::ResourceType type,
-                        const std::string& publicId);
-
-    void LogChange(int64_t internalId,
-                   ChangeType changeType,
-                   ResourceType resourceType,
-                   const std::string& publicId);
-
-    void SignalNewResource(ChangeType changeType,
-                           ResourceType level,
-                           const std::string& publicId,
-                           int64_t internalId);
-
-    uint64_t IncrementGlobalSequenceInternal(GlobalProperty property);
-
-    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                         const DatabaseLookup& source,
-                         ResourceType level) const;
-
-    SeriesStatus GetSeriesStatus(int64_t id,
-                                 int64_t expectedNumberOfInstances);
-
-  public:
-    ServerIndex(ServerContext& context,
-                IDatabaseWrapper& database,
-                unsigned int threadSleep);
-
-    ~ServerIndex();
-
-    void Stop();
-
-    uint64_t GetMaximumStorageSize() const
-    {
-      return maximumStorageSize_;
-    }
-
-    uint64_t GetMaximumPatientCount() const
-    {
-      return maximumPatients_;
-    }
-
-    // "size == 0" means no limit on the storage size
-    void SetMaximumStorageSize(uint64_t size);
-
-    // "count == 0" means no limit on the number of patients
-    void SetMaximumPatientCount(unsigned int count);
-
-    StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
-                      DicomInstanceToStore& instance,
-                      const Attachments& attachments,
-                      bool overwrite);
-
-    void GetGlobalStatistics(/* out */ uint64_t& diskSize,
-                             /* out */ uint64_t& uncompressedSize,
-                             /* out */ uint64_t& countPatients, 
-                             /* out */ uint64_t& countStudies, 
-                             /* out */ uint64_t& countSeries, 
-                             /* out */ uint64_t& countInstances);
-
-    bool LookupResource(Json::Value& result,
-                        const std::string& publicId,
-                        ResourceType expectedType);
-
-    bool LookupAttachment(FileInfo& attachment,
-                          const std::string& instanceUuid,
-                          FileContentType contentType);
-
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType);
-
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType,
-                     size_t since,
-                     size_t limit);
-
-    bool DeleteResource(Json::Value& target /* out */,
-                        const std::string& uuid,
-                        ResourceType expectedType);
-
-    void GetChanges(Json::Value& target,
-                    int64_t since,
-                    unsigned int maxResults);
-
-    void GetLastChange(Json::Value& target);
-
-    void LogExportedResource(const std::string& publicId,
-                             const std::string& remoteModality);
-
-    void GetExportedResources(Json::Value& target,
-                              int64_t since,
-                              unsigned int maxResults);
-
-    void GetLastExportedResource(Json::Value& target);
-
-    bool IsProtectedPatient(const std::string& publicId);
-
-    void SetProtectedPatient(const std::string& publicId,
-                             bool isProtected);
-
-    void GetChildren(std::list<std::string>& result,
-                     const std::string& publicId);
-
-    void GetChildInstances(std::list<std::string>& result,
-                           const std::string& publicId);
-
-    void SetMetadata(const std::string& publicId,
-                     MetadataType type,
-                     const std::string& value);
-
-    void DeleteMetadata(const std::string& publicId,
-                        MetadataType type);
-
-    void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                        const std::string& publicId);
-
-    bool LookupMetadata(std::string& target,
-                        const std::string& publicId,
-                        MetadataType type);
-
-    void ListAvailableAttachments(std::list<FileContentType>& target,
-                                  const std::string& publicId,
-                                  ResourceType expectedType);
-
-    bool LookupParent(std::string& target,
-                      const std::string& publicId);
-
-    uint64_t IncrementGlobalSequence(GlobalProperty sequence);
-
-    void LogChange(ChangeType changeType,
-                   const std::string& publicId);
-
-    void DeleteChanges();
-
-    void DeleteExportedResources();
-
-    void GetResourceStatistics(/* out */ ResourceType& type,
-                               /* out */ uint64_t& diskSize, 
-                               /* out */ uint64_t& uncompressedSize, 
-                               /* out */ unsigned int& countStudies, 
-                               /* out */ unsigned int& countSeries, 
-                               /* out */ unsigned int& countInstances, 
-                               /* out */ uint64_t& dicomDiskSize, 
-                               /* out */ uint64_t& dicomUncompressedSize, 
-                               const std::string& publicId);
-
-    void LookupIdentifierExact(std::vector<std::string>& result,
-                               ResourceType level,
-                               const DicomTag& tag,
-                               const std::string& value);
-
-    StoreStatus AddAttachment(const FileInfo& attachment,
-                              const std::string& publicId);
-
-    void DeleteAttachment(const std::string& publicId,
-                          FileContentType type);
-
-    void SetGlobalProperty(GlobalProperty property,
-                           const std::string& value);
-
-    bool LookupGlobalProperty(std::string& value,
-                              GlobalProperty property);
-
-    std::string GetGlobalProperty(GlobalProperty property,
-                                  const std::string& defaultValue);
-
-    bool GetMainDicomTags(DicomMap& result,
-                          const std::string& publicId,
-                          ResourceType expectedType,
-                          ResourceType levelOfInterest);
-
-    // Only applicable at the instance level
-    bool GetAllMainDicomTags(DicomMap& result,
-                             const std::string& instancePublicId);
-
-    bool LookupResourceType(ResourceType& type,
-                            const std::string& publicId);
-
-    unsigned int GetDatabaseVersion();
-
-    bool LookupParent(std::string& target,
-                      const std::string& publicId,
-                      ResourceType parentType);
-
-    void ReconstructInstance(ParsedDicomFile& dicom);
-
-    void ApplyLookupResources(std::vector<std::string>& resourcesId,
-                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
-                              const DatabaseLookup& lookup,
-                              ResourceType queryLevel,
-                              size_t limit);
-  };
-}
--- a/OrthancServer/ServerIndexChange.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/IDynamicObject.h"
-#include "../Core/SystemToolbox.h"
-
-#include <string>
-#include <json/value.h>
-
-namespace Orthanc
-{
-  struct ServerIndexChange : public IDynamicObject
-  {
-  private:
-    int64_t      seq_;
-    ChangeType   changeType_;
-    ResourceType resourceType_;
-    std::string  publicId_;
-    std::string  date_;
-
-  public:
-    ServerIndexChange(ChangeType changeType,
-                      ResourceType resourceType,
-                      const std::string& publicId) :
-      seq_(-1),
-      changeType_(changeType),
-      resourceType_(resourceType),
-      publicId_(publicId),
-      date_(SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */))
-    {
-    }
-
-    ServerIndexChange(int64_t seq,
-                      ChangeType changeType,
-                      ResourceType resourceType,
-                      const std::string& publicId,
-                      const std::string& date) :
-      seq_(seq),
-      changeType_(changeType),
-      resourceType_(resourceType),
-      publicId_(publicId),
-      date_(date)
-    {
-    }
-
-    ServerIndexChange(const ServerIndexChange& other) 
-    : seq_(other.seq_),
-      changeType_(other.changeType_),
-      resourceType_(other.resourceType_),
-      publicId_(other.publicId_),
-      date_(other.date_)
-    {
-    }
-
-    ServerIndexChange* Clone() const
-    {
-      return new ServerIndexChange(*this);
-    }
-
-    int64_t  GetSeq() const
-    {
-      return seq_;
-    }
-
-    ChangeType  GetChangeType() const
-    {
-      return changeType_;
-    }
-
-    ResourceType  GetResourceType() const
-    {
-      return resourceType_;
-    }
-
-    const std::string&  GetPublicId() const
-    {
-      return publicId_;
-    }
-
-    const std::string& GetDate() const
-    {
-      return date_;
-    }
-
-    void Format(Json::Value& item) const
-    {
-      item = Json::objectValue;
-      item["Seq"] = static_cast<int>(seq_);
-      item["ChangeType"] = EnumerationToString(changeType_);
-      item["ResourceType"] = EnumerationToString(resourceType_);
-      item["ID"] = publicId_;
-      item["Path"] = GetBasePath(resourceType_, publicId_);
-      item["Date"] = date_;
-    }
-  };
-}
--- a/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1114 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Cache/SharedArchive.h"
-#include "../../Core/Compression/HierarchicalZipWriter.h"
-#include "../../Core/DicomParsing/DicomDirWriter.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "../OrthancConfiguration.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;
-
-static const char* const MEDIA_IMAGES_FOLDER = "IMAGES"; 
-static const char* const KEY_DESCRIPTION = "Description";
-static const char* const KEY_INSTANCES_COUNT = "InstancesCount";
-static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB";
-static const char* const KEY_TRANSCODE = "Transcode";
-
-
-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::unique_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::unique_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,
-                 bool transcode,
-                 DicomTransferSyntax transferSyntax) 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());
-
-            bool transcodeSuccess = false;
-
-            std::unique_ptr<ParsedDicomFile> parsed;
-            
-            if (transcode)
-            {
-              // New in Orthanc 1.7.0
-              std::set<DicomTransferSyntax> syntaxes;
-              syntaxes.insert(transferSyntax);
-
-              IDicomTranscoder::DicomImage source, transcoded;
-              source.SetExternalBuffer(content);
-
-              if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
-              {
-                writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize());
-
-                if (dicomDir != NULL)
-                {
-                  std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
-                  dicomDir->Add(dicomDirFolder, filename_, *tmp);
-                }
-                
-                transcodeSuccess = true;
-              }
-              else
-              {
-                LOG(INFO) << "Cannot transcode instance " << instanceId_
-                          << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax);
-              }
-            }
-
-            if (!transcodeSuccess)
-            {
-              writer.Write(content);
-
-              if (dicomDir != NULL)
-              {
-                if (parsed.get() == NULL)
-                {
-                  parsed.reset(new ParsedDicomFile(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,
-                       bool transcode,
-                       DicomTransferSyntax transferSyntax) const
-    {
-      if (index >= commands_.size())
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder, transcode, transferSyntax);
-    }
-      
-  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_;
-    }
-
-    // "media" flavor (with DICOMDIR)
-    void Apply(HierarchicalZipWriter& writer,
-               ServerContext& context,
-               size_t index,
-               DicomDirWriter& dicomDir,
-               const std::string& dicomDirFolder,
-               bool transcode,
-               DicomTransferSyntax transferSyntax) const
-    {
-      ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder, transcode, transferSyntax);
-    }
-
-    // "archive" flavor (without DICOMDIR)
-    void Apply(HierarchicalZipWriter& writer,
-               ServerContext& context,
-               size_t index,
-               bool transcode,
-               DicomTransferSyntax transferSyntax) const
-    {
-      ApplyInternal(writer, context, index, NULL, "", transcode, transferSyntax);
-    }
-      
-    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::unique_ptr<HierarchicalZipWriter>  zip_;
-    std::unique_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,
-                 bool transcode,
-                 DicomTransferSyntax transferSyntax)
-    {
-      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, transcode, transferSyntax);
-        }
-        else
-        {
-          assert(dicomDir_.get() == NULL);
-          commands_.Apply(*zip_, context_, index, transcode, transferSyntax);
-        }
-      }
-    }
-
-    unsigned int GetInstancesCount() const
-    {
-      return commands_.GetInstancesCount();
-    }
-
-    uint64_t GetUncompressedSize() const
-    {
-      return commands_.GetUncompressedSize();
-    }
-  };
-
-
-  ArchiveJob::ArchiveJob(ServerContext& context,
-                         bool isMedia,
-                         bool enableExtendedSopClass) :
-    context_(context),
-    archive_(new ArchiveIndex(ResourceType_Patient)),  // root
-    isMedia_(isMedia),
-    enableExtendedSopClass_(enableExtendedSopClass),
-    currentStep_(0),
-    instancesCount_(0),
-    uncompressedSize_(0),
-    transcode_(false),
-    transferSyntax_(DicomTransferSyntax_LittleEndianImplicit)
-  {
-  }
-
-  
-  ArchiveJob::~ArchiveJob()
-  {
-    if (!mediaArchiveId_.empty())
-    {
-      context_.GetMediaArchive().Remove(mediaArchiveId_);
-    }
-  }
-
-
-  void ArchiveJob::SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& target)
-  {
-    if (target.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else if (writer_.get() != NULL ||  // Already started
-             synchronousTarget_.get() != NULL ||
-             asynchronousTarget_.get() != NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      synchronousTarget_ = target;
-    }
-  }
-
-
-  void ArchiveJob::SetDescription(const std::string& description)
-  {
-    if (writer_.get() != NULL)   // Already started
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      description_ = description;
-    }
-  }
-
-  
-  void ArchiveJob::AddResource(const std::string& publicId)
-  {
-    if (writer_.get() != NULL)   // Already started
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      ResourceIdentifiers resource(context_.GetIndex(), publicId);
-      archive_->Add(context_.GetIndex(), resource);
-    }
-  }
-
-
-  void ArchiveJob::SetTranscode(DicomTransferSyntax transferSyntax)
-  {
-    if (writer_.get() != NULL)   // Already started
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      transcode_ = true;
-      transferSyntax_ = transferSyntax;
-    }
-  }
-
-  
-  void ArchiveJob::Reset()
-  {
-    throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                           "Cannot resubmit the creation of an archive");
-  }
-
-  
-  void ArchiveJob::Start()
-  {
-    TemporaryFile* target = NULL;
-    
-    if (synchronousTarget_.get() == NULL)
-    {
-      {
-        OrthancConfiguration::ReaderLock lock;
-        asynchronousTarget_.reset(lock.GetConfiguration().CreateTemporaryFile());
-      }
-
-      target = asynchronousTarget_.get();
-    }
-    else
-    {
-      target = synchronousTarget_.get();
-    }
-
-    assert(target != NULL);
-    target->Touch();  // Make sure we can write to the temporary file
-    
-    if (writer_.get() != NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    writer_.reset(new ZipWriterIterator(*target, context_, *archive_,
-                                        isMedia_, enableExtendedSopClass_));
-
-    instancesCount_ = writer_->GetInstancesCount();
-    uncompressedSize_ = writer_->GetUncompressedSize();
-  }
-
-
-
-  namespace
-  {
-    class DynamicTemporaryFile : public IDynamicObject
-    {
-    private:
-      std::unique_ptr<TemporaryFile>   file_;
-
-    public:
-      DynamicTemporaryFile(TemporaryFile* f) : file_(f)
-      {
-        if (f == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-      }
-
-      const TemporaryFile& GetFile() const
-      {
-        assert(file_.get() != NULL);
-        return *file_;
-      }
-    };
-  }
-  
-
-  void ArchiveJob::FinalizeTarget()
-  {
-    writer_.reset();  // Flush all the results
-
-    if (asynchronousTarget_.get() != NULL)
-    {
-      // Asynchronous behavior: Move the resulting file into the media archive
-      mediaArchiveId_ = context_.GetMediaArchive().Add(
-        new DynamicTemporaryFile(asynchronousTarget_.release()));
-    }
-  }
-    
-
-  JobStepResult ArchiveJob::Step(const std::string& jobId)
-  {
-    assert(writer_.get() != NULL);
-
-    if (synchronousTarget_.get() != NULL &&
-        synchronousTarget_.unique())
-    {
-      LOG(WARNING) << "A client has disconnected while creating an archive";
-      return JobStepResult::Failure(ErrorCode_NetworkProtocol,
-                                    "A client has disconnected while creating an archive");
-    }
-        
-    if (writer_->GetStepsCount() == 0)
-    {
-      FinalizeTarget();
-      return JobStepResult::Success();
-    }
-    else
-    {
-      writer_->RunStep(currentStep_, transcode_, transferSyntax_);
-
-      currentStep_ ++;
-
-      if (currentStep_ == writer_->GetStepsCount())
-      {
-        FinalizeTarget();
-        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 = Json::objectValue;
-    value[KEY_DESCRIPTION] = description_;
-    value[KEY_INSTANCES_COUNT] = instancesCount_;
-    value[KEY_UNCOMPRESSED_SIZE_MB] =
-      static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
-
-    if (transcode_)
-    {
-      value[KEY_TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
-    }
-  }
-
-
-  bool ArchiveJob::GetOutput(std::string& output,
-                             MimeType& mime,
-                             const std::string& key)
-  {   
-    if (key == "archive" &&
-        !mediaArchiveId_.empty())
-    {
-      SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_);
-
-      if (accessor.IsValid())
-      {
-        const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem());
-        f.GetFile().Read(output);
-        mime = MimeType_Zip;
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }    
-    else
-    {
-      return false;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/ArchiveJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../Core/JobsEngine/IJob.h"
-#include "../../Core/TemporaryFile.h"
-
-#include <boost/shared_ptr.hpp>
-#include <stdint.h>
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class ArchiveJob : public IJob
-  {
-  private:
-    class ArchiveIndex;
-    class ArchiveIndexVisitor;
-    class IArchiveVisitor;
-    class MediaIndexVisitor;
-    class ResourceIdentifiers;
-    class ZipCommands;
-    class ZipWriterIterator;
-    
-    boost::shared_ptr<TemporaryFile>      synchronousTarget_;
-    std::unique_ptr<TemporaryFile>        asynchronousTarget_;
-    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_;
-    std::string                           mediaArchiveId_;
-
-    // New in Orthanc 1.7.0
-    bool                 transcode_;
-    DicomTransferSyntax  transferSyntax_;
-    
-    void FinalizeTarget();
-    
-  public:
-    ArchiveJob(ServerContext& context,
-               bool isMedia,
-               bool enableExtendedSopClass);
-    
-    virtual ~ArchiveJob();
-    
-    void SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& synchronousTarget);
-
-    void SetDescription(const std::string& description);
-
-    const std::string& GetDescription() const
-    {
-      return description_;
-    }
-
-    void AddResource(const std::string& publicId);
-
-    void SetTranscode(DicomTransferSyntax transferSyntax);
-
-    virtual void Reset() ORTHANC_OVERRIDE;
-
-    virtual void Start() ORTHANC_OVERRIDE;
-
-    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual float GetProgress() ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
-    
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
-    {
-      return false;  // Cannot serialize this kind of job
-    }
-
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) ORTHANC_OVERRIDE;
-  };
-}
--- a/OrthancServer/ServerJobs/CleaningInstancesJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "CleaningInstancesJob.h"
-
-#include "../../Core/SerializationToolbox.h"
-#include "../ServerContext.h"
-
-
-namespace Orthanc
-{
-  bool CleaningInstancesJob::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;
-  }
-
-  
-  void CleaningInstancesJob::SetKeepSource(bool keep)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    keepSource_ = keep;
-  }
-
-
-  static const char* KEEP_SOURCE = "KeepSource";
-
-
-  CleaningInstancesJob::CleaningInstancesJob(ServerContext& context,
-                                             const Json::Value& serialized,
-                                             bool defaultKeepSource) :
-    SetOfInstancesJob(serialized),  // (*)
-    context_(context)
-  {
-    if (!HasTrailingStep())
-    {
-      // Should have been set by (*)
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (serialized.isMember(KEEP_SOURCE))
-    {
-      keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
-    }
-    else
-    {
-      keepSource_ = defaultKeepSource;
-    }
-  }
-
-  
-  bool CleaningInstancesJob::Serialize(Json::Value& target)
-  {
-    if (!SetOfInstancesJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      target[KEEP_SOURCE] = keepSource_;
-      return true;
-    }
-  }
-
-
-  void CleaningInstancesJob::Start()
-  {
-    if (!HasTrailingStep())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "AddTrailingStep() should have been called before submitting the job");
-    }
-
-    SetOfInstancesJob::Start();
-  }
-}
--- a/OrthancServer/ServerJobs/CleaningInstancesJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class CleaningInstancesJob : public SetOfInstancesJob
-  {
-  private:
-    ServerContext&  context_;
-    bool            keepSource_;
-    
-  protected:
-    virtual bool HandleTrailingStep();
-    
-  public:
-    CleaningInstancesJob(ServerContext& context,
-                         bool keepSource) :
-      context_(context),
-      keepSource_(keepSource)
-    {
-    }
-
-    CleaningInstancesJob(ServerContext& context,
-                         const Json::Value& serialized,
-                         bool defaultKeepSource);
-
-    ServerContext& GetContext() const
-    {
-      return context_;
-    }
-    
-    bool IsKeepSource() const
-    {
-      return keepSource_;
-    }
-    
-    void SetKeepSource(bool keep);
-
-    virtual bool Serialize(Json::Value& target);
-
-    virtual void Start();
-  };
-}
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,309 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../Core/DicomNetworking/DicomAssociation.h"
-#include "../../Core/Logging.h"
-#include "../../Core/SerializationToolbox.h"
-#include "../ServerContext.h"
-#include "../StorageCommitmentReports.h"
-
-
-namespace Orthanc
-{
-  void DicomModalityStoreJob::OpenConnection()
-  {
-    if (connection_.get() == NULL)
-    {
-      connection_.reset(new DicomStoreUserConnection(parameters_));
-    }
-  }
-
-
-  bool DicomModalityStoreJob::HandleInstance(const std::string& instance)
-  {
-    assert(IsStarted());
-    OpenConnection();
-
-    LOG(INFO) << "Sending instance " << instance << " to modality \"" 
-              << parameters_.GetRemoteModality().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;
-    }
-    
-    std::string sopClassUid, sopInstanceUid;
-    context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom,
-                                  HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_);
-
-    if (storageCommitment_)
-    {
-      sopClassUids_.push_back(sopClassUid);
-      sopInstanceUids_.push_back(sopInstanceUid);
-
-      if (sopClassUids_.size() != sopInstanceUids_.size() ||
-          sopClassUids_.size() > GetInstancesCount())
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-      
-      if (sopClassUids_.size() == GetInstancesCount())
-      {
-        assert(IsStarted());
-        connection_.reset(NULL);
-        
-        const std::string& remoteAet = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-        
-        LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet;
-
-        // Create a "pending" storage commitment report BEFORE the
-        // actual SCU call in order to avoid race conditions
-        context_.GetStorageCommitmentReports().Store(
-          transactionUid_, new StorageCommitmentReports::Report(remoteAet));
-        
-        std::vector<std::string> a(sopClassUids_.begin(), sopClassUids_.end());
-        std::vector<std::string> b(sopInstanceUids_.begin(), sopInstanceUids_.end());
-
-        DicomAssociation::RequestStorageCommitment(parameters_, transactionUid_, a, b);
-      }
-    }
-
-    //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
-
-    return true;
-  }
-    
-
-  bool DicomModalityStoreJob::HandleTrailingStep()
-  {
-    throw OrthancException(ErrorCode_InternalError);
-  }
-
-
-  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
-    context_(context),
-    moveOriginatorId_(0),      // By default, not a C-MOVE
-    storageCommitment_(false)  // By default, no storage commitment
-  {
-    ResetStorageCommitment();
-  }
-
-
-  void DicomModalityStoreJob::SetLocalAet(const std::string& aet)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetLocalApplicationEntityTitle(aet);
-    }
-  }
-
-
-  void DicomModalityStoreJob::SetRemoteModality(const RemoteModalityParameters& remote)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetRemoteModality(remote);
-    }
-  }
-
-    
-  void DicomModalityStoreJob::SetTimeout(uint32_t seconds)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetTimeout(seconds);
-    }
-  }
-
-
-  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::ResetStorageCommitment()
-  {
-    if (storageCommitment_)
-    {
-      transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier();
-      sopClassUids_.clear();
-      sopInstanceUids_.clear();
-    }
-  }
-  
-
-  void DicomModalityStoreJob::Reset()
-  {
-    SetOfInstancesJob::Reset();
-
-    /**
-     * "After the N-EVENT-REPORT has been sent, the Transaction UID is
-     * no longer active and shall not be reused for other
-     * transactions." => Need to reset the transaction UID here
-     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
-     **/
-    ResetStorageCommitment();
-  }
-  
-
-  void DicomModalityStoreJob::EnableStorageCommitment(bool enabled)
-  {
-    storageCommitment_ = enabled;
-    ResetStorageCommitment();
-  }
-  
-
-  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
-  {
-    SetOfInstancesJob::GetPublicContent(value);
-    
-    value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
-    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-
-    if (HasMoveOriginator())
-    {
-      value["MoveOriginatorAET"] = GetMoveOriginatorAet();
-      value["MoveOriginatorID"] = GetMoveOriginatorId();
-    }
-
-    if (storageCommitment_)
-    {
-      value["StorageCommitmentTransactionUID"] = transactionUid_;
-    }
-  }
-
-
-  static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
-  static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
-  static const char* STORAGE_COMMITMENT = "StorageCommitment";
-  
-
-  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
-                                               const Json::Value& serialized) :
-    SetOfInstancesJob(serialized),
-    context_(context)
-  {
-    moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
-    moveOriginatorId_ = static_cast<uint16_t>
-      (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
-    EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT));
-
-    parameters_ = DicomAssociationParameters::UnserializeJob(serialized);
-  }
-
-
-  bool DicomModalityStoreJob::Serialize(Json::Value& target)
-  {
-    if (!SetOfInstancesJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      parameters_.SerializeJob(target);
-      target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
-      target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
-      target[STORAGE_COMMITMENT] = storageCommitment_;
-      return true;
-    }
-  }  
-}
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../Core/JobsEngine/SetOfInstancesJob.h"
-#include "../../Core/DicomNetworking/DicomStoreUserConnection.h"
-
-#include <list>
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class DicomModalityStoreJob : public SetOfInstancesJob
-  {
-  private:
-    ServerContext&                             context_;
-    DicomAssociationParameters                 parameters_;
-    std::string                                moveOriginatorAet_;
-    uint16_t                                   moveOriginatorId_;
-    std::unique_ptr<DicomStoreUserConnection>  connection_;
-    bool                                       storageCommitment_;
-
-    // For storage commitment
-    std::string             transactionUid_;
-    std::list<std::string>  sopInstanceUids_;
-    std::list<std::string>  sopClassUids_;
-
-    void OpenConnection();
-
-    void ResetStorageCommitment();
-
-  protected:
-    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE;
-    
-    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE;
-
-  public:
-    DicomModalityStoreJob(ServerContext& context);
-
-    DicomModalityStoreJob(ServerContext& context,
-                          const Json::Value& serialized);
-
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-
-    void SetLocalAet(const std::string& aet);
-
-    void SetRemoteModality(const RemoteModalityParameters& remote);
-
-    void SetTimeout(uint32_t seconds);
-
-    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) ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
-    {
-      target = "DicomModalityStore";
-    }
-
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
-
-    virtual void Reset() ORTHANC_OVERRIDE;
-
-    void EnableStorageCommitment(bool enabled);
-
-    bool HasStorageCommitment() const
-    {
-      return storageCommitment_;
-    }
-  };
-}
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-#include "../ServerContext.h"
-
-static const char* const LOCAL_AET = "LocalAet";
-static const char* const TARGET_AET = "TargetAet";
-static const char* const REMOTE = "Remote";
-static const char* const QUERY = "Query";
-static const char* const TIMEOUT = "Timeout";
-
-namespace Orthanc
-{
-  class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand
-  {
-  private:
-    DicomMoveScuJob&           that_;
-    std::unique_ptr<DicomMap>  findAnswer_;
-
-  public:
-    Command(DicomMoveScuJob& that,
-            const DicomMap&  findAnswer) :
-      that_(that),
-      findAnswer_(findAnswer.Clone())
-    {
-    }
-
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      that_.Retrieve(*findAnswer_);
-      return true;
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      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 DicomControlUserConnection(parameters_));
-    }
-    
-    connection_->Move(targetAet_, findAnswer);
-  }
-
-
-  static void AddTagIfString(Json::Value& target,
-                             const DicomMap& answer,
-                             const DicomTag& tag)
-  {
-    const DicomValue* value = answer.TestAndGetValue(tag);
-    if (value != NULL &&
-        !value->IsNull() &&
-        !value->IsBinary())
-    {
-      target[tag.Format()] = value->GetContent();
-    }
-  }
-  
-
-  void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
-  {
-    assert(query_.type() == Json::arrayValue);
-
-    // Copy the identifiers tags, if they exist
-    Json::Value item = Json::objectValue;
-    AddTagIfString(item, answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
-    AddTagIfString(item, answer, DICOM_TAG_PATIENT_ID);
-    AddTagIfString(item, answer, DICOM_TAG_STUDY_INSTANCE_UID);
-    AddTagIfString(item, answer, DICOM_TAG_SERIES_INSTANCE_UID);
-    AddTagIfString(item, answer, DICOM_TAG_SOP_INSTANCE_UID);
-    AddTagIfString(item, answer, DICOM_TAG_ACCESSION_NUMBER);
-    query_.append(item);
-    
-    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
-    {
-      parameters_.SetLocalApplicationEntityTitle(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
-    {
-      parameters_.SetRemoteModality(remote);
-    }
-  }
-
-
-  void DicomMoveScuJob::SetTimeout(uint32_t seconds)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      parameters_.SetTimeout(seconds);
-    }
-  }
-
-  
-  void DicomMoveScuJob::Stop(JobStopReason reason)
-  {
-    connection_.reset();
-  }
-  
-
-  void DicomMoveScuJob::GetPublicContent(Json::Value& value)
-  {
-    SetOfCommandsJob::GetPublicContent(value);
-
-    value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
-    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
-    value["Query"] = query_;
-  }
-
-
-  DicomMoveScuJob::DicomMoveScuJob(ServerContext& context,
-                                   const Json::Value& serialized) :
-    SetOfCommandsJob(new Unserializer(*this), serialized),
-    context_(context),
-    query_(Json::arrayValue)
-  {
-    parameters_ = DicomAssociationParameters::UnserializeJob(serialized);
-    targetAet_ = SerializationToolbox::ReadString(serialized, TARGET_AET);
-
-    if (serialized.isMember(QUERY) &&
-        serialized[QUERY].type() == Json::arrayValue)
-    {
-      query_ = serialized[QUERY];
-    }
-  }
-
-  
-  bool DicomMoveScuJob::Serialize(Json::Value& target)
-  {
-    if (!SetOfCommandsJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      parameters_.SerializeJob(target);
-      target[TARGET_AET] = targetAet_;
-      target[QUERY] = query_;
-      return true;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/DicomMoveScuJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../Core/DicomNetworking/DicomControlUserConnection.h"
-#include "../../Core/JobsEngine/SetOfCommandsJob.h"
-
-#include "../QueryRetrieveHandler.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class DicomMoveScuJob : public SetOfCommandsJob
-  {
-  private:
-    class Command;
-    class Unserializer;
-    
-    ServerContext&              context_;
-    DicomAssociationParameters  parameters_;
-    std::string                 targetAet_;
-    Json::Value                 query_;
-
-    std::unique_ptr<DicomControlUserConnection>  connection_;
-    
-    void Retrieve(const DicomMap& findAnswer);
-    
-  public:
-    DicomMoveScuJob(ServerContext& context) :
-      context_(context),
-      query_(Json::arrayValue)
-    {
-    }
-
-    DicomMoveScuJob(ServerContext& context,
-                    const Json::Value& serialized);
-
-    void AddFindAnswer(const DicomMap& answer);
-    
-    void AddFindAnswer(QueryRetrieveHandler& query,
-                       size_t i);
-
-    const DicomAssociationParameters& GetParameters() const
-    {
-      return parameters_;
-    }
-    
-    void SetLocalAet(const std::string& aet);
-
-    void SetRemoteModality(const RemoteModalityParameters& remote);
-
-    void SetTimeout(uint32_t timeout);
-
-    const std::string& GetTargetAet() const
-    {
-      return targetAet_;
-    }
-    
-    void SetTargetAet(const std::string& aet);
-
-    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);
-  };
-}
--- a/OrthancServer/ServerJobs/IStorageCommitmentFactory.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <vector>
-
-namespace Orthanc
-{
-  class IStorageCommitmentFactory : public boost::noncopyable
-  {
-  public:
-    class ILookupHandler : public boost::noncopyable
-    {
-    public:
-      virtual ~ILookupHandler()
-      {
-      }
-
-      virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
-                                                    const std::string& sopInstanceUid) = 0;
-    };
-
-    virtual ~IStorageCommitmentFactory()
-    {
-    }
-
-    virtual ILookupHandler* CreateStorageCommitment(const std::string& jobId,
-                                                    const std::string& transactionUid,
-                                                    const std::vector<std::string>& sopClassUids,
-                                                    const std::vector<std::string>& sopInstanceUids,
-                                                    const std::string& remoteAet,
-                                                    const std::string& calledAet) = 0;
-  };
-}
--- a/OrthancServer/ServerJobs/LuaJobManager.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,280 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancConfiguration.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)
-  {
-    unsigned int dicomTimeout;
-    
-    {
-      OrthancConfiguration::ReaderLock lock;
-      dicomTimeout = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
-    }
-
-    connectionManager_.SetInactivityTimeout(dicomTimeout * 1000);  // Milliseconds expected
-    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_->Register(that_);
-      that_.currentJob_->SetDescription("Lua");
-
-      {
-        jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
-        jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_);
-      }
-    }
-
-    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(ServerContext& context,
-                                                   const std::string& localAet,
-                                                   const RemoteModalityParameters& modality)
-  {
-    assert(jobLock_.get() != NULL);
-    return jobLock_->AddOperation(new StoreScuOperation(
-                                    context, that_.connectionManager_, 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);
-  }
-}
--- a/OrthancServer/ServerJobs/LuaJobManager.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomNetworking/TimeoutDicomConnectionManager.h"
-#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_;
-    TimeoutDicomConnectionManager  connectionManager_;
-
-    virtual void SignalDone(const SequenceOfOperationsJob& job);
-
-  public:
-    LuaJobManager();
-
-    void SetMaxOperationsPerJob(size_t count);
-
-    void SetPriority(int priority);
-
-    void SetTrailingOperationTimeout(unsigned int timeout);
-
-    void AwakeTrailingSleep();
-
-    TimeoutDicomConnectionManager& GetDicomConnectionManager()
-    {
-      return connectionManager_;
-    }
-
-    class Lock : public boost::noncopyable
-    {
-    private:
-      LuaJobManager&                                  that_;
-      boost::mutex::scoped_lock                       lock_;
-      JobsEngine&                                     engine_;
-      std::unique_ptr<SequenceOfOperationsJob::Lock>  jobLock_;
-      bool                                            isNewJob_;
-
-    public:
-      Lock(LuaJobManager& that,
-           JobsEngine& engine);
-
-      ~Lock();
-
-      size_t AddLogOperation();
-
-      size_t AddDeleteResourceOperation(ServerContext& context);
-
-      size_t AddStoreScuOperation(ServerContext& context,
-                                  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);
-    };
-  };
-}
--- a/OrthancServer/ServerJobs/MergeStudyJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,375 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-#include "../ServerContext.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;
-    GetContext().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_)
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "Cannot merge a study into the same study: " + study);
-    }
-    else
-    {
-      std::list<std::string> series;
-      GetContext().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)
-  {
-    if (!HasTrailingStep())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "AddTrailingStep() should have been called after AddSourceXXX()");
-    }
-    
-    /**
-     * Retrieve the DICOM instance to be modified
-     **/
-    
-    std::unique_ptr<ParsedDicomFile> modified;
-
-    try
-    {
-      ServerContext::DicomCacheLocker locker(GetContext(), 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);
-
-    // Fix since Orthanc 1.5.8: Assign new "SOPInstanceUID", as the instance has been modified
-    modified->ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
-
-    DicomInstanceToStore toStore;
-    toStore.SetOrigin(origin_);
-    toStore.SetParsedDicomFile(*modified);
-
-    std::string modifiedInstance;
-    if (GetContext().Store(modifiedInstance, toStore,
-                       StoreInstanceMode_Default) != StoreStatus_Success)
-    {
-      LOG(ERROR) << "Error while storing a modified instance " << instance;
-      return false;
-    }
-
-    return true;
-  }
-
-  
-  MergeStudyJob::MergeStudyJob(ServerContext& context,
-                               const std::string& targetStudy) :
-    CleaningInstancesJob(context, false /* by default, remove source instances */),
-    targetStudy_(targetStudy)
-  {
-    /**
-     * Check the validity of the input ID
-     **/
-    
-    ResourceType type;
-
-    if (!GetContext().GetIndex().LookupResourceType(type, targetStudy) ||
-        type != ResourceType_Study)
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "Cannot merge into an unknown study: " + targetStudy);
-    }
-
-
-    /**
-     * 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;
-    GetContext().GetIndex().GetChildInstances(instances, targetStudy);
-    
-    if (instances.empty())
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    DicomMap dicom;
-
-    {
-      ServerContext::DicomCacheLocker locker(GetContext(), 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 (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries))
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "Cannot find this resource: " + studyOrSeries);
-    }
-    else
-    {
-      switch (level)
-      {
-        case ResourceType_Study:
-          AddSourceStudyInternal(studyOrSeries);
-          break;
-          
-        case ResourceType_Series:
-          AddSourceSeries(studyOrSeries);
-          break;
-          
-        default:
-          throw OrthancException(ErrorCode_UnknownResource,
-                                 "This resource is neither a study, nor a series: " +
-                                 studyOrSeries + " is a " +
-                                 std::string(EnumerationToString(level)));
-      }
-    }    
-  }
-  
-
-  void MergeStudyJob::AddSourceSeries(const std::string& series)
-  {
-    std::string parent;
-
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study))
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "This resource is not a series: " + series);
-    }
-    else if (parent == targetStudy_)
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "Cannot merge series " + series +
-                             " into its parent study " + targetStudy_);
-    }
-    else
-    {
-      AddSourceSeriesInternal(series);
-    }    
-  }
-
-
-  void MergeStudyJob::AddSourceStudy(const std::string& study)
-  {
-    ResourceType actualLevel;
-
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else if (!GetContext().GetIndex().LookupResourceType(actualLevel, study) ||
-             actualLevel != ResourceType_Study)
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "This resource is not a study: " + study);
-    }
-    else
-    {
-      AddSourceStudyInternal(study);
-    }    
-  }
-
-
-  void MergeStudyJob::GetPublicContent(Json::Value& value)
-  {
-    CleaningInstancesJob::GetPublicContent(value);
-    value["TargetStudy"] = targetStudy_;
-  }
-
-
-  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) :
-    CleaningInstancesJob(context, serialized,
-                         false /* by default, remove source instances */)  // (*)
-  {
-    if (!HasTrailingStep())
-    {
-      // Should have been set by (*)
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    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 (!CleaningInstancesJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      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;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/MergeStudyJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../DicomInstanceOrigin.h"
-#include "CleaningInstancesJob.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class MergeStudyJob : public CleaningInstancesJob
-  {
-  private:
-    typedef std::map<std::string, std::string>  SeriesUidMap;
-    typedef std::map<DicomTag, std::string>     Replacements;
-    
-    
-    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);
-
-  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);
-
-    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);
-  };
-}
--- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../ServerContext.h"
-
-#include "../../../Core/Logging.h"
-#include "../../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  void DeleteResourceOperation::Apply(JobOperationValues& outputs,
-                                      const JobOperationValue& input)
-  {
-    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;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class DeleteResourceOperation : public IJobOperation
-  {
-  private:
-    ServerContext&  context_;
-
-  public:
-    DeleteResourceOperation(ServerContext& context) :
-    context_(context)
-    {
-    }
-
-    virtual void Apply(JobOperationValues& outputs,
-                       const JobOperationValue& input);
-
-    virtual void Serialize(Json::Value& result) const
-    {
-      result = Json::objectValue;
-      result["Type"] = "DeleteResource";
-    }
-  };
-}
-
--- a/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "DicomInstanceOperationValue.h"
-
-#include "../../ServerContext.h"
-
-namespace Orthanc
-{
-  void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const
-  {
-    context_.ReadDicom(dicom, id_);
-  }
-}
--- a/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  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;
-
-    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_;
-    }
-  };
-}
--- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../ServerContext.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)
-  {
-    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::unique_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, StoreInstanceMode_Default);
-
-      // 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"]));
-  }
-}
-
--- a/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../../Core/DicomParsing/DicomModification.h"
-#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class ModifyInstanceOperation : public IJobOperation
-  {
-  private:
-    ServerContext&                      context_;
-    RequestOrigin                       origin_;
-    std::unique_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);
-
-    virtual void Serialize(Json::Value& target) const;
-  };
-}
-
--- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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)
-  {
-    // 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"]);
-  }
-}
--- a/OrthancServer/ServerJobs/Operations/StorePeerOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-
-    virtual void Serialize(Json::Value& result) const;
-  };
-}
-
--- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../ServerContext.h"
-
-#include "../../../Core/Logging.h"
-#include "../../../Core/OrthancException.h"
-#include "../../../Core/SerializationToolbox.h"
-
-namespace Orthanc
-{
-  void StoreScuOperation::Apply(JobOperationValues& outputs,
-                                const JobOperationValue& input)
-  {
-    TimeoutDicomConnectionManager::Lock lock(connectionManager_, localAet_, modality_);
-    
-    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);
-
-      std::string sopClassUid, sopInstanceUid;  // Unused
-      context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom,
-                                    false /* Not a C-MOVE */, "", 0);
-    }
-    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(ServerContext& context,
-                                       TimeoutDicomConnectionManager& connectionManager,
-                                       const Json::Value& serialized) :
-    context_(context),
-    connectionManager_(connectionManager)
-  {
-    if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" ||
-        !serialized.isMember("LocalAET"))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    localAet_ = SerializationToolbox::ReadString(serialized, "LocalAET");
-    modality_ = RemoteModalityParameters(serialized["Modality"]);
-  }
-}
--- a/OrthancServer/ServerJobs/Operations/StoreScuOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/TimeoutDicomConnectionManager.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class StoreScuOperation : public IJobOperation
-  {
-  private:
-    ServerContext&                  context_;
-    TimeoutDicomConnectionManager&  connectionManager_;
-    std::string                     localAet_;
-    RemoteModalityParameters        modality_;
-    
-  public:
-    StoreScuOperation(ServerContext& context,
-                      TimeoutDicomConnectionManager& connectionManager,
-                      const std::string& localAet,
-                      const RemoteModalityParameters& modality) :
-      context_(context),
-      connectionManager_(connectionManager),
-      localAet_(localAet),
-      modality_(modality)
-    {
-    }
-
-    StoreScuOperation(ServerContext& context,
-                      TimeoutDicomConnectionManager& connectionManager,
-                      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);
-
-    virtual void Serialize(Json::Value& result) const;
-  };
-}
-
--- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,170 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-#include "../../../Core/SystemToolbox.h"
-#include "../../OrthancConfiguration.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)
-  {
-    std::vector<std::string> arguments = preArguments_;
-
-    arguments.reserve(arguments.size() + postArguments_.size() + 1);
-
-    std::unique_ptr<TemporaryFile> tmp;
-    
-    switch (input.GetType())
-    {
-      case JobOperationValue::Type_DicomInstance:
-      {
-        const DicomInstanceOperationValue& instance =
-          dynamic_cast<const DicomInstanceOperationValue&>(input);
-
-        std::string dicom;
-        instance.ReadDicom(dicom);
-
-        {
-          OrthancConfiguration::ReaderLock lock;
-          tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
-        }
-
-        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");
-  }
-}
--- a/OrthancServer/ServerJobs/Operations/SystemCallOperation.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-
-    virtual void Serialize(Json::Value& result) const;
-  };
-}
-
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,156 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../../Plugins/Engine/OrthancPlugins.h"
-#include "../ServerContext.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 "MergeStudyJob.h"
-#include "OrthancPeerStoreJob.h"
-#include "ResourceModificationJob.h"
-#include "SplitStudyJob.h"
-#include "StorageCommitmentScpJob.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::unique_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 if (type == "StorageCommitmentScp")
-    {
-      return new StorageCommitmentScpJob(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(
-        context_, context_.GetLuaScripting().GetDicomConnectionManager(), 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);
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/OrthancJobUnserializer.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/GenericJobUnserializer.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  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);
-  };
-}
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,245 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-#include "../../Core/SerializationToolbox.h"
-#include "../ServerContext.h"
-
-#include <dcmtk/dcmdata/dcfilefo.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
-    {
-      if (transcode_)
-      {
-        std::string dicom;
-        context_.ReadDicom(dicom, instance);
-
-        std::set<DicomTransferSyntax> syntaxes;
-        syntaxes.insert(transferSyntax_);
-        
-        IDicomTranscoder::DicomImage source, transcoded;
-        source.SetExternalBuffer(dicom);
-
-        if (context_.Transcode(transcoded, source, syntaxes, true))
-        {
-          client_->GetBody().assign(reinterpret_cast<const char*>(transcoded.GetBufferData()),
-                                    transcoded.GetBufferSize());
-        }
-        else
-        {
-          client_->GetBody().swap(dicom);
-        }
-      }
-      else
-      {
-        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;
-    }
-  }
-
-
-  DicomTransferSyntax OrthancPeerStoreJob::GetTransferSyntax() const
-  {
-    if (transcode_)
-    {
-      return transferSyntax_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-  
-
-  void OrthancPeerStoreJob::SetTranscode(DicomTransferSyntax syntax)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      transcode_ = true;
-      transferSyntax_ = syntax;
-    }    
-  }
-  
-
-  void OrthancPeerStoreJob::SetTranscode(const std::string& transferSyntaxUid)
-  {
-    DicomTransferSyntax s;
-    if (LookupTransferSyntax(s, transferSyntaxUid))
-    {
-      SetTranscode(s);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Unknown transfer syntax UID: " + transferSyntaxUid);
-    }
-  }
-
-
-  void OrthancPeerStoreJob::ClearTranscode()
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      transcode_ = false;
-    }
-  }
-
-
-  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;
-    
-    if (transcode_)
-    {
-      value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
-    }
-  }
-
-
-  static const char* PEER = "Peer";
-  static const char* TRANSCODE = "Transcode";
-
-  OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context,
-                                           const Json::Value& serialized) :
-    SetOfInstancesJob(serialized),
-    context_(context)
-  {
-    assert(serialized.type() == Json::objectValue);
-    peer_ = WebServiceParameters(serialized[PEER]);
-
-    if (serialized.isMember(TRANSCODE))
-    {
-      SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
-    }
-    else
-    {
-      transcode_ = false;
-    }
-  }
-
-
-  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
-  {
-    if (!SetOfInstancesJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      assert(target.type() == Json::objectValue);
-      peer_.Serialize(target[PEER],
-                      true /* force advanced format */,
-                      true /* include passwords */);
-
-      if (transcode_)
-      {
-        target[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
-      }
-      
-      return true;
-    }
-  }  
-}
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../Core/JobsEngine/SetOfInstancesJob.h"
-#include "../../Core/HttpClient.h"
-
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class OrthancPeerStoreJob : public SetOfInstancesJob
-  {
-  private:
-    ServerContext&               context_;
-    WebServiceParameters         peer_;
-    std::unique_ptr<HttpClient>  client_;
-    bool                         transcode_;
-    DicomTransferSyntax          transferSyntax_;
-
-  protected:
-    virtual bool HandleInstance(const std::string& instance);
-    
-    virtual bool HandleTrailingStep();
-
-  public:
-    OrthancPeerStoreJob(ServerContext& context) :
-      context_(context),
-      transcode_(false)
-    {
-    }
-
-    OrthancPeerStoreJob(ServerContext& context,
-                        const Json::Value& serialize);
-
-    void SetPeer(const WebServiceParameters& peer);
-
-    const WebServiceParameters& GetPeer() const
-    {
-      return peer_;
-    }
-
-    bool IsTranscode() const
-    {
-      return transcode_;
-    }
-
-    DicomTransferSyntax GetTransferSyntax() const;
-
-    void SetTranscode(DicomTransferSyntax syntax);
-
-    void SetTranscode(const std::string& transferSyntaxUid);
-
-    void ClearTranscode();
-
-    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);
-  };
-}
--- a/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,460 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-#include "../ServerContext.h"
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <cassert>
-
-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)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "No modification was provided for this job");
-    }
-
-      
-    LOG(INFO) << "Modifying instance in a job: " << instance;
-
-
-    /**
-     * Retrieve the original instance from the DICOM cache.
-     **/
-    
-    std::unique_ptr<DicomInstanceHasher> originalHasher;
-    std::unique_ptr<ParsedDicomFile> modified;
-
-    try
-    {
-      ServerContext::DicomCacheLocker locker(GetContext(), instance);
-      ParsedDicomFile& original = locker.GetDicom();
-
-      originalHasher.reset(new DicomInstanceHasher(original.GetHasher()));
-      modified.reset(original.Clone(true));
-    }
-    catch (OrthancException& e)
-    {
-      LOG(WARNING) << "An error occurred while executing a Modification job on instance " << instance << ": " << e.GetDetails();
-      return false;
-    }
-
-
-    /**
-     * Compute the resulting DICOM instance.
-     **/
-
-    modification_->Apply(*modified);
-
-    const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject());
-    
-    if (transcode_)
-    {
-      std::set<DicomTransferSyntax> syntaxes;
-      syntaxes.insert(transferSyntax_);
-
-      IDicomTranscoder::DicomImage source;
-      source.AcquireParsed(*modified);  // "modified" is invalid below this point
-      
-      IDicomTranscoder::DicomImage transcoded;
-      if (GetContext().Transcode(transcoded, source, syntaxes, true))
-      {
-        modified.reset(transcoded.ReleaseAsParsedDicomFile());
-
-        // Fix the SOP instance UID in order the preserve the
-        // references between instance UIDs in the DICOM hierarchy
-        // (the UID might have changed in the case of lossy transcoding)
-        if (modified.get() == NULL ||
-            modified->GetDcmtkObject().getDataset() == NULL ||
-            !modified->GetDcmtkObject().getDataset()->putAndInsertString(
-              DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good())
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-      else
-      {
-        LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance;
-        modified.reset(source.ReleaseAsParsedDicomFile());
-      }
-    }
-
-    assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()));
-
-    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 (GetContext().Store(modifiedInstance, toStore,
-                           StoreInstanceMode_Default) != StoreStatus_Success)
-    {
-      throw OrthancException(ErrorCode_CannotStoreInstance,
-                             "Error while storing a modified instance " + instance);
-    }
-
-    /**
-     * The assertion below will fail if automated transcoding to a
-     * lossy transfer syntax is enabled in the Orthanc core, and if
-     * the source instance is not in this transfer syntax.
-     **/
-    // assert(modifiedInstance == modifiedHasher.HashInstance());
-
-    output_->Update(modifiedHasher);
-
-    return true;
-  }
-
-
-  ResourceModificationJob::ResourceModificationJob(ServerContext& context) :
-    CleaningInstancesJob(context, true /* by default, keep source */),
-    modification_(new DicomModification),
-    isAnonymization_(false),
-    transcode_(false)
-  {
-  }
-
-
-  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_;
-    }
-  }
-
-
-  DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const
-  {
-    if (transcode_)
-    {
-      return transferSyntax_;
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-  }
-  
-
-  void ResourceModificationJob::SetTranscode(DicomTransferSyntax syntax)
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      transcode_ = true;
-      transferSyntax_ = syntax;
-    }    
-  }
-
-
-  void ResourceModificationJob::SetTranscode(const std::string& transferSyntaxUid)
-  {
-    DicomTransferSyntax s;
-    if (LookupTransferSyntax(s, transferSyntaxUid))
-    {
-      SetTranscode(s);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_BadFileFormat,
-                             "Unknown transfer syntax UID: " + transferSyntaxUid);
-    }
-  }
-
-
-  void ResourceModificationJob::ClearTranscode()
-  {
-    if (IsStarted())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      transcode_ = false;
-    }
-  }
-
-
-  void ResourceModificationJob::GetPublicContent(Json::Value& value)
-  {
-    CleaningInstancesJob::GetPublicContent(value);
-
-    value["IsAnonymization"] = isAnonymization_;
-
-    if (output_.get() != NULL)
-    {
-      output_->Format(value);
-    }
-
-    if (transcode_)
-    {
-      value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
-    }
-  }
-
-
-  static const char* MODIFICATION = "Modification";
-  static const char* ORIGIN = "Origin";
-  static const char* IS_ANONYMIZATION = "IsAnonymization";
-  static const char* TRANSCODE = "Transcode";
-  
-
-  ResourceModificationJob::ResourceModificationJob(ServerContext& context,
-                                                   const Json::Value& serialized) :
-    CleaningInstancesJob(context, serialized, true /* by default, keep source */)
-  {
-    assert(serialized.type() == Json::objectValue);
-
-    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
-    origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
-    modification_.reset(new DicomModification(serialized[MODIFICATION]));
-
-    if (serialized.isMember(TRANSCODE))
-    {
-      SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
-    }
-    else
-    {
-      transcode_ = false;
-    }
-  }
-  
-  bool ResourceModificationJob::Serialize(Json::Value& value)
-  {
-    if (!CleaningInstancesJob::Serialize(value))
-    {
-      return false;
-    }
-    else
-    {
-      assert(value.type() == Json::objectValue);
-      
-      value[IS_ANONYMIZATION] = isAnonymization_;
-
-      if (transcode_)
-      {
-        value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
-      }
-      
-      origin_.Serialize(value[ORIGIN]);
-      
-      Json::Value tmp;
-      modification_->Serialize(tmp);
-      value[MODIFICATION] = tmp;
-
-      return true;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/ResourceModificationJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../DicomInstanceOrigin.h"
-#include "CleaningInstancesJob.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class ResourceModificationJob : public CleaningInstancesJob
-  {
-  private:
-    class Output;
-    
-    std::unique_ptr<DicomModification>  modification_;
-    boost::shared_ptr<Output>           output_;
-    bool                                isAnonymization_;
-    DicomInstanceOrigin                 origin_;
-    bool                                transcode_;
-    DicomTransferSyntax                 transferSyntax_;
-
-  protected:
-    virtual bool HandleInstance(const std::string& instance);
-    
-  public:
-    ResourceModificationJob(ServerContext& context);
-
-    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_;
-    }
-
-    bool IsTranscode() const
-    {
-      return transcode_;
-    }
-
-    DicomTransferSyntax GetTransferSyntax() const;
-
-    void SetTranscode(DicomTransferSyntax syntax);
-
-    void SetTranscode(const std::string& transferSyntaxUid);
-
-    void ClearTranscode();
-
-    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);
-  };
-}
--- a/OrthancServer/ServerJobs/SplitStudyJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,351 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-#include "../ServerContext.h"
-
-
-namespace Orthanc
-{
-  void SplitStudyJob::CheckAllowedTag(const DicomTag& tag) const
-  {
-    if (allowedTags_.find(tag) == allowedTags_.end())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange,
-                             "Cannot modify the following tag while splitting a study "
-                             "(not in the patient/study modules): " +
-                             FromDcmtkBridge::GetTagName(tag, "") +
-                             " (" + tag.Format() + ")");
-    }
-  }
-
-  
-  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)
-  {
-    if (!HasTrailingStep())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls,
-                             "AddTrailingStep() should have been called after AddSourceSeries()");
-    }
-    
-    /**
-     * Retrieve the DICOM instance to be modified
-     **/
-    
-    std::unique_ptr<ParsedDicomFile> modified;
-
-    try
-    {
-      ServerContext::DicomCacheLocker locker(GetContext(), 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);
-
-    // Fix since Orthanc 1.5.8: Assign new "SOPInstanceUID", as the instance has been modified
-    modified->ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
-
-    if (targetStudy_.empty())
-    {
-      targetStudy_ = modified->GetHasher().HashStudy();
-    }
-    
-    DicomInstanceToStore toStore;
-    toStore.SetOrigin(origin_);
-    toStore.SetParsedDicomFile(*modified);
-
-    std::string modifiedInstance;
-    if (GetContext().Store(modifiedInstance, toStore,
-                           StoreInstanceMode_Default) != StoreStatus_Success)
-    {
-      LOG(ERROR) << "Error while storing a modified instance " << instance;
-      return false;
-    }
-
-    return true;
-  }
-
-  
-  SplitStudyJob::SplitStudyJob(ServerContext& context,
-                               const std::string& sourceStudy) :
-    CleaningInstancesJob(context, false /* by default, remove source instances */),
-    sourceStudy_(sourceStudy),
-    targetStudyUid_(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study))
-  {
-    Setup();
-    
-    ResourceType type;
-    
-    if (!GetContext().GetIndex().LookupResourceType(type, sourceStudy) ||
-        type != ResourceType_Study)
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "Cannot split unknown study " + sourceStudy);
-    }
-  }
-  
-
-  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 (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study) ||
-             parent != sourceStudy_)
-    {
-      throw OrthancException(ErrorCode_UnknownResource,
-                             "This series does not belong to the study to be split: " + series);
-    }
-    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;
-      GetContext().GetIndex().GetChildren(instances, series);
-
-      for (std::list<std::string>::const_iterator
-             it = instances.begin(); it != instances.end(); ++it)
-      {
-        AddInstance(*it);
-      }
-    }    
-  }
-
-
-  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)
-  {
-    CleaningInstancesJob::GetPublicContent(value);
-
-    if (!targetStudy_.empty())
-    {
-      value["TargetStudy"] = targetStudy_;
-    }
-    
-    value["TargetStudyUID"] = targetStudyUid_;
-  }
-
-
-  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) :
-    CleaningInstancesJob(context, serialized,
-                         false /* by default, remove source instances */)  // (*)
-  {
-    if (!HasTrailingStep())
-    {
-      // Should have been set by (*)
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    Setup();
-
-    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 (!CleaningInstancesJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      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;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/SplitStudyJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/DicomTag.h"
-#include "../DicomInstanceOrigin.h"
-#include "CleaningInstancesJob.h"
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class SplitStudyJob : public CleaningInstancesJob
-  {
-  private:
-    typedef std::map<std::string, std::string>  SeriesUidMap;
-    typedef std::map<DicomTag, std::string>     Replacements;
-    
-    
-    std::set<DicomTag>     allowedTags_;
-    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);
-
-  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 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/ServerJobs/StorageCommitmentScpJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,455 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "StorageCommitmentScpJob.h"
-
-#include "../../Core/DicomNetworking/DicomAssociation.h"
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/SerializationToolbox.h"
-#include "../OrthancConfiguration.h"
-#include "../ServerContext.h"
-
-
-static const char* ANSWER = "Answer";
-static const char* CALLED_AET = "CalledAet";
-static const char* INDEX = "Index";
-static const char* LOOKUP = "Lookup";
-static const char* REMOTE_MODALITY = "RemoteModality";
-static const char* SETUP = "Setup";
-static const char* SOP_CLASS_UIDS = "SopClassUids";
-static const char* SOP_INSTANCE_UIDS = "SopInstanceUids";
-static const char* TRANSACTION_UID = "TransactionUid";
-static const char* TYPE = "Type";
-
-
-
-namespace Orthanc
-{
-  class StorageCommitmentScpJob::StorageCommitmentCommand : public SetOfCommandsJob::ICommand
-  {
-  public:
-    virtual CommandType GetType() const = 0;
-  };
-
-  
-  class StorageCommitmentScpJob::SetupCommand : public StorageCommitmentCommand
-  {
-  private:
-    StorageCommitmentScpJob&  that_;
-
-  public:
-    SetupCommand(StorageCommitmentScpJob& that) :
-      that_(that)
-    {
-    }
-
-    virtual CommandType GetType() const ORTHANC_OVERRIDE
-    {
-      return CommandType_Setup;
-    }
-    
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      that_.Setup(jobId);
-      return true;
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      target = Json::objectValue;
-      target[TYPE] = SETUP;
-    }
-  };
-
-
-  class StorageCommitmentScpJob::LookupCommand : public StorageCommitmentCommand
-  {
-  private:
-    StorageCommitmentScpJob&        that_;
-    size_t                          index_;
-    bool                            hasFailureReason_;
-    StorageCommitmentFailureReason  failureReason_;
-
-  public:
-    LookupCommand(StorageCommitmentScpJob&  that,
-                  size_t index) :
-      that_(that),
-      index_(index),
-      hasFailureReason_(false)
-    {
-    }
-
-    virtual CommandType GetType() const ORTHANC_OVERRIDE
-    {
-      return CommandType_Lookup;
-    }
-    
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      failureReason_ = that_.Lookup(index_);
-      hasFailureReason_ = true;
-      return true;
-    }
-
-    size_t GetIndex() const
-    {
-      return index_;
-    }
-
-    StorageCommitmentFailureReason GetFailureReason() const
-    {
-      if (hasFailureReason_)
-      {
-        return failureReason_;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      target = Json::objectValue;
-      target[TYPE] = LOOKUP;
-      target[INDEX] = static_cast<unsigned int>(index_);
-    }
-  };
-
-  
-  class StorageCommitmentScpJob::AnswerCommand : public StorageCommitmentCommand
-  {
-  private:
-    StorageCommitmentScpJob&  that_;
-
-  public:
-    AnswerCommand(StorageCommitmentScpJob& that) :
-      that_(that)
-    {
-      if (that_.ready_)
-      {
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-      else
-      {
-        that_.ready_ = true;
-      }
-    }
-
-    virtual CommandType GetType() const ORTHANC_OVERRIDE
-    {
-      return CommandType_Answer;
-    }
-    
-    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      that_.Answer();
-      return true;
-    }
-
-    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
-    {
-      target = Json::objectValue;
-      target[TYPE] = ANSWER;
-    }
-  };
-    
-
-  class StorageCommitmentScpJob::Unserializer : public SetOfCommandsJob::ICommandUnserializer
-  {
-  private:
-    StorageCommitmentScpJob&  that_;
-
-  public:
-    Unserializer(StorageCommitmentScpJob& that) :
-      that_(that)
-    {
-      that_.ready_ = false;
-    }
-
-    virtual ICommand* Unserialize(const Json::Value& source) const
-    {
-      const std::string type = SerializationToolbox::ReadString(source, TYPE);
-
-      if (type == SETUP)
-      {
-        return new SetupCommand(that_);
-      }
-      else if (type == LOOKUP)
-      {
-        return new LookupCommand(that_, SerializationToolbox::ReadUnsignedInteger(source, INDEX));
-      }
-      else if (type == ANSWER)
-      {
-        return new AnswerCommand(that_);
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-    }
-  };
-
-
-  void StorageCommitmentScpJob::CheckInvariants()
-  {
-    const size_t n = GetCommandsCount();
-
-    if (n <= 1)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    for (size_t i = 0; i < n; i++)
-    {
-      const CommandType type = dynamic_cast<const StorageCommitmentCommand&>(GetCommand(i)).GetType();
-      
-      if ((i == 0 && type != CommandType_Setup) ||
-          (i >= 1 && i < n - 1 && type != CommandType_Lookup) ||
-          (i == n - 1 && type != CommandType_Answer))
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      if (type == CommandType_Lookup)
-      {
-        const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i));
-        if (lookup.GetIndex() != i - 1)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-    }
-  }
-    
-
-  void StorageCommitmentScpJob::Setup(const std::string& jobId)
-  {
-    CheckInvariants();
-
-    const std::string& remoteAet = remoteModality_.GetApplicationEntityTitle();
-    lookupHandler_.reset(context_.CreateStorageCommitment(jobId, transactionUid_, sopClassUids_,
-                                                          sopInstanceUids_, remoteAet, calledAet_));
-  }
-
-
-  StorageCommitmentFailureReason StorageCommitmentScpJob::Lookup(size_t index)
-  {
-#ifndef NDEBUG
-    CheckInvariants();
-#endif
-
-    if (index >= sopClassUids_.size())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    else if (lookupHandler_.get() != NULL)
-    {
-      return lookupHandler_->Lookup(sopClassUids_[index], sopInstanceUids_[index]);
-    }
-    else
-    {
-      // This is the default implementation of Orthanc (if no storage
-      // commitment plugin is installed)
-      bool success = false;
-      StorageCommitmentFailureReason reason =
-        StorageCommitmentFailureReason_NoSuchObjectInstance /* 0x0112 == 274 */;
-      
-      try
-      {
-        std::vector<std::string> orthancId;
-        context_.GetIndex().LookupIdentifierExact(orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids_[index]);
-
-        if (orthancId.size() == 1)
-        {
-          std::string a, b;
-
-          // Make sure that the DICOM file can be re-read by DCMTK
-          // from the file storage, and that the actual SOP
-          // class/instance UIDs do match
-          ServerContext::DicomCacheLocker locker(context_, orthancId[0]);
-          if (locker.GetDicom().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) &&
-              locker.GetDicom().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) &&
-              b == sopInstanceUids_[index])
-          {
-            if (a == sopClassUids_[index])
-            {
-              success = true;
-              reason = StorageCommitmentFailureReason_Success;
-            }
-            else
-            {
-              // Mismatch in the SOP class UID
-              reason = StorageCommitmentFailureReason_ClassInstanceConflict /* 0x0119 */;
-            }
-          }
-        }
-      }
-      catch (OrthancException&)
-      {
-      }
-
-      LOG(INFO) << "  Storage commitment SCP job: " << (success ? "Success" : "Failure")
-                << " while looking for " << sopClassUids_[index] << " / " << sopInstanceUids_[index];
-
-      return reason;
-    }
-  }
-  
-  
-  void StorageCommitmentScpJob::Answer()
-  {   
-    CheckInvariants();
-    LOG(INFO) << "  Storage commitment SCP job: Sending answer";
-
-    std::vector<StorageCommitmentFailureReason> failureReasons;
-    failureReasons.reserve(sopClassUids_.size());
-
-    for (size_t i = 1; i < GetCommandsCount() - 1; i++)
-    {
-      const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i));
-      failureReasons.push_back(lookup.GetFailureReason());
-    }
-
-    if (failureReasons.size() != sopClassUids_.size())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    DicomAssociationParameters parameters(calledAet_, remoteModality_);
-    DicomAssociation::ReportStorageCommitment(
-      parameters, transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons);
-  }
-    
-
-  StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context,
-                                                   const std::string& transactionUid,
-                                                   const std::string& remoteAet,
-                                                   const std::string& calledAet) :
-    context_(context),
-    ready_(false),
-    transactionUid_(transactionUid),
-    calledAet_(calledAet)
-  {
-    {
-      OrthancConfiguration::ReaderLock lock;
-      if (!lock.GetConfiguration().LookupDicomModalityUsingAETitle(remoteModality_, remoteAet))
-      {
-        throw OrthancException(ErrorCode_InexistentItem,
-                               "Unknown remote modality for storage commitment SCP: " + remoteAet);
-      }
-    }
-
-    AddCommand(new SetupCommand(*this));
-  }
-    
-
-  void StorageCommitmentScpJob::Reserve(size_t size)
-  {
-    if (ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      sopClassUids_.reserve(size);
-      sopInstanceUids_.reserve(size);
-    }
-  }
-    
-
-  void StorageCommitmentScpJob::AddInstance(const std::string& sopClassUid,
-                                            const std::string& sopInstanceUid)
-  {
-    if (ready_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      assert(sopClassUids_.size() == sopInstanceUids_.size());
-      AddCommand(new LookupCommand(*this, sopClassUids_.size()));
-      sopClassUids_.push_back(sopClassUid);
-      sopInstanceUids_.push_back(sopInstanceUid);
-    }
-  }
-    
-
-  void StorageCommitmentScpJob::MarkAsReady()
-  {
-    AddCommand(new AnswerCommand(*this));
-  }
-
-
-  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value)
-  {
-    SetOfCommandsJob::GetPublicContent(value);
-      
-    value["CalledAet"] = calledAet_;
-    value["RemoteAet"] = remoteModality_.GetApplicationEntityTitle();
-    value["TransactionUid"] = transactionUid_;
-  }
-
-
-  StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context,
-                                                   const Json::Value& serialized) :
-    SetOfCommandsJob(new Unserializer(*this), serialized),
-    context_(context)
-  {
-    transactionUid_ = SerializationToolbox::ReadString(serialized, TRANSACTION_UID);
-    remoteModality_ = RemoteModalityParameters(serialized[REMOTE_MODALITY]);
-    calledAet_ = SerializationToolbox::ReadString(serialized, CALLED_AET);
-    SerializationToolbox::ReadArrayOfStrings(sopClassUids_, serialized, SOP_CLASS_UIDS);
-    SerializationToolbox::ReadArrayOfStrings(sopInstanceUids_, serialized, SOP_INSTANCE_UIDS);
-  }
-  
-
-  bool StorageCommitmentScpJob::Serialize(Json::Value& target)
-  {
-    if (!SetOfCommandsJob::Serialize(target))
-    {
-      return false;
-    }
-    else
-    {
-      target[TRANSACTION_UID] = transactionUid_;
-      remoteModality_.Serialize(target[REMOTE_MODALITY], true /* force advanced format */);
-      target[CALLED_AET] = calledAet_;
-      SerializationToolbox::WriteArrayOfStrings(target, sopClassUids_, SOP_CLASS_UIDS);
-      SerializationToolbox::WriteArrayOfStrings(target, sopInstanceUids_, SOP_INSTANCE_UIDS);
-      return true;
-    }
-  }
-}
--- a/OrthancServer/ServerJobs/StorageCommitmentScpJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,111 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Compatibility.h"
-#include "../../Core/DicomNetworking/RemoteModalityParameters.h"
-#include "../../Core/JobsEngine/SetOfCommandsJob.h"
-#include "IStorageCommitmentFactory.h"
-
-#include <memory>
-#include <vector>
-
-namespace Orthanc
-{
-  class ServerContext;
-  
-  class StorageCommitmentScpJob : public SetOfCommandsJob
-  {
-  private:
-    enum CommandType
-    {
-      CommandType_Setup,
-      CommandType_Lookup,
-      CommandType_Answer
-    };
-    
-    class StorageCommitmentCommand;
-    class SetupCommand;
-    class LookupCommand;
-    class AnswerCommand;
-    class Unserializer;
-
-    ServerContext&            context_;
-    bool                      ready_;
-    std::string               transactionUid_;
-    RemoteModalityParameters  remoteModality_;
-    std::string               calledAet_;
-    std::vector<std::string>  sopClassUids_;
-    std::vector<std::string>  sopInstanceUids_;
-
-    std::unique_ptr<IStorageCommitmentFactory::ILookupHandler>  lookupHandler_;
-
-    void CheckInvariants();
-    
-    void Setup(const std::string& jobId);
-    
-    StorageCommitmentFailureReason Lookup(size_t index);
-    
-    void Answer();
-    
-  public:
-    StorageCommitmentScpJob(ServerContext& context,
-                            const std::string& transactionUid,
-                            const std::string& remoteAet,
-                            const std::string& calledAet);
-
-    StorageCommitmentScpJob(ServerContext& context,
-                            const Json::Value& serialized);
-
-    void Reserve(size_t size);
-    
-    void AddInstance(const std::string& sopClassUid,
-                     const std::string& sopInstanceUid);
-
-    void MarkAsReady();
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
-    {
-      target = "StorageCommitmentScp";
-    }
-
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
-  };
-}
--- a/OrthancServer/ServerToolbox.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,455 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ServerToolbox.h"
-
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/FileStorage/StorageAccessor.h"
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-#include "Database/IDatabaseWrapper.h"
-#include "Database/ResourcesContent.h"
-#include "ServerContext.h"
-
-#include <cassert>
-
-namespace Orthanc
-{
-  static const DicomTag PATIENT_IDENTIFIERS[] = 
-  {
-    DICOM_TAG_PATIENT_ID,
-    DICOM_TAG_PATIENT_NAME,
-    DICOM_TAG_PATIENT_BIRTH_DATE
-  };
-
-  static const DicomTag STUDY_IDENTIFIERS[] = 
-  {
-    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 SERIES_IDENTIFIERS[] = 
-  {
-    DICOM_TAG_SERIES_INSTANCE_UID
-  };
-
-  static const DicomTag INSTANCE_IDENTIFIERS[] = 
-  {
-    DICOM_TAG_SOP_INSTANCE_UID
-  };
-
-
-  static void StoreMainDicomTagsInternal(ResourcesContent& target,
-                                         int64_t resource,
-                                         const DicomMap& tags)
-  {
-    DicomArray flattened(tags);
-
-    for (size_t i = 0; i < flattened.GetSize(); i++)
-    {
-      const DicomElement& element = flattened.GetElement(i);
-      const DicomTag& tag = element.GetTag();
-      const DicomValue& value = element.GetValue();
-      if (!value.IsNull() && 
-          !value.IsBinary())
-      {
-        target.AddMainDicomTag(resource, tag, element.GetValue().GetContent());
-      }
-    }
-  }
-
-
-  static void StoreIdentifiers(ResourcesContent& target,
-                               int64_t resource,
-                               ResourceType level,
-                               const DicomMap& map)
-  {
-    const DicomTag* tags;
-    size_t size;
-
-    ServerToolbox::LoadIdentifiers(tags, size, level);
-
-    for (size_t i = 0; i < size; i++)
-    {
-      // The identifiers tags are a subset of the main DICOM tags
-      assert(DicomMap::IsMainDicomTag(tags[i]));
-        
-      const DicomValue* value = map.TestAndGetValue(tags[i]);
-      if (value != NULL &&
-          !value->IsNull() &&
-          !value->IsBinary())
-      {
-        std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent());
-        target.AddIdentifierTag(resource, tags[i], s);
-      }
-    }
-  }
-
-
-  void ResourcesContent::AddResource(int64_t resource,
-                                     ResourceType level,
-                                     const DicomMap& dicomSummary)
-  {
-    StoreIdentifiers(*this, resource, level, dicomSummary);
-
-    DicomMap tags;
-
-    switch (level)
-    {
-      case ResourceType_Patient:
-        dicomSummary.ExtractPatientInformation(tags);
-        break;
-
-      case ResourceType_Study:
-        // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
-        dicomSummary.ExtractPatientInformation(tags);
-        StoreMainDicomTagsInternal(*this, resource, tags);
-
-        dicomSummary.ExtractStudyInformation(tags);
-        break;
-
-      case ResourceType_Series:
-        dicomSummary.ExtractSeriesInformation(tags);
-        break;
-
-      case ResourceType_Instance:
-        dicomSummary.ExtractInstanceInformation(tags);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    StoreMainDicomTagsInternal(*this, resource, tags);
-  }
-
-
-  namespace ServerToolbox
-  {
-    void SimplifyTags(Json::Value& target,
-                      const Json::Value& source,
-                      DicomToJsonFormat format)
-    {
-      if (!source.isObject())
-      {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-      }
-
-      target = Json::objectValue;
-      Json::Value::Members members = source.getMemberNames();
-
-      for (size_t i = 0; i < members.size(); i++)
-      {
-        const Json::Value& v = source[members[i]];
-        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();
-        }
-        else if (type == "TooLong" ||
-                 type == "Null")
-        {
-          target[name] = Json::nullValue;
-        }
-        else if (type == "Sequence")
-        {
-          const Json::Value& array = v["Value"];
-          assert(array.isArray());
-
-          Json::Value children = Json::arrayValue;
-          for (Json::Value::ArrayIndex i = 0; i < array.size(); i++)
-          {
-            Json::Value c;
-            SimplifyTags(c, array[i], format);
-            children.append(c);
-          }
-
-          target[name] = children;
-        }
-        else
-        {
-          assert(0);
-        }
-      }
-    }
-
-
-    bool FindOneChildInstance(int64_t& result,
-                              IDatabaseWrapper& database,
-                              int64_t resource,
-                              ResourceType type)
-    {
-      for (;;)
-      {
-        if (type == ResourceType_Instance)
-        {
-          result = resource;
-          return true;
-        }
-
-        std::list<int64_t> children;
-        database.GetChildrenInternalId(children, resource);
-        if (children.empty())
-        {
-          return false;
-        }
-
-        resource = children.front();
-        type = GetChildResourceType(type);    
-      }
-    }
-
-
-    void ReconstructMainDicomTags(IDatabaseWrapper& database,
-                                  IStorageArea& storageArea,
-                                  ResourceType level)
-    {
-      // 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)
-      {
-        case ResourceType_Patient:
-          plural = "patients";
-          break;
-
-        case ResourceType_Study:
-          plural = "studies";
-          break;
-
-        case ResourceType_Series:
-          plural = "series";
-          break;
-
-        case ResourceType_Instance:
-          plural = "instances";
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_InternalError);
-      }
-
-      LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "...";
-
-      std::list<std::string> resources;
-      database.GetAllPublicIds(resources, level);
-
-      for (std::list<std::string>::const_iterator
-             it = resources.begin(); it != resources.end(); ++it)
-      {
-        // Locate the resource and one of its child instances
-        int64_t resource, instance;
-        ResourceType tmp;
-
-        if (!database.LookupResource(resource, tmp, *it) ||
-            tmp != level ||
-            !FindOneChildInstance(instance, database, resource, level))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Cannot find an instance for " +
-                                 std::string(EnumerationToString(level)) +
-                                 " with identifier " + *it);
-        }
-
-        // Get the DICOM file attached to some instances in the resource
-        FileInfo attachment;
-        if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Cannot retrieve the DICOM file associated with instance " +
-                                 database.GetPublicId(instance));
-        }
-
-        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);
-
-          ResourcesContent tags;
-          tags.AddResource(resource, level, dicomSummary);
-          database.SetResourcesContent(tags);
-        }
-        catch (OrthancException&)
-        {
-          LOG(ERROR) << "Cannot decode the DICOM file with UUID " << attachment.GetUuid()
-                     << " associated with instance " << database.GetPublicId(instance);
-          throw;
-        }
-      }
-    }
-
-
-    void LoadIdentifiers(const DicomTag*& tags,
-                         size_t& size,
-                         ResourceType level)
-    {
-      switch (level)
-      {
-        case ResourceType_Patient:
-          tags = PATIENT_IDENTIFIERS;
-          size = sizeof(PATIENT_IDENTIFIERS) / sizeof(DicomTag);
-          break;
-
-        case ResourceType_Study:
-          tags = STUDY_IDENTIFIERS;
-          size = sizeof(STUDY_IDENTIFIERS) / sizeof(DicomTag);
-          break;
-
-        case ResourceType_Series:
-          tags = SERIES_IDENTIFIERS;
-          size = sizeof(SERIES_IDENTIFIERS) / sizeof(DicomTag);
-          break;
-
-        case ResourceType_Instance:
-          tags = INSTANCE_IDENTIFIERS;
-          size = sizeof(INSTANCE_IDENTIFIERS) / sizeof(DicomTag);
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    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;
-
-      LoadIdentifiers(tags, size, level);
-
-      for (size_t i = 0; i < size; i++)
-      {
-        if (tag == tags[i])
-        {
-          return true;
-        }
-      }
-
-      return false;
-    }
-
-    
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <json/json.h>
-#include <boost/noncopyable.hpp>
-#include <list>
-
-namespace Orthanc
-{
-  class ServerContext;
-  class IDatabaseWrapper;
-  class IStorageArea;
-
-  namespace ServerToolbox
-  {
-    void SimplifyTags(Json::Value& target,
-                      const Json::Value& source,
-                      DicomToJsonFormat format);
-
-    bool FindOneChildInstance(int64_t& result,
-                              IDatabaseWrapper& database,
-                              int64_t resource,
-                              ResourceType type);
-
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,491 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "SliceOrdering.h"
-
-#include "../Core/Logging.h"
-#include "../Core/Toolbox.h"
-#include "ServerEnumerations.h"
-#include "ServerIndex.h"
-
-#include <algorithm>
-#include <boost/lexical_cast.hpp>
-#include <boost/noncopyable.hpp>
-
-
-namespace Orthanc
-{
-  static bool TokenizeVector(std::vector<float>& result,
-                             const std::string& value,
-                             unsigned int expectedSize)
-  {
-    std::vector<std::string> tokens;
-    Toolbox::TokenizeString(tokens, value, '\\');
-
-    if (tokens.size() != expectedSize)
-    {
-      return false;
-    }
-
-    result.resize(tokens.size());
-
-    for (size_t i = 0; i < tokens.size(); i++)
-    {
-      try
-      {
-        result[i] = boost::lexical_cast<float>(tokens[i]);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  static bool TokenizeVector(std::vector<float>& result,
-                             const DicomMap& map,
-                             const DicomTag& tag,
-                             unsigned int expectedSize)
-  {
-    const DicomValue* value = map.TestAndGetValue(tag);
-
-    if (value == NULL ||
-        value->IsNull() ||
-        value->IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      return TokenizeVector(result, value->GetContent(), expectedSize);
-    }
-  }
-
-
-  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_;
-
-  public:
-    Instance(ServerIndex& index,
-             const std::string& instanceId) :
-      instanceId_(instanceId),
-      framesCount_(1)
-    {
-      DicomMap instance;
-      if (!index.GetMainDicomTags(instance, instanceId, ResourceType_Instance, ResourceType_Instance))
-      {
-        throw OrthancException(ErrorCode_UnknownResource);
-      }
-
-      const DicomValue* frames = instance.TestAndGetValue(DICOM_TAG_NUMBER_OF_FRAMES);
-      if (frames != NULL &&
-          !frames->IsNull() &&
-          !frames->IsBinary())
-      {
-        try
-        {
-          framesCount_ = boost::lexical_cast<unsigned int>(frames->GetContent());
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-        }
-      }
-      
-      std::vector<float> tmp;
-      hasPosition_ = TokenizeVector(tmp, instance, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
-
-      if (hasPosition_)
-      {
-        position_[0] = tmp[0];
-        position_[1] = tmp[1];
-        position_[2] = tmp[2];
-      }
-
-      hasNormal_ = ComputeNormal(normal_, instance);
-
-      std::string s;
-      hasIndexInSeries_ = false;
-
-      try
-      {
-        if (index.LookupMetadata(s, instanceId, MetadataType_Instance_IndexInSeries))
-        {
-          indexInSeries_ = boost::lexical_cast<size_t>(s);
-          hasIndexInSeries_ = true;
-        }
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-      }
-    }
-
-    const std::string& GetIdentifier() const
-    {
-      return instanceId_;
-    }
-
-    bool HasPosition() const
-    {
-      return hasPosition_;
-    }
-
-    float ComputeRelativePosition(const Vector& normal) const
-    {
-      assert(HasPosition());
-      return (normal[0] * position_[0] + 
-              normal[1] * position_[1] +
-              normal[2] * position_[2]);
-    }
-
-    bool HasIndexInSeries() const
-    {
-      return hasIndexInSeries_;
-    }
-    
-    size_t GetIndexInSeries() const
-    {
-      assert(HasIndexInSeries());
-      return indexInSeries_;
-    }
-
-    unsigned int GetFramesCount() const
-    {
-      return framesCount_;
-    }
-
-    bool HasNormal() const
-    {
-      return hasNormal_;
-    }
-
-    const Vector& GetNormal() const
-    {
-      assert(hasNormal_);
-      return normal_;
-    }
-  };
-
-
-  class SliceOrdering::PositionComparator
-  {
-  private:
-    const Vector&  normal_;
-
-  public:
-    PositionComparator(const Vector& normal) : normal_(normal)
-    {
-    }
-    
-    int operator() (const Instance* a,
-                    const Instance* b) const
-    {
-      return a->ComputeRelativePosition(normal_) < b->ComputeRelativePosition(normal_);
-    }
-  };
-
-
-  bool SliceOrdering::IndexInSeriesComparator(const SliceOrdering::Instance* a,
-                                              const SliceOrdering::Instance* b)
-  {
-    return a->GetIndexInSeries() < b->GetIndexInSeries();
-  }  
-
-
-  void SliceOrdering::ComputeNormal()
-  {
-    DicomMap series;
-    if (!index_.GetMainDicomTags(series, seriesId_, ResourceType_Series, ResourceType_Series))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-
-    hasNormal_ = ComputeNormal(normal_, series);
-  }
-
-
-  void SliceOrdering::CreateInstances()
-  {
-    std::list<std::string> instancesId;
-    index_.GetChildren(instancesId, seriesId_);
-
-    instances_.reserve(instancesId.size());
-    for (std::list<std::string>::const_iterator
-           it = instancesId.begin(); it != instancesId.end(); ++it)
-    {
-      instances_.push_back(new Instance(index_, *it));
-    }
-  }
-  
-
-  bool SliceOrdering::SortUsingPositions()
-  {
-    if (instances_.size() <= 1)
-    {
-      // One single instance: It is sorted by default
-      return true;
-    }
-
-    if (!hasNormal_)
-    {
-      return false;
-    }
-
-    for (size_t i = 0; i < instances_.size(); i++)
-    {
-      assert(instances_[i] != NULL);
-
-      if (!instances_[i]->HasPosition() ||
-          (instances_[i]->HasNormal() &&
-           !IsParallelOrOpposite(instances_[i]->GetNormal(), normal_)))
-      {
-        return false;
-      }
-    }
-
-    PositionComparator comparator(normal_);
-    std::sort(instances_.begin(), instances_.end(), comparator);
-
-    float a = instances_[0]->ComputeRelativePosition(normal_);
-    for (size_t i = 1; i < instances_.size(); i++)
-    {
-      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;
-    }
-
-    // This is a 3D volume
-    isVolume_ = true;
-    return true;
-  }
-
-
-  bool SliceOrdering::SortUsingIndexInSeries()
-  {
-    if (instances_.size() <= 1)
-    {
-      // One single instance: It is sorted by default
-      return true;
-    }
-
-    for (size_t i = 0; i < instances_.size(); i++)
-    {
-      assert(instances_[i] != NULL);
-      if (!instances_[i]->HasIndexInSeries())
-      {
-        return false;
-      }
-    }
-
-    std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator);
-    
-    for (size_t i = 1; i < instances_.size(); i++)
-    {
-      if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries())
-      {
-        // The current "IndexInSeries" occurs 2 times: Not a proper ordering
-        LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway";
-        break;
-      }
-    }
-
-    return true;
-  }
-
-
-  SliceOrdering::SliceOrdering(ServerIndex& index,
-                               const std::string& seriesId) :
-    index_(index),
-    seriesId_(seriesId),
-    isVolume_(false)
-  {
-    ComputeNormal();
-    CreateInstances();
-
-    if (!SortUsingPositions() &&
-        !SortUsingIndexInSeries())
-    {
-      throw OrthancException(ErrorCode_CannotOrderSlices,
-                             "Unable to order the slices of series " + seriesId);
-    }
-  }
-
-
-  SliceOrdering::~SliceOrdering()
-  {
-    for (std::vector<Instance*>::iterator
-           it = instances_.begin(); it != instances_.end(); ++it)
-    {
-      if (*it != NULL)
-      {
-        delete *it;
-      }
-    }
-  }
-
-
-  const std::string& SliceOrdering::GetInstanceId(size_t index) const
-  {
-    if (index >= instances_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return instances_[index]->GetIdentifier();
-    }
-  }
-
-
-  unsigned int SliceOrdering::GetFramesCount(size_t index) const
-  {
-    if (index >= instances_.size())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return instances_[index]->GetFramesCount();
-    }
-  }
-
-
-  void SliceOrdering::Format(Json::Value& result) const
-  {
-    result = Json::objectValue;
-    result["Type"] = (isVolume_ ? "Volume" : "Sequence");
-    
-    Json::Value tmp = Json::arrayValue;
-    for (size_t i = 0; i < GetInstancesCount(); i++)
-    {
-      tmp.append(GetBasePath(ResourceType_Instance, GetInstanceId(i)) + "/file");
-    }
-
-    result["Dicom"] = tmp;
-
-    Json::Value slicesShort = Json::arrayValue;
-
-    tmp.clear();
-    for (size_t i = 0; i < GetInstancesCount(); i++)
-    {
-      std::string base = GetBasePath(ResourceType_Instance, GetInstanceId(i));
-      for (size_t j = 0; j < GetFramesCount(i); j++)
-      {
-        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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-namespace Orthanc
-{
-  class ServerIndex;
-  
-  class SliceOrdering
-  {
-  private:
-    typedef float Vector[3];
-
-    struct Instance;
-    class  PositionComparator;
-
-    ServerIndex&             index_;
-    std::string              seriesId_;
-    bool                     hasNormal_;
-    Vector                   normal_;
-    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);
-
-    void ComputeNormal();
-
-    void CreateInstances();
-
-    bool SortUsingPositions();
-
-    bool SortUsingIndexInSeries();
-
-  public:
-    SliceOrdering(ServerIndex& index,
-                  const std::string& seriesId);
-
-    ~SliceOrdering();
-
-    size_t  GetInstancesCount() const
-    {
-      return instances_.size();
-    }
-
-    const std::string& GetInstanceId(size_t index) const;
-
-    unsigned int GetFramesCount(size_t index) const;
-
-    void Format(Json::Value& result) const;
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,420 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DatabaseLookup.h"
+
+#include "../../../Core/OrthancException.h"
+#include "../../Search/DicomTagConstraint.h"
+#include "../../ServerToolbox.h"
+#include "SetOfResources.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    namespace
+    {
+      // Anonymous namespace to avoid clashes between compiler modules
+      class MainTagsConstraints : boost::noncopyable
+      {
+      private:
+        std::vector<DicomTagConstraint*>  constraints_;
+
+      public:
+        ~MainTagsConstraints()
+        {
+          for (size_t i = 0; i < constraints_.size(); i++)
+          {
+            assert(constraints_[i] != NULL);
+            delete constraints_[i];
+          }
+        }
+
+        void Reserve(size_t n)
+        {
+          constraints_.reserve(n);
+        }
+
+        size_t GetSize() const
+        {
+          return constraints_.size();
+        }
+
+        DicomTagConstraint& GetConstraint(size_t i) const
+        {
+          if (i >= constraints_.size())
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+          else
+          {
+            assert(constraints_[i] != NULL);
+            return *constraints_[i];
+          }
+        }
+        
+        void Add(const DatabaseConstraint& constraint)
+        {
+          constraints_.push_back(new DicomTagConstraint(constraint));
+        }          
+      };
+    }
+    
+    
+    static void ApplyIdentifierConstraint(SetOfResources& candidates,
+                                          ILookupResources& compatibility,
+                                          const DatabaseConstraint& constraint,
+                                          ResourceType level)
+    {
+      std::list<int64_t> matches;
+
+      switch (constraint.GetConstraintType())
+      {
+        case ConstraintType_Equal:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_Equal, constraint.GetSingleValue());
+          break;
+          
+        case ConstraintType_SmallerOrEqual:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_SmallerOrEqual, constraint.GetSingleValue());
+          break;
+          
+        case ConstraintType_GreaterOrEqual:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_GreaterOrEqual, constraint.GetSingleValue());
+
+          break;
+          
+        case ConstraintType_Wildcard:
+          compatibility.LookupIdentifier(matches, level, constraint.GetTag(),
+                                    IdentifierConstraintType_Wildcard, constraint.GetSingleValue());
+
+          break;
+          
+        case ConstraintType_List:
+          for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+          {
+            std::list<int64_t> tmp;
+            compatibility.LookupIdentifier(tmp, level, constraint.GetTag(),
+                                      IdentifierConstraintType_Wildcard, constraint.GetValue(i));
+            matches.splice(matches.end(), tmp);
+          }
+
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      candidates.Intersect(matches);
+    }
+
+    
+    static void ApplyIdentifierRange(SetOfResources& candidates,
+                                     ILookupResources& compatibility,
+                                     const DatabaseConstraint& smaller,
+                                     const DatabaseConstraint& greater,
+                                     ResourceType level)
+    {
+      assert(smaller.GetConstraintType() == ConstraintType_SmallerOrEqual &&
+             greater.GetConstraintType() == ConstraintType_GreaterOrEqual &&
+             smaller.GetTag() == greater.GetTag() &&
+             ServerToolbox::IsIdentifier(smaller.GetTag(), level));
+
+      std::list<int64_t> matches;
+      compatibility.LookupIdentifierRange(matches, level, smaller.GetTag(),
+                                     greater.GetSingleValue(), smaller.GetSingleValue());
+      candidates.Intersect(matches);
+    }
+
+    
+    static void ApplyLevel(SetOfResources& candidates,
+                           IDatabaseWrapper& database,
+                           ILookupResources& compatibility,
+                           const std::vector<DatabaseConstraint>& lookup,
+                           ResourceType level)
+    {
+      typedef std::set<const DatabaseConstraint*>  SetOfConstraints;
+      typedef std::map<DicomTag, SetOfConstraints> Identifiers;
+
+      // (1) Select which constraints apply to this level, and split
+      // them between "identifier tags" constraints and "main DICOM
+      // tags" constraints
+
+      Identifiers       identifiers;
+      SetOfConstraints  mainTags;
+      
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        if (lookup[i].GetLevel() == level)
+        {
+          if (lookup[i].IsIdentifier())
+          {
+            identifiers[lookup[i].GetTag()].insert(&lookup[i]);
+          }
+          else
+          {
+            mainTags.insert(&lookup[i]);
+          }
+        }
+      }
+
+      
+      // (2) Apply the constraints over the identifiers
+      
+      for (Identifiers::const_iterator it = identifiers.begin();
+           it != identifiers.end(); ++it)
+      {
+        // Check whether some range constraint over identifiers is
+        // present at this level
+        const DatabaseConstraint* smaller = NULL;
+        const DatabaseConstraint* greater = NULL;
+        
+        for (SetOfConstraints::const_iterator it2 = it->second.begin();
+             it2 != it->second.end(); ++it2)
+        {
+          assert(*it2 != NULL);
+        
+          if ((*it2)->GetConstraintType() == ConstraintType_SmallerOrEqual)
+          {
+            smaller = *it2;
+          }
+
+          if ((*it2)->GetConstraintType() == ConstraintType_GreaterOrEqual)
+          {
+            greater = *it2;
+          }
+        }
+
+        if (smaller != NULL &&
+            greater != NULL)
+        {
+          // There is a range constraint: Apply it, as it is more efficient
+          ApplyIdentifierRange(candidates, compatibility, *smaller, *greater, level);
+        }
+        else
+        {
+          smaller = NULL;
+          greater = NULL;
+        }
+
+        for (SetOfConstraints::const_iterator it2 = it->second.begin();
+             it2 != it->second.end(); ++it2)
+        {
+          // Check to avoid applying twice the range constraint
+          if (*it2 != smaller &&
+              *it2 != greater)
+          {
+            ApplyIdentifierConstraint(candidates, compatibility, **it2, level);
+          }
+        }
+      }
+
+
+      // (3) Apply the constraints over the main DICOM tags (no index
+      // here, so this is less efficient than filtering over the
+      // identifiers)
+      if (!mainTags.empty())
+      {
+        MainTagsConstraints c;
+        c.Reserve(mainTags.size());
+        
+        for (SetOfConstraints::const_iterator it = mainTags.begin();
+             it != mainTags.end(); ++it)
+        {
+          assert(*it != NULL);
+          c.Add(**it);
+        }
+
+        std::list<int64_t>  source;
+        candidates.Flatten(compatibility, source);
+        candidates.Clear();
+
+        std::list<int64_t>  filtered;
+        for (std::list<int64_t>::const_iterator candidate = source.begin(); 
+             candidate != source.end(); ++candidate)
+        {
+          DicomMap tags;
+          database.GetMainDicomTags(tags, *candidate);
+
+          bool match = true;
+
+          for (size_t i = 0; i < c.GetSize(); i++)
+          {
+            if (!c.GetConstraint(i).IsMatch(tags))
+            {
+              match = false;
+              break;
+            }
+          }
+        
+          if (match)
+          {
+            filtered.push_back(*candidate);
+          }
+        }
+
+        candidates.Intersect(filtered);
+      }
+    }
+
+
+    static std::string GetOneInstance(IDatabaseWrapper& compatibility,
+                                      int64_t resource,
+                                      ResourceType level)
+    {
+      for (int i = level; i < ResourceType_Instance; i++)
+      {
+        assert(compatibility.GetResourceType(resource) == static_cast<ResourceType>(i));
+
+        std::list<int64_t> children;
+        compatibility.GetChildrenInternalId(children, resource);
+          
+        if (children.empty())
+        {
+          throw OrthancException(ErrorCode_Database);
+        }
+          
+        resource = children.front();
+      }
+
+      return compatibility.GetPublicId(resource);
+    }
+                           
+
+    void DatabaseLookup::ApplyLookupResources(std::list<std::string>& resourcesId,
+                                              std::list<std::string>* instancesId,
+                                              const std::vector<DatabaseConstraint>& lookup,
+                                              ResourceType queryLevel,
+                                              size_t limit)
+    {
+      // This is a re-implementation of
+      // "../../../Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp"
+
+      assert(ResourceType_Patient < ResourceType_Study &&
+             ResourceType_Study < ResourceType_Series &&
+             ResourceType_Series < ResourceType_Instance);
+    
+      ResourceType upperLevel = queryLevel;
+      ResourceType lowerLevel = queryLevel;
+
+      for (size_t i = 0; i < lookup.size(); i++)
+      {
+        ResourceType level = lookup[i].GetLevel();
+
+        if (level < upperLevel)
+        {
+          upperLevel = level;
+        }
+
+        if (level > lowerLevel)
+        {
+          lowerLevel = level;
+        }
+      }
+
+      assert(upperLevel <= queryLevel &&
+             queryLevel <= lowerLevel);
+
+      SetOfResources candidates(database_, upperLevel);
+
+      for (int level = upperLevel; level <= lowerLevel; level++)
+      {
+        ApplyLevel(candidates, database_, compatibility_, lookup, static_cast<ResourceType>(level));
+
+        if (level != lowerLevel)
+        {
+          candidates.GoDown();
+        }
+      }
+
+      std::list<int64_t> resources;
+      candidates.Flatten(compatibility_, resources);
+
+      // Climb up, up to queryLevel
+
+      for (int level = lowerLevel; level > queryLevel; level--)
+      {
+        std::list<int64_t> parents;
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          int64_t parent;
+          if (database_.LookupParent(parent, *it))
+          {
+            parents.push_back(parent);
+          }
+        }
+
+        resources.swap(parents);
+      }
+
+      // Apply the limit, if given
+
+      if (limit != 0 &&
+          resources.size() > limit)
+      {
+        resources.resize(limit);
+      }
+
+      // Get the public ID of all the selected resources
+
+      size_t pos = 0;
+
+      for (std::list<int64_t>::const_iterator
+             it = resources.begin(); it != resources.end(); ++it, pos++)
+      {
+        assert(database_.GetResourceType(*it) == queryLevel);
+
+        const std::string resource = database_.GetPublicId(*it);
+        resourcesId.push_back(resource);
+
+        if (instancesId != NULL)
+        {
+          if (queryLevel == ResourceType_Instance)
+          {
+            // The resource is itself the instance
+            instancesId->push_back(resource);
+          }
+          else
+          {
+            // Collect one child instance for each of the selected resources
+            instancesId->push_back(GetOneInstance(database_, *it, queryLevel));
+          }
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/DatabaseLookup.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../IDatabaseWrapper.h"
+#include "ILookupResources.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class DatabaseLookup : public boost::noncopyable
+    {
+    private:
+      IDatabaseWrapper&  database_;
+      ILookupResources&  compatibility_;
+
+    public:
+      DatabaseLookup(IDatabaseWrapper& database,
+                     ILookupResources& compatibility) :
+        database_(database),
+        compatibility_(compatibility)
+      {
+      }
+
+      void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                std::list<std::string>* instancesId,
+                                const std::vector<DatabaseConstraint>& lookup,
+                                ResourceType queryLevel,
+                                size_t limit);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ICreateInstance.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ICreateInstance.h"
+
+#include "../../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    bool ICreateInstance::Apply(ICreateInstance& database,
+                                IDatabaseWrapper::CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& hashPatient,
+                                const std::string& hashStudy,
+                                const std::string& hashSeries,
+                                const std::string& hashInstance)
+    {
+      {
+        ResourceType type;
+        int64_t tmp;
+        
+        if (database.LookupResource(tmp, type, hashInstance))
+        {
+          // The instance already exists
+          assert(type == ResourceType_Instance);
+          instanceId = tmp;
+          return false;
+        }
+      }
+
+      instanceId = database.CreateResource(hashInstance, ResourceType_Instance);
+
+      result.isNewPatient_ = false;
+      result.isNewStudy_ = false;
+      result.isNewSeries_ = false;
+      result.patientId_ = -1;
+      result.studyId_ = -1;
+      result.seriesId_ = -1;
+      
+      // Detect up to which level the patient/study/series/instance
+      // hierarchy must be created
+
+      {
+        ResourceType dummy;
+
+        if (database.LookupResource(result.seriesId_, dummy, hashSeries))
+        {
+          assert(dummy == ResourceType_Series);
+          // The patient, the study and the series already exist
+
+          bool ok = (database.LookupResource(result.patientId_, dummy, hashPatient) &&
+                     database.LookupResource(result.studyId_, dummy, hashStudy));
+          assert(ok);
+        }
+        else if (database.LookupResource(result.studyId_, dummy, hashStudy))
+        {
+          assert(dummy == ResourceType_Study);
+
+          // New series: The patient and the study already exist
+          result.isNewSeries_ = true;
+
+          bool ok = database.LookupResource(result.patientId_, dummy, hashPatient);
+          assert(ok);
+        }
+        else if (database.LookupResource(result.patientId_, dummy, hashPatient))
+        {
+          assert(dummy == ResourceType_Patient);
+
+          // New study and series: The patient already exist
+          result.isNewStudy_ = true;
+          result.isNewSeries_ = true;
+        }
+        else
+        {
+          // New patient, study and series: Nothing exists
+          result.isNewPatient_ = true;
+          result.isNewStudy_ = true;
+          result.isNewSeries_ = true;
+        }
+      }
+
+      // Create the series if needed
+      if (result.isNewSeries_)
+      {
+        result.seriesId_ = database.CreateResource(hashSeries, ResourceType_Series);
+      }
+
+      // Create the study if needed
+      if (result.isNewStudy_)
+      {
+        result.studyId_ = database.CreateResource(hashStudy, ResourceType_Study);
+      }
+
+      // Create the patient if needed
+      if (result.isNewPatient_)
+      {
+        result.patientId_ = database.CreateResource(hashPatient, ResourceType_Patient);
+      }
+
+      // Create the parent-to-child links
+      database.AttachChild(result.seriesId_, instanceId);
+
+      if (result.isNewSeries_)
+      {
+        database.AttachChild(result.studyId_, result.seriesId_);
+      }
+
+      if (result.isNewStudy_)
+      {
+        database.AttachChild(result.patientId_, result.studyId_);
+      }
+
+      database.TagMostRecentPatient(result.patientId_);
+      
+      // Sanity checks
+      assert(result.patientId_ != -1);
+      assert(result.studyId_ != -1);
+      assert(result.seriesId_ != -1);
+      assert(instanceId != -1);
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ICreateInstance.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ICreateInstance : public boost::noncopyable
+    {
+    public:
+      virtual bool LookupResource(int64_t& id,
+                                  ResourceType& type,
+                                  const std::string& publicId) = 0;
+
+      virtual int64_t CreateResource(const std::string& publicId,
+                                     ResourceType type) = 0;
+
+      virtual void AttachChild(int64_t parent,
+                               int64_t child) = 0;
+
+      virtual void TagMostRecentPatient(int64_t patientId) = 0;
+      
+      static bool Apply(ICreateInstance& database,
+                        IDatabaseWrapper::CreateInstanceResult& result,
+                        int64_t& instanceId,
+                        const std::string& patient,
+                        const std::string& study,
+                        const std::string& series,
+                        const std::string& instance);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IGetChildrenMetadata.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void IGetChildrenMetadata::Apply(IGetChildrenMetadata& database,
+                                     std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata)
+    {
+      // This function comes from an optimization of
+      // "ServerIndex::GetSeriesStatus()" in Orthanc <= 1.5.1
+      // Loop over the instances of this series
+
+      target.clear();
+      
+      std::list<int64_t> children;
+      database.GetChildrenInternalId(children, resourceId);
+
+      for (std::list<int64_t>::const_iterator 
+             it = children.begin(); it != children.end(); ++it)
+      {
+        std::string value;
+        if (database.LookupMetadata(value, *it, metadata))
+        {
+          target.push_back(value);
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/IGetChildrenMetadata.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class IGetChildrenMetadata : public boost::noncopyable
+    {
+    public:
+      virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                         int64_t id) = 0;
+
+      virtual bool LookupMetadata(std::string& target,
+                                  int64_t id,
+                                  MetadataType type) = 0;
+
+      static void Apply(IGetChildrenMetadata& database,
+                        std::list<std::string>& target,
+                        int64_t resourceId,
+                        MetadataType metadata);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ILookupResourceAndParent.h"
+
+#include "../../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    bool ILookupResourceAndParent::Apply(ILookupResourceAndParent& database,
+                                         int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId)
+    {
+      if (!database.LookupResource(id, type, publicId))
+      {
+        return false;
+      }
+      else if (type == ResourceType_Patient)
+      {
+        parentPublicId.clear();
+        return true;
+      }
+      else
+      {
+        int64_t parentId;
+        if (!database.LookupParent(parentId, id))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        parentPublicId = database.GetPublicId(parentId);
+        return true;
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResourceAndParent.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ILookupResourceAndParent : public boost::noncopyable
+    {
+    public:
+      virtual bool LookupResource(int64_t& id,
+                                  ResourceType& type,
+                                  const std::string& publicId) = 0;
+
+      virtual bool LookupParent(int64_t& parentId,
+                                int64_t resourceId) = 0;
+
+      virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+      static bool Apply(ILookupResourceAndParent& database,
+                        int64_t& id,
+                        ResourceType& type,
+                        std::string& parentPublicId,
+                        const std::string& publicId);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ILookupResources.h"
+
+#include "DatabaseLookup.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void ILookupResources::Apply(
+      IDatabaseWrapper& database,
+      ILookupResources& compatibility,
+      std::list<std::string>& resourcesId,
+      std::list<std::string>* instancesId,
+      const std::vector<DatabaseConstraint>& lookup,
+      ResourceType queryLevel,
+      size_t limit)
+    {
+      Compatibility::DatabaseLookup compat(database, compatibility);
+      compat.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ILookupResources.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,78 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    /**
+     * This is a compatibility class that contains database primitives
+     * that were used in Orthanc <= 1.5.1, and that have been removed
+     * during the optimization of the database engine.
+     **/
+    class ILookupResources : public boost::noncopyable
+    {     
+    public:
+      virtual ~ILookupResources()
+      {
+      }
+      
+      virtual void GetAllInternalIds(std::list<int64_t>& target,
+                                     ResourceType resourceType) = 0;
+      
+      virtual void LookupIdentifier(std::list<int64_t>& result,
+                                    ResourceType level,
+                                    const DicomTag& tag,
+                                    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;
+
+      static void Apply(IDatabaseWrapper& database,
+                        ILookupResources& compatibility,
+                        std::list<std::string>& resourcesId,
+                        std::list<std::string>* instancesId,
+                        const std::vector<DatabaseConstraint>& lookup,
+                        ResourceType queryLevel,
+                        size_t limit);
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/ISetResourcesContent.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../ResourcesContent.h"
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ISetResourcesContent : public boost::noncopyable
+    {
+    public:
+      virtual ~ISetResourcesContent()
+      {
+      }
+      
+      virtual void SetMainDicomTag(int64_t id,
+                                   const DicomTag& tag,
+                                   const std::string& value) = 0;
+
+      virtual void SetIdentifierTag(int64_t id,
+                                    const DicomTag& tag,
+                                    const std::string& value) = 0;
+
+      virtual void SetMetadata(int64_t id,
+                               MetadataType type,
+                               const std::string& value) = 0;
+
+      static void Apply(ISetResourcesContent& that,
+                        const ResourcesContent& content)
+      {
+        content.Store(that);
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,169 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "SetOfResources.h"
+
+#include "../../../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    void SetOfResources::Intersect(const std::list<int64_t>& resources)
+    {
+      if (resources_.get() == NULL)
+      {
+        resources_.reset(new Resources);
+
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          resources_->insert(*it);
+        }
+      }
+      else
+      {
+        std::unique_ptr<Resources> filtered(new Resources);
+
+        for (std::list<int64_t>::const_iterator
+               it = resources.begin(); it != resources.end(); ++it)
+        {
+          if (resources_->find(*it) != resources_->end())
+          {
+            filtered->insert(*it);
+          }
+        }
+
+#if __cplusplus < 201103L
+        resources_.reset(filtered.release());
+#else
+        resources_ = std::move(filtered);
+#endif
+      }
+    }
+
+
+    void SetOfResources::GoDown()
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      if (resources_.get() != NULL)
+      {
+        std::unique_ptr<Resources> children(new Resources);
+
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          std::list<int64_t> tmp;
+          database_.GetChildrenInternalId(tmp, *it);
+
+          for (std::list<int64_t>::const_iterator
+                 child = tmp.begin(); child != tmp.end(); ++child)
+          {
+            children->insert(*child);
+          }
+        }
+
+#if __cplusplus < 201103L
+        resources_.reset(children.release());
+#else
+        resources_ = std::move(children);
+#endif
+      }
+
+      switch (level_)
+      {
+        case ResourceType_Patient:
+          level_ = ResourceType_Study;
+          break;
+
+        case ResourceType_Study:
+          level_ = ResourceType_Series;
+          break;
+
+        case ResourceType_Series:
+          level_ = ResourceType_Instance;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+
+    void SetOfResources::Flatten(std::list<std::string>& result)
+    {
+      result.clear();
+      
+      if (resources_.get() == NULL)
+      {
+        // All the resources of this level are part of the filter
+        database_.GetAllPublicIds(result, level_);
+      }
+      else
+      {
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          result.push_back(database_.GetPublicId(*it));
+        }
+      }
+    }
+
+
+    void SetOfResources::Flatten(ILookupResources& compatibility,
+                                 std::list<int64_t>& result)
+    {
+      result.clear();
+      
+      if (resources_.get() == NULL)
+      {
+        // All the resources of this level are part of the filter
+        compatibility.GetAllInternalIds(result, level_);
+      }
+      else
+      {
+        for (Resources::const_iterator it = resources_->begin(); 
+             it != resources_->end(); ++it)
+        {
+          result.push_back(*it);
+        }
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Compatibility/SetOfResources.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../IDatabaseWrapper.h"
+#include "ILookupResources.h"
+
+#include <set>
+#include <memory>
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class SetOfResources : public boost::noncopyable
+    {
+    private:
+      typedef std::set<int64_t>  Resources;
+
+      IDatabaseWrapper&           database_;
+      ResourceType                level_;
+      std::unique_ptr<Resources>  resources_;
+    
+    public:
+      SetOfResources(IDatabaseWrapper& database,
+                     ResourceType level) : 
+        database_(database),
+        level_(level)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      void Intersect(const std::list<int64_t>& resources);
+
+      void GoDown();
+
+      void Flatten(ILookupResources& compatibility,
+                   std::list<int64_t>& result);
+
+      void Flatten(std::list<std::string>& result);
+
+      void Clear()
+      {
+        resources_.reset(NULL);
+      }
+    };
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/IDatabaseListener.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../ServerIndexChange.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class IDatabaseListener : public boost::noncopyable
+  {
+  public:
+    virtual ~IDatabaseListener()
+    {
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType parentType,
+                                         const std::string& publicId) = 0;
+
+    virtual void SignalFileDeleted(const FileInfo& info) = 0;
+
+    virtual void SignalChange(const ServerIndexChange& change) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/IDatabaseWrapper.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,256 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/FileStorage/FileInfo.h"
+#include "../../Core/FileStorage/IStorageArea.h"
+#include "../../Core/SQLite/ITransaction.h"
+
+#include "../ExportedResource.h"
+#include "IDatabaseListener.h"
+
+#include <list>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DatabaseConstraint;
+  class ResourcesContent;
+
+  
+  class IDatabaseWrapper : public boost::noncopyable
+  {
+  public:
+    class ITransaction : public boost::noncopyable
+    {
+    public:
+      virtual ~ITransaction()
+      {
+      }
+
+      virtual void Begin() = 0;
+
+      virtual void Rollback() = 0;
+
+      virtual void Commit(int64_t fileSizeDelta) = 0;
+    };
+
+
+    struct CreateInstanceResult
+    {
+      bool     isNewPatient_;
+      bool     isNewStudy_;
+      bool     isNewSeries_;
+      int64_t  patientId_;
+      int64_t  studyId_;
+      int64_t  seriesId_;
+    };
+
+    virtual ~IDatabaseWrapper()
+    {
+    }
+
+    virtual void Open() = 0;
+
+    virtual void Close() = 0;
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment) = 0;
+
+    virtual void ClearChanges() = 0;
+
+    virtual void ClearExportedResources() = 0;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment) = 0;
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type) = 0;
+
+    virtual void DeleteResource(int64_t id) = 0;
+
+    virtual void FlushToDisk() = 0;
+
+    virtual bool HasFlushToDisk() const = 0;
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType) = 0;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit) = 0;
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults) = 0;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id) = 0;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id) = 0;
+
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults) = 0;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) = 0;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) = 0;
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id) = 0;
+
+    virtual std::string GetPublicId(int64_t resourceId) = 0;
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType) = 0;
+
+    virtual ResourceType 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 ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id) = 0;
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change) = 0;
+
+    virtual void LogExportedResource(const ExportedResource& resource) = 0;
+    
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType) = 0;
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property) = 0;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type) = 0;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId) = 0;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId) = 0;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid) = 0;
+
+    virtual void SetGlobalProperty(GlobalProperty property,
+                                   const std::string& value) = 0;
+
+    virtual void ClearMainDicomTags(int64_t id) = 0;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value) = 0;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected) = 0;
+
+    virtual ITransaction* StartTransaction() = 0;
+
+    virtual void SetListener(IDatabaseListener& listener) = 0;
+
+    virtual unsigned int GetDatabaseVersion() = 0;
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea) = 0;
+
+
+    /**
+     * Primitives introduced in Orthanc 1.5.2
+     **/
+    
+    virtual bool IsDiskSizeAbove(uint64_t threshold) = 0;
+    
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId, // Can be NULL if not needed
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit) = 0;
+
+    // Returns "true" iff. the instance is new and has been inserted
+    // into the database. If "false" is returned, the content of
+    // "result" is undefined, but "instanceId" must be properly
+    // set. This method must also tag the parent patient as the most
+    // recent in the patient recycling order if it is not protected
+    // (so as to fix issue #58).
+    virtual bool CreateInstance(CreateInstanceResult& result, /* out */
+                                int64_t& instanceId,          /* out */
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance) = 0;
+
+    // It is guaranteed that the resources to be modified have no main
+    // DICOM tags, and no DICOM identifiers associated with
+    // them. However, some metadata might be already existing, and
+    // have to be overwritten.
+    virtual void SetResourcesContent(const ResourcesContent& content) = 0;
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata) = 0;
+
+    virtual int64_t GetLastChangeIndex() = 0;
+
+
+    /**
+     * Primitives introduced in Orthanc 1.5.4
+     **/
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/InstallTrackAttachmentsSize.sql	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,22 @@
+CREATE TABLE GlobalIntegers(
+       key INTEGER PRIMARY KEY,
+       value INTEGER);
+
+INSERT INTO GlobalProperties VALUES (6, 1);  -- GlobalProperty_GetTotalSizeIsFast
+
+INSERT INTO GlobalIntegers SELECT 0, IFNULL(SUM(compressedSize), 0) FROM AttachedFiles;
+INSERT INTO GlobalIntegers SELECT 1, IFNULL(SUM(uncompressedSize), 0) FROM AttachedFiles;
+
+CREATE TRIGGER AttachedFileIncrementSize
+AFTER INSERT ON AttachedFiles
+BEGIN
+  UPDATE GlobalIntegers SET value = value + new.compressedSize WHERE key = 0;
+  UPDATE GlobalIntegers SET value = value + new.uncompressedSize WHERE key = 1;
+END;
+
+CREATE TRIGGER AttachedFileDecrementSize
+AFTER DELETE ON AttachedFiles
+BEGIN
+  UPDATE GlobalIntegers SET value = value - old.compressedSize WHERE key = 0;
+  UPDATE GlobalIntegers SET value = value - old.uncompressedSize WHERE key = 1;
+END;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/PrepareDatabase.sql	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,126 @@
+CREATE TABLE GlobalProperties(
+       property INTEGER PRIMARY KEY,
+       value TEXT
+       );
+
+CREATE TABLE Resources(
+       internalId INTEGER PRIMARY KEY AUTOINCREMENT,
+       resourceType INTEGER,
+       publicId TEXT,
+       parentId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE TABLE MainDicomTags(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+-- The following table was added in Orthanc 0.8.5 (database v5)
+CREATE TABLE DicomIdentifiers(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+CREATE TABLE Metadata(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       type INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, type)
+       );
+
+CREATE TABLE AttachedFiles(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       fileType INTEGER,
+       uuid TEXT,
+       compressedSize INTEGER,
+       uncompressedSize INTEGER,
+       compressionType INTEGER,
+       uncompressedMD5 TEXT,  -- New in Orthanc 0.7.3 (database v4)
+       compressedMD5 TEXT,    -- New in Orthanc 0.7.3 (database v4)
+       PRIMARY KEY(id, fileType)
+       );              
+
+CREATE TABLE Changes(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       changeType INTEGER,
+       internalId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       resourceType INTEGER,
+       date TEXT
+       );
+
+CREATE TABLE ExportedResources(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       resourceType INTEGER,
+       publicId TEXT,
+       remoteModality TEXT,
+       patientId TEXT,
+       studyInstanceUid TEXT,
+       seriesInstanceUid TEXT,
+       sopInstanceUid TEXT,
+       date TEXT
+       ); 
+
+CREATE TABLE PatientRecyclingOrder(
+       seq INTEGER PRIMARY KEY AUTOINCREMENT,
+       patientId INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE
+       );
+
+CREATE INDEX ChildrenIndex ON Resources(parentId);
+CREATE INDEX PublicIndex ON Resources(publicId);
+CREATE INDEX ResourceTypeIndex ON Resources(resourceType);
+CREATE INDEX PatientRecyclingIndex ON PatientRecyclingOrder(patientId);
+
+CREATE INDEX MainDicomTagsIndex1 ON MainDicomTags(id);
+-- The 2 following indexes were removed in Orthanc 0.8.5 (database v5), to speed up
+-- CREATE INDEX MainDicomTagsIndex2 ON MainDicomTags(tagGroup, tagElement);
+-- CREATE INDEX MainDicomTagsIndexValues ON MainDicomTags(value COLLATE BINARY);
+
+-- The 3 following indexes were added in Orthanc 0.8.5 (database v5)
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
+
+CREATE INDEX ChangesIndex ON Changes(internalId);
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
+                           old.uncompressedMD5, old.compressedMD5);
+END;
+
+CREATE TRIGGER ResourceDeleted
+AFTER DELETE ON Resources
+BEGIN
+  SELECT SignalResourceDeleted(old.publicId, old.resourceType);  -- New in Orthanc 0.8.5 (db v5)
+  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
+    FROM Resources AS parent WHERE internalId = old.parentId;
+END;
+
+-- Delete a parent resource when its unique child is deleted 
+CREATE TRIGGER ResourceDeletedParentCleaning
+AFTER DELETE ON Resources
+FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
+BEGIN
+  DELETE FROM Resources WHERE internalId = old.parentId;
+END;
+
+CREATE TRIGGER PatientAdded
+AFTER INSERT ON Resources
+FOR EACH ROW WHEN new.resourceType = 1  -- "1" corresponds to "ResourceType_Patient" in C++
+BEGIN
+  INSERT INTO PatientRecyclingOrder VALUES (NULL, new.internalId);
+END;
+
+
+-- Set the version of the database schema
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+INSERT INTO GlobalProperties VALUES (1, "6");
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/ResourcesContent.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ResourcesContent.h"
+
+#include "Compatibility/ISetResourcesContent.h"
+
+#include <cassert>
+
+
+namespace Orthanc
+{
+  void ResourcesContent::Store(Compatibility::ISetResourcesContent& compatibility) const
+  {
+    for (std::list<TagValue>::const_iterator
+           it = tags_.begin(); it != tags_.end(); ++it)
+    {
+      if (it->isIdentifier_)
+      {
+        compatibility.SetIdentifierTag(it->resourceId_, it->tag_,  it->value_);
+      }
+      else
+      {
+        compatibility.SetMainDicomTag(it->resourceId_, it->tag_,  it->value_);
+      }
+    }
+
+    for (std::list<Metadata>::const_iterator
+           it = metadata_.begin(); it != metadata_.end(); ++it)
+    {
+      compatibility.SetMetadata(it->resourceId_, it->metadata_,  it->value_);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/ResourcesContent.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../ServerEnumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <list>
+
+
+namespace Orthanc
+{
+  namespace Compatibility
+  {
+    class ISetResourcesContent;
+  }
+  
+  class ResourcesContent : public boost::noncopyable
+  {
+  public:
+    struct TagValue
+    {
+      int64_t      resourceId_;
+      bool         isIdentifier_;
+      DicomTag     tag_;
+      std::string  value_;
+
+      TagValue(int64_t resourceId,
+               bool isIdentifier,
+               const DicomTag& tag,
+               const std::string& value) :
+        resourceId_(resourceId),
+        isIdentifier_(isIdentifier),
+        tag_(tag),
+        value_(value)
+      {
+      }
+    };
+
+    struct Metadata
+    {
+      int64_t       resourceId_;
+      MetadataType  metadata_;
+      std::string   value_;
+
+      Metadata(int64_t  resourceId,
+               MetadataType metadata,
+               const std::string& value) :
+        resourceId_(resourceId),
+        metadata_(metadata),
+        value_(value)
+      {
+      }
+    };
+
+    typedef std::list<TagValue>  ListTags;
+    typedef std::list<Metadata>  ListMetadata;
+    
+  private:
+    ListTags       tags_;
+    ListMetadata   metadata_;
+
+  public:
+    void AddMainDicomTag(int64_t resourceId,
+                         const DicomTag& tag,
+                         const std::string& value)
+    {
+      tags_.push_back(TagValue(resourceId, false, tag, value));
+    }
+
+    void AddIdentifierTag(int64_t resourceId,
+                          const DicomTag& tag,
+                          const std::string& value)
+    {
+      tags_.push_back(TagValue(resourceId, true, tag, value));
+    }
+
+    void AddMetadata(int64_t resourceId,
+                     MetadataType metadata,
+                     const std::string& value)
+    {
+      metadata_.push_back(Metadata(resourceId, metadata, value));
+    }
+
+    void AddResource(int64_t resource,
+                     ResourceType level,
+                     const DicomMap& dicomSummary);
+
+    // WARNING: The database should be locked with a transaction!
+    void Store(Compatibility::ISetResourcesContent& target) const;
+
+    const ListTags& GetListTags() const
+    {
+      return tags_;
+    }
+
+    const ListMetadata& GetListMetadata() const
+    {
+      return metadata_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1353 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "SQLiteDatabaseWrapper.h"
+
+#include "../../Core/DicomFormat/DicomArray.h"
+#include "../../Core/Logging.h"
+#include "../../Core/SQLite/Transaction.h"
+#include "../Search/ISqlLookupFormatter.h"
+#include "../ServerToolbox.h"
+
+#include <OrthancServerResources.h>
+
+#include <stdio.h>
+#include <boost/lexical_cast.hpp>
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class SignalFileDeleted : public SQLite::IScalarFunction
+    {
+    private:
+      IDatabaseListener& listener_;
+
+    public:
+      SignalFileDeleted(IDatabaseListener& listener) :
+        listener_(listener)
+      {
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalFileDeleted";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 7;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        std::string uncompressedMD5, compressedMD5;
+
+        if (!context.IsNullValue(5))
+        {
+          uncompressedMD5 = context.GetStringValue(5);
+        }
+
+        if (!context.IsNullValue(6))
+        {
+          compressedMD5 = context.GetStringValue(6);
+        }
+
+        FileInfo info(context.GetStringValue(0),
+                      static_cast<FileContentType>(context.GetIntValue(1)),
+                      static_cast<uint64_t>(context.GetInt64Value(2)),
+                      uncompressedMD5,
+                      static_cast<CompressionType>(context.GetIntValue(3)),
+                      static_cast<uint64_t>(context.GetInt64Value(4)),
+                      compressedMD5);
+        
+        listener_.SignalFileDeleted(info);
+      }
+    };
+
+    class SignalResourceDeleted : public SQLite::IScalarFunction
+    {
+    private:
+      IDatabaseListener& listener_;
+
+    public:
+      SignalResourceDeleted(IDatabaseListener& listener) :
+        listener_(listener)
+      {
+      }
+
+      virtual const char* GetName() const
+      {
+        return "SignalResourceDeleted";
+      }
+
+      virtual unsigned int GetCardinality() const
+      {
+        return 2;
+      }
+
+      virtual void Compute(SQLite::FunctionContext& context)
+      {
+        ResourceType type = static_cast<ResourceType>(context.GetIntValue(1));
+        ServerIndexChange change(ChangeType_Deleted, type, context.GetStringValue(0));
+        listener_.SignalChange(change);
+      }
+    };
+
+    class SignalRemainingAncestor : public SQLite::IScalarFunction
+    {
+    private:
+      bool hasRemainingAncestor_;
+      std::string remainingPublicId_;
+      ResourceType 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(SQLite::FunctionContext& context)
+      {
+        VLOG(1) << "There exists a remaining ancestor with public ID \""
+                << context.GetStringValue(0)
+                << "\" of type "
+                << context.GetIntValue(1);
+
+        if (!hasRemainingAncestor_ ||
+            remainingType_ >= context.GetIntValue(1))
+        {
+          hasRemainingAncestor_ = true;
+          remainingPublicId_ = context.GetStringValue(0);
+          remainingType_ = static_cast<ResourceType>(context.GetIntValue(1));
+        }
+      }
+
+      bool HasRemainingAncestor() const
+      {
+        return hasRemainingAncestor_;
+      }
+
+      const std::string& GetRemainingAncestorId() const
+      {
+        assert(hasRemainingAncestor_);
+        return remainingPublicId_;
+      }
+
+      ResourceType GetRemainingAncestorType() const
+      {
+        assert(hasRemainingAncestor_);
+        return remainingType_;
+      }
+    };
+  }
+
+
+  void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::GetChildren(std::list<std::string>& childrenPublicIds,
+                                          int64_t id)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Resources WHERE parentId=?");
+    s.BindInt64(0, id);
+
+    childrenPublicIds.clear();
+    while (s.Step())
+    {
+      childrenPublicIds.push_back(s.ColumnString(0));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::DeleteResource(int64_t id)
+  {
+    signalRemainingAncestor_->Reset();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "DELETE FROM Resources WHERE internalId=?");
+    s.BindInt64(0, id);
+    s.Run();
+
+    if (signalRemainingAncestor_->HasRemainingAncestor() &&
+        listener_ != NULL)
+    {
+      listener_->SignalRemainingAncestor(signalRemainingAncestor_->GetRemainingAncestorType(),
+                                         signalRemainingAncestor_->GetRemainingAncestorId());
+    }
+  }
+
+
+  bool SQLiteDatabaseWrapper::GetParentPublicId(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.internalId = b.parentId AND b.internalId = ?");     
+    s.BindInt64(0, id);
+
+    if (s.Step())
+    {
+      target = s.ColumnString(0);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  int64_t SQLiteDatabaseWrapper::GetTableRecordCount(const std::string& table)
+  {
+    /**
+     * "Generally one cannot use SQL parameters/placeholders for
+     * database identifiers (tables, columns, views, schemas, etc.) or
+     * database functions (e.g., CURRENT_DATE), but instead only for
+     * binding literal values." => To avoid any SQL injection, we
+     * check that the "table" parameter has only alphabetic
+     * characters.
+     * https://stackoverflow.com/a/1274764/881731
+     **/
+    for (size_t i = 0; i < table.size(); i++)
+    {
+      if (!isalpha(table[i]))
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+    // Don't use "SQLITE_FROM_HERE", otherwise "table" would be cached
+    SQLite::Statement s(db_, "SELECT COUNT(*) FROM " + table);
+
+    if (s.Step())
+    {
+      int64_t c = s.ColumnInt(0);
+      assert(!s.Step());
+      return c;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+    
+  SQLiteDatabaseWrapper::SQLiteDatabaseWrapper(const std::string& path) : 
+    listener_(NULL), 
+    signalRemainingAncestor_(NULL),
+    version_(0)
+  {
+    db_.Open(path);
+  }
+
+
+  SQLiteDatabaseWrapper::SQLiteDatabaseWrapper() : 
+    listener_(NULL), 
+    signalRemainingAncestor_(NULL),
+    version_(0)
+  {
+    db_.OpenInMemory();
+  }
+
+
+  int SQLiteDatabaseWrapper::GetGlobalIntegerProperty(GlobalProperty property,
+                                                      int defaultValue)
+  {
+    std::string tmp;
+
+    if (!LookupGlobalProperty(tmp, GlobalProperty_DatabasePatchLevel))
+    {
+      return defaultValue;
+    }
+    else
+    {
+      try
+      {
+        return boost::lexical_cast<int>(tmp);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange,
+                               "Global property " + boost::lexical_cast<std::string>(property) +
+                               " should be an integer, but found: " + tmp);
+      }
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::Open()
+  {
+    db_.Execute("PRAGMA ENCODING=\"UTF-8\";");
+
+    // Performance tuning of SQLite with PRAGMAs
+    // 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");
+
+    // Make "LIKE" case-sensitive in SQLite 
+    db_.Execute("PRAGMA case_sensitive_like = true;");
+    
+    {
+      SQLite::Transaction t(db_);
+      t.Begin();
+
+      if (!db_.DoesTableExist("GlobalProperties"))
+      {
+        LOG(INFO) << "Creating the database";
+        std::string query;
+        ServerResources::GetFileResource(query, ServerResources::PREPARE_DATABASE);
+        db_.Execute(query);
+      }
+
+      // Check the version of the database
+      std::string tmp;
+      if (!LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion))
+      {
+        tmp = "Unknown";
+      }
+
+      bool ok = false;
+      try
+      {
+        LOG(INFO) << "Version of the Orthanc database: " << tmp;
+        version_ = boost::lexical_cast<unsigned int>(tmp);
+        ok = true;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+      }
+
+      if (!ok)
+      {
+        throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                               "Incompatible version of the Orthanc database: " + tmp);
+      }
+
+      // New in Orthanc 1.5.1
+      if (version_ == 6)
+      {
+        if (!LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) ||
+            tmp != "1")
+        {
+          LOG(INFO) << "Installing the SQLite triggers to track the size of the attachments";
+          std::string query;
+          ServerResources::GetFileResource(query, ServerResources::INSTALL_TRACK_ATTACHMENTS_SIZE);
+          db_.Execute(query);
+        }
+      }
+
+      t.Commit();
+    }
+
+    signalRemainingAncestor_ = new Internals::SignalRemainingAncestor;
+    db_.Register(signalRemainingAncestor_);
+  }
+
+
+  static void ExecuteUpgradeScript(SQLite::Connection& db,
+                                   ServerResources::FileResourceId script)
+  {
+    std::string upgrade;
+    ServerResources::GetFileResource(upgrade, script);
+    db.BeginTransaction();
+    db.Execute(upgrade);
+    db.CommitTransaction();    
+  }
+
+
+  void SQLiteDatabaseWrapper::Upgrade(unsigned int targetVersion,
+                                      IStorageArea& storageArea)
+  {
+    if (targetVersion != 6)
+    {
+      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    }
+
+    // This version of Orthanc is only compatible with versions 3, 4,
+    // 5 and 6 of the DB schema
+    if (version_ != 3 &&
+        version_ != 4 &&
+        version_ != 5 &&
+        version_ != 6)
+    {
+      throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
+    }
+
+    if (version_ == 3)
+    {
+      LOG(WARNING) << "Upgrading database version from 3 to 4";
+      ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_3_TO_4);
+      version_ = 4;
+    }
+
+    if (version_ == 4)
+    {
+      LOG(WARNING) << "Upgrading database version from 4 to 5";
+      ExecuteUpgradeScript(db_, ServerResources::UPGRADE_DATABASE_4_TO_5);
+      version_ = 5;
+    }
+
+    if (version_ == 5)
+    {
+      LOG(WARNING) << "Upgrading database version from 5 to 6";
+      // No change in the DB schema, the step from version 5 to 6 only
+      // consists in reconstructing the main DICOM tags information
+      // (as more tags got included).
+      db_.BeginTransaction();
+      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();
+      version_ = 6;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::SetListener(IDatabaseListener& listener)
+  {
+    listener_ = &listener;
+    db_.Register(new Internals::SignalFileDeleted(listener));
+    db_.Register(new Internals::SignalResourceDeleted(listener));
+  }
+
+
+  void SQLiteDatabaseWrapper::ClearTable(const std::string& tableName)
+  {
+    db_.Execute("DELETE FROM " + tableName);    
+  }
+
+
+  bool SQLiteDatabaseWrapper::LookupParent(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())
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (s.ColumnIsNull(0))
+    {
+      return false;
+    }
+    else
+    {
+      parentId = s.ColumnInt(0);
+      return true;
+    }
+  }
+
+
+  ResourceType SQLiteDatabaseWrapper::GetResourceType(int64_t resourceId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT resourceType FROM Resources WHERE internalId=?");
+    s.BindInt64(0, resourceId);
+    
+    if (s.Step())
+    {
+      return static_cast<ResourceType>(s.ColumnInt(0));
+    }
+    else
+    { 
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  std::string SQLiteDatabaseWrapper::GetPublicId(int64_t resourceId)
+  {
+    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
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                                         bool& done /*out*/,
+                                         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);
+    GetChangesInternal(target, done, s, maxResults);
+  }
+
+
+  void SQLiteDatabaseWrapper::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+  {
+    bool done;  // Ignored
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT * FROM Changes ORDER BY seq DESC LIMIT 1");
+    GetChangesInternal(target, done, s, 1);
+  }
+
+
+  class SQLiteDatabaseWrapper::Transaction : public IDatabaseWrapper::ITransaction
+  {
+  private:
+    SQLiteDatabaseWrapper&                that_;
+    std::unique_ptr<SQLite::Transaction>  transaction_;
+    int64_t                               initialDiskSize_;
+
+  public:
+    Transaction(SQLiteDatabaseWrapper& that) :
+      that_(that),
+      transaction_(new SQLite::Transaction(that_.db_))
+    {
+#if defined(NDEBUG)
+      // Release mode
+      initialDiskSize_ = 0;
+#else
+      // Debug mode
+      initialDiskSize_ = static_cast<int64_t>(that_.GetTotalCompressedSize());
+#endif
+    }
+
+    virtual void Begin()
+    {
+      transaction_->Begin();
+    }
+
+    virtual void Rollback() 
+    {
+      transaction_->Rollback();
+    }
+
+    virtual void Commit(int64_t fileSizeDelta /* only used in debug */)
+    {
+      transaction_->Commit();
+
+      assert(initialDiskSize_ + fileSizeDelta >= 0 &&
+             initialDiskSize_ + fileSizeDelta == static_cast<int64_t>(that_.GetTotalCompressedSize()));
+    }
+  };
+
+
+  IDatabaseWrapper::ITransaction* SQLiteDatabaseWrapper::StartTransaction()
+  {
+    return new Transaction(*this);
+  }
+
+
+  void SQLiteDatabaseWrapper::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                             int64_t id)
+  {
+    target.clear();
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT type, value FROM Metadata WHERE id=?");
+    s.BindInt64(0, id);
+
+    while (s.Step())
+    {
+      MetadataType key = static_cast<MetadataType>(s.ColumnInt(0));
+      target[key] = s.ColumnString(1);
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::GetTotalCompressedSize()
+  {
+    // Old SQL query that was used in Orthanc <= 1.5.0:
+    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(compressedSize) FROM AttachedFiles");
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=0");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+    
+  uint64_t SQLiteDatabaseWrapper::GetTotalUncompressedSize()
+  {
+    // Old SQL query that was used in Orthanc <= 1.5.0:
+    // SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT SUM(uncompressedSize) FROM AttachedFiles");
+
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT value FROM GlobalIntegers WHERE key=1");
+    s.Run();
+    return static_cast<uint64_t>(s.ColumnInt64(0));
+  }
+
+
+  uint64_t SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::IsProtectedPatient(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                        "SELECT * FROM PatientRecyclingOrder WHERE patientId = ?");
+    s.BindInt64(0, internalId);
+    return !s.Step();
+  }
+
+
+  void SQLiteDatabaseWrapper::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 SQLiteDatabaseWrapper::IsExistingResource(int64_t internalId)
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT * FROM Resources WHERE internalId=?");
+    s.BindInt64(0, internalId);
+    return s.Step();
+  }
+
+
+  bool SQLiteDatabaseWrapper::IsDiskSizeAbove(uint64_t threshold)
+  {
+    return GetTotalCompressedSize() > threshold;
+  }
+
+
+
+  class SQLiteDatabaseWrapper::LookupFormatter : public ISqlLookupFormatter
+  {
+  private:
+    std::list<std::string>  values_;
+
+  public:
+    virtual std::string GenerateParameter(const std::string& value)
+    {
+      values_.push_back(value);
+      return "?";
+    }
+    
+    virtual std::string FormatResourceType(ResourceType level)
+    {
+      return boost::lexical_cast<std::string>(level);
+    }
+
+    virtual std::string FormatWildcardEscape()
+    {
+      return "ESCAPE '\\'";
+    }
+
+    void Bind(SQLite::Statement& statement) const
+    {
+      size_t pos = 0;
+      
+      for (std::list<std::string>::const_iterator
+             it = values_.begin(); it != values_.end(); ++it, pos++)
+      {
+        statement.BindString(pos, *it);
+      }
+    }
+  };
+
+  
+  static void AnswerLookup(std::list<std::string>& resourcesId,
+                           std::list<std::string>& instancesId,
+                           SQLite::Connection& db,
+                           ResourceType level)
+  {
+    resourcesId.clear();
+    instancesId.clear();
+    
+    std::unique_ptr<SQLite::Statement> statement;
+    
+    switch (level)
+    {
+      case ResourceType_Patient:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE,
+            "SELECT patients.publicId, instances.publicID FROM Lookup AS patients "
+            "INNER JOIN Resources studies ON patients.internalId=studies.parentId "
+            "INNER JOIN Resources series ON studies.internalId=series.parentId "
+            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+            "GROUP BY patients.publicId"));
+      
+        break;
+      }
+
+      case ResourceType_Study:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE,
+            "SELECT studies.publicId, instances.publicID FROM Lookup AS studies "
+            "INNER JOIN Resources series ON studies.internalId=series.parentId "
+            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+            "GROUP BY studies.publicId"));
+      
+        break;
+      }
+
+      case ResourceType_Series:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE,
+            "SELECT series.publicId, instances.publicID FROM Lookup AS series "
+            "INNER JOIN Resources instances ON series.internalId=instances.parentId "
+            "GROUP BY series.publicId"));
+      
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        statement.reset(
+          new SQLite::Statement(
+            db, SQLITE_FROM_HERE, "SELECT publicId, publicId FROM Lookup"));
+        
+        break;
+      }
+      
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    assert(statement.get() != NULL);
+      
+    while (statement->Step())
+    {
+      resourcesId.push_back(statement->ColumnString(0));
+      instancesId.push_back(statement->ColumnString(1));
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::ApplyLookupResources(std::list<std::string>& resourcesId,
+                                                   std::list<std::string>* instancesId,
+                                                   const std::vector<DatabaseConstraint>& lookup,
+                                                   ResourceType queryLevel,
+                                                   size_t limit)
+  {
+    LookupFormatter formatter;
+
+    std::string sql;
+    LookupFormatter::Apply(sql, formatter, lookup, queryLevel, limit);
+
+    sql = "CREATE TEMPORARY TABLE Lookup AS " + sql;
+    
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "DROP TABLE IF EXISTS Lookup");
+      s.Run();
+    }
+
+    {
+      SQLite::Statement statement(db_, sql);
+      formatter.Bind(statement);
+      statement.Run();
+    }
+
+    if (instancesId != NULL)
+    {
+      AnswerLookup(resourcesId, *instancesId, db_, queryLevel);
+    }
+    else
+    {
+      resourcesId.clear();
+    
+      SQLite::Statement s(db_, SQLITE_FROM_HERE, "SELECT publicId FROM Lookup");
+        
+      while (s.Step())
+      {
+        resourcesId.push_back(s.ColumnString(0));
+      }
+    }
+  }
+
+
+  int64_t SQLiteDatabaseWrapper::GetLastChangeIndex()
+  {
+    SQLite::Statement s(db_, SQLITE_FROM_HERE, 
+                        "SELECT seq FROM sqlite_sequence WHERE name='Changes'");
+
+    if (s.Step())
+    {
+      int64_t c = s.ColumnInt(0);
+      assert(!s.Step());
+      return c;
+    }
+    else
+    {
+      // No change has been recorded so far in the database
+      return 0;
+    }
+  }
+
+
+  void SQLiteDatabaseWrapper::TagMostRecentPatient(int64_t patient)
+  {
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "DELETE FROM PatientRecyclingOrder WHERE patientId=?");
+      s.BindInt64(0, patient);
+      s.Run();
+
+      assert(db_.GetLastChangeCount() == 0 ||
+             db_.GetLastChangeCount() == 1);
+      
+      if (db_.GetLastChangeCount() == 0)
+      {
+        // The patient was protected, there was nothing to delete from the recycling order
+        return;
+      }
+    }
+
+    {
+      SQLite::Statement s(db_, SQLITE_FROM_HERE,
+                          "INSERT INTO PatientRecyclingOrder VALUES(NULL, ?)");
+      s.BindInt64(0, patient);
+      s.Run();
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/SQLiteDatabaseWrapper.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,372 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IDatabaseWrapper.h"
+
+#include "../../Core/SQLite/Connection.h"
+#include "Compatibility/ICreateInstance.h"
+#include "Compatibility/IGetChildrenMetadata.h"
+#include "Compatibility/ILookupResourceAndParent.h"
+#include "Compatibility/ISetResourcesContent.h"
+
+namespace Orthanc
+{
+  namespace Internals
+  {
+    class SignalRemainingAncestor;
+  }
+
+  /**
+   * This class manages an instance of the Orthanc SQLite database. It
+   * translates low-level requests into SQL statements. Mutual
+   * exclusion MUST be implemented at a higher level.
+   **/
+  class SQLiteDatabaseWrapper :
+    public IDatabaseWrapper,
+    public Compatibility::ICreateInstance,
+    public Compatibility::IGetChildrenMetadata,
+    public Compatibility::ILookupResourceAndParent,
+    public Compatibility::ISetResourcesContent
+  {
+  private:
+    class Transaction;
+    class LookupFormatter;
+
+    IDatabaseListener* listener_;
+    SQLite::Connection db_;
+    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);
+
+    // Unused => could be removed
+    int GetGlobalIntegerProperty(GlobalProperty property,
+                                 int defaultValue);
+
+  public:
+    SQLiteDatabaseWrapper(const std::string& path);
+
+    SQLiteDatabaseWrapper();
+
+    virtual void Open()
+      ORTHANC_OVERRIDE;
+
+    virtual void Close()
+      ORTHANC_OVERRIDE
+    {
+      db_.Close();
+    }
+
+    virtual void SetListener(IDatabaseListener& listener)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupParent(int64_t& parentId,
+                              int64_t resourceId)
+      ORTHANC_OVERRIDE;
+
+    virtual std::string GetPublicId(int64_t resourceId)
+      ORTHANC_OVERRIDE;
+
+    virtual ResourceType GetResourceType(int64_t resourceId)
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteResource(int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
+                            bool& done /*out*/,
+                            int64_t since,
+                            uint32_t maxResults)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
+      ORTHANC_OVERRIDE;
+
+    virtual IDatabaseWrapper::ITransaction* StartTransaction()
+      ORTHANC_OVERRIDE;
+
+    virtual void FlushToDisk()
+      ORTHANC_OVERRIDE
+    {
+      db_.FlushToDisk();
+    }
+
+    virtual bool HasFlushToDisk() const
+      ORTHANC_OVERRIDE
+    {
+      return true;
+    }
+
+    virtual void ClearChanges()
+      ORTHANC_OVERRIDE
+    {
+      ClearTable("Changes");
+    }
+
+    virtual void ClearExportedResources()
+      ORTHANC_OVERRIDE
+    {
+      ClearTable("ExportedResources");
+    }
+
+    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual unsigned int GetDatabaseVersion()
+      ORTHANC_OVERRIDE
+    {
+      return version_;
+    }
+
+    virtual void Upgrade(unsigned int targetVersion,
+                         IStorageArea& storageArea)
+      ORTHANC_OVERRIDE;
+
+
+    /**
+     * The methods declared below are for unit testing only!
+     **/
+
+    const char* GetErrorMessage() const
+    {
+      return db_.GetErrorMessage();
+    }
+
+    void GetChildren(std::list<std::string>& childrenPublicIds,
+                     int64_t id);
+
+    int64_t GetTableRecordCount(const std::string& table);
+    
+    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)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupGlobalProperty(std::string& target,
+                                      GlobalProperty property)
+      ORTHANC_OVERRIDE;
+
+    virtual int64_t CreateResource(const std::string& publicId,
+                                   ResourceType type)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupResource(int64_t& id,
+                                ResourceType& type,
+                                const std::string& publicId)
+      ORTHANC_OVERRIDE;
+
+    virtual void AttachChild(int64_t parent,
+                             int64_t child)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMetadata(int64_t id,
+                             MetadataType type,
+                             const std::string& value)
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteMetadata(int64_t id,
+                                MetadataType type)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupMetadata(std::string& target,
+                                int64_t id,
+                                MetadataType type)
+      ORTHANC_OVERRIDE;
+
+    virtual void AddAttachment(int64_t id,
+                               const FileInfo& attachment)
+      ORTHANC_OVERRIDE;
+
+    virtual void DeleteAttachment(int64_t id,
+                                  FileContentType attachment)
+      ORTHANC_OVERRIDE;
+
+    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
+                                          int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual bool LookupAttachment(FileInfo& attachment,
+                                  int64_t id,
+                                  FileContentType contentType)
+      ORTHANC_OVERRIDE;
+
+    virtual void ClearMainDicomTags(int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetMainDicomTag(int64_t id,
+                                 const DicomTag& tag,
+                                 const std::string& value)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetIdentifierTag(int64_t id,
+                                  const DicomTag& tag,
+                                  const std::string& value)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetMainDicomTags(DicomMap& map,
+                                  int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenPublicId(std::list<std::string>& target,
+                                     int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetChildrenInternalId(std::list<int64_t>& target,
+                                       int64_t id)
+      ORTHANC_OVERRIDE;
+
+    virtual void LogChange(int64_t internalId,
+                           const ServerIndexChange& change)
+      ORTHANC_OVERRIDE;
+
+    virtual void LogExportedResource(const ExportedResource& resource)
+      ORTHANC_OVERRIDE;
+    
+    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
+                                      bool& done /*out*/,
+                                      int64_t since,
+                                      uint32_t maxResults)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetTotalCompressedSize()
+      ORTHANC_OVERRIDE;
+    
+    virtual uint64_t GetTotalUncompressedSize()
+      ORTHANC_OVERRIDE;
+
+    virtual uint64_t GetResourceCount(ResourceType resourceType)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType)
+      ORTHANC_OVERRIDE;
+
+    virtual void GetAllPublicIds(std::list<std::string>& target,
+                                 ResourceType resourceType,
+                                 size_t since,
+                                 size_t limit)
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId)
+      ORTHANC_OVERRIDE;
+
+    virtual bool SelectPatientToRecycle(int64_t& internalId,
+                                        int64_t patientIdToAvoid)
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsProtectedPatient(int64_t internalId)
+      ORTHANC_OVERRIDE;
+
+    virtual void SetProtectedPatient(int64_t internalId, 
+                                     bool isProtected)
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsExistingResource(int64_t internalId)
+      ORTHANC_OVERRIDE;
+
+    virtual bool IsDiskSizeAbove(uint64_t threshold)
+      ORTHANC_OVERRIDE;
+
+    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
+                                      std::list<std::string>* instancesId,
+                                      const std::vector<DatabaseConstraint>& lookup,
+                                      ResourceType queryLevel,
+                                      size_t limit)
+      ORTHANC_OVERRIDE;
+
+    virtual bool CreateInstance(CreateInstanceResult& result,
+                                int64_t& instanceId,
+                                const std::string& patient,
+                                const std::string& study,
+                                const std::string& series,
+                                const std::string& instance)
+      ORTHANC_OVERRIDE
+    {
+      return ICreateInstance::Apply
+        (*this, result, instanceId, patient, study, series, instance);
+    }
+
+    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
+      ORTHANC_OVERRIDE
+    {
+      ISetResourcesContent::Apply(*this, content);
+    }
+
+    virtual void GetChildrenMetadata(std::list<std::string>& target,
+                                     int64_t resourceId,
+                                     MetadataType metadata)
+      ORTHANC_OVERRIDE
+    {
+      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
+    }
+
+    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
+
+    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
+
+    virtual bool LookupResourceAndParent(int64_t& id,
+                                         ResourceType& type,
+                                         std::string& parentPublicId,
+                                         const std::string& publicId)
+      ORTHANC_OVERRIDE
+    {
+      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Upgrade3To4.sql	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,24 @@
+-- This SQLite script updates the version of the Orthanc database from 3 to 4.
+
+-- Add 2 new columns at "AttachedFiles"
+
+ALTER TABLE AttachedFiles ADD COLUMN uncompressedMD5 TEXT;
+ALTER TABLE AttachedFiles ADD COLUMN compressedMD5 TEXT;
+
+-- Update the "AttachedFileDeleted" trigger
+
+DROP TRIGGER AttachedFileDeleted;
+
+CREATE TRIGGER AttachedFileDeleted
+AFTER DELETE ON AttachedFiles
+BEGIN
+  SELECT SignalFileDeleted(old.uuid, old.fileType, old.uncompressedSize, 
+                           old.compressionType, old.compressedSize,
+                           -- These 2 arguments are new in Orthanc 0.7.3 (database v4)
+                           old.uncompressedMD5, old.compressedMD5);
+END;
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="4" WHERE property=1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Database/Upgrade4To5.sql	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,66 @@
+-- This SQLite script updates the version of the Orthanc database from 4 to 5.
+
+
+-- Remove 2 indexes to speed up
+
+DROP INDEX MainDicomTagsIndex2;
+DROP INDEX MainDicomTagsIndexValues;
+
+
+-- Add a new table to index the DICOM identifiers
+
+CREATE TABLE DicomIdentifiers(
+       id INTEGER REFERENCES Resources(internalId) ON DELETE CASCADE,
+       tagGroup INTEGER,
+       tagElement INTEGER,
+       value TEXT,
+       PRIMARY KEY(id, tagGroup, tagElement)
+       );
+
+CREATE INDEX DicomIdentifiersIndex1 ON DicomIdentifiers(id);
+CREATE INDEX DicomIdentifiersIndex2 ON DicomIdentifiers(tagGroup, tagElement);
+CREATE INDEX DicomIdentifiersIndexValues ON DicomIdentifiers(value COLLATE BINARY);
+
+
+-- Migrate data from MainDicomTags to MainResourcesTags and MainInstancesTags
+
+INSERT INTO DicomIdentifiers SELECT * FROM MainDicomTags
+       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
+              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
+              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
+              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
+              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
+
+DELETE FROM MainDicomTags
+       WHERE ((tagGroup = 16 AND tagElement = 32) OR  -- PatientID (0x0010, 0x0020)
+              (tagGroup = 32 AND tagElement = 13) OR  -- StudyInstanceUID (0x0020, 0x000d)
+              (tagGroup = 8  AND tagElement = 80) OR  -- AccessionNumber (0x0008, 0x0050)
+              (tagGroup = 32 AND tagElement = 14) OR  -- SeriesInstanceUID (0x0020, 0x000e)
+              (tagGroup = 8  AND tagElement = 24));   -- SOPInstanceUID (0x0008, 0x0018)
+
+
+-- Upgrade the "ResourceDeleted" trigger
+
+DROP TRIGGER ResourceDeleted;
+DROP TRIGGER ResourceDeletedParentCleaning;
+
+CREATE TRIGGER ResourceDeleted
+AFTER DELETE ON Resources
+BEGIN
+  SELECT SignalResourceDeleted(old.publicId, old.resourceType);
+  SELECT SignalRemainingAncestor(parent.publicId, parent.resourceType) 
+    FROM Resources AS parent WHERE internalId = old.parentId;
+END;
+
+CREATE TRIGGER ResourceDeletedParentCleaning
+AFTER DELETE ON Resources
+FOR EACH ROW WHEN (SELECT COUNT(*) FROM Resources WHERE parentId = old.parentId) = 0
+BEGIN
+  DELETE FROM Resources WHERE internalId = old.parentId;
+END;
+
+
+-- Change the database version
+-- The "1" corresponds to the "GlobalProperty_DatabaseSchemaVersion" enumeration
+
+UPDATE GlobalProperties SET value="5" WHERE property=1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/DicomInstanceOrigin.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Sources/DicomInstanceOrigin.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/DicomInstanceToStore.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,508 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomInstanceToStore.h"
+
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+
+
+namespace Orthanc
+{
+  // Anonymous namespace to avoid clashes between compilation modules
+  namespace
+  {
+    template <typename T>
+    class SmartContainer
+    {
+    private:
+      T* content_;
+      bool toDelete_;
+      bool isReadOnly_;
+
+      void Deallocate()
+      {
+        if (content_ && toDelete_)
+        {
+          delete content_;
+          toDelete_ = false;
+          content_ = NULL;
+        }
+      }
+
+    public:
+      SmartContainer() : content_(NULL), toDelete_(false), isReadOnly_(true)
+      {
+      }
+
+      ~SmartContainer()
+      {
+        Deallocate();
+      }
+
+      void Allocate()
+      {
+        Deallocate();
+        content_ = new T;
+        toDelete_ = true;
+        isReadOnly_ = false;
+      }
+
+      void TakeOwnership(T* content)
+      {
+        if (content == NULL)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+
+        Deallocate();
+        content_ = content;
+        toDelete_ = true;
+        isReadOnly_ = false;
+      }
+
+      void SetReference(T& content)   // Read and write assign, without transfering ownership
+      {
+        Deallocate();
+        content_ = &content;
+        toDelete_ = false;
+        isReadOnly_ = false;
+      }
+
+      void SetConstReference(const T& content)   // Read-only assign, without transfering ownership
+      {
+        Deallocate();
+        content_ = &const_cast<T&>(content);
+        toDelete_ = false;
+        isReadOnly_ = true;
+      }
+
+      bool HasContent() const
+      {
+        return content_ != NULL;
+      }
+
+      T& GetContent()
+      {
+        if (content_ == NULL)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+
+        if (isReadOnly_)
+        {
+          throw OrthancException(ErrorCode_ReadOnly);
+        }
+
+        return *content_;
+      }
+
+      const T& GetConstContent() const
+      {
+        if (content_ == NULL)
+        {
+          throw OrthancException(ErrorCode_BadSequenceOfCalls);
+        }
+
+        return *content_;
+      }
+    };
+  }
+
+
+  class DicomInstanceToStore::PImpl
+  {
+  public:
+    DicomInstanceOrigin                  origin_;
+    bool                                 hasBuffer_;
+    std::unique_ptr<std::string>         ownBuffer_;
+    const void*                          bufferData_;
+    size_t                               bufferSize_;
+    SmartContainer<ParsedDicomFile>      parsed_;
+    SmartContainer<DicomMap>             summary_;
+    SmartContainer<Json::Value>          json_;
+    MetadataMap                          metadata_;
+
+    PImpl() :
+      hasBuffer_(false),
+      bufferData_(NULL),
+      bufferSize_(0)
+    {
+    }
+
+  private:
+    std::unique_ptr<DicomInstanceHasher>  hasher_;
+
+    void ParseDicomFile()
+    {
+      if (!parsed_.HasContent())
+      {
+        if (!hasBuffer_)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      
+        if (ownBuffer_.get() != NULL)
+        {
+          parsed_.TakeOwnership(new ParsedDicomFile(*ownBuffer_));
+        }
+        else
+        {
+          parsed_.TakeOwnership(new ParsedDicomFile(bufferData_, bufferSize_));
+        }
+      }
+    }
+
+    void ComputeMissingInformation()
+    {
+      if (hasBuffer_ &&
+          summary_.HasContent() &&
+          json_.HasContent())
+      {
+        // Fine, everything is available
+        return; 
+      }
+    
+      if (!hasBuffer_)
+      {
+        if (!parsed_.HasContent())
+        {
+          if (!summary_.HasContent())
+          {
+            throw OrthancException(ErrorCode_NotImplemented);
+          }
+          else
+          {
+            parsed_.TakeOwnership(new ParsedDicomFile(summary_.GetConstContent(),
+                                                      GetDefaultDicomEncoding(),
+                                                      false /* be strict */));
+          }                                
+        }
+
+        // Serialize the parsed DICOM file
+        ownBuffer_.reset(new std::string);
+        if (!FromDcmtkBridge::SaveToMemoryBuffer(*ownBuffer_,
+                                                 *parsed_.GetContent().GetDcmtkObject().getDataset()))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Unable to serialize a DICOM file to a memory buffer");
+        }
+
+        hasBuffer_ = true;
+      }
+
+      if (summary_.HasContent() &&
+          json_.HasContent())
+      {
+        return;
+      }
+
+      // At this point, we know that the DICOM file is available as a
+      // memory buffer, but that its summary or its JSON version is
+      // missing
+
+      ParseDicomFile();
+      assert(parsed_.HasContent());
+
+      // At this point, we have parsed the DICOM file
+    
+      if (!summary_.HasContent())
+      {
+        summary_.Allocate();
+        FromDcmtkBridge::ExtractDicomSummary(summary_.GetContent(), 
+                                             *parsed_.GetContent().GetDcmtkObject().getDataset());
+      }
+    
+      if (!json_.HasContent())
+      {
+        json_.Allocate();
+
+        std::set<DicomTag> ignoreTagLength;
+        FromDcmtkBridge::ExtractDicomAsJson(json_.GetContent(), 
+                                            *parsed_.GetContent().GetDcmtkObject().getDataset(),
+                                            ignoreTagLength);
+      }
+    }
+
+
+  public:
+    void SetBuffer(const void* data,
+                   size_t size)
+    {
+      ownBuffer_.reset(NULL);
+      bufferData_ = data;
+      bufferSize_ = size;
+      hasBuffer_ = true;
+    }
+    
+    const void* GetBufferData()
+    {
+      ComputeMissingInformation();
+
+      if (!hasBuffer_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (ownBuffer_.get() != NULL)
+      {
+        if (ownBuffer_->empty())
+        {
+          return NULL;
+        }
+        else
+        {
+          return ownBuffer_->c_str();
+        }
+      }
+      else
+      {
+        return bufferData_;
+      }
+    }
+
+
+    size_t GetBufferSize()
+    {
+      ComputeMissingInformation();
+    
+      if (!hasBuffer_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (ownBuffer_.get() != NULL)
+      {
+        return ownBuffer_->size();
+      }
+      else
+      {
+        return bufferSize_;
+      }
+    }
+
+
+    const DicomMap& GetSummary()
+    {
+      ComputeMissingInformation();
+    
+      if (!summary_.HasContent())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return summary_.GetConstContent();
+    }
+
+    
+    const Json::Value& GetJson()
+    {
+      ComputeMissingInformation();
+    
+      if (!json_.HasContent())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return json_.GetConstContent();
+    }
+
+
+    DicomInstanceHasher& GetHasher()
+    {
+      if (hasher_.get() == NULL)
+      {
+        hasher_.reset(new DicomInstanceHasher(GetSummary()));
+      }
+
+      if (hasher_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return *hasher_;
+    }
+
+    
+    bool LookupTransferSyntax(std::string& result)
+    {
+      ComputeMissingInformation();
+
+      DicomMap header;
+      if (DicomMap::ParseDicomMetaInformation(header, GetBufferData(), GetBufferSize()))
+      {
+        const DicomValue* value = header.TestAndGetValue(DICOM_TAG_TRANSFER_SYNTAX_UID);
+        if (value != NULL &&
+            !value->IsBinary() &&
+            !value->IsNull())
+        {
+          result = Toolbox::StripSpaces(value->GetContent());
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+
+    ParsedDicomFile& GetParsedDicomFile()
+    {
+      ComputeMissingInformation();
+      ParseDicomFile();
+      
+      if (parsed_.HasContent())
+      {
+        return parsed_.GetContent();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  };
+
+
+  DicomInstanceToStore::DicomInstanceToStore() :
+    pimpl_(new PImpl)
+  {
+  }
+
+
+  void DicomInstanceToStore::SetOrigin(const DicomInstanceOrigin& origin)
+  {
+    pimpl_->origin_ = origin;
+  }
+
+    
+  const DicomInstanceOrigin& DicomInstanceToStore::GetOrigin() const
+  {
+    return pimpl_->origin_;
+  }
+
+    
+  void DicomInstanceToStore::SetBuffer(const void* dicom,
+                                       size_t size)
+  {
+    pimpl_->SetBuffer(dicom, size);
+  }
+
+
+  void DicomInstanceToStore::SetParsedDicomFile(ParsedDicomFile& parsed)
+  {
+    pimpl_->parsed_.SetReference(parsed);
+  }
+
+
+  void DicomInstanceToStore::SetSummary(const DicomMap& summary)
+  {
+    pimpl_->summary_.SetConstReference(summary);
+  }
+
+
+  void DicomInstanceToStore::SetJson(const Json::Value& json)
+  {
+    pimpl_->json_.SetConstReference(json);
+  }
+
+
+  const DicomInstanceToStore::MetadataMap& DicomInstanceToStore::GetMetadata() const
+  {
+    return pimpl_->metadata_;
+  }
+
+
+  DicomInstanceToStore::MetadataMap& DicomInstanceToStore::GetMetadata()
+  {
+    return pimpl_->metadata_;
+  }
+
+
+  void DicomInstanceToStore::AddMetadata(ResourceType level,
+                                         MetadataType metadata,
+                                         const std::string& value)
+  {
+    pimpl_->metadata_[std::make_pair(level, metadata)] = value;
+  }
+
+
+  const void* DicomInstanceToStore::GetBufferData() const
+  {
+    return const_cast<PImpl&>(*pimpl_).GetBufferData();
+  }
+
+
+  size_t DicomInstanceToStore::GetBufferSize() const
+  {
+    return const_cast<PImpl&>(*pimpl_).GetBufferSize();
+  }
+
+
+  const DicomMap& DicomInstanceToStore::GetSummary()
+  {
+    return pimpl_->GetSummary();
+  }
+
+    
+  const Json::Value& DicomInstanceToStore::GetJson() const
+  {
+    return const_cast<PImpl&>(*pimpl_).GetJson();
+  }
+
+
+  bool DicomInstanceToStore::LookupTransferSyntax(std::string& result) const
+  {
+    return const_cast<PImpl&>(*pimpl_).LookupTransferSyntax(result);
+  }
+
+
+  DicomInstanceHasher& DicomInstanceToStore::GetHasher()
+  {
+    return pimpl_->GetHasher();
+  }
+
+  bool DicomInstanceToStore::HasPixelData() const
+  {
+    return const_cast<PImpl&>(*pimpl_).GetParsedDicomFile().HasTag(DICOM_TAG_PIXEL_DATA);
+  }
+
+  ParsedDicomFile& DicomInstanceToStore::GetParsedDicomFile() const
+  {
+    return const_cast<PImpl&>(*pimpl_).GetParsedDicomFile();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/DicomInstanceToStore.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomFormat/DicomMap.h"
+#include "DicomInstanceOrigin.h"
+#include "ServerEnumerations.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class ParsedDicomFile;
+
+  class DicomInstanceToStore : public boost::noncopyable
+  {
+  public:
+    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
+
+  private:
+    class PImpl;
+    boost::shared_ptr<PImpl>  pimpl_;
+
+  public:
+    DicomInstanceToStore();
+
+    void SetOrigin(const DicomInstanceOrigin& origin);
+    
+    const DicomInstanceOrigin& GetOrigin() const;
+
+    // WARNING: The buffer is not copied, it must not be removed as
+    // long as the "DicomInstanceToStore" object is alive
+    void SetBuffer(const void* dicom,
+                   size_t size);
+
+    void SetParsedDicomFile(ParsedDicomFile& parsed);
+
+    void SetSummary(const DicomMap& summary);
+
+    void SetJson(const Json::Value& json);
+
+    const MetadataMap& GetMetadata() const;
+
+    MetadataMap& GetMetadata();
+
+    void AddMetadata(ResourceType level,
+                     MetadataType metadata,
+                     const std::string& value);
+
+    const void* GetBufferData() const;
+
+    size_t GetBufferSize() const;
+
+    const DicomMap& GetSummary();
+    
+    const Json::Value& GetJson() const;
+
+    bool LookupTransferSyntax(std::string& result) const;
+
+    DicomInstanceHasher& GetHasher();
+
+    bool HasPixelData() const;
+
+    ParsedDicomFile& GetParsedDicomFile() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "EmbeddedResourceHttpHandler.h"
+
+#include "../Core/HttpServer/HttpOutput.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+#include "../Core/SystemToolbox.h"
+
+
+namespace Orthanc
+{
+  EmbeddedResourceHttpHandler::EmbeddedResourceHttpHandler(
+    const std::string& baseUri,
+    ServerResources::DirectoryResourceId resourceId)
+  {
+    Toolbox::SplitUriComponents(baseUri_, baseUri);
+    resourceId_ = resourceId;
+  }
+
+
+  bool EmbeddedResourceHttpHandler::Handle(
+    HttpOutput& output,
+    RequestOrigin /*origin*/,
+    const char* /*remoteIp*/,
+    const char* /*username*/,
+    HttpMethod method,
+    const UriComponents& uri,
+    const Arguments& headers,
+    const GetArguments& arguments,
+    const void* /*bodyData*/,
+    size_t /*bodySize*/)
+  {
+    if (!Toolbox::IsChildUri(baseUri_, uri))
+    {
+      // This URI is not served by this handler
+      return false;
+    }
+
+    if (method != HttpMethod_Get)
+    {
+      output.SendMethodNotAllowed("GET");
+      return true;
+    }
+
+    std::string resourcePath = Toolbox::FlattenUri(uri, baseUri_.size());
+    MimeType contentType = SystemToolbox::AutodetectMimeType(resourcePath);
+
+    try
+    {
+      const void* buffer = ServerResources::GetDirectoryResourceBuffer(resourceId_, resourcePath.c_str());
+      size_t size = ServerResources::GetDirectoryResourceSize(resourceId_, resourcePath.c_str());
+
+      output.SetContentType(contentType);
+      output.Answer(buffer, size);
+    }
+    catch (OrthancException&)
+    {
+      LOG(WARNING) << "Unable to find HTTP resource: " << resourcePath;
+      output.SendStatus(HttpStatus_404_NotFound);
+    }
+
+    return true;
+  } 
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/EmbeddedResourceHttpHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/HttpServer/IHttpHandler.h"
+
+#include <OrthancServerResources.h>   // Autogenerated file
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class EmbeddedResourceHttpHandler : public IHttpHandler
+  {
+  private:
+    UriComponents baseUri_;
+    ServerResources::DirectoryResourceId resourceId_;
+
+  public:
+    EmbeddedResourceHttpHandler(
+      const std::string& baseUri,
+      ServerResources::DirectoryResourceId resourceId);
+
+    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                            RequestOrigin origin,
+                                            const char* remoteIp,
+                                            const char* username,
+                                            HttpMethod method,
+                                            const UriComponents& uri,
+                                            const Arguments& headers)
+    {
+      return false;
+    }
+
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& arguments,
+                        const void* /*bodyData*/,
+                        size_t /*bodySize*/);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ExportedResource.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ExportedResource.h"
+
+#include "../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void ExportedResource::Format(Json::Value& item) const
+  {
+    item = Json::objectValue;
+    item["Seq"] = static_cast<int>(seq_);
+    item["ResourceType"] = EnumerationToString(resourceType_);
+    item["ID"] = publicId_;
+    item["Path"] = GetBasePath(resourceType_, publicId_);
+    item["RemoteModality"] = modality_;
+    item["Date"] = date_;
+
+    // WARNING: Do not add "break" below and do not reorder the case items!
+    switch (resourceType_)
+    {
+      case ResourceType_Instance:
+        item["SOPInstanceUID"] = sopInstanceUid_;
+
+      case ResourceType_Series:
+        item["SeriesInstanceUID"] = seriesInstanceUid_;
+
+      case ResourceType_Study:
+        item["StudyInstanceUID"] = studyInstanceUid_;
+
+      case ResourceType_Patient:
+        item["PatientID"] = patientId_;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ExportedResource.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Toolbox.h"
+
+#include <string>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class ExportedResource
+  {
+  private:
+    int64_t      seq_;
+    ResourceType resourceType_;
+    std::string  publicId_;
+    std::string  modality_;
+    std::string  date_;
+    std::string  patientId_;
+    std::string  studyInstanceUid_;
+    std::string  seriesInstanceUid_;
+    std::string  sopInstanceUid_;
+
+  public:
+    ExportedResource(int64_t seq,
+                     ResourceType 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) :
+      seq_(seq),
+      resourceType_(resourceType),
+      publicId_(publicId),
+      modality_(modality),
+      date_(date),
+      patientId_(patientId),
+      studyInstanceUid_(studyInstanceUid),
+      seriesInstanceUid_(seriesInstanceUid),
+      sopInstanceUid_(sopInstanceUid)
+    {
+    }
+
+    int64_t  GetSeq() const
+    {
+      return seq_;
+    }
+
+    ResourceType  GetResourceType() const
+    {
+      return resourceType_;
+    }
+
+    const std::string&  GetPublicId() const
+    {
+      return publicId_;
+    }
+
+    const std::string& GetModality() const
+    {
+      return modality_;
+    }
+
+    const std::string& GetDate() const
+    {
+      return date_;
+    }
+
+    const std::string& GetPatientId() const
+    {
+      return patientId_;
+    }
+
+    const std::string& GetStudyInstanceUid() const
+    {
+      return studyInstanceUid_;
+    }
+
+    const std::string& GetSeriesInstanceUid() const
+    {
+      return seriesInstanceUid_;
+    }
+
+    const std::string& GetSopInstanceUid() const
+    {
+      return sopInstanceUid_;
+    }
+
+    void Format(Json::Value& item) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/IDicomImageDecoder.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/IServerListener.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomInstanceToStore.h"
+#include "ServerIndexChange.h"
+
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class IServerListener : public boost::noncopyable
+  {
+  public:
+    virtual ~IServerListener()
+    {
+    }
+
+    virtual void SignalStoredInstance(const std::string& publicId,
+                                      const DicomInstanceToStore& instance,
+                                      const Json::Value& simplifiedTags) = 0;
+    
+    virtual void SignalChange(const ServerIndexChange& change) = 0;
+
+    virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                        const Json::Value& simplified) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/LuaScripting.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,950 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "LuaScripting.h"
+
+#include "OrthancConfiguration.h"
+#include "OrthancRestApi/OrthancRestApi.h"
+#include "ServerContext.h"
+
+#include "../Core/HttpServer/StringHttpOutput.h"
+#include "../Core/Logging.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+
+#include <OrthancServerResources.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;
+      
+      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()))
+      {
+        std::map<MetadataType, std::string> metadata;
+        that.context_.GetIndex().GetAllMetadata(metadata, change_.GetPublicId());
+        
+        Json::Value formattedMetadata = Json::objectValue;
+
+        for (std::map<MetadataType, std::string>::const_iterator 
+               it = metadata.begin(); it != metadata.end(); ++it)
+        {
+          std::string key = EnumerationToString(it->first);
+          formattedMetadata[key] = it->second;
+        }      
+
+        {
+          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(formattedMetadata);
+            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();
+        }
+      }
+    }
+  };
+
+
+  class LuaScripting::DeleteEvent : public LuaScripting::IEvent
+  {
+  private:
+    ResourceType  level_;
+    std::string   publicId_;
+
+  public:
+    DeleteEvent(ResourceType level,
+                const std::string& publicId) :
+      level_(level),
+      publicId_(publicId)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      std::string functionName;
+      
+      switch (level_)
+      {
+        case ResourceType_Patient:
+          functionName = "OnDeletedPatient";
+          break;
+
+        case ResourceType_Study:
+          functionName = "OnDeletedStudy";
+          break;
+
+        case ResourceType_Series:
+          functionName = "OnDeletedSeries";
+          break;
+
+        case ResourceType_Instance:
+          functionName = "OnDeletedInstance";
+          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(publicId_);
+          call.Execute();
+        }
+      }
+    }
+  };
+
+
+  class LuaScripting::UpdateEvent : public LuaScripting::IEvent
+  {
+  private:
+    ResourceType  level_;
+    std::string   publicId_;
+
+  public:
+    UpdateEvent(ResourceType level,
+                const std::string& publicId) :
+      level_(level),
+      publicId_(publicId)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      std::string functionName;
+      
+      switch (level_)
+      {
+        case ResourceType_Patient:
+          functionName = "OnUpdatedPatient";
+          break;
+
+        case ResourceType_Study:
+          functionName = "OnUpdatedStudy";
+          break;
+
+        case ResourceType_Series:
+          functionName = "OnUpdatedSeries";
+          break;
+
+        case ResourceType_Instance:
+          functionName = "OnUpdatedInstance";
+          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(publicId_);
+          call.Execute();
+        }
+      }
+    }
+  };
+
+
+  ServerContext* LuaScripting::GetServerContext(lua_State *state)
+  {
+    const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext");
+    return const_cast<ServerContext*>(reinterpret_cast<const ServerContext*>(value));
+  }
+
+
+  // Syntax in Lua: RestApiGet(uri, builtin)
+  int LuaScripting::RestApiGet(lua_State *state)
+  {
+    ServerContext* serverContext = GetServerContext(state);
+    if (serverContext == NULL)
+    {
+      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs < 1 || nArgs > 3 || 
+        !lua_isstring(state, 1) ||                 // URI
+        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
+    {
+      LOG(ERROR) << "Lua: Bad parameters to RestApiGet()";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    const char* uri = lua_tostring(state, 1);
+    bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);
+
+    std::map<std::string, std::string> headers;
+    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
+
+    try
+    {
+      std::string result;
+      if (HttpToolbox::SimpleGet(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
+                                 RequestOrigin_Lua, uri, headers))
+      {
+        lua_pushlstring(state, result.c_str(), result.size());
+        return 1;
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: " << e.What();
+    }
+
+    LOG(ERROR) << "Lua: Error in RestApiGet() for URI: " << uri;
+    lua_pushnil(state);
+    return 1;
+  }
+
+
+  int LuaScripting::RestApiPostOrPut(lua_State *state,
+                                     bool isPost)
+  {
+    ServerContext* serverContext = GetServerContext(state);
+    if (serverContext == NULL)
+    {
+      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs < 2 || nArgs > 4 || 
+        !lua_isstring(state, 1) ||                 // URI
+        !lua_isstring(state, 2) ||                 // Body
+        (nArgs >= 3 && !lua_isboolean(state, 3)))  // Restrict to built-in API?
+    {
+      LOG(ERROR) << "Lua: Bad parameters to " << (isPost ? "RestApiPost()" : "RestApiPut()");
+      lua_pushnil(state);
+      return 1;
+    }
+
+    const char* uri = lua_tostring(state, 1);
+    size_t bodySize = 0;
+    const char* bodyData = lua_tolstring(state, 2, &bodySize);
+    bool builtin = (nArgs == 3 ? lua_toboolean(state, 3) != 0 : false);
+
+    std::map<std::string, std::string> headers;
+    LuaContext::GetDictionaryArgument(headers, state, 4, true /* HTTP header key to lower case */);
+        
+    try
+    {
+      std::string result;
+      if (isPost ?
+          HttpToolbox::SimplePost(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
+                                  RequestOrigin_Lua, uri, bodyData, bodySize, headers) :
+          HttpToolbox::SimplePut(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
+                                 RequestOrigin_Lua, uri, bodyData, bodySize, headers))
+      {
+        lua_pushlstring(state, result.c_str(), result.size());
+        return 1;
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: " << e.What();
+    }
+
+    LOG(ERROR) << "Lua: Error in " << (isPost ? "RestApiPost()" : "RestApiPut()") << " for URI: " << uri;
+    lua_pushnil(state);
+    return 1;
+  }
+
+
+  // Syntax in Lua: RestApiPost(uri, body, builtin)
+  int LuaScripting::RestApiPost(lua_State *state)
+  {
+    return RestApiPostOrPut(state, true);
+  }
+
+
+  // Syntax in Lua: RestApiPut(uri, body, builtin)
+  int LuaScripting::RestApiPut(lua_State *state)
+  {
+    return RestApiPostOrPut(state, false);
+  }
+
+
+  // Syntax in Lua: RestApiDelete(uri, builtin)
+  int LuaScripting::RestApiDelete(lua_State *state)
+  {
+    ServerContext* serverContext = GetServerContext(state);
+    if (serverContext == NULL)
+    {
+      LOG(ERROR) << "Lua: The Orthanc API is unavailable";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    // Check the types of the arguments
+    int nArgs = lua_gettop(state);
+    if (nArgs < 1 || nArgs > 3 ||
+        !lua_isstring(state, 1) ||                 // URI
+        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
+    {
+      LOG(ERROR) << "Lua: Bad parameters to RestApiDelete()";
+      lua_pushnil(state);
+      return 1;
+    }
+
+    const char* uri = lua_tostring(state, 1);
+    bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);
+
+    std::map<std::string, std::string> headers;
+    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
+    
+    try
+    {
+      if (HttpToolbox::SimpleDelete(serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
+                                    RequestOrigin_Lua, uri, headers))
+      {
+        lua_pushboolean(state, 1);
+        return 1;
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: " << e.What();
+    }
+
+    LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri;
+    lua_pushnil(state);
+
+    return 1;
+  }
+
+
+  // Syntax in Lua: GetOrthancConfiguration()
+  int LuaScripting::GetOrthancConfiguration(lua_State *state)
+  {
+    Json::Value configuration;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      configuration = lock.GetJson();
+    }
+
+    LuaContext::GetLuaContext(state).PushJson(configuration);
+
+    return 1;
+  }
+
+
+  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 lock.AddDeleteResourceOperation(context_);
+    }
+
+    if (operation == "store-scu")
+    {
+      std::string localAet;
+      if (parameters.isMember("LocalAet"))
+      {
+        localAet = parameters["LocalAet"].asString();
+      }
+      else
+      {
+        localAet = context_.GetDefaultLocalApplicationEntityTitle();
+      }
+
+      std::string name = parameters["Modality"].asString();
+      RemoteModalityParameters modality;
+
+      {
+        OrthancConfiguration::ReaderLock configLock;
+        modality = configLock.GetConfiguration().GetModalityUsingSymbolicName(name);
+      }
+
+      // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()"
+      return lock.AddStoreScuOperation(context_, localAet, modality);
+    }
+
+    if (operation == "store-peer")
+    {
+      OrthancConfiguration::ReaderLock configLock;
+      std::string name = parameters["Peer"].asString();
+
+      WebServiceParameters peer;
+      if (configLock.GetConfiguration().LookupOrthancPeer(peer, name))
+      {
+        return lock.AddStorePeerOperation(peer);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource,
+                               "No peer with symbolic name: " + name);
+      }
+    }
+
+    if (operation == "modify")
+    {
+      std::unique_ptr<DicomModification> modification(new DicomModification);
+      modification->ParseModifyRequest(parameters);
+
+      return lock.AddModifyInstanceOperation(context_, modification.release());
+    }
+
+    if (operation == "call-system")
+    {
+      LOG(INFO) << "Lua script to call system command on " << parameters["Resource"].asString();
+
+      const Json::Value& argsIn = parameters["Arguments"];
+      if (argsIn.type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType);
+      }
+
+      std::vector<std::string> args;
+      args.reserve(argsIn.size());
+      for (Json::Value::ArrayIndex i = 0; i < argsIn.size(); ++i)
+      {
+        // http://jsoncpp.sourceforge.net/namespace_json.html#7d654b75c16a57007925868e38212b4e
+        switch (argsIn[i].type())
+        {
+          case Json::stringValue:
+            args.push_back(argsIn[i].asString());
+            break;
+
+          case Json::intValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asInt()));
+            break;
+
+          case Json::uintValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asUInt()));
+            break;
+
+          case Json::realValue:
+            args.push_back(boost::lexical_cast<std::string>(argsIn[i].asFloat()));
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_BadParameterType);
+        }
+      }
+
+      std::string command = parameters["Command"].asString();
+      std::vector<std::string> postArgs;
+
+      return lock.AddSystemCallOperation(command, args, postArgs);
+    }
+
+    throw OrthancException(ErrorCode_ParameterOutOfRange);
+  }
+
+
+  void LuaScripting::InitializeJob()
+  {
+    lua_.Execute("_InitializeJob()");
+  }
+
+
+  void LuaScripting::SubmitJob()
+  {
+    Json::Value operations;
+    LuaFunctionCall call2(lua_, "_AccessJob");
+    call2.ExecuteToJson(operations, false);
+     
+    if (operations.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    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)
+    {
+      if (operations[i].type() != Json::objectValue ||
+          !operations[i].isMember("Operation"))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      const Json::Value& parameters = 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())
+      {
+        lock.AddDicomInstanceInput(index, context_, resource);
+      }
+      else if (!isFirst)
+      {
+        lock.Connect(previous, index);
+      }
+
+      isFirst = false;
+      previous = index;
+    }
+  }
+
+
+  LuaScripting::LuaScripting(ServerContext& context) : 
+    context_(context),
+    state_(State_Setup)
+  {
+    lua_.SetGlobalVariable("_ServerContext", &context);
+    lua_.RegisterFunction("RestApiGet", RestApiGet);
+    lua_.RegisterFunction("RestApiPost", RestApiPost);
+    lua_.RegisterFunction("RestApiPut", RestApiPut);
+    lua_.RegisterFunction("RestApiDelete", RestApiDelete);
+    lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
+
+    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::EventThread(LuaScripting* that)
+  {
+    for (;;)
+    {
+      std::unique_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 (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();
+        }
+      }
+
+      that->jobManager_.GetDicomConnectionManager().CloseIfInactive();
+    }
+  }
+
+
+  void LuaScripting::Start()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    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;
+    }
+  }
+
+
+  void LuaScripting::Stop()
+  {
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Running)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      state_ = State_Done;
+    }
+
+    jobManager_.AwakeTrailingSleep();
+
+    if (eventThread_.joinable())
+    {
+      LOG(INFO) << "Stopping the Lua engine";
+      eventThread_.join();
+      LOG(INFO) << "The Lua engine has stopped";
+    }
+  }
+
+
+  void LuaScripting::SignalStoredInstance(const std::string& publicId,
+                                          const DicomInstanceToStore& instance,
+                                          const Json::Value& simplifiedTags)
+  {
+    Json::Value metadata = Json::objectValue;
+
+    for (ServerIndex::MetadataMap::const_iterator 
+           it = instance.GetMetadata().begin(); 
+         it != instance.GetMetadata().end(); ++it)
+    {
+      if (it->first.first == ResourceType_Instance)
+      {
+        metadata[EnumerationToString(it->first.second)] = it->second;
+      }
+    }
+
+    pendingEvents_.Enqueue(new OnStoredInstanceEvent(publicId, simplifiedTags, metadata, instance));
+  }
+
+
+  void LuaScripting::SignalChange(const ServerIndexChange& change)
+  {
+    if (change.GetChangeType() == ChangeType_StablePatient ||
+        change.GetChangeType() == ChangeType_StableStudy ||
+        change.GetChangeType() == ChangeType_StableSeries)
+    {
+      pendingEvents_.Enqueue(new StableResourceEvent(change));
+    }
+    else if (change.GetChangeType() == ChangeType_Deleted)
+    {
+      pendingEvents_.Enqueue(new DeleteEvent(change.GetResourceType(), change.GetPublicId()));
+    }
+    else if (change.GetChangeType() == ChangeType_UpdatedAttachment ||
+             change.GetChangeType() == ChangeType_UpdatedMetadata)
+    {
+      pendingEvents_.Enqueue(new UpdateEvent(change.GetResourceType(), change.GetPublicId()));
+    }
+  }
+
+
+  bool LuaScripting::FilterIncomingInstance(const DicomInstanceToStore& instance,
+                                            const Json::Value& simplified)
+  {
+    static const char* NAME = "ReceivedInstanceFilter";
+
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (lua_.IsExistingFunction(NAME))
+    {
+      LuaFunctionCall call(lua_, NAME);
+      call.PushJson(simplified);
+
+      Json::Value origin;
+      instance.GetOrigin().Format(origin);
+      call.PushJson(origin);
+
+      Json::Value info = Json::objectValue;
+      info["HasPixelData"] = instance.HasPixelData();
+
+      std::string s;
+      if (instance.LookupTransferSyntax(s))
+      {
+        info["TransferSyntaxUID"] = s;
+      }
+
+      call.PushJson(info);
+
+      if (!call.ExecutePredicate())
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  void LuaScripting::Execute(const std::string& command)
+  {
+    pendingEvents_.Enqueue(new ExecuteEvent(command));
+  }
+
+
+  void LuaScripting::LoadGlobalConfiguration()
+  {
+    OrthancConfiguration::ReaderLock configLock;
+
+    {
+      std::string command;
+      Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
+      lua_.Execute(command);
+    }    
+
+    std::list<std::string> luaScripts;
+    configLock.GetConfiguration().GetListOfStringsParameter(luaScripts, "LuaScripts");
+
+    LuaScripting::Lock lock(*this);
+
+    for (std::list<std::string>::const_iterator
+           it = luaScripts.begin(); it != luaScripts.end(); ++it)
+    {
+      std::string path = configLock.GetConfiguration().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));
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/LuaScripting.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,145 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomInstanceToStore.h"
+#include "ServerIndexChange.h"
+#include "ServerJobs/LuaJobManager.h"
+
+#include "../Core/MultiThreading/SharedMessageQueue.h"
+#include "../Core/Lua/LuaContext.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+
+  class LuaScripting : public boost::noncopyable
+  {
+  private:
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Done
+    };
+    
+    class ExecuteEvent;
+    class IEvent;
+    class OnStoredInstanceEvent;
+    class StableResourceEvent;
+    class JobEvent;
+    class DeleteEvent;
+    class UpdateEvent;
+
+    static ServerContext* GetServerContext(lua_State *state);
+
+    static int RestApiPostOrPut(lua_State *state,
+                                bool isPost);
+    static int RestApiGet(lua_State *state);
+    static int RestApiPost(lua_State *state);
+    static int RestApiPut(lua_State *state);
+    static int RestApiDelete(lua_State *state);
+    static int GetOrthancConfiguration(lua_State *state);
+
+    size_t ParseOperation(LuaJobManager::Lock& lock,
+                          const std::string& operation,
+                          const Json::Value& parameters);
+
+    void InitializeJob();
+
+    void SubmitJob();
+
+    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 Lock : public boost::noncopyable
+    {
+    private:
+      LuaScripting&                        that_;
+      boost::recursive_mutex::scoped_lock  lock_;
+
+    public:
+      explicit Lock(LuaScripting& that) : 
+        that_(that), 
+        lock_(that.mutex_)
+      {
+      }
+
+      LuaContext& GetLua()
+      {
+        return that_.lua_;
+      }
+    };
+
+    LuaScripting(ServerContext& context);
+
+    ~LuaScripting();
+
+    void Start();
+
+    void Stop();
+    
+    void SignalStoredInstance(const std::string& publicId,
+                              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);
+
+    TimeoutDicomConnectionManager& GetDicomConnectionManager()
+    {
+      return jobManager_.GetDicomConnectionManager();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancConfiguration.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,899 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancConfiguration.h"
+
+#include "../Core/HttpServer/HttpServer.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+#include "../Core/SystemToolbox.h"
+#include "../Core/TemporaryFile.h"
+#include "../Core/Toolbox.h"
+
+#include "ServerIndex.h"
+
+
+static const char* const DICOM_MODALITIES = "DicomModalities";
+static const char* const DICOM_MODALITIES_IN_DB = "DicomModalitiesInDatabase";
+static const char* const ORTHANC_PEERS = "OrthancPeers";
+static const char* const ORTHANC_PEERS_IN_DB = "OrthancPeersInDatabase";
+static const char* const TEMPORARY_DIRECTORY = "TemporaryDirectory";
+
+namespace Orthanc
+{
+  static void AddFileToConfiguration(Json::Value& target,
+                                     const boost::filesystem::path& path)
+  {
+    std::map<std::string, std::string> env;
+    SystemToolbox::GetEnvironmentVariables(env);
+    
+    LOG(WARNING) << "Reading the configuration from: " << path;
+
+    Json::Value config;
+
+    {
+      std::string content;
+      SystemToolbox::ReadFile(content, path.string());
+
+      content = Toolbox::SubstituteVariables(content, env);
+
+      Json::Value tmp;
+      Json::Reader reader;
+      if (!reader.parse(content, tmp) ||
+          tmp.type() != Json::objectValue)
+      {
+        throw OrthancException(ErrorCode_BadJson,
+                               "The configuration file does not follow the JSON syntax: " + path.string());
+      }
+
+      Toolbox::CopyJsonWithoutComments(config, tmp);
+    }
+
+    if (target.size() == 0)
+    {
+      target = config;
+    }
+    else
+    {
+      // Merge the newly-added file with the previous content of "target"
+      Json::Value::Members members = config.getMemberNames();
+      for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
+      {
+        if (target.isMember(members[i]))
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "The configuration section \"" + members[i] +
+                                 "\" is defined in 2 different configuration files");
+        }
+        else
+        {
+          target[members[i]] = config[members[i]];
+        }
+      }
+    }
+  }
+
+    
+  static void ScanFolderForConfiguration(Json::Value& target,
+                                         const char* folder)
+  {
+    using namespace boost::filesystem;
+
+    LOG(WARNING) << "Scanning folder \"" << folder << "\" for configuration files";
+
+    directory_iterator end_it; // default construction yields past-the-end
+    for (directory_iterator it(folder);
+         it != end_it;
+         ++it)
+    {
+      if (!is_directory(it->status()))
+      {
+        std::string extension = boost::filesystem::extension(it->path());
+        Toolbox::ToLowerCase(extension);
+
+        if (extension == ".json")
+        {
+          AddFileToConfiguration(target, it->path().string());
+        }
+      }
+    }
+  }
+
+    
+  static void ReadConfiguration(Json::Value& target,
+                                const char* configurationFile)
+  {
+    target = Json::objectValue;
+
+    if (configurationFile != NULL)
+    {
+      if (!boost::filesystem::exists(configurationFile))
+      {
+        throw OrthancException(ErrorCode_InexistentFile,
+                               "Inexistent path to configuration: " +
+                               std::string(configurationFile));
+      }
+      
+      if (boost::filesystem::is_directory(configurationFile))
+      {
+        ScanFolderForConfiguration(target, configurationFile);
+      }
+      else
+      {
+        AddFileToConfiguration(target, configurationFile);
+      }
+    }
+    else
+    {
+#if ORTHANC_STANDALONE == 1
+      // No default path for the standalone configuration
+      LOG(WARNING) << "Using the default Orthanc configuration";
+      return;
+
+#else
+      // 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";
+
+      AddFileToConfiguration(target, p);
+#endif
+    }
+  }
+
+
+  static void CheckAlphanumeric(const std::string& s)
+  {
+    for (size_t j = 0; j < s.size(); j++)
+    {
+      if (!isalnum(s[j]) && 
+          s[j] != '-')
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Only alphanumeric and dash characters are allowed "
+                               "in the names of modalities/peers, but found: " + s);
+      }
+    }
+  }
+
+
+  void OrthancConfiguration::LoadModalitiesFromJson(const Json::Value& source)
+  {
+    modalities_.clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Bad format of the \"" + std::string(DICOM_MODALITIES) +
+                             "\" configuration section");
+    }
+
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      CheckAlphanumeric(name);
+
+      RemoteModalityParameters modality;
+      modality.Unserialize(source[name]);
+      modalities_[name] = modality;
+    }
+  }
+
+
+  void OrthancConfiguration::LoadPeersFromJson(const Json::Value& source)
+  {
+    peers_.clear();
+
+    if (source.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Bad format of the \"" + std::string(ORTHANC_PEERS) +
+                             "\" configuration section");
+    }
+
+    Json::Value::Members members = source.getMemberNames();
+
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      CheckAlphanumeric(name);
+
+      WebServiceParameters peer;
+      peer.Unserialize(source[name]);
+      peers_[name] = peer;
+    }
+  }
+
+
+  void OrthancConfiguration::LoadModalities()
+  {
+    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
+    {
+      // Modalities are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Modalities, "{}");
+
+        Json::Reader reader;
+        Json::Value modalities;
+        if (reader.parse(property, modalities))
+        {
+          LoadModalitiesFromJson(modalities);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot unserialize the list of modalities from the Orthanc database");
+        }
+      }
+    }
+    else
+    {
+      // Modalities are stored in the configuration files
+      if (json_.isMember(DICOM_MODALITIES))
+      {
+        LoadModalitiesFromJson(json_[DICOM_MODALITIES]);
+      }
+      else
+      {
+        modalities_.clear();
+      }
+    }
+  }
+
+  void OrthancConfiguration::LoadPeers()
+  {
+    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
+    {
+      // Peers are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        std::string property = serverIndex_->GetGlobalProperty(GlobalProperty_Peers, "{}");
+
+        Json::Reader reader;
+        Json::Value peers;
+        if (reader.parse(property, peers))
+        {
+          LoadPeersFromJson(peers);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot unserialize the list of peers from the Orthanc database");
+        }
+      }
+    }
+    else
+    {
+      // Peers are stored in the configuration files
+      if (json_.isMember(ORTHANC_PEERS))
+      {
+        LoadPeersFromJson(json_[ORTHANC_PEERS]);
+      }
+      else
+      {
+        peers_.clear();
+      }
+    }
+  }
+
+
+  void OrthancConfiguration::SaveModalitiesToJson(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it)
+    {
+      Json::Value modality;
+      it->second.Serialize(modality, true /* force advanced format */);
+
+      target[it->first] = modality;
+    }
+  }
+
+    
+  void OrthancConfiguration::SavePeersToJson(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
+    {
+      Json::Value peer;
+      it->second.Serialize(peer, 
+                           false /* use simple format if possible */, 
+                           true  /* include passwords */);
+
+      target[it->first] = peer;
+    }
+  }  
+    
+    
+  void OrthancConfiguration::SaveModalities()
+  {
+    if (GetBooleanParameter(DICOM_MODALITIES_IN_DB, false))
+    {
+      // Modalities are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Json::Value modalities;
+        SaveModalitiesToJson(modalities);
+
+        Json::FastWriter writer;
+        std::string s = writer.write(modalities);
+
+        serverIndex_->SetGlobalProperty(GlobalProperty_Modalities, s);
+      }
+    }
+    else
+    {
+      // Modalities are stored in the configuration files
+      if (!modalities_.empty() ||
+          json_.isMember(DICOM_MODALITIES))
+      {
+        SaveModalitiesToJson(json_[DICOM_MODALITIES]);
+      }
+    }
+  }
+
+
+  void OrthancConfiguration::SavePeers()
+  {
+    if (GetBooleanParameter(ORTHANC_PEERS_IN_DB, false))
+    {
+      // Peers are stored in the database
+      if (serverIndex_ == NULL)
+      {
+        throw Orthanc::OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        Json::Value peers;
+        SavePeersToJson(peers);
+
+        Json::FastWriter writer;
+        std::string s = writer.write(peers);
+
+        serverIndex_->SetGlobalProperty(GlobalProperty_Peers, s);
+      }
+    }
+    else
+    {
+      // Peers are stored in the configuration files
+      if (!peers_.empty() ||
+          json_.isMember(ORTHANC_PEERS))
+      {
+        SavePeersToJson(json_[ORTHANC_PEERS]);
+      }
+    }
+  }
+
+
+  OrthancConfiguration& OrthancConfiguration::GetInstance()
+  {
+    static OrthancConfiguration configuration;
+    return configuration;
+  }
+
+
+  bool OrthancConfiguration::LookupStringParameter(std::string& target,
+                                                   const std::string& parameter) const
+  {
+    if (json_.isMember(parameter))
+    {
+      if (json_[parameter].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType,
+                               "The configuration option \"" + parameter + "\" must be a string");
+      }
+      else
+      {
+        target = json_[parameter].asString();
+        return true;
+      }
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  std::string OrthancConfiguration::GetStringParameter(const std::string& parameter,
+                                                       const std::string& defaultValue) const
+  {
+    std::string value;
+    if (LookupStringParameter(value, parameter))
+    {
+      return value;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+    
+  int OrthancConfiguration::GetIntegerParameter(const std::string& parameter,
+                                                int defaultValue) const
+  {
+    if (json_.isMember(parameter))
+    {
+      if (json_[parameter].type() != Json::intValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType,
+                               "The configuration option \"" + parameter + "\" must be an integer");
+      }
+      else
+      {
+        return json_[parameter].asInt();
+      }
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+    
+  unsigned int OrthancConfiguration::GetUnsignedIntegerParameter(
+    const std::string& parameter,
+    unsigned int defaultValue) const
+  {
+    int v = GetIntegerParameter(parameter, defaultValue);
+
+    if (v < 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "The configuration option \"" + parameter + "\" must be a positive integer");
+    }
+    else
+    {
+      return static_cast<unsigned int>(v);
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupBooleanParameter(bool& target,
+                                                    const std::string& parameter) const
+  {
+    if (json_.isMember(parameter))
+    {
+      if (json_[parameter].type() != Json::booleanValue)
+      {
+        throw OrthancException(ErrorCode_BadParameterType,
+                               "The configuration option \"" + parameter +
+                               "\" must be a Boolean (true or false)");
+      }
+      else
+      {
+        target = json_[parameter].asBool();
+        return true;
+      }
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  bool OrthancConfiguration::GetBooleanParameter(const std::string& parameter,
+                                                 bool defaultValue) const
+  {
+    bool value;
+    if (LookupBooleanParameter(value, parameter))
+    {
+      return value;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  void OrthancConfiguration::Read(const char* configurationFile)
+  {
+    // Read the content of the configuration
+    configurationFileArg_ = configurationFile;
+    ReadConfiguration(json_, configurationFile);
+
+    // Adapt the paths to the configurations
+    defaultDirectory_ = boost::filesystem::current_path();
+    configurationAbsolutePath_ = "";
+
+    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
+    }
+  }
+
+
+  void OrthancConfiguration::LoadModalitiesAndPeers()
+  {
+    LoadModalities();
+    LoadPeers();
+  }
+
+
+  void OrthancConfiguration::RegisterFont(ServerResources::FileResourceId resource)
+  {
+    std::string content;
+    ServerResources::GetFileResource(content, resource);
+    fontRegistry_.AddFromMemory(content);
+  }
+
+
+  void OrthancConfiguration::GetDicomModalityUsingSymbolicName(
+    RemoteModalityParameters& modality,
+    const std::string& name) const
+  {
+    Modalities::const_iterator found = modalities_.find(name);
+
+    if (found == modalities_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "No modality with symbolic name: " + name);
+    }
+    else
+    {
+      modality = found->second;
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupOrthancPeer(WebServiceParameters& peer,
+                                               const std::string& name) const
+  {
+    Peers::const_iterator found = peers_.find(name);
+
+    if (found == peers_.end())
+    {
+      LOG(ERROR) << "No peer with symbolic name: " << name;
+      return false;
+    }
+    else
+    {
+      peer = found->second;
+      return true;
+    }
+  }
+
+
+  void OrthancConfiguration::GetListOfDicomModalities(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (Modalities::const_iterator 
+           it = modalities_.begin(); it != modalities_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  void OrthancConfiguration::GetListOfOrthancPeers(std::set<std::string>& target) const
+  {
+    target.clear();
+
+    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  bool OrthancConfiguration::SetupRegisteredUsers(HttpServer& httpServer) const
+  {
+    httpServer.ClearUsers();
+
+    if (!json_.isMember("RegisteredUsers"))
+    {
+      return false;
+    }
+
+    const Json::Value& users = json_["RegisteredUsers"];
+    if (users.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of users");
+    }
+
+    bool hasUser = false;
+    Json::Value::Members usernames = users.getMemberNames();
+    for (size_t i = 0; i < usernames.size(); i++)
+    {
+      const std::string& username = usernames[i];
+      std::string password = users[username].asString();
+      httpServer.RegisterUser(username.c_str(), password.c_str());
+      hasUser = true;
+    }
+
+    return hasUser;
+  }
+    
+
+  std::string OrthancConfiguration::InterpretStringParameterAsPath(
+    const std::string& parameter) const
+  {
+    return SystemToolbox::InterpretRelativePath(defaultDirectory_.string(), parameter);
+  }
+
+    
+  void OrthancConfiguration::GetListOfStringsParameter(std::list<std::string>& target,
+                                                       const std::string& key) const
+  {
+    target.clear();
+  
+    if (!json_.isMember(key))
+    {
+      return;
+    }
+
+    const Json::Value& lst = json_[key];
+
+    if (lst.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Badly formatted list of strings");
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < lst.size(); i++)
+    {
+      target.push_back(lst[i].asString());
+    }    
+  }
+
+    
+  bool OrthancConfiguration::IsSameAETitle(const std::string& aet1,
+                                           const std::string& aet2) const
+  {
+    if (GetBooleanParameter("StrictAetComparison", false))
+    {
+      // Case-sensitive matching
+      return aet1 == aet2;
+    }
+    else
+    {
+      // Case-insensitive matching (default)
+      std::string tmp1, tmp2;
+      Toolbox::ToLowerCase(tmp1, aet1);
+      Toolbox::ToLowerCase(tmp2, aet2);
+      return tmp1 == tmp2;
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
+                                                             const std::string& aet) const
+  {
+    for (Modalities::const_iterator it = modalities_.begin(); it != modalities_.end(); ++it)
+    {
+      if (IsSameAETitle(aet, it->second.GetApplicationEntityTitle()))
+      {
+        modality = it->second;
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  bool OrthancConfiguration::IsKnownAETitle(const std::string& aet,
+                                            const std::string& ip) const
+  {
+    RemoteModalityParameters modality;
+    
+    if (!LookupDicomModalityUsingAETitle(modality, aet))
+    {
+      LOG(WARNING) << "Modality \"" << aet
+                   << "\" is not listed in the \"DicomModalities\" configuration option";
+      return false;
+    }
+    else if (!GetBooleanParameter("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;
+    }
+  }
+
+
+  RemoteModalityParameters 
+  OrthancConfiguration::GetModalityUsingSymbolicName(const std::string& name) const
+  {
+    RemoteModalityParameters modality;
+    GetDicomModalityUsingSymbolicName(modality, name);
+
+    return modality;
+  }
+
+    
+  RemoteModalityParameters 
+  OrthancConfiguration::GetModalityUsingAet(const std::string& aet) const
+  {
+    RemoteModalityParameters modality;
+      
+    if (LookupDicomModalityUsingAETitle(modality, aet))
+    {
+      return modality;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "Unknown modality for AET: " + aet);
+    }
+  }
+
+    
+  void OrthancConfiguration::UpdateModality(const std::string& symbolicName,
+                                            const RemoteModalityParameters& modality)
+  {
+    CheckAlphanumeric(symbolicName);
+    
+    modalities_[symbolicName] = modality;
+    SaveModalities();
+  }
+
+
+  void OrthancConfiguration::RemoveModality(const std::string& symbolicName)
+  {
+    modalities_.erase(symbolicName);
+    SaveModalities();
+  }
+
+    
+  void OrthancConfiguration::UpdatePeer(const std::string& symbolicName,
+                                        const WebServiceParameters& peer)
+  {
+    CheckAlphanumeric(symbolicName);
+    
+    peer.CheckClientCertificate();
+
+    peers_[symbolicName] = peer;
+    SavePeers();
+  }
+
+
+  void OrthancConfiguration::RemovePeer(const std::string& symbolicName)
+  {
+    peers_.erase(symbolicName);
+    SavePeers();
+  }
+
+
+  void OrthancConfiguration::Format(std::string& result) const
+  {
+    Json::StyledWriter w;
+    result = w.write(json_);
+  }
+
+
+  void OrthancConfiguration::SetDefaultEncoding(Encoding encoding)
+  {
+    SetDefaultDicomEncoding(encoding);
+
+    // Propagate the encoding to the configuration file that is
+    // stored in memory
+    json_["DefaultEncoding"] = EnumerationToString(encoding);
+  }
+
+
+  bool OrthancConfiguration::HasConfigurationChanged() const
+  {
+    Json::Value current;
+    ReadConfiguration(current, configurationFileArg_);
+
+    Json::FastWriter writer;
+    std::string a = writer.write(json_);
+    std::string b = writer.write(current);
+
+    return a != b;
+  }
+
+
+  void OrthancConfiguration::SetServerIndex(ServerIndex& index)
+  {
+    serverIndex_ = &index;
+  }
+
+
+  void OrthancConfiguration::ResetServerIndex()
+  {
+    serverIndex_ = NULL;
+  }
+
+  
+  TemporaryFile* OrthancConfiguration::CreateTemporaryFile() const
+  {
+    if (json_.isMember(TEMPORARY_DIRECTORY))
+    {
+      return new TemporaryFile(InterpretStringParameterAsPath(GetStringParameter(TEMPORARY_DIRECTORY, ".")), "");
+    }
+    else
+    {
+      return new TemporaryFile;
+    }
+  }
+
+
+  std::string OrthancConfiguration::GetDefaultPrivateCreator() const
+  {
+    // New configuration option in Orthanc 1.6.0
+    return GetStringParameter("DefaultPrivateCreator", "");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancConfiguration.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,237 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/FontRegistry.h"
+#include "../Core/WebServiceParameters.h"
+#include "../Core/DicomNetworking/RemoteModalityParameters.h"
+
+#include <OrthancServerResources.h>
+
+#include <boost/filesystem.hpp>
+#include <boost/thread/shared_mutex.hpp>
+#include <boost/thread/lock_types.hpp>
+
+namespace Orthanc
+{
+  class HttpServer;
+  class ServerIndex;
+  class TemporaryFile;
+  
+  class OrthancConfiguration : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, RemoteModalityParameters>   Modalities;
+    typedef std::map<std::string, WebServiceParameters>       Peers;
+
+    boost::shared_mutex      mutex_;
+    Json::Value              json_;
+    boost::filesystem::path  defaultDirectory_;
+    std::string              configurationAbsolutePath_;
+    FontRegistry             fontRegistry_;
+    const char*              configurationFileArg_;
+    Modalities               modalities_;
+    Peers                    peers_;
+    ServerIndex*             serverIndex_;
+
+    OrthancConfiguration() :
+      configurationFileArg_(NULL)
+    {
+    }
+
+    void LoadModalitiesFromJson(const Json::Value& source);
+    
+    void LoadPeersFromJson(const Json::Value& source);
+    
+    void LoadModalities();
+    
+    void LoadPeers();
+    
+    void SaveModalitiesToJson(Json::Value& target);
+    
+    void SavePeersToJson(Json::Value& target);
+    
+    void SaveModalities();
+    
+    void SavePeers();
+
+    static OrthancConfiguration& GetInstance();
+
+  public:
+    class ReaderLock : public boost::noncopyable
+    {
+    private:
+      OrthancConfiguration&                    configuration_;
+      boost::shared_lock<boost::shared_mutex>  lock_;
+
+    public:
+      ReaderLock() :
+        configuration_(GetInstance()),
+        lock_(configuration_.mutex_)
+      {
+      }
+
+      const OrthancConfiguration& GetConfiguration() const
+      {
+        return configuration_;
+      }
+
+      const Json::Value& GetJson() const
+      {
+        return configuration_.json_;
+      }
+    };
+
+
+    class WriterLock : public boost::noncopyable
+    {
+    private:
+      OrthancConfiguration&                    configuration_;
+      boost::unique_lock<boost::shared_mutex>  lock_;
+
+    public:
+      WriterLock() :
+        configuration_(GetInstance()),
+        lock_(configuration_.mutex_)
+      {
+      }
+
+      OrthancConfiguration& GetConfiguration()
+      {
+        return configuration_;
+      }
+
+      const OrthancConfiguration& GetConfiguration() const
+      {
+        return configuration_;
+      }
+
+      const Json::Value& GetJson() const
+      {
+        return configuration_.json_;
+      }
+    };
+
+
+    const std::string& GetConfigurationAbsolutePath() const
+    {
+      return configurationAbsolutePath_;
+    }
+
+    const FontRegistry& GetFontRegistry() const
+    {
+      return fontRegistry_;
+    }
+
+    void Read(const char* configurationFile);
+
+    void LoadModalitiesAndPeers();
+    
+    void RegisterFont(ServerResources::FileResourceId resource);
+
+    bool LookupStringParameter(std::string& target,
+                               const std::string& parameter) const;
+
+    std::string GetStringParameter(const std::string& parameter,
+                                   const std::string& defaultValue) const;
+    
+    int GetIntegerParameter(const std::string& parameter,
+                            int defaultValue) const;
+    
+    unsigned int GetUnsignedIntegerParameter(const std::string& parameter,
+                                             unsigned int defaultValue) const;
+
+    bool LookupBooleanParameter(bool& target,
+                                const std::string& parameter) const;
+
+    bool GetBooleanParameter(const std::string& parameter,
+                             bool defaultValue) const;
+
+    void GetDicomModalityUsingSymbolicName(RemoteModalityParameters& modality,
+                                           const std::string& name) const;
+
+    bool LookupOrthancPeer(WebServiceParameters& peer,
+                           const std::string& name) const;
+
+    void GetListOfDicomModalities(std::set<std::string>& target) const;
+
+    void GetListOfOrthancPeers(std::set<std::string>& target) const;
+
+    // Returns "true" iff. at least one user is registered
+    bool SetupRegisteredUsers(HttpServer& httpServer) const;
+
+    std::string InterpretStringParameterAsPath(const std::string& parameter) const;
+    
+    void GetListOfStringsParameter(std::list<std::string>& target,
+                                   const std::string& key) const;
+    
+    bool IsSameAETitle(const std::string& aet1,
+                       const std::string& aet2) const;
+
+    bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
+                                         const std::string& aet) const;
+
+    bool IsKnownAETitle(const std::string& aet,
+                        const std::string& ip) const;
+
+    RemoteModalityParameters GetModalityUsingSymbolicName(const std::string& name) const;
+    
+    RemoteModalityParameters GetModalityUsingAet(const std::string& aet) const;
+    
+    void UpdateModality(const std::string& symbolicName,
+                        const RemoteModalityParameters& modality);
+
+    void RemoveModality(const std::string& symbolicName);
+    
+    void UpdatePeer(const std::string& symbolicName,
+                    const WebServiceParameters& peer);
+
+    void RemovePeer(const std::string& symbolicName);
+
+
+    void Format(std::string& result) const;
+    
+    void SetDefaultEncoding(Encoding encoding);
+
+    bool HasConfigurationChanged() const;
+
+    void SetServerIndex(ServerIndex& index);
+
+    void ResetServerIndex();
+
+    TemporaryFile* CreateTemporaryFile() const;
+
+    std::string GetDefaultPrivateCreator() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,733 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancFindRequestHandler.h"
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/Logging.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+#include "../Core/MetricsRegistry.h"
+#include "OrthancConfiguration.h"
+#include "Search/DatabaseLookup.h"
+#include "ServerContext.h"
+#include "ServerToolbox.h"
+
+#include <boost/regex.hpp> 
+
+
+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 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;
+
+      for (std::list<std::string>::const_iterator
+             it = series.begin(); it != series.end(); ++it)
+      {
+        DicomMap tags;
+        if (index.GetMainDicomTags(tags, *it, ResourceType_Series, ResourceType_Series))
+        {
+          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
+
+          if (value != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            values.insert(value->GetContent());
+          }
+        }
+      }
+
+      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;
+
+      for (std::list<std::string>::const_iterator
+             it = instances.begin(); it != instances.end(); ++it)
+      {
+        std::string value;
+        if (context.LookupOrReconstructMetadata(value, *it, MetadataType_Instance_SopClassUid))
+        {
+          values.insert(value);
+        }
+      }
+
+      StoreSetOfStrings(result, DICOM_TAG_SOP_CLASSES_IN_STUDY, values);
+    }
+  }
+
+
+  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::unique_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 DicomMap& mainDicomTags,
+                        const Json::Value* dicomAsJson,
+                        const DicomArray& query,
+                        const std::list<DicomTag>& sequencesToReturn,
+                        const DicomMap* counters,
+                        const std::string& defaultPrivateCreator,
+                        const std::map<uint16_t, std::string>& privateCreators)
+  {
+    DicomMap match;
+
+    if (dicomAsJson != NULL)
+    {
+      match.FromDicomAsJson(*dicomAsJson);
+    }
+    else
+    {
+      match.Assign(mainDicomTags);
+    }
+    
+    DicomMap result;
+
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      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
+      {
+        const DicomTag& tag = query.GetElement(i).GetTag();
+        const DicomValue* value = match.TestAndGetValue(tag);
+
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          result.SetValue(tag, value->GetContent(), false);
+        }
+        else
+        {
+          result.SetValue(tag, "", false);
+        }
+      }
+    }
+
+    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 if (dicomAsJson == NULL)
+    {
+      LOG(WARNING) << "C-FIND query requesting a sequence, but reading JSON from disk is disabled";
+      answers.Add(result);
+    }
+    else
+    {
+      ParsedDicomFile dicom(result, GetDefaultDicomEncoding(),
+                            true /* be permissive, cf. issue #136 */, defaultPrivateCreator, privateCreators);
+
+      for (std::list<DicomTag>::const_iterator tag = sequencesToReturn.begin();
+           tag != sequencesToReturn.end(); ++tag)
+      {
+        assert(dicomAsJson != NULL);
+        const Json::Value& source = (*dicomAsJson) [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);
+          }
+
+          if (tag->IsPrivate())
+          {
+            std::map<uint16_t, std::string>::const_iterator found = privateCreators.find(tag->GetGroup());
+            
+            if (found != privateCreators.end())
+            {
+              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, found->second.c_str());
+            }
+            else
+            {
+              dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, defaultPrivateCreator);
+            }
+          }
+          else
+          {
+            dicom.Replace(*tag, content, false, DicomReplaceMode_InsertIfAbsent, "" /* no private creator */);
+          }
+        }
+      }
+
+      answers.Add(dicom);
+    }
+  }
+
+
+
+  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)
+  {
+  }
+
+
+  class OrthancFindRequestHandler::LookupVisitor : public ServerContext::ILookupVisitor
+  {
+  private:
+    DicomFindAnswers&           answers_;
+    ServerContext&              context_;
+    ResourceType                level_;
+    const DicomMap&             query_;
+    DicomArray                  queryAsArray_;
+    const std::list<DicomTag>&  sequencesToReturn_;
+    std::string                 defaultPrivateCreator_;       // the private creator to use if the group is not defined in the query itself
+    const std::map<uint16_t, std::string>& privateCreators_;  // the private creators defined in the query itself
+
+  public:
+    LookupVisitor(DicomFindAnswers&  answers,
+                  ServerContext& context,
+                  ResourceType level,
+                  const DicomMap& query,
+                  const std::list<DicomTag>& sequencesToReturn,
+                  const std::map<uint16_t, std::string>& privateCreators) :
+      answers_(answers),
+      context_(context),
+      level_(level),
+      query_(query),
+      queryAsArray_(query),
+      sequencesToReturn_(sequencesToReturn),
+      privateCreators_(privateCreators)
+    {
+      answers_.SetComplete(false);
+
+      {
+        OrthancConfiguration::ReaderLock lock;
+        defaultPrivateCreator_ = lock.GetConfiguration().GetDefaultPrivateCreator();
+      }
+    }
+
+    virtual bool IsDicomAsJsonNeeded() const
+    {
+      // Ask the "DICOM-as-JSON" attachment only if sequences are to
+      // be returned OR if "query_" contains non-main DICOM tags!
+
+      DicomMap withoutSpecialTags;
+      withoutSpecialTags.Assign(query_);
+
+      // Check out "ComputeCounters()"
+      withoutSpecialTags.Remove(DICOM_TAG_MODALITIES_IN_STUDY);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES);
+      withoutSpecialTags.Remove(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES);
+      withoutSpecialTags.Remove(DICOM_TAG_SOP_CLASSES_IN_STUDY);
+
+      // Check out "AddAnswer()"
+      withoutSpecialTags.Remove(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+      withoutSpecialTags.Remove(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+      
+      return (!sequencesToReturn_.empty() ||
+              !withoutSpecialTags.HasOnlyMainDicomTags());
+    }
+      
+    virtual void MarkAsComplete()
+    {
+      answers_.SetComplete(true);
+    }
+
+    virtual void Visit(const std::string& publicId,
+                       const std::string& instanceId,
+                       const DicomMap& mainDicomTags,
+                       const Json::Value* dicomAsJson) 
+    {
+      std::unique_ptr<DicomMap> counters(ComputeCounters(context_, instanceId, level_, query_));
+
+      AddAnswer(answers_, mainDicomTags, dicomAsJson,
+                queryAsArray_, sequencesToReturn_, counters.get(), defaultPrivateCreator_, privateCreators_);
+    }
+  };
+
+
+  void OrthancFindRequestHandler::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)
+  {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms");
+
+    /**
+     * Possibly apply the user-supplied Lua filter.
+     **/
+
+    DicomMap lua;
+    const DicomMap* filteredInput = &input;
+
+    if (ApplyLuaFilter(lua, input, remoteIp, remoteAet, calledAet, manufacturer))
+    {
+      filteredInput = &lua;
+    }
+
+
+    /**
+     * Retrieve the query level.
+     **/
+
+    assert(filteredInput != NULL);
+    const DicomValue* levelTmp = filteredInput->TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    if (levelTmp == NULL ||
+        levelTmp->IsNull() ||
+        levelTmp->IsBinary())
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "C-FIND request without the tag 0008,0052 (QueryRetrieveLevel)");
+    }
+
+    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());
+
+    if (level != ResourceType_Patient &&
+        level != ResourceType_Study &&
+        level != ResourceType_Series &&
+        level != ResourceType_Instance)
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+
+    DicomArray query(*filteredInput);
+    LOG(INFO) << "DICOM C-Find request at level: " << EnumerationToString(level);
+
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      if (!query.GetElement(i).GetValue().IsNull())
+      {
+        LOG(INFO) << "  " << 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";
+    }
+
+    // collect the private creators from the query itself
+    std::map<uint16_t, std::string> privateCreators;
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      const DicomElement& element = query.GetElement(i);
+      if (element.GetTag().IsPrivate() && element.GetTag().GetElement() == 0x10)
+      {
+        privateCreators[element.GetTag().GetGroup()] = element.GetValue().GetContent();
+      }
+    }
+
+    /**
+     * Build up the query object.
+     **/
+
+    DatabaseLookup lookup;
+
+    bool caseSensitivePN;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
+    }
+
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      const DicomElement& element = query.GetElement(i);
+      const DicomTag tag = element.GetTag();
+
+      if (element.GetValue().IsNull() ||
+          tag == DICOM_TAG_QUERY_RETRIEVE_LEVEL ||
+          tag == DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        continue;
+      }
+
+      std::string value = element.GetValue().GetContent();
+      if (value.size() == 0)
+      {
+        // An empty string corresponds to an universal constraint, so we ignore it
+        continue;
+      }
+
+      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_PersonName)
+        {
+          sensitive = caseSensitivePN;
+        }
+
+        lookup.AddDicomConstraint(tag, value, sensitive, true /* mandatory */);
+      }
+      else
+      {
+        LOG(INFO) << "Because of a patch for the manufacturer of the remote modality, " 
+                  << "ignoring constraint on tag (" << tag.Format() << ") " << FromDcmtkBridge::GetTagName(element);
+      }
+    }
+
+
+    /**
+     * Run the query.
+     **/
+
+    size_t limit = (level == ResourceType_Instance) ? maxInstances_ : maxResults_;
+
+
+    LookupVisitor visitor(answers, context_, level, *filteredInput, sequencesToReturn, privateCreators);
+    context_.Apply(visitor, lookup, level, 0 /* "since" is not relevant to C-FIND */, limit);
+  }
+
+
+  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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancFindRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomNetworking/IFindRequestHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class OrthancFindRequestHandler : public IFindRequestHandler
+  {
+  private:
+    class LookupVisitor;
+
+    ServerContext& context_;
+    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);
+
+    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);
+
+    unsigned int GetMaxResults() const
+    {
+      return maxResults_;
+    }
+
+    void SetMaxResults(unsigned int results)
+    {
+      maxResults_ = results;
+    }
+
+    unsigned int GetMaxInstances() const
+    {
+      return maxInstances_;
+    }
+
+    void SetMaxInstances(unsigned int instances)
+    {
+      maxInstances_ = instances;
+    }
+
+    static void FormatOrigin(Json::Value& origin,
+                             const std::string& remoteIp,
+                             const std::string& remoteAet,
+                             const std::string& calledAet,
+                             ModalityManufacturer manufacturer);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,583 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancGetRequestHandler.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
+#include "OrthancConfiguration.h"
+#include "ServerContext.h"
+#include "ServerJobs/DicomModalityStoreJob.h"
+
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcistrmb.h>
+#include <dcmtk/dcmnet/assoc.h>
+#include <dcmtk/dcmnet/dimse.h>
+#include <dcmtk/dcmnet/diutil.h>
+#include <dcmtk/ofstd/ofstring.h>
+
+#include <sstream>  // For std::stringstream
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    
+    static void GetSubOpProgressCallback(
+      void * /* callbackData == pointer to the "OrthancGetRequestHandler" object */,
+      T_DIMSE_StoreProgress *progress,
+      T_DIMSE_C_StoreRQ * /*req*/)
+    {
+      // SBL - no logging to be done here.
+    }
+  }
+
+  OrthancGetRequestHandler::Status
+  OrthancGetRequestHandler::DoNext(T_ASC_Association* assoc)
+  {
+    if (position_ >= instances_.size())
+    {
+      return Status_Failure;
+    }
+    
+    const std::string& id = instances_[position_++];
+
+    std::string dicom;
+    context_.ReadDicom(dicom, id);
+    
+    if (dicom.size() <= 0)
+    {
+      return Status_Failure;
+    }
+
+    std::unique_ptr<DcmFileFormat> parsed(
+      FromDcmtkBridge::LoadFromMemoryBuffer(dicom.c_str(), dicom.size()));
+
+    if (parsed.get() == NULL ||
+        parsed->getDataset() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    DcmDataset& dataset = *parsed->getDataset();
+    
+    OFString a, b;
+    if (!dataset.findAndGetOFString(DCM_SOPClassUID, a).good() ||
+        !dataset.findAndGetOFString(DCM_SOPInstanceUID, b).good())
+    {
+      throw OrthancException(ErrorCode_NoSopClassOrInstance,
+                             "Unable to determine the SOP class/instance for C-STORE with AET " +
+                             originatorAet_);
+    }
+
+    std::string sopClassUid(a.c_str());
+    std::string sopInstanceUid(b.c_str());
+    
+    OFCondition cond = PerformGetSubOp(assoc, sopClassUid, sopInstanceUid, parsed.release());
+    
+    if (getCancelled_)
+    {
+      LOG(INFO) << "C-GET SCP: Received C-Cancel RQ";
+    }
+    
+    if (cond.bad() || getCancelled_)
+    {
+      return Status_Failure;
+    }
+    
+    return Status_Success;
+  }
+
+  
+  void OrthancGetRequestHandler::AddFailedUIDInstance(const std::string& sopInstance)
+  {
+    if (failedUIDs_.empty())
+    {
+      failedUIDs_ = sopInstance;
+    }
+    else
+    {
+      failedUIDs_ += "\\" + sopInstance;
+    }
+  }
+
+
+  static bool SelectPresentationContext(T_ASC_PresentationContextID& selectedPresentationId,
+                                        DicomTransferSyntax& selectedSyntax,
+                                        T_ASC_Association* assoc,
+                                        const std::string& sopClassUid,
+                                        DicomTransferSyntax sourceSyntax,
+                                        bool allowTranscoding)
+  {
+    typedef std::map<DicomTransferSyntax, T_ASC_PresentationContextID> Accepted;
+
+    Accepted accepted;
+
+    /**
+     * 1. Inspect and index all the accepted transfer syntaxes. This
+     * is similar to the code from "DicomAssociation::Open()".
+     **/
+
+    LST_HEAD **l = &assoc->params->DULparams.acceptedPresentationContext;
+    if (*l != NULL)
+    {
+      DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l);
+      LST_Position(l, (LST_NODE*)pc);
+      while (pc)
+      {
+        DicomTransferSyntax transferSyntax;
+        if (pc->result == ASC_P_ACCEPTANCE &&
+            LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax))
+        {
+          VLOG(0) << "C-GET SCP accepted: SOP class " << sopClassUid
+                  << " with transfer syntax " << GetTransferSyntaxUid(transferSyntax);
+          if (std::string(pc->abstractSyntax) == sopClassUid)
+          {
+            accepted[transferSyntax] = pc->presentationContextID;
+          }
+        }
+        else
+        {
+          LOG(WARNING) << "C-GET: Unknown transfer syntax received: "
+                       << pc->acceptedTransferSyntax;
+        }
+            
+        pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l);
+      }
+    }
+
+    
+    /**
+     * 2. Select the preferred transfer syntaxes, which corresponds to
+     * the source transfer syntax, plus all the uncompressed transfer
+     * syntaxes if transcoding is enabled.
+     **/
+    
+    std::list<DicomTransferSyntax> preferred;
+    preferred.push_back(sourceSyntax);
+
+    if (allowTranscoding)
+    {
+      if (sourceSyntax != DicomTransferSyntax_LittleEndianImplicit)
+      {
+        // Default Transfer Syntax for DICOM
+        preferred.push_back(DicomTransferSyntax_LittleEndianImplicit);
+      }
+
+      if (sourceSyntax != DicomTransferSyntax_LittleEndianExplicit)
+      {
+        preferred.push_back(DicomTransferSyntax_LittleEndianExplicit);
+      }
+
+      if (sourceSyntax != DicomTransferSyntax_BigEndianExplicit)
+      {
+        // Retired
+        preferred.push_back(DicomTransferSyntax_BigEndianExplicit);
+      }
+    }
+
+
+    /**
+     * 3. Lookup whether one of the preferred transfer syntaxes was
+     * accepted.
+     **/
+    
+    for (std::list<DicomTransferSyntax>::const_iterator
+           it = preferred.begin(); it != preferred.end(); ++it)
+    {
+      Accepted::const_iterator found = accepted.find(*it);
+      if (found != accepted.end())
+      {
+        selectedPresentationId = found->second;
+        selectedSyntax = *it;
+        return true;
+      }
+    }
+
+    // No preferred syntax was accepted
+    return false;
+  }                                                           
+
+
+  OFCondition OrthancGetRequestHandler::PerformGetSubOp(T_ASC_Association* assoc,
+                                                        const std::string& sopClassUid,
+                                                        const std::string& sopInstanceUid,
+                                                        DcmFileFormat* dicomRaw)
+  {
+    assert(dicomRaw != NULL);
+    std::unique_ptr<DcmFileFormat> dicom(dicomRaw);
+    
+    DicomTransferSyntax sourceSyntax;
+    if (!FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *dicom))
+    {
+      nFailed_++;
+      AddFailedUIDInstance(sopInstanceUid);
+      LOG(ERROR) << "C-GET SCP: Unknown transfer syntax: ("
+                 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
+      return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+    }
+
+    bool allowTranscoding = (context_.IsTranscodeDicomProtocol() &&
+                             remote_.IsTranscodingAllowed());
+    
+    T_ASC_PresentationContextID presId = 0;  // Unnecessary initialization, makes code clearer
+    DicomTransferSyntax selectedSyntax;
+    if (!SelectPresentationContext(presId, selectedSyntax, assoc, sopClassUid,
+                                   sourceSyntax, allowTranscoding) ||
+        presId == 0)
+    {
+      nFailed_++;
+      AddFailedUIDInstance(sopInstanceUid);
+      LOG(ERROR) << "C-GET SCP: storeSCU: No presentation context for: ("
+                 << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
+      return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+    }
+    else
+    {
+      LOG(INFO) << "C-GET SCP selected transfer syntax " << GetTransferSyntaxUid(selectedSyntax)
+                << ", for source instance with SOP class " << sopClassUid
+                << " and transfer syntax " << GetTransferSyntaxUid(sourceSyntax);
+
+      // make sure that we can send images in this presentation context
+      T_ASC_PresentationContext pc;
+      ASC_findAcceptedPresentationContext(assoc->params, presId, &pc);
+      // the acceptedRole is the association requestor role
+
+      if (pc.acceptedRole != ASC_SC_ROLE_DEFAULT &&  // "DEFAULT" is necessary for GinkgoCADx
+          pc.acceptedRole != ASC_SC_ROLE_SCP &&
+          pc.acceptedRole != ASC_SC_ROLE_SCUSCP)
+      {
+        // the role is not appropriate
+        nFailed_++;
+        AddFailedUIDInstance(sopInstanceUid);
+        LOG(ERROR) << "C-GET SCP: storeSCU: [No presentation context with requestor SCP role for: ("
+                   << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ") " << sopClassUid;
+        return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+      }
+    }
+
+    const DIC_US msgId = assoc->nextMsgID++;
+    
+    T_DIMSE_C_StoreRQ req;
+    memset(&req, 0, sizeof(req));
+    req.MessageID = msgId;
+    strncpy(req.AffectedSOPClassUID, sopClassUid.c_str(), DIC_UI_LEN);
+    strncpy(req.AffectedSOPInstanceUID, sopInstanceUid.c_str(), DIC_UI_LEN);
+    req.DataSetType = DIMSE_DATASET_PRESENT;
+    req.Priority = DIMSE_PRIORITY_MEDIUM;
+    req.opts = 0;
+    
+    T_DIMSE_C_StoreRSP rsp;
+    memset(&rsp, 0, sizeof(rsp));
+
+    LOG(INFO) << "Store SCU RQ: MsgID " << msgId << ", ("
+              << dcmSOPClassUIDToModality(sopClassUid.c_str(), "OT") << ")";
+    
+    T_DIMSE_DetectedCancelParameters cancelParameters;
+    memset(&cancelParameters, 0, sizeof(cancelParameters));
+
+    std::unique_ptr<DcmDataset> stDetail;
+
+    OFCondition cond;
+
+    if (sourceSyntax == selectedSyntax)
+    {
+      // No transcoding is required
+      DcmDataset *stDetailTmp = NULL;
+      cond = DIMSE_storeUser(
+        assoc, presId, &req, NULL /* imageFileName */, dicom->getDataset(),
+        GetSubOpProgressCallback, this /* callbackData */,
+        (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
+        &rsp, &stDetailTmp, &cancelParameters);
+      stDetail.reset(stDetailTmp);
+    }
+    else
+    {
+      // Transcoding to the selected uncompressed transfer syntax
+      IDicomTranscoder::DicomImage source, transcoded;
+      source.AcquireParsed(dicom.release());
+
+      std::set<DicomTransferSyntax> ts;
+      ts.insert(selectedSyntax);
+      
+      if (context_.Transcode(transcoded, source, ts, true))
+      {
+        // Transcoding has succeeded
+        DcmDataset *stDetailTmp = NULL;
+        cond = DIMSE_storeUser(
+          assoc, presId, &req, NULL /* imageFileName */,
+          transcoded.GetParsed().getDataset(),
+          GetSubOpProgressCallback, this /* callbackData */,
+          (timeout_ > 0 ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), timeout_,
+          &rsp, &stDetailTmp, &cancelParameters);
+        stDetail.reset(stDetailTmp);
+      }
+      else
+      {
+        // Cannot transcode
+        nFailed_++;
+        AddFailedUIDInstance(sopInstanceUid);
+        LOG(ERROR) << "C-GET SCP: Cannot transcode " << sopClassUid
+                   << " from transfer syntax " << GetTransferSyntaxUid(sourceSyntax)
+                   << " to " << GetTransferSyntaxUid(selectedSyntax);
+        return DIMSE_NOVALIDPRESENTATIONCONTEXTID;
+      }      
+    }
+    
+    if (cond.good())
+    {
+      if (cancelParameters.cancelEncountered)
+      {
+        if (origPresId_ == cancelParameters.presId &&
+            origMsgId_ == cancelParameters.req.MessageIDBeingRespondedTo)
+        {
+          getCancelled_ = OFTrue;
+        }
+        else
+        {
+          LOG(ERROR) << "C-GET SCP: Unexpected C-Cancel-RQ encountered: pid=" << (int)cancelParameters.presId
+                     << ", mid=" << (int)cancelParameters.req.MessageIDBeingRespondedTo;
+        }
+      }
+      
+      if (rsp.DimseStatus == STATUS_Success)
+      {
+        // everything ok
+        nCompleted_++;
+      }
+      else if ((rsp.DimseStatus & 0xf000) == 0xb000)
+      {
+        // a warning status message
+        warningCount_++;
+        LOG(ERROR) << "C-GET SCP: Store Warning: Response Status: "
+                   << DU_cstoreStatusString(rsp.DimseStatus);
+      }
+      else
+      {
+        nFailed_++;
+        AddFailedUIDInstance(sopInstanceUid);
+        // print a status message
+        LOG(ERROR) << "C-GET SCP: Store Failed: Response Status: "
+                   << DU_cstoreStatusString(rsp.DimseStatus);
+      }
+    }
+    else
+    {
+      nFailed_++;
+      AddFailedUIDInstance(sopInstanceUid);
+      OFString temp_str;
+      LOG(ERROR) << "C-GET SCP: storeSCU: Store Request Failed: " << DimseCondition::dump(temp_str, cond);
+    }
+    
+    if (stDetail.get() != NULL)
+    {
+      // It is impossible to directly use the "<<" stream construct
+      // with "DcmObject::PrintHelper" using MSVC2008
+      std::stringstream s;
+      DcmObject::PrintHelper obj(*stDetail);
+      obj.dcmobj_.print(s);
+
+      LOG(INFO) << "  Status Detail: " << s.str();
+    }
+    
+    return cond;
+  }
+
+  bool OrthancGetRequestHandler::LookupIdentifiers(std::list<std::string>& publicIds,
+                                                   ResourceType level,
+                                                   const DicomMap& input) const
+  {
+    DicomTag tag(0, 0);   // Dummy initialization
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        tag = DICOM_TAG_PATIENT_ID;
+        break;
+
+      case ResourceType_Study:
+        tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ?
+               DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+        
+      case ResourceType_Series:
+        tag = DICOM_TAG_SERIES_INSTANCE_UID;
+        break;
+        
+      case ResourceType_Instance:
+        tag = DICOM_TAG_SOP_INSTANCE_UID;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!input.HasTag(tag))
+    {
+      return false;
+    }
+
+    const DicomValue& value = input.GetValue(tag);
+    if (value.IsNull() ||
+        value.IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      std::vector<std::string> tokens;
+      Toolbox::TokenizeString(tokens, value.GetContent(), '\\');
+
+      for (size_t i = 0; i < tokens.size(); i++)
+      {
+        std::vector<std::string> tmp;
+        context_.GetIndex().LookupIdentifierExact(tmp, level, tag, tokens[i]);
+
+        if (tmp.empty())
+        {
+          LOG(ERROR) << "C-GET: Cannot locate resource \"" << tokens[i]
+                     << "\" at the " << EnumerationToString(level) << " level";
+          return false;
+        }
+        else
+        {
+          for (size_t i = 0; i < tmp.size(); i++)
+          {
+            publicIds.push_back(tmp[i]);
+          }
+        }
+      }
+
+      return true;      
+    }
+  }
+
+
+    OrthancGetRequestHandler::OrthancGetRequestHandler(ServerContext& context) :
+      context_(context)
+    {
+      position_ = 0;
+      nRemaining_ = 0;
+      nCompleted_  = 0;
+      warningCount_ = 0;
+      nFailed_ = 0;
+      timeout_ = 0;
+    }
+
+
+  bool OrthancGetRequestHandler::Handle(const DicomMap& input,
+                                        const std::string& originatorIp,
+                                        const std::string& originatorAet,
+                                        const std::string& calledAet,
+                                        uint32_t timeout)
+  {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_get_scp_duration_ms");
+
+    LOG(WARNING) << "C-GET-SCU request received from AET \"" << originatorAet << "\"";
+
+    {
+      DicomArray query(input);
+      for (size_t i = 0; i < query.GetSize(); i++)
+      {
+        if (!query.GetElement(i).GetValue().IsNull())
+        {
+          LOG(INFO) << "  " << query.GetElement(i).GetTag()
+                    << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
+                    << " = " << query.GetElement(i).GetValue().GetContent();
+        }
+      }
+    }
+
+    /**
+     * Retrieve the query level.
+     **/
+
+    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+
+    assert(levelTmp != NULL);
+    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());      
+
+
+    /**
+     * Lookup for the resource to be sent.
+     **/
+
+    std::list<std::string> publicIds;
+
+    if (!LookupIdentifiers(publicIds, level, input))
+    {
+      LOG(ERROR) << "Cannot determine what resources are requested by C-GET";
+      return false; 
+    }
+
+    localAet_ = context_.GetDefaultLocalApplicationEntityTitle();
+    position_ = 0;
+    originatorAet_ = originatorAet;
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      remote_ = lock.GetConfiguration().GetModalityUsingAet(originatorAet);
+    }
+
+    for (std::list<std::string>::const_iterator
+           resource = publicIds.begin(); resource != publicIds.end(); ++resource)
+    {
+      LOG(INFO) << "C-GET: Sending resource " << *resource
+                << " to modality \"" << originatorAet << "\"";
+      
+      std::list<std::string> tmp;
+      context_.GetIndex().GetChildInstances(tmp, *resource);
+      
+      instances_.reserve(tmp.size());
+      for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
+      {
+        instances_.push_back(*it);
+      }
+    }
+
+    failedUIDs_.clear();
+    getCancelled_ = OFFalse;
+
+    nRemaining_ = GetSubOperationCount();
+    nCompleted_ = 0;
+    nFailed_ = 0;
+    warningCount_ = 0;
+    timeout_ = timeout;
+
+    return true;
+  }
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancGetRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomNetworking/IGetRequestHandler.h"
+#include "../Core/DicomNetworking/RemoteModalityParameters.h"
+
+#include <dcmtk/dcmnet/dimse.h>
+
+#include <list>
+
+class DcmFileFormat;
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class OrthancGetRequestHandler : public IGetRequestHandler
+  {
+  private:
+    ServerContext& context_;
+    std::string localAet_;
+    std::vector<std::string> instances_;
+    size_t position_;
+    RemoteModalityParameters remote_;
+    std::string originatorAet_;
+    
+    unsigned int nRemaining_;
+    unsigned int nCompleted_;
+    unsigned int warningCount_;
+    unsigned int nFailed_;
+    std::string failedUIDs_;
+    
+    DIC_US origMsgId_;
+    T_ASC_PresentationContextID origPresId_;
+    
+    bool getCancelled_;
+    uint32_t timeout_;
+
+    bool LookupIdentifiers(std::list<std::string>& publicIds,
+                           ResourceType level,
+                           const DicomMap& input) const;
+    
+    OFCondition PerformGetSubOp(T_ASC_Association *assoc,
+                                const std::string& sopClassUid,
+                                const std::string& sopInstanceUid,
+                                DcmFileFormat* datasetRaw);
+    
+    void AddFailedUIDInstance(const std::string& sopInstance);
+
+  public:
+    OrthancGetRequestHandler(ServerContext& context);
+    
+    virtual bool Handle(const DicomMap& input,
+                        const std::string& originatorIp,
+                        const std::string& originatorAet,
+                        const std::string& calledAet,
+                        uint32_t timeout) ORTHANC_OVERRIDE;
+    
+    virtual Status DoNext(T_ASC_Association *assoc) ORTHANC_OVERRIDE;
+    
+    virtual unsigned int GetSubOperationCount() const ORTHANC_OVERRIDE
+    {
+      return static_cast<unsigned int>(instances_.size());
+    }
+    
+    virtual unsigned int GetRemainingCount() const ORTHANC_OVERRIDE
+    {
+      return nRemaining_;
+    }
+    
+    virtual unsigned int GetCompletedCount() const ORTHANC_OVERRIDE
+    {
+      return nCompleted_;
+    }
+    
+    virtual unsigned int GetWarningCount() const ORTHANC_OVERRIDE
+    {
+      return warningCount_;
+    }
+    
+    virtual unsigned int GetFailedCount() const ORTHANC_OVERRIDE
+    {
+      return nFailed_;
+    }
+    
+    virtual const std::string& GetFailedUids() const ORTHANC_OVERRIDE
+    {
+      return failedUIDs_;
+    }    
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancHttpHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,127 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancHttpHandler.h"
+
+#include "../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  bool OrthancHttpHandler::CreateChunkedRequestReader(
+    std::unique_ptr<IHttpHandler::IChunkedRequestReader>& target,
+    RequestOrigin origin,
+    const char* remoteIp,
+    const char* username,
+    HttpMethod method,
+    const UriComponents& uri,
+    const Arguments& headers)
+  {
+    if (method != HttpMethod_Post &&
+        method != HttpMethod_Put)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) 
+    {
+      if ((*it)->CreateChunkedRequestReader
+          (target, origin, remoteIp, username, method, uri, headers))
+      {
+        if (target.get() == NULL)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  bool OrthancHttpHandler::Handle(HttpOutput& output,
+                                  RequestOrigin origin,
+                                  const char* remoteIp,
+                                  const char* username,
+                                  HttpMethod method,
+                                  const UriComponents& uri,
+                                  const Arguments& headers,
+                                  const GetArguments& getArguments,
+                                  const void* bodyData,
+                                  size_t bodySize)
+  {
+    for (Handlers::const_iterator it = handlers_.begin(); it != handlers_.end(); ++it) 
+    {
+      if ((*it)->Handle(output, origin, remoteIp, username, method, uri, 
+                        headers, getArguments, bodyData, bodySize))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+
+  void OrthancHttpHandler::Register(IHttpHandler& handler,
+                                    bool isOrthancRestApi)
+  {
+    handlers_.push_back(&handler);
+
+    if (isOrthancRestApi)
+    {
+      orthancRestApi_ = &handler;
+    }
+  }
+
+
+  IHttpHandler& OrthancHttpHandler::RestrictToOrthancRestApi(bool restrict)
+  {
+    if (restrict)
+    {
+      if (orthancRestApi_ == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      return *orthancRestApi_;
+    }
+    else
+    {
+      return *this;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancHttpHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/HttpServer/IHttpHandler.h"
+
+namespace Orthanc
+{
+  class OrthancHttpHandler : public IHttpHandler
+  {
+  private:
+    typedef std::list<IHttpHandler*> Handlers;
+
+    Handlers      handlers_;
+    IHttpHandler *orthancRestApi_;
+
+  public:
+    OrthancHttpHandler() : orthancRestApi_(NULL)
+    {
+    }
+
+    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
+                                            RequestOrigin origin,
+                                            const char* remoteIp,
+                                            const char* username,
+                                            HttpMethod method,
+                                            const UriComponents& uri,
+                                            const Arguments& headers);
+
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const void* bodyData,
+                        size_t bodySize);
+
+    void Register(IHttpHandler& handler,
+                  bool isOrthancRestApi);
+
+    bool HasOrthancRestApi() const
+    {
+      return orthancRestApi_ != NULL;
+    }
+
+    IHttpHandler& RestrictToOrthancRestApi(bool restrict);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancInitialization.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,391 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+#if defined(_WIN32)
+// "Please include winsock2.h before windows.h"
+#  include <winsock2.h>
+#endif
+
+#include "OrthancInitialization.h"
+
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/FileStorage/FilesystemStorage.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+
+#include "Database/SQLiteDatabaseWrapper.h"
+#include "OrthancConfiguration.h"
+
+#include <OrthancServerResources.h>
+
+#include <dcmtk/dcmnet/dul.h>   // For dcmDisableGethostbyaddr()
+
+
+
+namespace Orthanc
+{
+  static void RegisterUserMetadata(const Json::Value& config)
+  {
+    if (config.isMember("UserMetadata"))
+    {
+      const Json::Value& parameter = config["UserMetadata"];
+
+      Json::Value::Members members = parameter.getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const std::string& name = members[i];
+
+        if (!parameter[name].isInt())
+        {
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Not a number in this user-defined metadata: " + name);
+        }
+
+        int metadata = parameter[name].asInt();        
+
+        LOG(INFO) << "Registering user-defined metadata: " << name << " (index " 
+                  << metadata << ")";
+
+        try
+        {
+          RegisterUserMetadata(metadata, name);
+        }
+        catch (OrthancException&)
+        {
+          LOG(ERROR) << "Cannot register this user-defined metadata: " << name;
+          throw;
+        }
+      }
+    }
+  }
+
+
+  static void RegisterUserContentType(const Json::Value& config)
+  {
+    if (config.isMember("UserContentType"))
+    {
+      const Json::Value& parameter = config["UserContentType"];
+
+      Json::Value::Members members = parameter.getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const std::string& name = members[i];
+        std::string mime = MIME_BINARY;
+
+        const Json::Value& value = parameter[name];
+        int contentType;
+
+        if (value.isArray() &&
+            value.size() == 2 &&
+            value[0].isInt() &&
+            value[1].isString())
+        {
+          contentType = value[0].asInt();
+          mime = value[1].asString();
+        }
+        else if (value.isInt())
+        {
+          contentType = value.asInt();
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadParameterType,
+                                 "Not a number in this user-defined attachment type: " + name);
+        }
+
+        LOG(INFO) << "Registering user-defined attachment type: " << name << " (index " 
+                  << contentType << ") with MIME type \"" << mime << "\"";
+
+        try
+        {
+          RegisterUserContentType(contentType, name, mime);
+        }
+        catch (OrthancException&)
+        {
+          throw;
+        }
+      }
+    }
+  }
+
+
+  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, "The definition of the '" + tags[i] + "' dictionary entry is invalid.");
+      }
+
+      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)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "No path to the PKCS#11 module (DLL or .so) is provided "
+                             "for HTTPS client authentication");
+    }
+
+    std::string pin;
+    if (config.isMember("Pin"))
+    {
+      if (config["Pin"].type() == Json::stringValue)
+      {
+        pin = config["Pin"].asString();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The PIN number in the PKCS#11 configuration must be a string");
+      }
+    }
+
+    bool verbose = false;
+    if (config.isMember("Verbose"))
+    {
+      if (config["Verbose"].type() == Json::booleanValue)
+      {
+        verbose = config["Verbose"].asBool();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The Verbose option in the PKCS#11 configuration must be a Boolean");
+      }
+    }
+
+    HttpClient::InitializePkcs11(config["Module"].asString(), pin, verbose);
+  }
+
+
+
+  void OrthancInitialize(const char* configurationFile)
+  {
+    OrthancConfiguration::WriterLock lock;
+
+    InitializeServerEnumerations();
+
+    // Read the user-provided configuration
+    lock.GetConfiguration().Read(configurationFile);
+
+    {
+      std::string locale;
+      
+      if (lock.GetJson().isMember("Locale"))
+      {
+        locale = lock.GetConfiguration().GetStringParameter("Locale", "");
+      }
+      
+      bool loadPrivate = lock.GetConfiguration().GetBooleanParameter("LoadPrivateDictionary", true);
+      Orthanc::InitializeFramework(locale, loadPrivate);
+    }
+
+    // The Orthanc framework is now initialized
+
+    if (lock.GetJson().isMember("DefaultEncoding"))
+    {
+      std::string encoding = lock.GetConfiguration().GetStringParameter("DefaultEncoding", "");
+      SetDefaultDicomEncoding(StringToEncoding(encoding.c_str()));
+    }
+    else
+    {
+      SetDefaultDicomEncoding(ORTHANC_DEFAULT_DICOM_ENCODING);
+    }
+
+    if (lock.GetJson().isMember("Pkcs11"))
+    {
+      ConfigurePkcs11(lock.GetJson()["Pkcs11"]);
+    }
+
+    RegisterUserMetadata(lock.GetJson());
+    RegisterUserContentType(lock.GetJson());
+
+    LoadCustomDictionary(lock.GetJson());
+
+    lock.GetConfiguration().RegisterFont(ServerResources::FONT_UBUNTU_MONO_BOLD_16);
+  }
+
+
+
+  void OrthancFinalize()
+  {
+    OrthancConfiguration::WriterLock lock;
+    Orthanc::FinalizeFramework();
+  }
+
+
+  static IDatabaseWrapper* CreateSQLiteWrapper()
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    std::string storageDirectoryStr = 
+      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
+
+    // Open the database
+    boost::filesystem::path indexDirectory = lock.GetConfiguration().InterpretStringParameterAsPath(
+      lock.GetConfiguration().GetStringParameter("IndexDirectory", storageDirectoryStr));
+
+    LOG(WARNING) << "SQLite index directory: " << indexDirectory;
+
+    try
+    {
+      boost::filesystem::create_directories(indexDirectory);
+    }
+    catch (boost::filesystem::filesystem_error&)
+    {
+    }
+
+    return new SQLiteDatabaseWrapper(indexDirectory.string() + "/index");
+  }
+
+
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class FilesystemStorageWithoutDicom : public IStorageArea
+    {
+    private:
+      FilesystemStorage storage_;
+
+    public:
+      FilesystemStorageWithoutDicom(const std::string& path) : storage_(path)
+      {
+      }
+
+      virtual void Create(const std::string& uuid,
+                          const void* content, 
+                          size_t size,
+                          FileContentType type)
+      {
+        if (type != FileContentType_Dicom)
+        {
+          storage_.Create(uuid, content, size, type);
+        }
+      }
+
+      virtual void Read(std::string& content,
+                        const std::string& uuid,
+                        FileContentType type)
+      {
+        if (type != FileContentType_Dicom)
+        {
+          storage_.Read(content, uuid, type);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+      }
+
+      virtual void Remove(const std::string& uuid,
+                          FileContentType type) 
+      {
+        if (type != FileContentType_Dicom)
+        {
+          storage_.Remove(uuid, type);
+        }
+      }
+    };
+  }
+
+
+  static IStorageArea* CreateFilesystemStorage()
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    std::string storageDirectoryStr = 
+      lock.GetConfiguration().GetStringParameter("StorageDirectory", "OrthancStorage");
+
+    boost::filesystem::path storageDirectory = 
+      lock.GetConfiguration().InterpretStringParameterAsPath(storageDirectoryStr);
+
+    LOG(WARNING) << "Storage directory: " << storageDirectory;
+
+    if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true))
+    {
+      return new FilesystemStorage(storageDirectory.string());
+    }
+    else
+    {
+      LOG(WARNING) << "The DICOM files will not be stored, Orthanc running in index-only mode";
+      return new FilesystemStorageWithoutDicom(storageDirectory.string());
+    }
+  }
+
+
+  IDatabaseWrapper* CreateDatabaseWrapper()
+  {
+    return CreateSQLiteWrapper();
+  }
+
+
+  IStorageArea* CreateStorageArea()
+  {
+    return CreateFilesystemStorage();
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancInitialization.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,48 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/FileStorage/IStorageArea.h"
+#include "Database/IDatabaseWrapper.h"
+
+namespace Orthanc
+{
+  void OrthancInitialize(const char* configurationFile = NULL);
+
+  void OrthancFinalize();
+
+  IDatabaseWrapper* CreateDatabaseWrapper();
+
+  IStorageArea* CreateStorageArea();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,378 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancMoveRequestHandler.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
+#include "OrthancConfiguration.h"
+#include "ServerContext.h"
+#include "ServerJobs/DicomModalityStoreJob.h"
+
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+
+    class SynchronousMove : public IMoveRequestIterator
+    {
+    private:
+      ServerContext& context_;
+      const std::string& localAet_;
+      std::vector<std::string> instances_;
+      size_t position_;
+      RemoteModalityParameters remote_;
+      std::string originatorAet_;
+      uint16_t originatorId_;
+      std::unique_ptr<DicomStoreUserConnection> connection_;
+
+    public:
+      SynchronousMove(ServerContext& context,
+                      const std::string& targetAet,
+                      const std::vector<std::string>& publicIds,
+                      const std::string& originatorAet,
+                      uint16_t originatorId) :
+        context_(context),
+        localAet_(context.GetDefaultLocalApplicationEntityTitle()),
+        position_(0),
+        originatorAet_(originatorAet),
+        originatorId_(originatorId)
+      {
+        {
+          OrthancConfiguration::ReaderLock lock;
+          remote_ = lock.GetConfiguration().GetModalityUsingAet(targetAet);
+        }
+
+        for (size_t i = 0; i < publicIds.size(); i++)
+        {
+          LOG(INFO) << "Sending resource " << publicIds[i] << " to modality \""
+                    << targetAet << "\" in synchronous mode";
+
+          std::list<std::string> tmp;
+          context_.GetIndex().GetChildInstances(tmp, publicIds[i]);
+
+          instances_.reserve(tmp.size());
+          for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
+          {
+            instances_.push_back(*it);
+          }
+        }
+      }
+
+      virtual unsigned int GetSubOperationCount() const
+      {
+        return instances_.size();
+      }
+
+      virtual Status DoNext()
+      {
+        if (position_ >= instances_.size())
+        {
+          return Status_Failure;
+        }
+
+        const std::string& id = instances_[position_++];
+
+        std::string dicom;
+        context_.ReadDicom(dicom, id);
+
+        if (connection_.get() == NULL)
+        {
+          DicomAssociationParameters params(localAet_, remote_);
+          connection_.reset(new DicomStoreUserConnection(params));
+        }
+
+        std::string sopClassUid, sopInstanceUid;  // Unused
+
+        const void* data = dicom.empty() ? NULL : dicom.c_str();
+        connection_->Store(sopClassUid, sopInstanceUid, data, dicom.size(),
+                           true, originatorAet_, originatorId_);
+
+        return Status_Success;
+      }
+    };
+
+
+    class AsynchronousMove : public IMoveRequestIterator
+    {
+    private:
+      ServerContext&                          context_;
+      std::unique_ptr<DicomModalityStoreJob>  job_;
+      size_t                                  position_;
+      size_t                                  countInstances_;
+      
+    public:
+      AsynchronousMove(ServerContext& context,
+                       const std::string& targetAet,
+                       const std::vector<std::string>& publicIds,
+                       const std::string& originatorAet,
+                       uint16_t originatorId) :
+        context_(context),
+        job_(new DicomModalityStoreJob(context)),
+        position_(0)
+      {
+        job_->SetDescription("C-MOVE");
+        //job_->SetPermissive(true);  // This was the behavior of Orthanc < 1.6.0
+        job_->SetPermissive(false);
+        job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle());
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          job_->SetRemoteModality(lock.GetConfiguration().GetModalityUsingAet(targetAet));
+        }
+
+        if (originatorId != 0)
+        {
+          job_->SetMoveOriginator(originatorAet, originatorId);
+        }
+
+        for (size_t i = 0; i < publicIds.size(); i++)
+        {
+          LOG(INFO) << "Sending resource " << publicIds[i] << " to modality \""
+                    << targetAet << "\" in asynchronous mode";
+
+          std::list<std::string> tmp;
+          context_.GetIndex().GetChildInstances(tmp, publicIds[i]);
+
+          countInstances_ = tmp.size();
+
+          job_->Reserve(job_->GetCommandsCount() + tmp.size());
+
+          for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
+          {
+            job_->AddInstance(*it);
+          }
+        }
+      }
+
+      virtual unsigned int GetSubOperationCount() const
+      {
+        return countInstances_;
+      }
+
+      virtual Status DoNext()
+      {
+        if (position_ >= countInstances_)
+        {
+          return Status_Failure;
+        }
+        
+        if (position_ == 0)
+        {
+          context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */);
+        }
+        
+        position_ ++;
+        return Status_Success;
+      }
+    };
+  }
+
+
+  bool OrthancMoveRequestHandler::LookupIdentifiers(std::vector<std::string>& publicIds,
+                                                    ResourceType level,
+                                                    const DicomMap& input)
+  {
+    DicomTag tag(0, 0);   // Dummy initialization
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        tag = DICOM_TAG_PATIENT_ID;
+        break;
+
+      case ResourceType_Study:
+        tag = (input.HasTag(DICOM_TAG_ACCESSION_NUMBER) ? 
+               DICOM_TAG_ACCESSION_NUMBER : DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+        
+      case ResourceType_Series:
+        tag = DICOM_TAG_SERIES_INSTANCE_UID;
+        break;
+        
+      case ResourceType_Instance:
+        tag = DICOM_TAG_SOP_INSTANCE_UID;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!input.HasTag(tag))
+    {
+      return false;
+    }
+
+    const DicomValue& value = input.GetValue(tag);
+    if (value.IsNull() ||
+        value.IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      const std::string& content = value.GetContent();
+
+      /**
+       * This tokenization fixes issue 154 ("Matching against list of
+       * UID-s by C-MOVE").
+       * https://bitbucket.org/sjodogne/orthanc/issues/154/
+       **/
+
+      std::vector<std::string> tokens;
+      Toolbox::TokenizeString(tokens, content, '\\');
+      for (size_t i = 0; i < tokens.size(); i++)
+      {
+        std::vector<std::string> matches;
+        context_.GetIndex().LookupIdentifierExact(matches, level, tag, tokens[i]);
+
+        // Concatenate "publicIds" with "matches"
+        publicIds.insert(publicIds.end(), matches.begin(), matches.end());
+      }
+
+      return true;
+    }
+  }
+
+
+  static IMoveRequestIterator* CreateIterator(ServerContext& context,
+                                              const std::string& targetAet,
+                                              const std::vector<std::string>& publicIds,
+                                              const std::string& originatorAet,
+                                              uint16_t originatorId)
+  {
+    if (publicIds.empty())
+    {
+      throw OrthancException(ErrorCode_BadRequest,
+                             "C-MOVE request matching no resource stored in Orthanc");
+    }
+    
+    bool synchronous;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      synchronous = lock.GetConfiguration().GetBooleanParameter("SynchronousCMove", true);
+    }
+
+    if (synchronous)
+    {
+      return new SynchronousMove(context, targetAet, publicIds, originatorAet, originatorId);
+    }
+    else
+    {
+      return new AsynchronousMove(context, targetAet, publicIds, originatorAet, originatorId);
+    }
+  }
+
+
+  IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet,
+                                                          const DicomMap& input,
+                                                          const std::string& originatorIp,
+                                                          const std::string& originatorAet,
+                                                          const std::string& calledAet,
+                                                          uint16_t originatorId)
+  {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_move_scp_duration_ms");
+
+    LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\"";
+
+    {
+      DicomArray query(input);
+      for (size_t i = 0; i < query.GetSize(); i++)
+      {
+        if (!query.GetElement(i).GetValue().IsNull())
+        {
+          LOG(INFO) << "  " << query.GetElement(i).GetTag()
+                    << "  " << FromDcmtkBridge::GetTagName(query.GetElement(i))
+                    << " = " << query.GetElement(i).GetValue().GetContent();
+        }
+      }
+    }
+
+    /**
+     * Retrieve the query level.
+     **/
+
+    const DicomValue* levelTmp = input.TestAndGetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+
+    if (levelTmp == NULL ||
+        levelTmp->IsNull() ||
+        levelTmp->IsBinary())
+    {
+      // The query level is not present in the C-Move request, which
+      // does not follow the DICOM standard. This is for instance the
+      // behavior of Tudor DICOM. Try and automatically deduce the
+      // query level: Start from the instance level, going up to the
+      // patient level until a valid DICOM identifier is found.
+
+      std::vector<std::string> publicIds;
+
+      if (LookupIdentifiers(publicIds, ResourceType_Instance, input) ||
+          LookupIdentifiers(publicIds, ResourceType_Series, input) ||
+          LookupIdentifiers(publicIds, ResourceType_Study, input) ||
+          LookupIdentifiers(publicIds, ResourceType_Patient, input))
+      {
+        return CreateIterator(context_, targetAet, publicIds, originatorAet, originatorId);
+      }
+      else
+      {
+        // No identifier is present in the request.
+        throw OrthancException(ErrorCode_BadRequest, "Invalid fields in a C-MOVE request");
+      }
+    }
+
+    assert(levelTmp != NULL);
+    ResourceType level = StringToResourceType(levelTmp->GetContent().c_str());      
+
+
+    /**
+     * Lookup for the resource to be sent.
+     **/
+
+    std::vector<std::string> publicIds;
+
+    if (LookupIdentifiers(publicIds, level, input))
+    {
+      return CreateIterator(context_, targetAet, publicIds, originatorAet, originatorId);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadRequest, "Invalid fields in a C-MOVE request");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancMoveRequestHandler.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomNetworking/IMoveRequestHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class OrthancMoveRequestHandler : public IMoveRequestHandler
+  {
+  private:
+    ServerContext& context_;
+
+    bool LookupIdentifiers(std::vector<std::string>& publicIds,
+                           ResourceType level,
+                           const DicomMap& input);
+
+  public:
+    OrthancMoveRequestHandler(ServerContext& context) :
+    context_(context)
+    {
+    }
+
+    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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,855 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../OrthancConfiguration.h"
+#include "../ServerContext.h"
+#include "../ServerJobs/MergeStudyJob.h"
+#include "../ServerJobs/ResourceModificationJob.h"
+#include "../ServerJobs/SplitStudyJob.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace Orthanc
+{
+  // Modification of DICOM instances ------------------------------------------
+
+  
+  static std::string GeneratePatientName(ServerContext& context)
+  {
+    uint64_t seq = context.GetIndex().IncrementGlobalSequence(GlobalProperty_AnonymizationSequence);
+    return "Anonymized" + boost::lexical_cast<std::string>(seq);
+  }
+
+
+  static void ParseModifyRequest(Json::Value& request,
+                                 DicomModification& target,
+                                 const RestApiPostCall& call)
+  {
+    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}'
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator());
+    }
+    
+    if (call.ParseJsonRequest(request))
+    {
+      target.ParseModifyRequest(request);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  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": true,"Remove":["Modality"]}' > Anonymized.dcm
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      target.SetPrivateCreator(lock.GetConfiguration().GetDefaultPrivateCreator());
+    }
+    
+    if (call.ParseJsonRequest(request) &&
+        request.isObject())
+    {
+      bool patientNameReplaced;
+      target.ParseAnonymizationRequest(patientNameReplaced, request);
+
+      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);
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  static void AnonymizeOrModifyInstance(DicomModification& modification,
+                                        RestApiPostCall& call,
+                                        bool transcode,
+                                        DicomTransferSyntax targetSyntax)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string id = call.GetUriComponent("id", "");
+
+    std::unique_ptr<ParsedDicomFile> modified;
+
+    {
+      ServerContext::DicomCacheLocker locker(context, id);
+      modified.reset(locker.GetDicom().Clone(true));
+    }
+    
+    modification.Apply(*modified);
+
+    if (transcode)
+    {
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(*modified);  // "modified" is invalid below this point
+      
+      IDicomTranscoder::DicomImage transcoded;
+
+      std::set<DicomTransferSyntax> s;
+      s.insert(targetSyntax);
+      
+      if (context.Transcode(transcoded, source, s, true))
+      {      
+        call.GetOutput().AnswerBuffer(transcoded.GetBufferData(),
+                                      transcoded.GetBufferSize(), MimeType_Dicom);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot transcode to transfer syntax: " +
+                               std::string(GetTransferSyntaxUid(targetSyntax)));
+      }
+    }
+    else
+    {
+      modified->Answer(call.GetOutput());
+    }
+  }
+
+
+  static void ModifyInstance(RestApiPostCall& call)
+  {
+    DicomModification modification;
+    modification.SetAllowManualIdentifiers(true);
+
+    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))
+    {
+      modification.SetLevel(ResourceType_Study);
+    }
+    else if (modification.IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification.SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification.SetLevel(ResourceType_Instance);
+    }
+
+    static const char* TRANSCODE = "Transcode";
+    if (request.isMember(TRANSCODE))
+    {
+      std::string s = SerializationToolbox::ReadString(request, TRANSCODE);
+      
+      DicomTransferSyntax syntax;
+      if (LookupTransferSyntax(syntax, s))
+      {
+        AnonymizeOrModifyInstance(modification, call, true, syntax);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown transfer syntax: " + s);
+      }
+    }
+    else
+    {
+      AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
+                                DicomTransferSyntax_LittleEndianImplicit /* unused */);
+    }
+  }
+
+
+  static void AnonymizeInstance(RestApiPostCall& call)
+  {
+    DicomModification modification;
+    modification.SetAllowManualIdentifiers(true);
+
+    Json::Value request;
+    ParseAnonymizationRequest(request, modification, call);
+
+    AnonymizeOrModifyInstance(modification, call, false /* no transcoding */,
+                              DicomTransferSyntax_LittleEndianImplicit /* unused */);
+  }
+
+
+  static void SetKeepSource(CleaningInstancesJob& job,
+                            const Json::Value& body)
+  {
+    static const char* KEEP_SOURCE = "KeepSource";
+    if (body.isMember(KEEP_SOURCE))
+    {
+      job.SetKeepSource(SerializationToolbox::ReadBoolean(body, KEEP_SOURCE));
+    }
+  }
+
+
+  static void SubmitModificationJob(std::unique_ptr<DicomModification>& modification,
+                                    bool isAnonymization,
+                                    RestApiPostCall& call,
+                                    const Json::Value& body,
+                                    ResourceType level)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::unique_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
+
+    job->SetModification(modification.release(), level, isAnonymization);
+    job->SetOrigin(call);
+    SetKeepSource(*job, body);
+
+    static const char* TRANSCODE = "Transcode";
+    if (body.isMember(TRANSCODE))
+    {
+      job->SetTranscode(SerializationToolbox::ReadString(body, TRANSCODE));
+    }
+    
+    context.AddChildInstances(*job, call.GetUriComponent("id", ""));
+    job->AddTrailingStep();
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, body);
+  }
+
+
+  template <enum ResourceType resourceType>
+  static void ModifyResource(RestApiPostCall& call)
+  {
+    std::unique_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)
+  {
+    std::unique_ptr<DicomModification> modification(new DicomModification);
+
+    Json::Value body;
+    ParseAnonymizationRequest(body, *modification, call);
+
+    SubmitModificationJob(modification, true /* anonymization */,
+                          call, body, resourceType);
+  }
+
+
+  static void StoreCreatedInstance(std::string& id /* out */,
+                                   RestApiPostCall& call,
+                                   ParsedDicomFile& dicom,
+                                   bool sendAnswer)
+  {
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
+    toStore.SetParsedDicomFile(dicom);
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    StoreStatus status = context.Store(id, toStore, StoreInstanceMode_Default);
+
+    if (status == StoreStatus_Failure)
+    {
+      throw OrthancException(ErrorCode_CannotStoreInstance);
+    }
+
+    if (sendAnswer)
+    {
+      OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, id);
+    }
+  }
+
+
+  static void CreateDicomV1(ParsedDicomFile& dicom,
+                            RestApiPostCall& call,
+                            const Json::Value& request)
+  {
+    // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}'
+    // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}'
+
+    assert(request.isObject());
+    LOG(WARNING) << "Using a deprecated call to /tools/create-dicom";
+
+    Json::Value::Members members = request.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      if (request[name].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_CreateDicomNotString);
+      }
+
+      std::string value = request[name].asString();
+
+      DicomTag tag = FromDcmtkBridge::ParseTag(name);
+      if (tag == DICOM_TAG_PIXEL_DATA)
+      {
+        dicom.EmbedContent(value);
+      }
+      else
+      {
+        // This is V1, don't try and decode data URI scheme
+        dicom.ReplacePlainString(tag, value);
+      }
+    }
+  }
+
+
+  static void InjectTags(ParsedDicomFile& dicom,
+                         const Json::Value& tags,
+                         bool decodeBinaryTags,
+                         const std::string& privateCreator)
+  {
+    if (tags.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, "Tags field is not an array");
+    }
+
+    // Inject the user-specified tags
+    Json::Value::Members members = tags.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      DicomTag tag = FromDcmtkBridge::ParseTag(name);
+
+      if (tag != DICOM_TAG_SPECIFIC_CHARACTER_SET)
+      {
+        if (tag != DICOM_TAG_PATIENT_ID &&
+            tag != DICOM_TAG_ACQUISITION_DATE &&
+            tag != DICOM_TAG_ACQUISITION_TIME &&
+            tag != DICOM_TAG_CONTENT_DATE &&
+            tag != DICOM_TAG_CONTENT_TIME &&
+            tag != DICOM_TAG_INSTANCE_CREATION_DATE &&
+            tag != DICOM_TAG_INSTANCE_CREATION_TIME &&
+            tag != DICOM_TAG_SERIES_DATE &&
+            tag != DICOM_TAG_SERIES_TIME &&
+            tag != DICOM_TAG_STUDY_DATE &&
+            tag != DICOM_TAG_STUDY_TIME &&
+            dicom.HasTag(tag))
+        {
+          throw OrthancException(ErrorCode_CreateDicomOverrideTag, name);
+        }
+
+        if (tag == DICOM_TAG_PIXEL_DATA)
+        {
+          throw OrthancException(ErrorCode_CreateDicomUseContent);
+        }
+        else
+        {
+          dicom.Replace(tag, tags[name], decodeBinaryTags, DicomReplaceMode_InsertIfAbsent, privateCreator);
+        }
+      }
+    }
+  }
+
+
+  static void CreateSeries(RestApiPostCall& call,
+                           ParsedDicomFile& base /* in */,
+                           const Json::Value& content,
+                           bool decodeBinaryTags,
+                           const std::string& privateCreator)
+  {
+    assert(content.isArray());
+    assert(content.size() > 0);
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    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;
+
+    try
+    {
+      for (Json::ArrayIndex i = 0; i < content.size(); i++)
+      {
+        std::unique_ptr<ParsedDicomFile> dicom(base.Clone(false));
+        const Json::Value* payload = NULL;
+
+        if (content[i].type() == Json::stringValue)
+        {
+          payload = &content[i];
+        }
+        else if (content[i].type() == Json::objectValue)
+        {
+          if (!content[i].isMember("Content"))
+          {
+            throw OrthancException(ErrorCode_CreateDicomNoPayload);
+          }
+
+          payload = &content[i]["Content"];
+
+          if (content[i].isMember("Tags"))
+          {
+            InjectTags(*dicom, content[i]["Tags"], decodeBinaryTags, privateCreator);
+          }
+        }
+
+        if (payload == NULL ||
+            payload->type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme);
+        }
+
+        dicom->EmbedContent(payload->asString());
+        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, false);
+      }
+    }
+    catch (OrthancException&)
+    {
+      // Error: Remove the newly-created series
+      
+      std::string series;
+      if (context.GetIndex().LookupParent(series, someInstance))
+      {
+        Json::Value dummy;
+        context.GetIndex().DeleteResource(dummy, series, ResourceType_Series);
+      }
+
+      throw;
+    }
+
+    std::string series;
+    if (context.GetIndex().LookupParent(series, someInstance))
+    {
+      OrthancRestApi::GetApi(call).AnswerStoredResource(call, series, ResourceType_Series, StoreStatus_Success);
+    }
+  }
+
+
+  static void CreateDicomV2(RestApiPostCall& call,
+                            const Json::Value& request)
+  {
+    assert(request.isObject());
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    if (!request.isMember("Tags") ||
+        request["Tags"].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    ParsedDicomFile dicom(true);
+
+    {
+      Encoding encoding;
+
+      if (request["Tags"].isMember("SpecificCharacterSet"))
+      {
+        const char* tmp = request["Tags"]["SpecificCharacterSet"].asCString();
+        if (!GetDicomEncoding(encoding, tmp))
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Unknown specific character set: " + std::string(tmp));
+        }
+      }
+      else
+      {
+        encoding = GetDefaultDicomEncoding();
+      }
+
+      dicom.SetEncoding(encoding);
+    }
+
+    ResourceType parentType = ResourceType_Instance;
+
+    if (request.isMember("Parent"))
+    {
+      // Locate the parent tags
+      std::string parent = request["Parent"].asString();
+      if (!context.GetIndex().LookupResourceType(parentType, parent))
+      {
+        throw OrthancException(ErrorCode_CreateDicomBadParent);
+      }
+
+      if (parentType == ResourceType_Instance)
+      {
+        throw OrthancException(ErrorCode_CreateDicomParentIsInstance);
+      }
+
+      // Select one existing child instance of the parent resource, to
+      // retrieve all its tags
+      Json::Value siblingTags;
+      std::string siblingInstanceId;
+
+      {
+        // Retrieve all the instances of the parent resource
+        std::list<std::string>  siblingInstances;
+        context.GetIndex().GetChildInstances(siblingInstances, parent);
+
+        if (siblingInstances.empty())
+	{
+	  // Error: No instance (should never happen)
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        siblingInstanceId = siblingInstances.front();
+        context.ReadDicomAsJson(siblingTags, siblingInstanceId);
+      }
+
+
+      // Choose the same encoding as the parent resource
+      {
+        static const char* SPECIFIC_CHARACTER_SET = "0008,0005";
+
+        if (siblingTags.isMember(SPECIFIC_CHARACTER_SET))
+        {
+          Encoding encoding;
+
+          if (!siblingTags[SPECIFIC_CHARACTER_SET].isMember("Value") ||
+              siblingTags[SPECIFIC_CHARACTER_SET]["Value"].type() != Json::stringValue ||
+              !GetDicomEncoding(encoding, siblingTags[SPECIFIC_CHARACTER_SET]["Value"].asCString()))
+          {
+            LOG(WARNING) << "Instance with an incorrect Specific Character Set, "
+                         << "using the default Orthanc encoding: " << siblingInstanceId;
+            encoding = GetDefaultDicomEncoding();
+          }
+
+          dicom.SetEncoding(encoding);
+        }
+      }
+
+
+      // Retrieve the tags for all the parent modules
+      typedef std::set<DicomTag> ModuleTags;
+      ModuleTags moduleTags;
+
+      ResourceType type = parentType;
+      for (;;)
+      {
+        DicomTag::AddTagsForModule(moduleTags, GetModule(type));
+      
+        if (type == ResourceType_Patient)
+        {
+          break;   // We're done
+        }
+
+        // Go up
+        std::string tmp;
+        if (!context.GetIndex().LookupParent(tmp, parent))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        parent = tmp;
+        type = GetParentResourceType(type);
+      }
+
+      for (ModuleTags::const_iterator it = moduleTags.begin();
+           it != moduleTags.end(); ++it)
+      {
+        std::string t = it->Format();
+        if (siblingTags.isMember(t))
+        {
+          const Json::Value& tag = siblingTags[t];
+          if (tag["Type"] == "Null")
+          {
+            dicom.ReplacePlainString(*it, "");
+          }
+          else if (tag["Type"] == "String")
+          {
+            std::string value = tag["Value"].asString();  // This is an UTF-8 value (as it comes from JSON)
+            dicom.ReplacePlainString(*it, value);
+          }
+        }
+      }
+    }
+
+
+    bool decodeBinaryTags = true;
+    if (request.isMember("InterpretBinaryTags"))
+    {
+      const Json::Value& v = request["InterpretBinaryTags"];
+      if (v.type() != Json::booleanValue)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      decodeBinaryTags = v.asBool();
+    }
+
+
+    // New argument in Orthanc 1.6.0
+    std::string privateCreator;
+    if (request.isMember("PrivateCreator"))
+    {
+      const Json::Value& v = request["PrivateCreator"];
+      if (v.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+
+      privateCreator = v.asString();
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+      privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
+    }
+
+    
+    // Inject time-related information
+    std::string date, 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.ReplacePlainString(DICOM_TAG_SERIES_DATE, date);
+      dicom.ReplacePlainString(DICOM_TAG_SERIES_TIME, time);
+    }
+
+    if (parentType == ResourceType_Patient ||
+        parentType == ResourceType_Instance /* no parent */)
+    {
+      dicom.ReplacePlainString(DICOM_TAG_STUDY_DATE, date);
+      dicom.ReplacePlainString(DICOM_TAG_STUDY_TIME, time);
+    }
+
+
+    InjectTags(dicom, request["Tags"], decodeBinaryTags, privateCreator);
+
+
+    // Inject the content (either an image, or a PDF file)
+    if (request.isMember("Content"))
+    {
+      const Json::Value& content = request["Content"];
+
+      if (content.type() == Json::stringValue)
+      {
+        dicom.EmbedContent(request["Content"].asString());
+
+      }
+      else if (content.type() == Json::arrayValue)
+      {
+        if (content.size() > 0)
+        {
+          // Let's create a series instead of a single instance
+          CreateSeries(call, dicom, content, decodeBinaryTags, privateCreator);
+          return;
+        }
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_CreateDicomUseDataUriScheme);
+      }
+    }
+
+    std::string id;
+    StoreCreatedInstance(id, call, dicom, true);
+  }
+
+
+  static void CreateDicom(RestApiPostCall& call)
+  {
+    Json::Value request;
+    if (!call.ParseJsonRequest(request) ||
+        !request.isObject())
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+
+    if (request.isMember("Tags"))
+    {
+      CreateDicomV2(call, request);
+    }
+    else
+    {
+      // Compatibility with Orthanc <= 0.9.3
+      ParsedDicomFile dicom(true);
+      CreateDicomV1(dicom, call, request);
+
+      std::string id;
+      StoreCreatedInstance(id, call, dicom, true);
+    }
+  }
+
+
+  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::unique_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();
+
+    SetKeepSource(*job, request);
+
+    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::unique_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();
+
+    SetKeepSource(*job, request);
+
+    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<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<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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,333 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/Logging.h"
+#include "../../Core/MetricsRegistry.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+
+#include <boost/algorithm/string/predicate.hpp>
+
+namespace Orthanc
+{
+  static void SetupResourceAnswer(Json::Value& result,
+                                  const std::string& publicId,
+                                  ResourceType resourceType,
+                                  StoreStatus status)
+  {
+    result = Json::objectValue;
+
+    if (status != StoreStatus_Failure)
+    {
+      result["ID"] = publicId;
+      result["Path"] = GetBasePath(resourceType, publicId);
+    }
+    
+    result["Status"] = EnumerationToString(status);
+  }
+
+
+  void OrthancRestApi::AnswerStoredInstance(RestApiPostCall& call,
+                                            DicomInstanceToStore& instance,
+                                            StoreStatus status,
+                                            const std::string& instanceId) const
+  {
+    Json::Value result;
+    SetupResourceAnswer(result, instanceId, ResourceType_Instance, status);
+
+    result["ParentPatient"] = instance.GetHasher().HashPatient();
+    result["ParentStudy"] = instance.GetHasher().HashStudy();
+    result["ParentSeries"] = instance.GetHasher().HashSeries();
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  void OrthancRestApi::AnswerStoredResource(RestApiPostCall& call,
+                                            const std::string& publicId,
+                                            ResourceType resourceType,
+                                            StoreStatus status) const
+  {
+    Json::Value result;
+    SetupResourceAnswer(result, publicId, resourceType, status);
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  void OrthancRestApi::ResetOrthanc(RestApiPostCall& call)
+  {
+    OrthancRestApi::GetApi(call).leaveBarrier_ = true;
+    OrthancRestApi::GetApi(call).resetRequestReceived_ = true;
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+  }
+
+
+  void OrthancRestApi::ShutdownOrthanc(RestApiPostCall& call)
+  {
+    OrthancRestApi::GetApi(call).leaveBarrier_ = true;
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+    LOG(WARNING) << "Shutdown request received";
+  }
+
+
+
+
+
+  // Upload of DICOM files through HTTP ---------------------------------------
+
+  static void UploadDicomFile(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    LOG(INFO) << "Receiving a DICOM file of " << call.GetBodySize() << " bytes through HTTP";
+
+    if (call.GetBodySize() == 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Received an empty DICOM file");
+    }
+
+    // The lifetime of "dicom" must be longer than "toStore", as the
+    // latter can possibly store a reference to the former (*)
+    std::string dicom;
+
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
+
+    if (boost::iequals(call.GetHttpHeader("content-encoding", ""), "gzip"))
+    {
+      GzipCompressor compressor;
+      compressor.Uncompress(dicom, call.GetBodyData(), call.GetBodySize());
+      toStore.SetBuffer(dicom.c_str(), dicom.size());  // (*)
+    }
+    else
+    {
+      toStore.SetBuffer(call.GetBodyData(), call.GetBodySize());
+    }    
+
+    std::string publicId;
+    StoreStatus status = context.Store(publicId, toStore, StoreInstanceMode_Default);
+
+    OrthancRestApi::GetApi(call).AnswerStoredInstance(call, toStore, status, publicId);
+  }
+
+
+
+  // Registration of the various REST handlers --------------------------------
+
+  OrthancRestApi::OrthancRestApi(ServerContext& context) : 
+    context_(context),
+    leaveBarrier_(false),
+    resetRequestReceived_(false),
+    activeRequests_(context.GetMetricsRegistry(), 
+                    "orthanc_rest_api_active_requests", 
+                    MetricsType_MaxOver10Seconds)
+  {
+    RegisterSystem();
+
+    RegisterChanges();
+    RegisterResources();
+    RegisterModalities();
+    RegisterAnonymizeModify();
+    RegisterArchive();
+
+    Register("/instances", UploadDicomFile);
+
+    // Auto-generated directories
+    Register("/tools", RestApi::AutoListChildren);
+    Register("/tools/reset", ResetOrthanc);
+    Register("/tools/shutdown", ShutdownOrthanc);
+    Register("/instances/{id}/frames/{frame}", RestApi::AutoListChildren);
+  }
+
+
+  bool OrthancRestApi::Handle(HttpOutput& output,
+                              RequestOrigin origin,
+                              const char* remoteIp,
+                              const char* username,
+                              HttpMethod method,
+                              const UriComponents& uri,
+                              const Arguments& headers,
+                              const GetArguments& getArguments,
+                              const void* bodyData,
+                              size_t bodySize)
+  {
+    MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_rest_api_duration_ms");
+    MetricsRegistry::ActiveCounter counter(activeRequests_);
+
+    return RestApi::Handle(output, origin, remoteIp, username, method,
+                           uri, headers, getArguments, bodyData, bodySize);
+  }
+
+
+  ServerContext& OrthancRestApi::GetContext(RestApiCall& call)
+  {
+    return GetApi(call).context_;
+  }
+
+
+  ServerIndex& OrthancRestApi::GetIndex(RestApiCall& call)
+  {
+    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";
+
+  
+  bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous,
+                                               const Json::Value& body)
+  {
+    if (body.type() != Json::objectValue)
+    {
+      return isDefaultSynchronous;
+    }
+    else if (body.isMember(KEY_SYNCHRONOUS))
+    {
+      return SerializationToolbox::ReadBoolean(body, KEY_SYNCHRONOUS);
+    }
+    else if (body.isMember(KEY_ASYNCHRONOUS))
+    {
+      return !SerializationToolbox::ReadBoolean(body, KEY_ASYNCHRONOUS);
+    }
+    else
+    {
+      return isDefaultSynchronous;
+    }
+  }
+
+  
+  unsigned int OrthancRestApi::GetJobRequestPriority(const Json::Value& body)
+  {
+    if (body.type() != Json::objectValue ||
+        !body.isMember(KEY_PRIORITY))
+    {
+      return 0;   // Default priority
+    }
+    else 
+    {
+      return SerializationToolbox::ReadInteger(body, KEY_PRIORITY);
+    }
+  }
+  
+
+  void OrthancRestApi::SubmitGenericJob(RestApiOutput& output,
+                                        ServerContext& context,
+                                        IJob* job,
+                                        bool synchronous,
+                                        int priority)
+  {
+    std::unique_ptr<IJob> raii(job);
+    
+    if (job == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    if (synchronous)
+    {
+      Json::Value successContent;
+      context.GetJobsEngine().GetRegistry().SubmitAndWait
+        (successContent, raii.release(), priority);
+
+      // Success in synchronous execution
+      output.AnswerJson(successContent);
+    }
+    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;
+      output.AnswerJson(v);
+    }
+  }
+
+  
+  void OrthancRestApi::SubmitGenericJob(RestApiPostCall& call,
+                                        IJob* job,
+                                        bool isDefaultSynchronous,
+                                        const Json::Value& body) const
+  {
+    std::unique_ptr<IJob> raii(job);
+
+    if (body.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    bool synchronous = IsSynchronousJobRequest(isDefaultSynchronous, body);
+    int priority = GetJobRequestPriority(body);
+
+    SubmitGenericJob(call.GetOutput(), context_, raii.release(), synchronous, priority);
+  }
+
+  
+  void OrthancRestApi::SubmitCommandsJob(RestApiPostCall& call,
+                                         SetOfCommandsJob* job,
+                                         bool isDefaultSynchronous,
+                                         const Json::Value& body) const
+  {
+    std::unique_ptr<SetOfCommandsJob> raii(job);
+    
+    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);
+    }
+
+    SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/SetOfCommandsJob.h"
+#include "../../Core/MetricsRegistry.h"
+#include "../../Core/RestApi/RestApi.h"
+#include "../ServerEnumerations.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class ServerContext;
+  class ServerIndex;
+  class DicomInstanceToStore;
+
+  class OrthancRestApi : public RestApi
+  {
+  public:
+    typedef std::set<std::string> SetOfStrings;
+
+  private:
+    ServerContext&                  context_;
+    bool                            leaveBarrier_;
+    bool                            resetRequestReceived_;
+    MetricsRegistry::SharedMetrics  activeRequests_;
+
+    void RegisterSystem();
+
+    void RegisterChanges();
+
+    void RegisterResources();
+
+    void RegisterModalities();
+
+    void RegisterAnonymizeModify();
+
+    void RegisterArchive();
+
+    static void ResetOrthanc(RestApiPostCall& call);
+
+    static void ShutdownOrthanc(RestApiPostCall& call);
+
+  public:
+    OrthancRestApi(ServerContext& context);
+
+    virtual bool Handle(HttpOutput& output,
+                        RequestOrigin origin,
+                        const char* remoteIp,
+                        const char* username,
+                        HttpMethod method,
+                        const UriComponents& uri,
+                        const Arguments& headers,
+                        const GetArguments& getArguments,
+                        const void* bodyData,
+                        size_t bodySize) ORTHANC_OVERRIDE;
+
+    const bool& LeaveBarrierFlag() const
+    {
+      return leaveBarrier_;
+    }
+
+    bool IsResetRequestReceived() const
+    {
+      return resetRequestReceived_;
+    }
+
+    static OrthancRestApi& GetApi(RestApiCall& call)
+    {
+      return dynamic_cast<OrthancRestApi&>(call.GetContext());
+    }
+
+    static ServerContext& GetContext(RestApiCall& call);
+
+    static ServerIndex& GetIndex(RestApiCall& call);
+
+    // WARNING: "instanceId" can be different from
+    // "instance.GetHasher().HashInstance()" if transcoding is enabled
+    void AnswerStoredInstance(RestApiPostCall& call,
+                              DicomInstanceToStore& instance,
+                              StoreStatus status,
+                              const std::string& instanceId) const;
+
+    void AnswerStoredResource(RestApiPostCall& call,
+                              const std::string& publicId,
+                              ResourceType resourceType,
+                              StoreStatus status) const;
+
+    static bool IsSynchronousJobRequest(bool isDefaultSynchronous,
+                                        const Json::Value& body);
+    
+    static unsigned int GetJobRequestPriority(const Json::Value& body);
+    
+    static void SubmitGenericJob(RestApiOutput& output,
+                                 ServerContext& context,
+                                 IJob* job,
+                                 bool synchronous,
+                                 int priority);
+    
+    void SubmitGenericJob(RestApiPostCall& call,
+                          IJob* job,
+                          bool isDefaultSynchronous,
+                          const Json::Value& body) const;
+
+    void SubmitCommandsJob(RestApiPostCall& call,
+                           SetOfCommandsJob* job,
+                           bool isDefaultSynchronous,
+                           const Json::Value& body) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,334 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../../Core/HttpServer/FilesystemHttpSender.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../OrthancConfiguration.h"
+#include "../ServerContext.h"
+#include "../ServerJobs/ArchiveJob.h"
+
+
+namespace Orthanc
+{
+  static const char* const KEY_RESOURCES = "Resources";
+  static const char* const KEY_EXTENDED = "Extended";
+  static const char* const KEY_TRANSCODE = "Transcode";
+  
+  static void AddResourcesOfInterestFromArray(ArchiveJob& job,
+                                              const Json::Value& resources)
+  {
+    if (resources.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Expected a list of strings (Orthanc identifiers)");
+    }
+    
+    for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+    {
+      if (resources[i].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Expected a list of strings (Orthanc identifiers)");
+      }
+      else
+      {
+        job.AddResource(resources[i].asString());
+      }
+    }
+  }
+
+  
+  static void AddResourcesOfInterest(ArchiveJob& job         /* inout */,
+                                     const Json::Value& body /* in */)
+  {
+    if (body.type() == Json::arrayValue)
+    {
+      AddResourcesOfInterestFromArray(job, body);
+    }
+    else if (body.type() == Json::objectValue)
+    {
+      if (body.isMember(KEY_RESOURCES))
+      {
+        AddResourcesOfInterestFromArray(job, body[KEY_RESOURCES]);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing field " + std::string(KEY_RESOURCES) +
+                               " in the JSON body");
+      }
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  static DicomTransferSyntax GetTransferSyntax(const std::string& value)
+  {
+    DicomTransferSyntax syntax;
+    if (LookupTransferSyntax(syntax, value))
+    {
+      return syntax;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Unknown transfer syntax: " + value);
+    }
+  }
+  
+  
+  static void GetJobParameters(bool& synchronous,            /* out */
+                               bool& extended,               /* out */
+                               bool& transcode,              /* out */
+                               DicomTransferSyntax& syntax,  /* out */
+                               int& priority,                /* out */
+                               const Json::Value& body,      /* in */
+                               const bool defaultExtended    /* in */)
+  {
+    synchronous = OrthancRestApi::IsSynchronousJobRequest
+      (true /* synchronous by default */, body);
+
+    priority = OrthancRestApi::GetJobRequestPriority(body);
+
+    if (body.type() == Json::objectValue &&
+        body.isMember(KEY_EXTENDED))
+    {
+      extended = SerializationToolbox::ReadBoolean(body, KEY_EXTENDED);
+    }
+    else
+    {
+      extended = defaultExtended;
+    }
+
+    if (body.type() == Json::objectValue &&
+        body.isMember(KEY_TRANSCODE))
+    {
+      transcode = true;
+      syntax = GetTransferSyntax(SerializationToolbox::ReadString(body, KEY_TRANSCODE));
+    }
+    else
+    {
+      transcode = false;
+    }
+  }
+
+
+  static void SubmitJob(RestApiOutput& output,
+                        ServerContext& context,
+                        std::unique_ptr<ArchiveJob>& job,
+                        int priority,
+                        bool synchronous,
+                        const std::string& filename)
+  {
+    if (job.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    job->SetDescription("REST API");
+
+    if (synchronous)
+    {
+      boost::shared_ptr<TemporaryFile> tmp;
+
+      {
+        OrthancConfiguration::ReaderLock lock;
+        tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
+      }
+
+      job->SetSynchronousTarget(tmp);
+    
+      Json::Value publicContent;
+      context.GetJobsEngine().GetRegistry().SubmitAndWait
+        (publicContent, job.release(), priority);
+      
+      {
+        // The archive is now created: Prepare the sending of the ZIP file
+        FilesystemHttpSender sender(tmp->GetPath(), MimeType_Zip);
+        sender.SetContentFilename(filename);
+
+        // Send the ZIP
+        output.AnswerStream(sender);
+      }
+    }
+    else
+    {
+      OrthancRestApi::SubmitGenericJob(output, context, job.release(), false, priority);
+    }
+  }
+
+  
+  template <bool IS_MEDIA,
+            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
+  static void CreateBatch(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value body;
+    if (call.ParseJsonRequest(body))
+    {
+      bool synchronous, extended, transcode;
+      DicomTransferSyntax transferSyntax;
+      int priority;
+      GetJobParameters(synchronous, extended, transcode, transferSyntax,
+                       priority, body, DEFAULT_IS_EXTENDED);
+      
+      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
+      AddResourcesOfInterest(*job, body);
+
+      if (transcode)
+      {
+        job->SetTranscode(transferSyntax);
+      }
+      
+      SubmitJob(call.GetOutput(), context, job, priority, synchronous, "Archive.zip");
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Expected a list of resources to archive in the body");
+    }
+  }
+  
+
+  template <bool IS_MEDIA,
+            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
+  static void CreateSingleGet(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string id = call.GetUriComponent("id", "");
+
+    bool extended;
+    if (IS_MEDIA)
+    {
+      extended = call.HasArgument("extended");
+    }
+    else
+    {
+      extended = false;
+    }
+
+    std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
+    job->AddResource(id);
+
+    static const char* const TRANSCODE = "transcode";
+    if (call.HasArgument(TRANSCODE))
+    {
+      job->SetTranscode(GetTransferSyntax(call.GetArgument(TRANSCODE, "")));
+    }
+
+    SubmitJob(call.GetOutput(), context, job, 0 /* priority */,
+              true /* synchronous */, id + ".zip");
+  }
+
+
+  template <bool IS_MEDIA,
+            bool DEFAULT_IS_EXTENDED  /* only makes sense for media (i.e. not ZIP archives) */ >
+  static void CreateSinglePost(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string id = call.GetUriComponent("id", "");
+
+    Json::Value body;
+    if (call.ParseJsonRequest(body))
+    {
+      bool synchronous, extended, transcode;
+      DicomTransferSyntax transferSyntax;
+      int priority;
+      GetJobParameters(synchronous, extended, transcode, transferSyntax,
+                       priority, body, DEFAULT_IS_EXTENDED);
+      
+      std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended));
+      job->AddResource(id);
+
+      if (transcode)
+      {
+        job->SetTranscode(transferSyntax);
+      }
+
+      SubmitJob(call.GetOutput(), context, job, priority, synchronous, id + ".zip");
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+    
+  void OrthancRestApi::RegisterArchive()
+  {
+    Register("/patients/{id}/archive",
+             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/studies/{id}/archive",
+             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/series/{id}/archive",
+             CreateSingleGet<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+
+    Register("/patients/{id}/archive",
+             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/studies/{id}/archive",
+             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+    Register("/series/{id}/archive",
+             CreateSinglePost<false /* ZIP */, false /* extended makes no sense in ZIP */>);
+
+    Register("/patients/{id}/media",
+             CreateSingleGet<true /* media */, false /* not extended by default */>);
+    Register("/studies/{id}/media",
+             CreateSingleGet<true /* media */, false /* not extended by default */>);
+    Register("/series/{id}/media",
+             CreateSingleGet<true /* media */, false /* not extended by default */>);
+
+    Register("/patients/{id}/media",
+             CreateSinglePost<true /* media */, false /* not extended by default */>);
+    Register("/studies/{id}/media",
+             CreateSinglePost<true /* media */, false /* not extended by default */>);
+    Register("/series/{id}/media",
+             CreateSinglePost<true /* media */, false /* not extended by default */>);
+
+    Register("/tools/create-archive",
+             CreateBatch<false /* ZIP */,  false /* extended makes no sense in ZIP */>);
+    Register("/tools/create-media",
+             CreateBatch<true /* media */, false /* not extended by default */>);
+    Register("/tools/create-media-extended",
+             CreateBatch<true /* media */, true /* extended by default */>);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestChanges.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,141 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  // Changes API --------------------------------------------------------------
+ 
+  static void GetSinceAndLimit(int64_t& since,
+                               unsigned int& limit,
+                               bool& last,
+                               const RestApiGetCall& call)
+  {
+    static const unsigned int DEFAULT_LIMIT = 100;
+    
+    if (call.HasArgument("last"))
+    {
+      last = true;
+      return;
+    }
+
+    last = false;
+
+    try
+    {
+      since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0"));
+      limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", boost::lexical_cast<std::string>(DEFAULT_LIMIT)));
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      since = 0;
+      limit = DEFAULT_LIMIT;
+      return;
+    }
+  }
+
+  static void GetChanges(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    //std::string filter = GetArgument(getArguments, "filter", "");
+    int64_t since;
+    unsigned int limit;
+    bool last;
+    GetSinceAndLimit(since, limit, last, call);
+
+    Json::Value result;
+    if (last)
+    {
+      context.GetIndex().GetLastChange(result);
+    }
+    else
+    {
+      context.GetIndex().GetChanges(result, since, limit);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void DeleteChanges(RestApiDeleteCall& call)
+  {
+    OrthancRestApi::GetIndex(call).DeleteChanges();
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  // Exports API --------------------------------------------------------------
+ 
+  static void GetExports(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    int64_t since;
+    unsigned int limit;
+    bool last;
+    GetSinceAndLimit(since, limit, last, call);
+
+    Json::Value result;
+    if (last)
+    {
+      context.GetIndex().GetLastExportedResource(result);
+    }
+    else
+    {
+      context.GetIndex().GetExportedResources(result, since, limit);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void DeleteExports(RestApiDeleteCall& call)
+  {
+    OrthancRestApi::GetIndex(call).DeleteExportedResources();
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+  
+
+  void OrthancRestApi::RegisterChanges()
+  {
+    Register("/changes", GetChanges);
+    Register("/changes", DeleteChanges);
+    Register("/exports", GetExports);
+    Register("/exports", DeleteExports);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1647 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../../Core/Cache/SharedArchive.h"
+#include "../../Core/DicomNetworking/DicomAssociation.h"
+#include "../../Core/DicomNetworking/DicomControlUserConnection.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+
+#include "../OrthancConfiguration.h"
+#include "../QueryRetrieveHandler.h"
+#include "../ServerContext.h"
+#include "../ServerJobs/DicomModalityStoreJob.h"
+#include "../ServerJobs/DicomMoveScuJob.h"
+#include "../ServerJobs/OrthancPeerStoreJob.h"
+#include "../ServerToolbox.h"
+#include "../StorageCommitmentReports.h"
+
+
+namespace Orthanc
+{
+  static const char* const KEY_LEVEL = "Level";
+  static const char* const KEY_LOCAL_AET = "LocalAet";
+  static const char* const KEY_NORMALIZE = "Normalize";
+  static const char* const KEY_QUERY = "Query";
+  static const char* const KEY_RESOURCES = "Resources";
+  static const char* const KEY_TARGET_AET = "TargetAet";
+  static const char* const KEY_TIMEOUT = "Timeout";
+  static const char* const SOP_CLASS_UID = "SOPClassUID";
+  static const char* const SOP_INSTANCE_UID = "SOPInstanceUID";
+  
+  static RemoteModalityParameters MyGetModalityUsingSymbolicName(const std::string& name)
+  {
+    OrthancConfiguration::ReaderLock lock;
+    return lock.GetConfiguration().GetModalityUsingSymbolicName(name);
+  }
+
+
+  static void InjectAssociationTimeout(DicomAssociationParameters& params,
+                                       const Json::Value& body)
+  {
+    if (body.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
+    }
+    else if (body.isMember(KEY_TIMEOUT))
+    {
+      // New in Orthanc 1.7.0
+      params.SetTimeout(SerializationToolbox::ReadUnsignedInteger(body, KEY_TIMEOUT));
+    }
+  }
+
+  static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call,
+                                                             const Json::Value& body)
+  {   
+    const std::string& localAet =
+      OrthancRestApi::GetContext(call).GetDefaultLocalApplicationEntityTitle();
+    const RemoteModalityParameters remote =
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    DicomAssociationParameters params(localAet, remote);
+    InjectAssociationTimeout(params, body);
+    
+    return params;
+  }
+
+
+  static DicomAssociationParameters GetAssociationParameters(RestApiPostCall& call)
+  {
+    Json::Value body;
+    call.ParseJsonRequest(body);
+    return GetAssociationParameters(call, body);
+  }
+  
+
+  /***************************************************************************
+   * DICOM C-Echo SCU
+   ***************************************************************************/
+
+  static void DicomEcho(RestApiPostCall& call)
+  {
+    DicomControlUserConnection connection(GetAssociationParameters(call));
+
+    if (connection.Echo())
+    {
+      // Echo has succeeded
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+      return;
+    }
+    else
+    {
+      // Echo has failed
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }
+  }
+
+
+
+  /***************************************************************************
+   * DICOM C-Find SCU => DEPRECATED!
+   ***************************************************************************/
+
+  static bool MergeQueryAndTemplate(DicomMap& result,
+                                    const RestApiCall& call)
+  {
+    Json::Value query;
+
+    if (!call.ParseJsonRequest(query) ||
+        query.type() != Json::objectValue)
+    {
+      return false;
+    }
+
+    Json::Value::Members members = query.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
+      result.SetValue(t, query[members[i]].asString(), false);
+    }
+
+    return true;
+  }
+
+
+  static void FindPatient(DicomFindAnswers& result,
+                          DicomControlUserConnection& connection,
+                          const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the patient
+    DicomMap s;
+    fields.ExtractPatientInformation(s);
+    connection.Find(result, ResourceType_Patient, s, true /* normalize */);
+  }
+
+
+  static void FindStudy(DicomFindAnswers& result,
+                        DicomControlUserConnection& connection,
+                        const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the study
+    DicomMap s;
+    fields.ExtractStudyInformation(s);
+
+    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
+    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
+    s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY);
+
+    connection.Find(result, ResourceType_Study, s, true /* normalize */);
+  }
+
+  static void FindSeries(DicomFindAnswers& result,
+                         DicomControlUserConnection& connection,
+                         const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the series
+    DicomMap s;
+    fields.ExtractSeriesInformation(s);
+
+    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
+    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
+    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
+
+    connection.Find(result, ResourceType_Series, s, true /* normalize */);
+  }
+
+  static void FindInstance(DicomFindAnswers& result,
+                           DicomControlUserConnection& connection,
+                           const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the instance
+    DicomMap s;
+    fields.ExtractInstanceInformation(s);
+
+    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
+    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
+    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
+    s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID);
+
+    connection.Find(result, ResourceType_Instance, s, true /* normalize */);
+  }
+
+
+  static void DicomFindPatient(RestApiPostCall& call)
+  {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
+
+    DicomMap fields;
+    DicomMap::SetupFindPatientTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call))
+    {
+      return;
+    }
+
+    DicomFindAnswers answers(false);
+
+    {
+      DicomControlUserConnection connection(GetAssociationParameters(call));
+      FindPatient(answers, connection, fields);
+    }
+
+    Json::Value result;
+    answers.ToJson(result, true);
+    call.GetOutput().AnswerJson(result);
+  }
+
+  static void DicomFindStudy(RestApiPostCall& call)
+  {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
+
+    DicomMap fields;
+    DicomMap::SetupFindStudyTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call))
+    {
+      return;
+    }
+
+    if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
+        fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2)
+    {
+      return;
+    }        
+      
+    DicomFindAnswers answers(false);
+
+    {
+      DicomControlUserConnection connection(GetAssociationParameters(call));
+      FindStudy(answers, connection, fields);
+    }
+
+    Json::Value result;
+    answers.ToJson(result, true);
+    call.GetOutput().AnswerJson(result);
+  }
+
+  static void DicomFindSeries(RestApiPostCall& call)
+  {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
+
+    DicomMap fields;
+    DicomMap::SetupFindSeriesTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call))
+    {
+      return;
+    }
+
+    if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
+         fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
+        fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2)
+    {
+      return;
+    }        
+         
+    DicomFindAnswers answers(false);
+
+    {
+      DicomControlUserConnection connection(GetAssociationParameters(call));
+      FindSeries(answers, connection, fields);
+    }
+
+    Json::Value result;
+    answers.ToJson(result, true);
+    call.GetOutput().AnswerJson(result);
+  }
+
+  static void DicomFindInstance(RestApiPostCall& call)
+  {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
+
+    DicomMap fields;
+    DicomMap::SetupFindInstanceTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call))
+    {
+      return;
+    }
+
+    if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).GetContent().size() <= 2 &&
+         fields.GetValue(DICOM_TAG_PATIENT_ID).GetContent().size() <= 2) ||
+        fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent().size() <= 2 ||
+        fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent().size() <= 2)
+    {
+      return;
+    }        
+         
+    DicomFindAnswers answers(false);
+
+    {
+      DicomControlUserConnection connection(GetAssociationParameters(call));
+      FindInstance(answers, connection, fields);
+    }
+
+    Json::Value result;
+    answers.ToJson(result, true);
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  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();
+
+    DicomMap m;
+    DicomMap::SetupFindPatientTemplate(m);
+    if (!MergeQueryAndTemplate(m, call))
+    {
+      return;
+    }
+ 
+    DicomControlUserConnection connection(GetAssociationParameters(call));
+    
+    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;
+      patients.ToJson(patient, i, true);
+
+      DicomMap::SetupFindStudyTemplate(m);
+      if (!MergeQueryAndTemplate(m, call))
+      {
+        return;
+      }
+
+      CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
+
+      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;
+        studies.ToJson(study, j, true);
+
+        DicomMap::SetupFindSeriesTemplate(m);
+        if (!MergeQueryAndTemplate(m, call))
+        {
+          return;
+        }
+
+        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;
+          series.ToJson(series2, k, true);
+          study["Series"].append(series2);
+        }
+
+        patient["Studies"].append(study);
+      }
+
+      result.append(patient);
+    }
+    
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+
+  /***************************************************************************
+   * DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0
+   ***************************************************************************/
+
+  static void AnswerQueryHandler(RestApiPostCall& call,
+                                 std::unique_ptr<QueryRetrieveHandler>& handler)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    if (handler.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    handler->Run();
+    
+    std::string s = context.GetQueryRetrieveArchive().Add(handler.release());
+    Json::Value result = Json::objectValue;
+    result["ID"] = s;
+    result["Path"] = "/queries/" + s;
+    
+    call.GetOutput().AnswerJson(result);
+  }
+
+  
+  static void DicomQuery(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    Json::Value request;
+
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
+    }
+    else if (!request.isMember(KEY_LEVEL) ||
+             request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "The JSON body must contain field " + std::string(KEY_LEVEL));
+    }
+    else if (request.isMember(KEY_NORMALIZE) &&
+             request[KEY_NORMALIZE].type() != Json::booleanValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "The field " + std::string(KEY_NORMALIZE) + " must contain a Boolean");
+    }
+    else if (request.isMember(KEY_QUERY) &&
+             request[KEY_QUERY].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "The field " + std::string(KEY_QUERY) + " must contain a JSON object");
+    }
+    else
+    {
+      std::unique_ptr<QueryRetrieveHandler>  handler(new QueryRetrieveHandler(context));
+      
+      handler->SetModality(call.GetUriComponent("id", ""));
+      handler->SetLevel(StringToResourceType(request[KEY_LEVEL].asCString()));
+
+      if (request.isMember(KEY_QUERY))
+      {
+        std::map<DicomTag, std::string> query;
+        SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
+
+        for (std::map<DicomTag, std::string>::const_iterator
+               it = query.begin(); it != query.end(); ++it)
+        {
+          handler->SetQuery(it->first, it->second);
+        }
+      }
+
+      if (request.isMember(KEY_NORMALIZE))
+      {
+        handler->SetFindNormalized(request[KEY_NORMALIZE].asBool());
+      }
+
+      AnswerQueryHandler(call, handler);
+    }
+  }
+
+
+  static void ListQueries(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::list<std::string> queries;
+    context.GetQueryRetrieveArchive().List(queries);
+
+    Json::Value result = Json::arrayValue;
+    for (std::list<std::string>::const_iterator
+           it = queries.begin(); it != queries.end(); ++it)
+    {
+      result.append(*it);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  namespace
+  {
+    class QueryAccessor
+    {
+    private:
+      ServerContext&            context_;
+      SharedArchive::Accessor   accessor_;
+      QueryRetrieveHandler*     handler_;
+
+    public:
+      QueryAccessor(RestApiCall& call) :
+        context_(OrthancRestApi::GetContext(call)),
+        accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")),
+        handler_(NULL)
+      {
+        if (accessor_.IsValid())
+        {
+          handler_ = &dynamic_cast<QueryRetrieveHandler&>(accessor_.GetItem());
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_UnknownResource);
+        }
+      }                     
+
+      QueryRetrieveHandler& GetHandler() const
+      {
+        assert(handler_ != NULL);
+        return *handler_;
+      }
+    };
+
+    static void AnswerDicomMap(RestApiCall& call,
+                               const DicomMap& value,
+                               bool simplify)
+    {
+      Json::Value full = Json::objectValue;
+      FromDcmtkBridge::ToJson(full, value, simplify);
+      call.GetOutput().AnswerJson(full);
+    }
+  }
+
+
+  static void ListQueryAnswers(RestApiGetCall& call)
+  {
+    const bool expand = call.HasArgument("expand");
+    const bool simplify = call.HasArgument("simplify");
+    
+    QueryAccessor query(call);
+    size_t count = query.GetHandler().GetAnswersCount();
+
+    Json::Value result = Json::arrayValue;
+    for (size_t i = 0; i < count; i++)
+    {
+      if (expand)
+      {
+        // New in Orthanc 1.5.0
+        DicomMap value;
+        query.GetHandler().GetAnswer(value, i);
+        
+        Json::Value json = Json::objectValue;
+        FromDcmtkBridge::ToJson(json, value, simplify);
+
+        result.append(json);
+      }
+      else
+      {
+        result.append(boost::lexical_cast<std::string>(i));
+      }
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void GetQueryOneAnswer(RestApiGetCall& call)
+  {
+    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+
+    QueryAccessor query(call);
+
+    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;
+    int timeout = -1;
+    
+    Json::Value body;
+    if (call.ParseJsonRequest(body))
+    {
+      targetAet = Toolbox::GetJsonStringField(body, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
+      timeout = Toolbox::GetJsonIntegerField(body, KEY_TIMEOUT, -1);
+    }
+    else
+    {
+      body = Json::objectValue;
+      if (call.GetBodySize() > 0)
+      {
+        call.BodyToString(targetAet);
+      }
+      else
+      {
+        targetAet = context.GetDefaultLocalApplicationEntityTitle();
+      }
+    }
+    
+    std::unique_ptr<DicomMoveScuJob> job(new DicomMoveScuJob(context));
+    
+    {
+      QueryAccessor query(call);
+      job->SetTargetAet(targetAet);
+      job->SetLocalAet(query.GetHandler().GetLocalAet());
+      job->SetRemoteModality(query.GetHandler().GetRemoteModality());
+
+      if (timeout >= 0)
+      {
+        // New in Orthanc 1.7.0
+        job->SetTimeout(static_cast<uint32_t>(timeout));
+      }
+
+      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", ""));
+    SubmitRetrieveJob(call, false, index);
+  }
+
+
+  static void RetrieveAllAnswers(RestApiPostCall& call)
+  {
+    SubmitRetrieveJob(call, true, 0);
+  }
+
+
+  static void GetQueryArguments(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    AnswerDicomMap(call, query.GetHandler().GetQuery(), call.HasArgument("simplify"));
+  }
+
+
+  static void GetQueryLevel(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    call.GetOutput().AnswerBuffer(EnumerationToString(query.GetHandler().GetLevel()), MimeType_PlainText);
+  }
+
+
+  static void GetQueryModality(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    call.GetOutput().AnswerBuffer(query.GetHandler().GetModalitySymbolicName(), MimeType_PlainText);
+  }
+
+
+  static void DeleteQuery(RestApiDeleteCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", ""));
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  static void ListQueryOperations(RestApiGetCall& call)
+  {
+    // Ensure that the query of interest does exist
+    QueryAccessor query(call);  
+
+    RestApi::AutoListChildren(call);
+  }
+
+
+  static void ListQueryAnswerOperations(RestApiGetCall& call)
+  {
+    // Ensure that the query of interest does exist
+    QueryAccessor query(call);
+
+    // Ensure that the answer of interest does exist
+    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+
+    DicomMap map;
+    query.GetHandler().GetAnswer(map, index);
+
+    Json::Value answer = Json::arrayValue;
+    answer.append("content");
+    answer.append("retrieve");
+
+    switch (query.GetHandler().GetLevel())
+    {
+      case ResourceType_Patient:
+        answer.append("query-study");
+
+      case ResourceType_Study:
+        answer.append("query-series");
+
+      case ResourceType_Series:
+        answer.append("query-instances");
+        break;
+
+      default:
+        break;
+    }
+    
+    call.GetOutput().AnswerJson(answer);
+  }
+
+
+  template <ResourceType CHILDREN_LEVEL>
+  static void QueryAnswerChildren(RestApiPostCall& call)
+  {
+    // New in Orthanc 1.5.0
+    assert(CHILDREN_LEVEL == ResourceType_Study ||
+           CHILDREN_LEVEL == ResourceType_Series ||
+           CHILDREN_LEVEL == ResourceType_Instance);
+    
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::unique_ptr<QueryRetrieveHandler>  handler(new QueryRetrieveHandler(context));
+      
+    {
+      const QueryAccessor parent(call);
+      const ResourceType level = parent.GetHandler().GetLevel();
+    
+      const size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+
+      Json::Value request;
+
+      if (index >= parent.GetHandler().GetAnswersCount())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else if (CHILDREN_LEVEL == ResourceType_Study &&
+               level != ResourceType_Patient)
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+      else if (CHILDREN_LEVEL == ResourceType_Series &&
+               level != ResourceType_Patient &&
+               level != ResourceType_Study)
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }      
+      else if (CHILDREN_LEVEL == ResourceType_Instance &&
+               level != ResourceType_Patient &&
+               level != ResourceType_Study &&
+               level != ResourceType_Series)
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+      else if (!call.ParseJsonRequest(request))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
+      }
+      else
+      {
+        handler->SetFindNormalized(parent.GetHandler().IsFindNormalized());
+        handler->SetModality(parent.GetHandler().GetModalitySymbolicName());
+        handler->SetLevel(CHILDREN_LEVEL);
+
+        if (request.isMember(KEY_QUERY))
+        {
+          std::map<DicomTag, std::string> query;
+          SerializationToolbox::ReadMapOfTags(query, request, KEY_QUERY);
+
+          for (std::map<DicomTag, std::string>::const_iterator
+                 it = query.begin(); it != query.end(); ++it)
+          {
+            handler->SetQuery(it->first, it->second);
+          }
+        }
+
+        DicomMap answer;
+        parent.GetHandler().GetAnswer(answer, index);
+
+        // This switch-case mimics "DicomControlUserConnection::Move()"
+        switch (parent.GetHandler().GetLevel())
+        {
+          case ResourceType_Patient:
+            handler->CopyStringTag(answer, DICOM_TAG_PATIENT_ID);
+            break;
+
+          case ResourceType_Study:
+            handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+            break;
+
+          case ResourceType_Series:
+            handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+            handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+            break;
+
+          case ResourceType_Instance:
+            handler->CopyStringTag(answer, DICOM_TAG_STUDY_INSTANCE_UID);
+            handler->CopyStringTag(answer, DICOM_TAG_SERIES_INSTANCE_UID);
+            handler->CopyStringTag(answer, DICOM_TAG_SOP_INSTANCE_UID);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+      
+    AnswerQueryHandler(call, handler);
+  }
+  
+
+
+  /***************************************************************************
+   * DICOM C-Store SCU
+   ***************************************************************************/
+
+  static void GetInstancesToExport(Json::Value& otherArguments,
+                                   SetOfInstancesJob& job,
+                                   const std::string& remote,
+                                   RestApiPostCall& call)
+  {
+    otherArguments = Json::objectValue;
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+    if (Toolbox::IsSHA1(call.GetBodyData(), call.GetBodySize()))
+    {
+      std::string s;
+      call.BodyToString(s);
+
+      // This is for compatibility with Orthanc <= 0.5.1.
+      request = Json::arrayValue;
+      request.append(Toolbox::StripSpaces(s));
+    }
+    else if (!call.ParseJsonRequest(request))
+    {
+      // Bad JSON request
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON value");
+    }
+
+    if (request.isString())
+    {
+      std::string item = request.asString();
+      request = Json::arrayValue;
+      request.append(item);
+    }
+    else if (!request.isArray() &&
+             !request.isObject())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object, or a JSON array of strings");
+    }
+
+    const Json::Value* resources;
+    if (request.isArray())
+    {
+      resources = &request;
+    }
+    else
+    {
+      if (request.type() != Json::objectValue ||
+          !request.isMember(KEY_RESOURCES))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Missing field in JSON: \"" + std::string(KEY_RESOURCES) + "\"");
+      }
+
+      resources = &request[KEY_RESOURCES];
+      if (!resources->isArray())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "JSON field \"" + std::string(KEY_RESOURCES) + "\" must contain an array");
+      }
+
+      // Copy the remaining arguments
+      Json::Value::Members members = request.getMemberNames();
+      for (Json::Value::ArrayIndex i = 0; i < members.size(); i++)
+      {
+        otherArguments[members[i]] = request[members[i]];
+      }
+    }
+
+    bool logExportedResources;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      logExportedResources = lock.GetConfiguration().GetBooleanParameter("LogExportedResources", false);
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < resources->size(); i++)
+    {
+      if (!(*resources) [i].isString())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "Resources to be exported must be specified as a JSON array of strings");
+      }
+
+      std::string stripped = Toolbox::StripSpaces((*resources) [i].asString());
+      if (!Toolbox::IsSHA1(stripped))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "This string is not a valid Orthanc identifier: " + stripped);
+      }
+
+      job.AddParentResource(stripped);  // New in Orthanc 1.5.7
+      
+      context.AddChildInstances(job, stripped);
+
+      if (logExportedResources)
+      {
+        context.GetIndex().LogExportedResource(stripped, remote);
+      }
+    }
+  }
+
+
+  static void DicomStore(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string remote = call.GetUriComponent("id", "");
+
+    Json::Value request;
+    std::unique_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
+
+    GetInstancesToExport(request, *job, remote, call);
+
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, KEY_LOCAL_AET, 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(MyGetModalityUsingSymbolicName(remote));
+
+    if (moveOriginatorID != 0)
+    {
+      job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
+    }
+
+    // New in Orthanc 1.6.0
+    if (Toolbox::GetJsonBooleanField(request, "StorageCommitment", false))
+    {
+      job->EnableStorageCommitment(true);
+    }
+
+    // New in Orthanc 1.7.0
+    if (request.isMember(KEY_TIMEOUT))
+    {
+      job->SetTimeout(SerializationToolbox::ReadUnsignedInteger(request, KEY_TIMEOUT));
+    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+  }
+
+
+  static void DicomStoreStraight(RestApiPostCall& call)
+  {
+    Json::Value body = Json::objectValue;  // No body
+    DicomStoreUserConnection connection(GetAssociationParameters(call, body));
+
+    std::string sopClassUid, sopInstanceUid;
+    connection.Store(sopClassUid, sopInstanceUid, call.GetBodyData(),
+                     call.GetBodySize(), false /* Not a C-MOVE */, "", 0);
+
+    Json::Value answer = Json::objectValue;
+    answer[SOP_CLASS_UID] = sopClassUid;
+    answer[SOP_INSTANCE_UID] = sopInstanceUid;
+    
+    call.GetOutput().AnswerJson(answer);
+  }
+
+
+  /***************************************************************************
+   * DICOM C-Move SCU
+   ***************************************************************************/
+  
+  static void DicomMove(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue ||
+        !request.isMember(KEY_RESOURCES) ||
+        !request.isMember(KEY_LEVEL) ||
+        request[KEY_RESOURCES].type() != Json::arrayValue ||
+        request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON body containing fields " +
+                             std::string(KEY_RESOURCES) + " and " + std::string(KEY_LEVEL));
+    }
+
+    ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+    
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, KEY_LOCAL_AET, context.GetDefaultLocalApplicationEntityTitle());
+    std::string targetAet = Toolbox::GetJsonStringField
+      (request, KEY_TARGET_AET, context.GetDefaultLocalApplicationEntityTitle());
+
+    const RemoteModalityParameters source =
+      MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    DicomAssociationParameters params(localAet, source);
+    InjectAssociationTimeout(params, request);
+
+    DicomControlUserConnection connection(params);
+
+    for (Json::Value::ArrayIndex i = 0; i < request[KEY_RESOURCES].size(); i++)
+    {
+      DicomMap resource;
+      FromDcmtkBridge::FromJson(resource, request[KEY_RESOURCES][i]);
+      
+      connection.Move(targetAet, level, resource);
+    }
+
+    // Move has succeeded
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+  }
+
+
+
+  /***************************************************************************
+   * Orthanc Peers => Store client
+   ***************************************************************************/
+
+  static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers,
+                             const std::string& id)
+  {
+    return peers.find(id) != peers.end();
+  }
+
+  static void ListPeers(RestApiGetCall& call)
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    OrthancRestApi::SetOfStrings peers;
+    lock.GetConfiguration().GetListOfOrthancPeers(peers);
+
+    if (call.HasArgument("expand"))
+    {
+      Json::Value result = Json::objectValue;
+      for (OrthancRestApi::SetOfStrings::const_iterator
+             it = peers.begin(); it != peers.end(); ++it)
+      {
+        WebServiceParameters peer;
+        
+        if (lock.GetConfiguration().LookupOrthancPeer(peer, *it))
+        {
+          Json::Value info;
+          peer.FormatPublic(info);
+          result[*it] = info;
+        }
+      }
+      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);
+    }
+  }
+
+  static void ListPeerOperations(RestApiGetCall& call)
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    OrthancRestApi::SetOfStrings peers;
+    lock.GetConfiguration().GetListOfOrthancPeers(peers);
+
+    std::string id = call.GetUriComponent("id", "");
+    if (IsExistingPeer(peers, id))
+    {
+      RestApi::AutoListChildren(call);
+    }
+  }
+
+  static void PeerStore(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string remote = call.GetUriComponent("id", "");
+
+    Json::Value request;
+    std::unique_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
+
+    GetInstancesToExport(request, *job, remote, call);
+
+    static const char* TRANSCODE = "Transcode";
+    if (request.type() == Json::objectValue &&
+        request.isMember(TRANSCODE))
+    {
+      job->SetTranscode(SerializationToolbox::ReadString(request, TRANSCODE));
+    }
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      
+      WebServiceParameters peer;
+      if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
+      {
+        job->SetPeer(peer);    
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource,
+                               "No peer with symbolic name: " + remote);
+      }
+    }
+
+    OrthancRestApi::GetApi(call).SubmitCommandsJob
+      (call, job.release(), true /* synchronous by default */, request);
+  }
+
+  static void PeerSystem(RestApiGetCall& call)
+  {
+    std::string remote = call.GetUriComponent("id", "");
+
+    OrthancConfiguration::ReaderLock lock;
+
+    WebServiceParameters peer;
+    if (lock.GetConfiguration().LookupOrthancPeer(peer, remote))
+    {
+      HttpClient client(peer, "system");
+      std::string answer;
+
+      client.SetMethod(HttpMethod_Get);
+
+      if (!client.Apply(answer))
+      {
+        LOG(ERROR) << "Unable to get the system info from remote Orthanc peer: " << peer.GetUrl();
+        call.GetOutput().SignalError(client.GetLastStatus());
+        return;
+      }
+
+      call.GetOutput().AnswerBuffer(answer, MimeType_Json);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "No peer with symbolic name: " + remote);
+    }
+  }
+
+  // DICOM bridge -------------------------------------------------------------
+
+  static bool IsExistingModality(const OrthancRestApi::SetOfStrings& modalities,
+                                 const std::string& id)
+  {
+    return modalities.find(id) != modalities.end();
+  }
+
+  static void ListModalities(RestApiGetCall& call)
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    OrthancRestApi::SetOfStrings modalities;
+    lock.GetConfiguration().GetListOfDicomModalities(modalities);
+
+    if (call.HasArgument("expand"))
+    {
+      Json::Value result = Json::objectValue;
+      for (OrthancRestApi::SetOfStrings::const_iterator
+             it = modalities.begin(); it != modalities.end(); ++it)
+      {
+        const RemoteModalityParameters& remote = lock.GetConfiguration().GetModalityUsingSymbolicName(*it);
+        
+        Json::Value info;
+        remote.Serialize(info, true /* force advanced format */);
+        result[*it] = info;
+      }
+      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);
+    }
+  }
+
+
+  static void ListModalityOperations(RestApiGetCall& call)
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    OrthancRestApi::SetOfStrings modalities;
+    lock.GetConfiguration().GetListOfDicomModalities(modalities);
+
+    std::string id = call.GetUriComponent("id", "");
+    if (IsExistingModality(modalities, id))
+    {
+      RestApi::AutoListChildren(call);
+    }
+  }
+
+
+  static void UpdateModality(RestApiPutCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value json;
+    if (call.ParseJsonRequest(json))
+    {
+      RemoteModalityParameters modality;
+      modality.Unserialize(json);
+
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().UpdateModality(call.GetUriComponent("id", ""), modality);
+      }
+
+      context.SignalUpdatedModalities();
+
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+  }
+
+
+  static void DeleteModality(RestApiDeleteCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().RemoveModality(call.GetUriComponent("id", ""));
+    }
+
+    context.SignalUpdatedModalities();
+
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  static void UpdatePeer(RestApiPutCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value json;
+    if (call.ParseJsonRequest(json))
+    {
+      WebServiceParameters peer;
+      peer.Unserialize(json);
+
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().UpdatePeer(call.GetUriComponent("id", ""), peer);
+      }
+
+      context.SignalUpdatedPeers();
+
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+  }
+
+
+  static void DeletePeer(RestApiDeleteCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().RemovePeer(call.GetUriComponent("id", ""));
+    }
+
+    context.SignalUpdatedPeers();
+
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  static void DicomFindWorklist(RestApiPostCall& call)
+  {
+    Json::Value json;
+    if (call.ParseJsonRequest(json))
+    {
+      std::unique_ptr<ParsedDicomFile> query
+        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0),
+                                         "" /* no private creator */));
+
+      DicomFindAnswers answers(true);
+
+      {
+        DicomControlUserConnection connection(GetAssociationParameters(call, json));
+        connection.FindWorklist(answers, *query);
+      }
+
+      Json::Value result;
+      answers.ToJson(result, true);
+      call.GetOutput().AnswerJson(result);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Must provide a JSON object");
+    }
+  }
+
+
+  // Storage commitment SCU ---------------------------------------------------
+
+  static void StorageCommitmentScu(RestApiPostCall& call)
+  {
+    static const char* const ORTHANC_RESOURCES = "Resources";
+    static const char* const DICOM_INSTANCES = "DicomInstances";
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value json;
+    if (!call.ParseJsonRequest(json) ||
+        json.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Must provide a JSON object with a list of resources");
+    }
+    else if (!json.isMember(ORTHANC_RESOURCES) &&
+             !json.isMember(DICOM_INSTANCES))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Empty storage commitment request, one of these fields is mandatory: \"" +
+                             std::string(ORTHANC_RESOURCES) + "\" or \"" + std::string(DICOM_INSTANCES) + "\"");
+    }
+    else
+    {
+      std::list<std::string> sopClassUids, sopInstanceUids;
+
+      if (json.isMember(ORTHANC_RESOURCES))
+      {
+        const Json::Value& resources = json[ORTHANC_RESOURCES];
+          
+        if (resources.type() != Json::arrayValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "The \"" + std::string(ORTHANC_RESOURCES) +
+                                 "\" field must provide an array of Orthanc resources");
+        }
+        else
+        {
+          for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+          {
+            if (resources[i].type() != Json::stringValue)
+            {
+              throw OrthancException(ErrorCode_BadFileFormat,
+                                     "The \"" + std::string(ORTHANC_RESOURCES) +
+                                     "\" field must provide an array of strings, found: " + resources[i].toStyledString());
+            }
+
+            std::list<std::string> instances;
+            context.GetIndex().GetChildInstances(instances, resources[i].asString());
+            
+            for (std::list<std::string>::const_iterator
+                   it = instances.begin(); it != instances.end(); ++it)
+            {
+              std::string sopClassUid, sopInstanceUid;
+              DicomMap tags;
+              if (context.LookupOrReconstructMetadata(sopClassUid, *it, MetadataType_Instance_SopClassUid) &&
+                  context.GetIndex().GetAllMainDicomTags(tags, *it) &&
+                  tags.LookupStringValue(sopInstanceUid, DICOM_TAG_SOP_INSTANCE_UID, false))
+              {
+                sopClassUids.push_back(sopClassUid);
+                sopInstanceUids.push_back(sopInstanceUid);
+              }
+              else
+              {
+                throw OrthancException(ErrorCode_InternalError,
+                                       "Cannot retrieve SOP Class/Instance UID of Orthanc instance: " + *it);
+              }
+            }
+          }
+        }
+      }
+
+      if (json.isMember(DICOM_INSTANCES))
+      {
+        const Json::Value& instances = json[DICOM_INSTANCES];
+          
+        if (instances.type() != Json::arrayValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat,
+                                 "The \"" + std::string(DICOM_INSTANCES) +
+                                 "\" field must provide an array of DICOM instances");
+        }
+        else
+        {
+          for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+          {
+            if (instances[i].type() == Json::arrayValue)
+            {
+              if (instances[i].size() != 2 ||
+                  instances[i][0].type() != Json::stringValue ||
+                  instances[i][1].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadFileFormat,
+                                       "An instance entry must provide an array with 2 strings: "
+                                       "SOP Class UID and SOP Instance UID");
+              }
+              else
+              {
+                sopClassUids.push_back(instances[i][0].asString());
+                sopInstanceUids.push_back(instances[i][1].asString());
+              }
+            }
+            else if (instances[i].type() == Json::objectValue)
+            {
+              if (!instances[i].isMember(SOP_CLASS_UID) ||
+                  !instances[i].isMember(SOP_INSTANCE_UID) ||
+                  instances[i][SOP_CLASS_UID].type() != Json::stringValue ||
+                  instances[i][SOP_INSTANCE_UID].type() != Json::stringValue)
+              {
+                throw OrthancException(ErrorCode_BadFileFormat,
+                                       "An instance entry must provide an object with 2 string fiels: "
+                                       "\"" + std::string(SOP_CLASS_UID) + "\" and \"" +
+                                       std::string(SOP_INSTANCE_UID));
+              }
+              else
+              {
+                sopClassUids.push_back(instances[i][SOP_CLASS_UID].asString());
+                sopInstanceUids.push_back(instances[i][SOP_INSTANCE_UID].asString());
+              }
+            }
+            else
+            {
+              throw OrthancException(ErrorCode_BadFileFormat,
+                                     "JSON array or object is expected to specify one "
+                                     "instance to be queried, found: " + instances[i].toStyledString());
+            }
+          }
+        }
+      }
+
+      if (sopClassUids.size() != sopInstanceUids.size())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      const std::string transactionUid = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+
+      if (sopClassUids.empty())
+      {
+        LOG(WARNING) << "Issuing an outgoing storage commitment request that is empty: " << transactionUid;
+      }
+
+      {
+        const RemoteModalityParameters remote =
+          MyGetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+        const std::string& remoteAet = remote.GetApplicationEntityTitle();
+        const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
+        
+        // Create a "pending" storage commitment report BEFORE the
+        // actual SCU call in order to avoid race conditions
+        context.GetStorageCommitmentReports().Store(
+          transactionUid, new StorageCommitmentReports::Report(remoteAet));
+
+        DicomAssociationParameters parameters(localAet, remote);
+        
+        std::vector<std::string> a(sopClassUids.begin(), sopClassUids.end());
+        std::vector<std::string> b(sopInstanceUids.begin(), sopInstanceUids.end());
+        DicomAssociation::RequestStorageCommitment(parameters, transactionUid, a, b);
+      }
+
+      Json::Value result = Json::objectValue;
+      result["ID"] = transactionUid;
+      result["Path"] = "/storage-commitment/" + transactionUid;
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+
+  static void GetStorageCommitmentReport(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    const std::string& transactionUid = call.GetUriComponent("id", "");
+
+    {
+      StorageCommitmentReports::Accessor accessor(
+        context.GetStorageCommitmentReports(), transactionUid);
+
+      if (accessor.IsValid())
+      {
+        Json::Value json;
+        accessor.GetReport().Format(json);
+        call.GetOutput().AnswerJson(json);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_InexistentItem,
+                               "No storage commitment transaction with UID: " + transactionUid);
+      }
+    }
+  }
+  
+
+  static void RemoveAfterStorageCommitment(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    const std::string& transactionUid = call.GetUriComponent("id", "");
+
+    {
+      StorageCommitmentReports::Accessor accessor(
+        context.GetStorageCommitmentReports(), transactionUid);
+
+      if (!accessor.IsValid())
+      {
+        throw OrthancException(ErrorCode_InexistentItem,
+                               "No storage commitment transaction with UID: " + transactionUid);
+      }
+      else if (accessor.GetReport().GetStatus() != StorageCommitmentReports::Report::Status_Success)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                               "Cannot remove DICOM instances after failure "
+                               "in storage commitment transaction: " + transactionUid);
+      }
+      else
+      {
+        std::vector<std::string> sopInstanceUids;
+        accessor.GetReport().GetSuccessSopInstanceUids(sopInstanceUids);
+
+        for (size_t i = 0; i < sopInstanceUids.size(); i++)
+        {
+          std::vector<std::string> orthancId;
+          context.GetIndex().LookupIdentifierExact(
+            orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids[i]);
+
+          for (size_t j = 0; j < orthancId.size(); j++)
+          {
+            LOG(INFO) << "Storage commitment - Removing SOP instance UID / Orthanc ID: "
+                      << sopInstanceUids[i] << " / " << orthancId[j];
+
+            Json::Value tmp;
+            context.GetIndex().DeleteResource(tmp, orthancId[j], ResourceType_Instance);
+          }
+        }
+          
+        call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+      }
+    }
+  }
+  
+
+  void OrthancRestApi::RegisterModalities()
+  {
+    Register("/modalities", ListModalities);
+    Register("/modalities/{id}", ListModalityOperations);
+    Register("/modalities/{id}", UpdateModality);
+    Register("/modalities/{id}", DeleteModality);
+    Register("/modalities/{id}/echo", DicomEcho);
+    Register("/modalities/{id}/find-patient", DicomFindPatient);
+    Register("/modalities/{id}/find-study", DicomFindStudy);
+    Register("/modalities/{id}/find-series", DicomFindSeries);
+    Register("/modalities/{id}/find-instance", DicomFindInstance);
+    Register("/modalities/{id}/find", DicomFind);
+    Register("/modalities/{id}/store", DicomStore);
+    Register("/modalities/{id}/store-straight", DicomStoreStraight);  // New in 1.6.1
+    Register("/modalities/{id}/move", DicomMove);
+
+    // For Query/Retrieve
+    Register("/modalities/{id}/query", DicomQuery);
+    Register("/queries", ListQueries);
+    Register("/queries/{id}", DeleteQuery);
+    Register("/queries/{id}", ListQueryOperations);
+    Register("/queries/{id}/answers", ListQueryAnswers);
+    Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations);
+    Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer);
+    Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer);
+    Register("/queries/{id}/answers/{index}/query-instances",
+             QueryAnswerChildren<ResourceType_Instance>);
+    Register("/queries/{id}/answers/{index}/query-series",
+             QueryAnswerChildren<ResourceType_Series>);
+    Register("/queries/{id}/answers/{index}/query-studies",
+             QueryAnswerChildren<ResourceType_Study>);
+    Register("/queries/{id}/level", GetQueryLevel);
+    Register("/queries/{id}/modality", GetQueryModality);
+    Register("/queries/{id}/query", GetQueryArguments);
+    Register("/queries/{id}/retrieve", RetrieveAllAnswers);
+
+    Register("/peers", ListPeers);
+    Register("/peers/{id}", ListPeerOperations);
+    Register("/peers/{id}", UpdatePeer);
+    Register("/peers/{id}", DeletePeer);
+    Register("/peers/{id}/store", PeerStore);
+    Register("/peers/{id}/system", PeerSystem);
+
+    Register("/modalities/{id}/find-worklist", DicomFindWorklist);
+
+    // Storage commitment
+    Register("/modalities/{id}/storage-commitment", StorageCommitmentScu);
+    Register("/storage-commitment/{id}", GetStorageCommitmentReport);
+    Register("/storage-commitment/{id}/remove", RemoveAfterStorageCommitment);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2165 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../../Core/Compression/GzipCompressor.h"
+#include "../../Core/DicomFormat/DicomImageInformation.h"
+#include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../../Core/HttpServer/HttpContentNegociation.h"
+#include "../../Core/Images/Image.h"
+#include "../../Core/Images/ImageProcessing.h"
+#include "../../Core/Logging.h"
+#include "../../Core/MultiThreading/Semaphore.h"
+#include "../OrthancConfiguration.h"
+#include "../Search/DatabaseLookup.h"
+#include "../ServerContext.h"
+#include "../ServerToolbox.h"
+#include "../SliceOrdering.h"
+
+#include "../../Plugins/Engine/OrthancPlugins.h"
+
+// This "include" is mandatory for Release builds using Linux Standard Base
+#include <boost/math/special_functions/round.hpp>
+
+
+/**
+ * This semaphore is used to limit the number of concurrent HTTP
+ * requests on CPU-intensive routes of the REST API, in order to
+ * prevent exhaustion of resources (new in Orthanc 1.7.0).
+ **/
+static Orthanc::Semaphore throttlingSemaphore_(4);  // TODO => PARAMETER?
+
+
+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,
+                                    ServerIndex& index,
+                                    const std::list<std::string>& resources,
+                                    ResourceType level,
+                                    bool expand)
+  {
+    Json::Value answer = Json::arrayValue;
+
+    for (std::list<std::string>::const_iterator
+           resource = resources.begin(); resource != resources.end(); ++resource)
+    {
+      if (expand)
+      {
+        Json::Value item;
+        if (index.LookupResource(item, *resource, level))
+        {
+          answer.append(item);
+        }
+      }
+      else
+      {
+        answer.append(*resource);
+      }
+    }
+
+    output.AnswerJson(answer);
+  }
+
+
+  template <enum ResourceType resourceType>
+  static void ListResources(RestApiGetCall& call)
+  {
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+
+    std::list<std::string> result;
+
+    if (call.HasArgument("limit") ||
+        call.HasArgument("since"))
+    {
+      if (!call.HasArgument("limit"))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Missing \"limit\" argument for GET request against: " +
+                               call.FlattenUri());
+      }
+
+      if (!call.HasArgument("since"))
+      {
+        throw OrthancException(ErrorCode_BadRequest,
+                               "Missing \"since\" argument for GET request against: " +
+                               call.FlattenUri());
+      }
+
+      size_t since = boost::lexical_cast<size_t>(call.GetArgument("since", ""));
+      size_t limit = boost::lexical_cast<size_t>(call.GetArgument("limit", ""));
+      index.GetAllUuids(result, resourceType, since, limit);
+    }
+    else
+    {
+      index.GetAllUuids(result, resourceType);
+    }
+
+
+    AnswerListOfResources(call.GetOutput(), index, result, resourceType, call.HasArgument("expand"));
+  }
+
+  template <enum ResourceType resourceType>
+  static void GetSingleResource(RestApiGetCall& call)
+  {
+    Json::Value result;
+    if (OrthancRestApi::GetIndex(call).LookupResource(result, call.GetUriComponent("id", ""), resourceType))
+    {
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+  template <enum ResourceType resourceType>
+  static void DeleteSingleResource(RestApiDeleteCall& call)
+  {
+    Json::Value result;
+    if (OrthancRestApi::GetContext(call).DeleteResource(result, call.GetUriComponent("id", ""), resourceType))
+    {
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+
+  // Get information about a single patient -----------------------------------
+ 
+  static void IsProtectedPatient(RestApiGetCall& call)
+  {
+    std::string publicId = call.GetUriComponent("id", "");
+    bool isProtected = OrthancRestApi::GetIndex(call).IsProtectedPatient(publicId);
+    call.GetOutput().AnswerBuffer(isProtected ? "1" : "0", MimeType_PlainText);
+  }
+
+
+  static void SetPatientProtection(RestApiPutCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+
+    std::string body;
+    call.BodyToString(body);
+    body = Toolbox::StripSpaces(body);
+
+    if (body == "0")
+    {
+      context.GetIndex().SetProtectedPatient(publicId, false);
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+    else if (body == "1")
+    {
+      context.GetIndex().SetProtectedPatient(publicId, true);
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+    else
+    {
+      // Bad request
+    }
+  }
+
+
+  // Get information about a single instance ----------------------------------
+ 
+  static void GetInstanceFile(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+
+    IHttpHandler::Arguments::const_iterator accept = call.GetHttpHeaders().find("accept");
+    if (accept != call.GetHttpHeaders().end())
+    {
+      // New in Orthanc 1.5.4
+      try
+      {
+        MimeType mime = StringToMimeType(accept->second.c_str());
+
+        if (mime == MimeType_DicomWebJson ||
+            mime == MimeType_DicomWebXml)
+        {
+          DicomWebJsonVisitor visitor;
+          
+          {
+            ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), publicId);
+            locker.GetDicom().Apply(visitor);
+          }
+
+          if (mime == MimeType_DicomWebJson)
+          {
+            std::string s = visitor.GetResult().toStyledString();
+            call.GetOutput().AnswerBuffer(s, MimeType_DicomWebJson);
+          }
+          else
+          {
+            std::string xml;
+            visitor.FormatXml(xml);
+            call.GetOutput().AnswerBuffer(xml, MimeType_DicomWebXml);
+          }
+          
+          return;
+        }
+      }
+      catch (OrthancException&)
+      {
+      }
+    }
+
+    context.AnswerAttachment(call.GetOutput(), publicId, FileContentType_Dicom);
+  }
+
+
+  static void ExportInstanceFile(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+
+    std::string dicom;
+    context.ReadDicom(dicom, publicId);
+
+    std::string target;
+    call.BodyToString(target);
+    SystemToolbox::WriteFile(dicom, target);
+
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+  }
+
+
+  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 (format != DicomToJsonFormat_Full ||
+        !ignoreTagLength.empty())
+    {
+      Json::Value full;
+      context.ReadDicomAsJson(full, publicId, ignoreTagLength);
+      AnswerDicomAsJson(call, full, format);
+    }
+    else
+    {
+      // This path allows one 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, MimeType_Json);
+    }
+  }
+
+
+  static void GetInstanceTagsBis(RestApiGetCall& call)
+  {
+    switch (GetDicomFormat(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)
+  {
+    std::string publicId = call.GetUriComponent("id", "");
+
+    unsigned int numberOfFrames;
+      
+    {
+      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);
+  }
+
+
+  namespace
+  {
+    class ImageToEncode
+    {
+    private:
+      std::unique_ptr<ImageAccessor>&  image_;
+      ImageExtractionMode            mode_;
+      bool                           invert_;
+      MimeType                       format_;
+      std::string                    answer_;
+
+    public:
+      ImageToEncode(std::unique_ptr<ImageAccessor>& image,
+                    ImageExtractionMode mode,
+                    bool invert) :
+        image_(image),
+        mode_(mode),
+        invert_(invert)
+      {
+      }
+
+      void Answer(RestApiOutput& output)
+      {
+        output.AnswerBuffer(answer_, format_);
+      }
+
+      void EncodeUsingPng()
+      {
+        format_ = MimeType_Png;
+        DicomImageDecoder::ExtractPngImage(answer_, image_, mode_, invert_);
+      }
+
+      void EncodeUsingPam()
+      {
+        format_ = MimeType_Pam;
+        DicomImageDecoder::ExtractPamImage(answer_, image_, mode_, invert_);
+      }
+
+      void EncodeUsingJpeg(uint8_t quality)
+      {
+        format_ = MimeType_Jpeg;
+        DicomImageDecoder::ExtractJpegImage(answer_, image_, mode_, invert_, quality);
+      }
+    };
+
+    class EncodePng : public HttpContentNegociation::IHandler
+    {
+    private:
+      ImageToEncode&  image_;
+
+    public:
+      EncodePng(ImageToEncode& image) : image_(image)
+      {
+      }
+
+      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)
+        {
+          throw OrthancException(
+            ErrorCode_BadRequest,
+            "Bad quality for a JPEG encoding (must be a number between 0 and 100): " + v);
+        }
+      }
+
+      virtual void Handle(const std::string& type,
+                          const std::string& subtype)
+      {
+        assert(type == "image");
+        assert(subtype == "jpeg");
+        image_.EncodeUsingJpeg(quality_);
+      }
+    };
+  }
+
+
+  namespace
+  {
+    class IDecodedFrameHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~IDecodedFrameHandler()
+      {
+      }
+
+      virtual void Handle(RestApiGetCall& call,
+                          std::unique_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) = 0;
+
+      virtual bool RequiresDicomTags() const = 0;
+
+      static void Apply(RestApiGetCall& call,
+                        IDecodedFrameHandler& handler)
+      {
+        ServerContext& context = OrthancRestApi::GetContext(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;
+        }
+
+        DicomMap dicom;
+        std::unique_ptr<ImageAccessor> decoded;
+
+        try
+        {
+          std::string publicId = call.GetUriComponent("id", "");
+
+          decoded.reset(context.DecodeDicomFrame(publicId, frame));
+
+          if (decoded.get() == NULL)
+          {
+            throw OrthancException(ErrorCode_NotImplemented,
+                                   "Cannot decode DICOM instance with ID: " + publicId);
+          }
+          
+          if (handler.RequiresDicomTags())
+          {
+            /**
+             * Retrieve a summary of the DICOM tags, which is
+             * necessary to deal with MONOCHROME1 photometric
+             * interpretation, and with windowing parameters.
+             **/ 
+            ServerContext::DicomCacheLocker locker(context, publicId);
+            locker.GetDicom().ExtractDicomSummary(dicom);
+          }
+        }
+        catch (OrthancException& e)
+        {
+          if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange ||
+              e.GetErrorCode() == ErrorCode_UnknownResource)
+          {
+            // The frame number is out of the range for this DICOM
+            // instance, the resource is not existent
+          }
+          else
+          {
+            std::string root = "";
+            for (size_t i = 1; i < call.GetFullUri().size(); i++)
+            {
+              root += "../";
+            }
+
+            call.GetOutput().Redirect(root + "app/images/unsupported.png");
+          }
+          return;
+        }
+
+        handler.Handle(call, decoded, dicom);
+      }
+
+
+      static void DefaultHandler(RestApiGetCall& call,
+                                 std::unique_ptr<ImageAccessor>& decoded,
+                                 ImageExtractionMode mode,
+                                 bool invert)
+      {
+        ImageToEncode image(decoded, mode, invert);
+
+        HttpContentNegociation negociation;
+        EncodePng png(image);
+        negociation.Register(MIME_PNG, png);
+
+        EncodeJpeg jpeg(image, call);
+        negociation.Register(MIME_JPEG, jpeg);
+
+        EncodePam pam(image);
+        negociation.Register(MIME_PAM, pam);
+
+        if (negociation.Apply(call.GetHttpHeaders()))
+        {
+          image.Answer(call.GetOutput());
+        }
+      }
+    };
+
+
+    class GetImageHandler : public IDecodedFrameHandler
+    {
+    private:
+      ImageExtractionMode mode_;
+
+    public:
+      GetImageHandler(ImageExtractionMode mode) :
+        mode_(mode)
+      {
+      }
+
+      virtual void Handle(RestApiGetCall& call,
+                          std::unique_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) ORTHANC_OVERRIDE
+      {
+        bool invert = false;
+
+        if (mode_ == ImageExtractionMode_Preview)
+        {
+          DicomImageInformation info(dicom);
+          invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
+        }
+
+        DefaultHandler(call, decoded, mode_, invert);
+      }
+
+      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
+      {
+        return mode_ == ImageExtractionMode_Preview;
+      }
+    };
+
+
+    class RenderedFrameHandler : public IDecodedFrameHandler
+    {
+    private:
+      static void GetDicomParameters(bool& invert,
+                                     float& rescaleSlope,
+                                     float& rescaleIntercept,
+                                     float& windowWidth,
+                                     float& windowCenter,
+                                     const DicomMap& dicom)
+      {
+        DicomImageInformation info(dicom);
+
+        invert = (info.GetPhotometricInterpretation() == PhotometricInterpretation_Monochrome1);
+
+        rescaleSlope = 1.0f;
+        rescaleIntercept = 0.0f;
+
+        if (dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_SLOPE) &&
+            dicom.HasTag(Orthanc::DICOM_TAG_RESCALE_INTERCEPT))
+        {
+          dicom.ParseFloat(rescaleSlope, Orthanc::DICOM_TAG_RESCALE_SLOPE);
+          dicom.ParseFloat(rescaleIntercept, Orthanc::DICOM_TAG_RESCALE_INTERCEPT);
+        }
+
+        windowWidth = static_cast<float>(1 << info.GetBitsStored()) * rescaleSlope;
+        windowCenter = windowWidth / 2.0f + rescaleIntercept;
+
+        if (dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_CENTER) &&
+            dicom.HasTag(Orthanc::DICOM_TAG_WINDOW_WIDTH))
+        {
+          dicom.ParseFirstFloat(windowCenter, Orthanc::DICOM_TAG_WINDOW_CENTER);
+          dicom.ParseFirstFloat(windowWidth, Orthanc::DICOM_TAG_WINDOW_WIDTH);
+        }
+      }
+
+      static void GetUserArguments(float& windowWidth /* inout */,
+                                   float& windowCenter /* inout */,
+                                   unsigned int& argWidth,
+                                   unsigned int& argHeight,
+                                   bool& smooth,
+                                   RestApiGetCall& call)
+      {
+        static const char* ARG_WINDOW_CENTER = "window-center";
+        static const char* ARG_WINDOW_WIDTH = "window-width";
+        static const char* ARG_WIDTH = "width";
+        static const char* ARG_HEIGHT = "height";
+        static const char* ARG_SMOOTH = "smooth";
+
+        if (call.HasArgument(ARG_WINDOW_WIDTH))
+        {
+          try
+          {
+            windowWidth = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_WIDTH, ""));
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_WINDOW_WIDTH));
+          }
+        }
+
+        if (call.HasArgument(ARG_WINDOW_CENTER))
+        {
+          try
+          {
+            windowCenter = boost::lexical_cast<float>(call.GetArgument(ARG_WINDOW_CENTER, ""));
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_WINDOW_CENTER));
+          }
+        }
+
+        argWidth = 0;
+        argHeight = 0;
+
+        if (call.HasArgument(ARG_WIDTH))
+        {
+          try
+          {
+            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_WIDTH, ""));
+            if (tmp < 0)
+            {
+              throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                     "Argument cannot be negative: " + std::string(ARG_WIDTH));
+            }
+            else
+            {
+              argWidth = static_cast<unsigned int>(tmp);
+            }
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_WIDTH));
+          }
+        }
+
+        if (call.HasArgument(ARG_HEIGHT))
+        {
+          try
+          {
+            int tmp = boost::lexical_cast<int>(call.GetArgument(ARG_HEIGHT, ""));
+            if (tmp < 0)
+            {
+              throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                     "Argument cannot be negative: " + std::string(ARG_HEIGHT));
+            }
+            else
+            {
+              argHeight = static_cast<unsigned int>(tmp);
+            }
+          }
+          catch (boost::bad_lexical_cast&)
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Bad value for argument: " + std::string(ARG_HEIGHT));
+          }
+        }
+
+        smooth = false;
+
+        if (call.HasArgument(ARG_SMOOTH))
+        {
+          std::string value = call.GetArgument(ARG_SMOOTH, "");
+          if (value == "0" ||
+              value == "false")
+          {
+            smooth = false;
+          }
+          else if (value == "1" ||
+                   value == "true")
+          {
+            smooth = true;
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Argument must be Boolean: " + std::string(ARG_SMOOTH));
+          }
+        }        
+      }
+                                
+      
+    public:
+      virtual void Handle(RestApiGetCall& call,
+                          std::unique_ptr<ImageAccessor>& decoded,
+                          const DicomMap& dicom) ORTHANC_OVERRIDE
+      {
+        bool invert;
+        float rescaleSlope, rescaleIntercept, windowWidth, windowCenter;
+        GetDicomParameters(invert, rescaleSlope, rescaleIntercept, windowWidth, windowCenter, dicom);
+
+        unsigned int argWidth, argHeight;
+        bool smooth;
+        GetUserArguments(windowWidth, windowCenter, argWidth, argHeight, smooth, call);
+
+        unsigned int targetWidth = decoded->GetWidth();
+        unsigned int targetHeight = decoded->GetHeight();
+
+        if (decoded->GetWidth() != 0 &&
+            decoded->GetHeight() != 0)
+        {
+          float ratio = 1;
+
+          if (argWidth != 0 &&
+              argHeight != 0)
+          {
+            float ratioX = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
+            float ratioY = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
+            ratio = std::min(ratioX, ratioY);
+          }
+          else if (argWidth != 0)
+          {
+            ratio = static_cast<float>(argWidth) / static_cast<float>(decoded->GetWidth());
+          }
+          else if (argHeight != 0)
+          {
+            ratio = static_cast<float>(argHeight) / static_cast<float>(decoded->GetHeight());
+          }
+          
+          targetWidth = boost::math::iround(ratio * static_cast<float>(decoded->GetWidth()));
+          targetHeight = boost::math::iround(ratio * static_cast<float>(decoded->GetHeight()));
+        }
+        
+        if (decoded->GetFormat() == PixelFormat_RGB24)
+        {
+          if (targetWidth == decoded->GetWidth() &&
+              targetHeight == decoded->GetHeight())
+          {
+            DefaultHandler(call, decoded, ImageExtractionMode_Preview, false);
+          }
+          else
+          {
+            std::unique_ptr<ImageAccessor> resized(
+              new Image(decoded->GetFormat(), targetWidth, targetHeight, false));
+            
+            if (smooth &&
+                (targetWidth < decoded->GetWidth() ||
+                 targetHeight < decoded->GetHeight()))
+            {
+              ImageProcessing::SmoothGaussian5x5(*decoded);
+            }
+            
+            ImageProcessing::Resize(*resized, *decoded);
+            DefaultHandler(call, resized, ImageExtractionMode_Preview, false);
+          }
+        }
+        else
+        {
+          // Grayscale image: (1) convert to Float32, (2) apply
+          // windowing to get a Grayscale8, (3) possibly resize
+
+          Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false);
+          ImageProcessing::Convert(converted, *decoded);
+
+          // Avoid divisions by zero
+          if (windowWidth <= 1.0f)
+          {
+            windowWidth = 1;
+          }
+
+          if (std::abs(rescaleSlope) <= 0.1f)
+          {
+            rescaleSlope = 0.1f;
+          }
+
+          const float scaling = 255.0f * rescaleSlope / windowWidth;
+          const float offset = (rescaleIntercept - windowCenter + windowWidth / 2.0f) / rescaleSlope;
+
+          std::unique_ptr<ImageAccessor> rescaled(new Image(PixelFormat_Grayscale8, decoded->GetWidth(), decoded->GetHeight(), false));
+          ImageProcessing::ShiftScale(*rescaled, converted, offset, scaling, false);
+
+          if (targetWidth == decoded->GetWidth() &&
+              targetHeight == decoded->GetHeight())
+          {
+            DefaultHandler(call, rescaled, ImageExtractionMode_UInt8, invert);
+          }
+          else
+          {
+            std::unique_ptr<ImageAccessor> resized(
+              new Image(PixelFormat_Grayscale8, targetWidth, targetHeight, false));
+            
+            if (smooth &&
+                (targetWidth < decoded->GetWidth() ||
+                 targetHeight < decoded->GetHeight()))
+            {
+              ImageProcessing::SmoothGaussian5x5(*rescaled);
+            }
+            
+            ImageProcessing::Resize(*resized, *rescaled);
+            DefaultHandler(call, resized, ImageExtractionMode_UInt8, invert);
+          }
+        }
+      }
+
+      virtual bool RequiresDicomTags() const ORTHANC_OVERRIDE
+      {
+        return true;
+      }
+    };
+  }
+
+
+  template <enum ImageExtractionMode mode>
+  static void GetImage(RestApiGetCall& call)
+  {
+    Semaphore::Locker locker(throttlingSemaphore_);
+        
+    GetImageHandler handler(mode);
+    IDecodedFrameHandler::Apply(call, handler);
+  }
+
+
+  static void GetRenderedFrame(RestApiGetCall& call)
+  {
+    Semaphore::Locker locker(throttlingSemaphore_);
+        
+    RenderedFrameHandler handler;
+    IDecodedFrameHandler::Apply(call, handler);
+  }
+
+
+  static void GetMatlabImage(RestApiGetCall& call)
+  {
+    Semaphore::Locker locker(throttlingSemaphore_);
+        
+    ServerContext& context = OrthancRestApi::GetContext(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::unique_ptr<ImageAccessor> decoded(context.DecodeDicomFrame(publicId, frame));
+
+    if (decoded.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NotImplemented,
+                             "Cannot decode DICOM instance with ID: " + publicId);
+    }
+    else
+    {
+      std::string result;
+      decoded->ToMatlabString(result);
+      call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
+    }
+  }
+
+
+  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;
+    MimeType 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, MimeType_Gzip);
+    }
+    else
+    {
+      call.GetOutput().AnswerBuffer(raw, mime);
+    }
+  }
+
+
+  static void GetResourceStatistics(RestApiGetCall& call)
+  {
+    static const uint64_t MEGA_BYTES = 1024 * 1024;
+
+    std::string publicId = call.GetUriComponent("id", "");
+
+    ResourceType type;
+    uint64_t diskSize, uncompressedSize, dicomDiskSize, dicomUncompressedSize;
+    unsigned int countStudies, countSeries, countInstances;
+    OrthancRestApi::GetIndex(call).GetResourceStatistics(
+      type, diskSize, uncompressedSize, countStudies, countSeries, 
+      countInstances, dicomDiskSize, dicomUncompressedSize, publicId);
+
+    Json::Value result = Json::objectValue;
+    result["DiskSize"] = boost::lexical_cast<std::string>(diskSize);
+    result["DiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
+    result["UncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
+    result["UncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
+
+    result["DicomDiskSize"] = boost::lexical_cast<std::string>(dicomDiskSize);
+    result["DicomDiskSizeMB"] = static_cast<unsigned int>(dicomDiskSize / MEGA_BYTES);
+    result["DicomUncompressedSize"] = boost::lexical_cast<std::string>(dicomUncompressedSize);
+    result["DicomUncompressedSizeMB"] = static_cast<unsigned int>(dicomUncompressedSize / MEGA_BYTES);
+
+    switch (type)
+    {
+      // Do NOT add "break" below this point!
+      case ResourceType_Patient:
+        result["CountStudies"] = countStudies;
+
+      case ResourceType_Study:
+        result["CountSeries"] = countSeries;
+
+      case ResourceType_Series:
+        result["CountInstances"] = countInstances;
+
+      case ResourceType_Instance:
+      default:
+        break;
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+
+  // Handling of metadata -----------------------------------------------------
+
+  static void CheckValidResourceType(RestApiCall& call)
+  {
+    std::string resourceType = call.GetUriComponent("resourceType", "");
+    StringToResourceType(resourceType.c_str());
+  }
+
+
+  static void ListMetadata(RestApiGetCall& call)
+  {
+    CheckValidResourceType(call);
+    
+    std::string publicId = call.GetUriComponent("id", "");
+    std::map<MetadataType, std::string> metadata;
+
+    OrthancRestApi::GetIndex(call).GetAllMetadata(metadata, publicId);
+
+    Json::Value result;
+
+    if (call.HasArgument("expand"))
+    {
+      result = Json::objectValue;
+      
+      for (std::map<MetadataType, std::string>::const_iterator 
+             it = metadata.begin(); it != metadata.end(); ++it)
+      {
+        std::string key = EnumerationToString(it->first);
+        result[key] = it->second;
+      }      
+    }
+    else
+    {
+      result = Json::arrayValue;
+      
+      for (std::map<MetadataType, std::string>::const_iterator 
+             it = metadata.begin(); it != metadata.end(); ++it)
+      {       
+        result.append(EnumerationToString(it->first));
+      }
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void GetMetadata(RestApiGetCall& call)
+  {
+    CheckValidResourceType(call);
+    
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+    MetadataType metadata = StringToMetadata(name);
+
+    std::string value;
+    if (OrthancRestApi::GetIndex(call).LookupMetadata(value, publicId, metadata))
+    {
+      call.GetOutput().AnswerBuffer(value, MimeType_PlainText);
+    }
+  }
+
+
+  static void DeleteMetadata(RestApiDeleteCall& call)
+  {
+    CheckValidResourceType(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+    MetadataType metadata = StringToMetadata(name);
+
+    if (IsUserMetadata(metadata))  // It is forbidden to modify internal metadata
+    {      
+      OrthancRestApi::GetIndex(call).DeleteMetadata(publicId, metadata);
+      call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
+  }
+
+
+  static void SetMetadata(RestApiPutCall& call)
+  {
+    CheckValidResourceType(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+    MetadataType metadata = StringToMetadata(name);
+
+    std::string value;
+    call.BodyToString(value);
+
+    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("", MimeType_PlainText);
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
+  }
+
+
+
+
+  // Handling of attached files -----------------------------------------------
+
+  static void ListAttachments(RestApiGetCall& call)
+  {
+    std::string resourceType = call.GetUriComponent("resourceType", "");
+    std::string publicId = call.GetUriComponent("id", "");
+    std::list<FileContentType> attachments;
+    OrthancRestApi::GetIndex(call).ListAvailableAttachments(attachments, publicId, StringToResourceType(resourceType.c_str()));
+
+    Json::Value result = Json::arrayValue;
+
+    for (std::list<FileContentType>::const_iterator 
+           it = attachments.begin(); it != attachments.end(); ++it)
+    {
+      result.append(EnumerationToString(*it));
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static bool GetAttachmentInfo(FileInfo& info, RestApiCall& call)
+  {
+    CheckValidResourceType(call);
+ 
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+    FileContentType contentType = StringToContentType(name);
+
+    return OrthancRestApi::GetIndex(call).LookupAttachment(info, publicId, contentType);
+  }
+
+
+  static void GetAttachmentOperations(RestApiGetCall& call)
+  {
+    FileInfo info;
+    if (GetAttachmentInfo(info, call))
+    {
+      Json::Value operations = Json::arrayValue;
+
+      operations.append("compress");
+      operations.append("compressed-data");
+
+      if (info.GetCompressedMD5() != "")
+      {
+        operations.append("compressed-md5");
+      }
+
+      operations.append("compressed-size");
+      operations.append("data");
+      operations.append("is-compressed");
+
+      if (info.GetUncompressedMD5() != "")
+      {
+        operations.append("md5");
+      }
+
+      operations.append("size");
+      operations.append("uncompress");
+
+      if (info.GetCompressedMD5() != "" &&
+          info.GetUncompressedMD5() != "")
+      {
+        operations.append("verify-md5");
+      }
+
+      call.GetOutput().AnswerJson(operations);
+    }
+  }
+
+  
+  template <int uncompress>
+  static void GetAttachmentData(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    CheckValidResourceType(call);
+ 
+    std::string publicId = call.GetUriComponent("id", "");
+    FileContentType type = StringToContentType(call.GetUriComponent("name", ""));
+
+    if (uncompress)
+    {
+      context.AnswerAttachment(call.GetOutput(), publicId, type);
+    }
+    else
+    {
+      // Return the raw data (possibly compressed), as stored on the filesystem
+      std::string content;
+      context.ReadAttachment(content, publicId, type, false);
+      call.GetOutput().AnswerBuffer(content, MimeType_Binary);
+    }
+  }
+
+
+  static void GetAttachmentSize(RestApiGetCall& call)
+  {
+    FileInfo info;
+    if (GetAttachmentInfo(info, call))
+    {
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedSize()), MimeType_PlainText);
+    }
+  }
+
+
+  static void GetAttachmentCompressedSize(RestApiGetCall& call)
+  {
+    FileInfo info;
+    if (GetAttachmentInfo(info, call))
+    {
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedSize()), MimeType_PlainText);
+    }
+  }
+
+
+  static void GetAttachmentMD5(RestApiGetCall& call)
+  {
+    FileInfo info;
+    if (GetAttachmentInfo(info, call) &&
+        info.GetUncompressedMD5() != "")
+    {
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetUncompressedMD5()), MimeType_PlainText);
+    }
+  }
+
+
+  static void GetAttachmentCompressedMD5(RestApiGetCall& call)
+  {
+    FileInfo info;
+    if (GetAttachmentInfo(info, call) &&
+        info.GetCompressedMD5() != "")
+    {
+      call.GetOutput().AnswerBuffer(boost::lexical_cast<std::string>(info.GetCompressedMD5()), MimeType_PlainText);
+    }
+  }
+
+
+  static void VerifyAttachment(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    CheckValidResourceType(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+
+    FileInfo info;
+    if (!GetAttachmentInfo(info, call) ||
+        info.GetCompressedMD5() == "" ||
+        info.GetUncompressedMD5() == "")
+    {
+      // Inexistent resource, or no MD5 available
+      return;
+    }
+
+    bool ok = false;
+
+    // First check whether the compressed data is correctly stored in the disk
+    std::string data;
+    context.ReadAttachment(data, publicId, StringToContentType(name), false);
+
+    std::string actualMD5;
+    Toolbox::ComputeMD5(actualMD5, data);
+    
+    if (actualMD5 == info.GetCompressedMD5())
+    {
+      // The compressed data is OK. If a compression algorithm was
+      // applied to it, now check the MD5 of the uncompressed data.
+      if (info.GetCompressionType() == CompressionType_None)
+      {
+        ok = true;
+      }
+      else
+      {
+        context.ReadAttachment(data, publicId, StringToContentType(name), true);        
+        Toolbox::ComputeMD5(actualMD5, data);
+        ok = (actualMD5 == info.GetUncompressedMD5());
+      }
+    }
+
+    if (ok)
+    {
+      LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has the right MD5";
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+    }
+    else
+    {
+      LOG(INFO) << "The attachment " << name << " of resource " << publicId << " has bad MD5!";
+    }
+  }
+
+
+  static void UploadAttachment(RestApiPutCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    CheckValidResourceType(call);
+ 
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+
+    FileContentType contentType = StringToContentType(name);
+    if (IsUserContentType(contentType) &&  // It is forbidden to modify internal attachments
+        context.AddAttachment(publicId, StringToContentType(name), call.GetBodyData(), call.GetBodySize()))
+    {
+      call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
+  }
+
+
+  static void DeleteAttachment(RestApiDeleteCall& call)
+  {
+    CheckValidResourceType(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+    FileContentType contentType = StringToContentType(name);
+
+    bool allowed;
+    if (IsUserContentType(contentType))
+    {
+      allowed = true;
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+
+      if (lock.GetConfiguration().GetBooleanParameter("StoreDicom", true) &&
+          contentType == FileContentType_DicomAsJson)
+      {
+        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("{}", MimeType_Json);
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+    }
+  }
+
+
+  template <enum CompressionType compression>
+  static void ChangeAttachmentCompression(RestApiPostCall& call)
+  {
+    CheckValidResourceType(call);
+
+    std::string publicId = call.GetUriComponent("id", "");
+    std::string name = call.GetUriComponent("name", "");
+    FileContentType contentType = StringToContentType(name);
+
+    OrthancRestApi::GetContext(call).ChangeAttachmentCompression(publicId, contentType, compression);
+    call.GetOutput().AnswerBuffer("{}", MimeType_Json);
+  }
+
+
+  static void IsAttachmentCompressed(RestApiGetCall& call)
+  {
+    FileInfo info;
+    if (GetAttachmentInfo(info, call))
+    {
+      std::string answer = (info.GetCompressionType() == CompressionType_None) ? "0" : "1";
+      call.GetOutput().AnswerBuffer(answer, MimeType_PlainText);
+    }
+  }
+
+
+  // Raw access to the DICOM tags of an instance ------------------------------
+
+  static void GetRawContent(RestApiGetCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+
+    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
+
+    locker.GetDicom().SendPathValue(call.GetOutput(), call.GetTrailingUri());
+  }
+
+
+
+  static bool ExtractSharedTags(Json::Value& shared,
+                                ServerContext& context,
+                                const std::string& publicId)
+  {
+    // Retrieve all the instances of this patient/study/series
+    typedef std::list<std::string> Instances;
+    Instances instances;
+    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
+
+    // Loop over the instances
+    bool isFirst = true;
+    shared = Json::objectValue;
+
+    for (Instances::const_iterator it = instances.begin();
+         it != instances.end(); ++it)
+    {
+      // Get the tags of the current instance, in the simplified format
+      Json::Value tags;
+
+      try
+      {
+        context.ReadDicomAsJson(tags, *it);
+      }
+      catch (OrthancException&)
+      {
+        // Race condition: This instance has been removed since
+        // (*). Ignore this instance.
+        continue;
+      }
+
+      if (tags.type() != Json::objectValue)
+      {
+        return false;   // Error
+      }
+
+      // Only keep the tags that are mapped to a string
+      Json::Value::Members members = tags.getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const Json::Value& tag = tags[members[i]];
+        if (tag.type() != Json::objectValue ||
+            tag["Type"].type() != Json::stringValue ||
+            tag["Type"].asString() != "String")
+        {
+          tags.removeMember(members[i]);
+        }
+      }
+
+      if (isFirst)
+      {
+        // This is the first instance, keep its tags as such
+        shared = tags;
+        isFirst = false;
+      }
+      else
+      {
+        // Loop over all the members of the shared tags extracted so
+        // far. If the value of one of these tags does not match its
+        // value in the current instance, remove it.
+        members = shared.getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          if (!tags.isMember(members[i]) ||
+              tags[members[i]]["Value"].asString() != shared[members[i]]["Value"].asString())
+          {
+            shared.removeMember(members[i]);
+          }
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  static void GetSharedTags(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+
+    Json::Value sharedTags;
+    if (ExtractSharedTags(sharedTags, context, publicId))
+    {
+      // Success: Send the value of the shared tags
+      AnswerDicomAsJson(call, sharedTags);
+    }
+  }
+
+
+  static void GetModuleInternal(RestApiGetCall& call,
+                                ResourceType resourceType,
+                                DicomModule module)
+  {
+    if (!((resourceType == ResourceType_Patient && module == DicomModule_Patient) ||
+          (resourceType == ResourceType_Study && module == DicomModule_Patient) ||
+          (resourceType == ResourceType_Study && module == DicomModule_Study) ||
+          (resourceType == ResourceType_Series && module == DicomModule_Series) ||
+          (resourceType == ResourceType_Instance && module == DicomModule_Instance) ||
+          (resourceType == ResourceType_Instance && module == DicomModule_Image)))
+    {
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+
+    std::set<DicomTag> ignoreTagLength;
+    ParseSetOfTags(ignoreTagLength, call, "ignore-length");
+
+    typedef std::set<DicomTag> ModuleTags;
+    ModuleTags moduleTags;
+    DicomTag::AddTagsForModule(moduleTags, module);
+
+    Json::Value tags;
+
+    if (resourceType != ResourceType_Instance)
+    {
+      // Retrieve all the instances of this patient/study/series
+      typedef std::list<std::string> Instances;
+      Instances instances;
+      context.GetIndex().GetChildInstances(instances, publicId);
+
+      if (instances.empty())
+      {
+        return;   // Error: No instance (should never happen)
+      }
+
+      // Select one child instance
+      publicId = instances.front();
+    }
+
+    context.ReadDicomAsJson(tags, publicId, ignoreTagLength);
+    
+    // Filter the tags of the instance according to the module
+    Json::Value result = Json::objectValue;
+    for (ModuleTags::const_iterator tag = moduleTags.begin(); tag != moduleTags.end(); ++tag)
+    {
+      std::string s = tag->Format();
+      if (tags.isMember(s))
+      {
+        result[s] = tags[s];
+      }      
+    }
+
+    AnswerDicomAsJson(call, result);
+  }
+    
+
+
+  template <enum ResourceType resourceType, 
+            enum DicomModule module>
+  static void GetModule(RestApiGetCall& call)
+  {
+    GetModuleInternal(call, resourceType, module);
+  }
+
+
+  namespace
+  {
+    typedef std::list< std::pair<ResourceType, std::string> >  LookupResults;
+  }
+
+
+  static void AccumulateLookupResults(LookupResults& result,
+                                      ServerIndex& index,
+                                      const DicomTag& tag,
+                                      const std::string& value,
+                                      ResourceType level)
+  {
+    std::vector<std::string> tmp;
+    index.LookupIdentifierExact(tmp, level, tag, value);
+
+    for (size_t i = 0; i < tmp.size(); i++)
+    {
+      result.push_back(std::make_pair(level, tmp[i]));
+    }
+  }
+
+
+  static void Lookup(RestApiPostCall& call)
+  {
+    std::string tag;
+    call.BodyToString(tag);
+
+    LookupResults resources;
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    AccumulateLookupResults(resources, index, DICOM_TAG_PATIENT_ID, tag, ResourceType_Patient);
+    AccumulateLookupResults(resources, index, DICOM_TAG_STUDY_INSTANCE_UID, tag, ResourceType_Study);
+    AccumulateLookupResults(resources, index, DICOM_TAG_SERIES_INSTANCE_UID, tag, ResourceType_Series);
+    AccumulateLookupResults(resources, index, DICOM_TAG_SOP_INSTANCE_UID, tag, ResourceType_Instance);
+
+    Json::Value result = Json::arrayValue;    
+    for (LookupResults::const_iterator 
+           it = resources.begin(); it != resources.end(); ++it)
+    {     
+      ResourceType type = it->first;
+      const std::string& id = it->second;
+      
+      Json::Value item = Json::objectValue;
+      item["Type"] = EnumerationToString(type);
+      item["ID"] = id;
+      item["Path"] = GetBasePath(type, id);
+    
+      result.append(item);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  namespace 
+  {
+    class FindVisitor : public ServerContext::ILookupVisitor
+    {
+    private:
+      bool                    isComplete_;
+      std::list<std::string>  resources_;
+
+    public:
+      FindVisitor() :
+        isComplete_(false)
+      {
+      }
+
+      virtual bool IsDicomAsJsonNeeded() const
+      {
+        return false;   // (*)
+      }
+      
+      virtual void MarkAsComplete()
+      {
+        isComplete_ = true;  // Unused information as of Orthanc 1.5.0
+      }
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId   /* unused     */,
+                         const DicomMap& mainDicomTags   /* unused     */,
+                         const Json::Value* dicomAsJson  /* unused (*) */) 
+      {
+        resources_.push_back(publicId);
+      }
+
+      void Answer(RestApiOutput& output,
+                  ServerIndex& index,
+                  ResourceType level,
+                  bool expand) const
+      {
+        AnswerListOfResources(output, index, resources_, level, expand);
+      }
+    };
+  }
+
+
+  static void Find(RestApiPostCall& call)
+  {
+    static const char* const KEY_CASE_SENSITIVE = "CaseSensitive";
+    static const char* const KEY_EXPAND = "Expand";
+    static const char* const KEY_LEVEL = "Level";
+    static const char* const KEY_LIMIT = "Limit";
+    static const char* const KEY_QUERY = "Query";
+    static const char* const KEY_SINCE = "Since";
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value request;
+    if (!call.ParseJsonRequest(request) ||
+        request.type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "The body must contain a JSON object");
+    }
+    else if (!request.isMember(KEY_LEVEL) ||
+             request[KEY_LEVEL].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LEVEL) + "\" is missing, or should be a string");
+    }
+    else if (!request.isMember(KEY_QUERY) &&
+             request[KEY_QUERY].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_QUERY) + "\" is missing, or should be a JSON object");
+    }
+    else if (request.isMember(KEY_CASE_SENSITIVE) && 
+             request[KEY_CASE_SENSITIVE].type() != Json::booleanValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_CASE_SENSITIVE) + "\" should be a Boolean");
+    }
+    else if (request.isMember(KEY_LIMIT) && 
+             request[KEY_LIMIT].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_LIMIT) + "\" should be an integer");
+    }
+    else if (request.isMember(KEY_SINCE) &&
+             request[KEY_SINCE].type() != Json::intValue)
+    {
+      throw OrthancException(ErrorCode_BadRequest, 
+                             "Field \"" + std::string(KEY_SINCE) + "\" should be an integer");
+    }
+    else
+    {
+      bool expand = false;
+      if (request.isMember(KEY_EXPAND))
+      {
+        expand = request[KEY_EXPAND].asBool();
+      }
+
+      bool caseSensitive = false;
+      if (request.isMember(KEY_CASE_SENSITIVE))
+      {
+        caseSensitive = request[KEY_CASE_SENSITIVE].asBool();
+      }
+
+      size_t limit = 0;
+      if (request.isMember(KEY_LIMIT))
+      {
+        int tmp = request[KEY_LIMIT].asInt();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_LIMIT) + "\" should be a positive integer");
+        }
+
+        limit = static_cast<size_t>(tmp);
+      }
+
+      size_t since = 0;
+      if (request.isMember(KEY_SINCE))
+      {
+        int tmp = request[KEY_SINCE].asInt();
+        if (tmp < 0)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                 "Field \"" + std::string(KEY_SINCE) + "\" should be a positive integer");
+        }
+
+        since = static_cast<size_t>(tmp);
+      }
+
+      ResourceType level = StringToResourceType(request[KEY_LEVEL].asCString());
+
+      DatabaseLookup query;
+
+      Json::Value::Members members = request[KEY_QUERY].getMemberNames();
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        if (request[KEY_QUERY][members[i]].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadRequest,
+                                 "Tag \"" + members[i] + "\" should be associated with a string");
+        }
+
+        const std::string value = request[KEY_QUERY][members[i]].asString();
+
+        if (!value.empty())
+        {
+          // An empty string corresponds to an universal constraint,
+          // so we ignore it. This mimics the behavior of class
+          // "OrthancFindRequestHandler"
+          query.AddRestConstraint(FromDcmtkBridge::ParseTag(members[i]), 
+                                  value, caseSensitive, true);
+        }
+      }
+
+      FindVisitor visitor;
+      context.Apply(visitor, query, level, since, limit);
+      visitor.Answer(call.GetOutput(), context.GetIndex(), level, expand);
+    }
+  }
+
+
+  template <enum ResourceType start, 
+            enum ResourceType end>
+  static void GetChildResources(RestApiGetCall& call)
+  {
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+
+    std::list<std::string> a, b, c;
+    a.push_back(call.GetUriComponent("id", ""));
+
+    ResourceType type = start;
+    while (type != end)
+    {
+      b.clear();
+
+      for (std::list<std::string>::const_iterator
+             it = a.begin(); it != a.end(); ++it)
+      {
+        index.GetChildren(c, *it);
+        b.splice(b.begin(), c);
+      }
+
+      type = GetChildResourceType(type);
+
+      a.clear();
+      a.splice(a.begin(), b);
+    }
+
+    Json::Value result = Json::arrayValue;
+
+    for (std::list<std::string>::const_iterator
+           it = a.begin(); it != a.end(); ++it)
+    {
+      Json::Value item;
+
+      if (OrthancRestApi::GetIndex(call).LookupResource(item, *it, end))
+      {
+        result.append(item);
+      }
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void GetChildInstancesTags(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    std::string publicId = call.GetUriComponent("id", "");
+    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;
+    Instances instances;
+
+    context.GetIndex().GetChildInstances(instances, publicId);  // (*)
+
+    Json::Value result = Json::objectValue;
+
+    for (Instances::const_iterator it = instances.begin();
+         it != instances.end(); ++it)
+    {
+      Json::Value full;
+      context.ReadDicomAsJson(full, *it, ignoreTagLength);
+
+      if (format != DicomToJsonFormat_Full)
+      {
+        Json::Value simplified;
+        ServerToolbox::SimplifyTags(simplified, full, format);
+        result[*it] = simplified;
+      }
+      else
+      {
+        result[*it] = full;
+      }
+    }
+    
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+
+  template <enum ResourceType start, 
+            enum ResourceType end>
+  static void GetParentResource(RestApiGetCall& call)
+  {
+    assert(start > end);
+
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    
+    std::string current = call.GetUriComponent("id", "");
+    ResourceType currentType = start;
+    while (currentType > end)
+    {
+      std::string parent;
+      if (!index.LookupParent(parent, current))
+      {
+        // Error that could happen if the resource gets deleted by
+        // another concurrent call
+        return;
+      }
+      
+      current = parent;
+      currentType = GetParentResourceType(currentType);
+    }
+
+    assert(currentType == end);
+
+    Json::Value result;
+    if (index.LookupResource(result, current, end))
+    {
+      call.GetOutput().AnswerJson(result);
+    }
+  }
+
+
+  static void ExtractPdf(RestApiGetCall& call)
+  {
+    const std::string id = call.GetUriComponent("id", "");
+
+    std::string pdf;
+    ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), id);
+
+    if (locker.GetDicom().ExtractPdf(pdf))
+    {
+      call.GetOutput().AnswerBuffer(pdf, MimeType_Pdf);
+      return;
+    }
+  }
+
+
+  static void OrderSlices(RestApiGetCall& call)
+  {
+    const std::string id = call.GetUriComponent("id", "");
+
+    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    SliceOrdering ordering(index, id);
+
+    Json::Value result;
+    ordering.Format(result);
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  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("", MimeType_PlainText);
+  }
+
+
+  template <enum ResourceType type>
+  static void ReconstructResource(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""));
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  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("", MimeType_PlainText);
+  }
+
+
+  void OrthancRestApi::RegisterResources()
+  {
+    Register("/instances", ListResources<ResourceType_Instance>);
+    Register("/patients", ListResources<ResourceType_Patient>);
+    Register("/series", ListResources<ResourceType_Series>);
+    Register("/studies", ListResources<ResourceType_Study>);
+
+    Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>);
+    Register("/instances/{id}", GetSingleResource<ResourceType_Instance>);
+    Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>);
+    Register("/patients/{id}", GetSingleResource<ResourceType_Patient>);
+    Register("/series/{id}", DeleteSingleResource<ResourceType_Series>);
+    Register("/series/{id}", GetSingleResource<ResourceType_Series>);
+    Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>);
+    Register("/studies/{id}", GetSingleResource<ResourceType_Study>);
+
+    Register("/instances/{id}/statistics", GetResourceStatistics);
+    Register("/patients/{id}/statistics", GetResourceStatistics);
+    Register("/studies/{id}/statistics", GetResourceStatistics);
+    Register("/series/{id}/statistics", GetResourceStatistics);
+
+    Register("/patients/{id}/shared-tags", GetSharedTags);
+    Register("/series/{id}/shared-tags", GetSharedTags);
+    Register("/studies/{id}/shared-tags", GetSharedTags);
+
+    Register("/instances/{id}/module", GetModule<ResourceType_Instance, DicomModule_Instance>);
+    Register("/patients/{id}/module", GetModule<ResourceType_Patient, DicomModule_Patient>);
+    Register("/series/{id}/module", GetModule<ResourceType_Series, DicomModule_Series>);
+    Register("/studies/{id}/module", GetModule<ResourceType_Study, DicomModule_Study>);
+    Register("/studies/{id}/module-patient", GetModule<ResourceType_Study, DicomModule_Patient>);
+
+    Register("/instances/{id}/file", GetInstanceFile);
+    Register("/instances/{id}/export", ExportInstanceFile);
+    Register("/instances/{id}/tags", GetInstanceTagsBis);
+    Register("/instances/{id}/simplified-tags", GetInstanceTags<DicomToJsonFormat_Human>);
+    Register("/instances/{id}/frames", ListFrames);
+
+    Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>);
+    Register("/instances/{id}/frames/{frame}/rendered", GetRenderedFrame);
+    Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
+    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}/rendered", GetRenderedFrame);
+    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);
+
+    Register("/{resourceType}/{id}/metadata", ListMetadata);
+    Register("/{resourceType}/{id}/metadata/{name}", DeleteMetadata);
+    Register("/{resourceType}/{id}/metadata/{name}", GetMetadata);
+    Register("/{resourceType}/{id}/metadata/{name}", SetMetadata);
+
+    Register("/{resourceType}/{id}/attachments", ListAttachments);
+    Register("/{resourceType}/{id}/attachments/{name}", DeleteAttachment);
+    Register("/{resourceType}/{id}/attachments/{name}", GetAttachmentOperations);
+    Register("/{resourceType}/{id}/attachments/{name}", UploadAttachment);
+    Register("/{resourceType}/{id}/attachments/{name}/compress", ChangeAttachmentCompression<CompressionType_ZlibWithSize>);
+    Register("/{resourceType}/{id}/attachments/{name}/compressed-data", GetAttachmentData<0>);
+    Register("/{resourceType}/{id}/attachments/{name}/compressed-md5", GetAttachmentCompressedMD5);
+    Register("/{resourceType}/{id}/attachments/{name}/compressed-size", GetAttachmentCompressedSize);
+    Register("/{resourceType}/{id}/attachments/{name}/data", GetAttachmentData<1>);
+    Register("/{resourceType}/{id}/attachments/{name}/is-compressed", IsAttachmentCompressed);
+    Register("/{resourceType}/{id}/attachments/{name}/md5", GetAttachmentMD5);
+    Register("/{resourceType}/{id}/attachments/{name}/size", GetAttachmentSize);
+    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);
+
+    Register("/patients/{id}/studies", GetChildResources<ResourceType_Patient, ResourceType_Study>);
+    Register("/patients/{id}/series", GetChildResources<ResourceType_Patient, ResourceType_Series>);
+    Register("/patients/{id}/instances", GetChildResources<ResourceType_Patient, ResourceType_Instance>);
+    Register("/studies/{id}/series", GetChildResources<ResourceType_Study, ResourceType_Series>);
+    Register("/studies/{id}/instances", GetChildResources<ResourceType_Study, ResourceType_Instance>);
+    Register("/series/{id}/instances", GetChildResources<ResourceType_Series, ResourceType_Instance>);
+
+    Register("/studies/{id}/patient", GetParentResource<ResourceType_Study, ResourceType_Patient>);
+    Register("/series/{id}/patient", GetParentResource<ResourceType_Series, ResourceType_Patient>);
+    Register("/series/{id}/study", GetParentResource<ResourceType_Series, ResourceType_Study>);
+    Register("/instances/{id}/patient", GetParentResource<ResourceType_Instance, ResourceType_Patient>);
+    Register("/instances/{id}/study", GetParentResource<ResourceType_Instance, ResourceType_Study>);
+    Register("/instances/{id}/series", GetParentResource<ResourceType_Instance, ResourceType_Series>);
+
+    Register("/patients/{id}/instances-tags", GetChildInstancesTags);
+    Register("/studies/{id}/instances-tags", GetChildInstancesTags);
+    Register("/series/{id}/instances-tags", GetChildInstancesTags);
+
+    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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestSystem.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,584 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi.h"
+
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/MetricsRegistry.h"
+#include "../../Plugins/Engine/OrthancPlugins.h"
+#include "../../Plugins/Engine/PluginsManager.h"
+#include "../OrthancConfiguration.h"
+#include "../ServerContext.h"
+
+
+static const char* LOG_LEVEL_DEFAULT = "default";
+static const char* LOG_LEVEL_VERBOSE = "verbose";
+static const char* LOG_LEVEL_TRACE = "trace";
+
+
+namespace Orthanc
+{
+  // System information -------------------------------------------------------
+
+  static void ServeRoot(RestApiGetCall& call)
+  {
+    call.GetOutput().Redirect("app/explorer.html");
+  }
+ 
+  static void GetSystemInformation(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    Json::Value result = Json::objectValue;
+
+    result["ApiVersion"] = ORTHANC_API_VERSION;
+    result["Version"] = ORTHANC_VERSION;
+    result["DatabaseVersion"] = OrthancRestApi::GetIndex(call).GetDatabaseVersion();
+    result["IsHttpServerSecure"] = context.IsHttpServerSecure();  // New in Orthanc 1.5.8
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      result["DicomAet"] = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
+      result["DicomPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242);
+      result["HttpPort"] = lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042);
+      result["Name"] = lock.GetConfiguration().GetStringParameter("Name", "");
+    }
+
+    result["StorageAreaPlugin"] = Json::nullValue;
+    result["DatabaseBackendPlugin"] = Json::nullValue;
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    result["PluginsEnabled"] = true;
+    const OrthancPlugins& plugins = context.GetPlugins();
+
+    if (plugins.HasStorageArea())
+    {
+      std::string p = plugins.GetStorageAreaLibrary().GetPath();
+      result["StorageAreaPlugin"] = boost::filesystem::canonical(p).string();
+    }
+
+    if (plugins.HasDatabaseBackend())
+    {
+      std::string p = plugins.GetDatabaseBackendLibrary().GetPath();
+      result["DatabaseBackendPlugin"] = boost::filesystem::canonical(p).string();     
+    }
+#else
+    result["PluginsEnabled"] = false;
+#endif
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+  static void GetStatistics(RestApiGetCall& call)
+  {
+    static const uint64_t MEGA_BYTES = 1024 * 1024;
+
+    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+    OrthancRestApi::GetIndex(call).GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                                       countStudies, countSeries, countInstances);
+    
+    Json::Value result = Json::objectValue;
+    result["TotalDiskSize"] = boost::lexical_cast<std::string>(diskSize);
+    result["TotalUncompressedSize"] = boost::lexical_cast<std::string>(uncompressedSize);
+    result["TotalDiskSizeMB"] = static_cast<unsigned int>(diskSize / MEGA_BYTES);
+    result["TotalUncompressedSizeMB"] = static_cast<unsigned int>(uncompressedSize / MEGA_BYTES);
+    result["CountPatients"] = static_cast<unsigned int>(countPatients);
+    result["CountStudies"] = static_cast<unsigned int>(countStudies);
+    result["CountSeries"] = static_cast<unsigned int>(countSeries);
+    result["CountInstances"] = static_cast<unsigned int>(countInstances);
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+  static void GenerateUid(RestApiGetCall& call)
+  {
+    std::string level = call.GetArgument("level", "");
+    if (level == "patient")
+    {
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Patient), MimeType_PlainText);
+    }
+    else if (level == "study")
+    {
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study), MimeType_PlainText);
+    }
+    else if (level == "series")
+    {
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Series), MimeType_PlainText);
+    }
+    else if (level == "instance")
+    {
+      call.GetOutput().AnswerBuffer(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance), MimeType_PlainText);
+    }
+  }
+
+  static void ExecuteScript(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    if (!context.IsExecuteLuaEnabled())
+    {
+      LOG(ERROR) << "The URI /tools/execute-script is disallowed for security, "
+                 << "check your configuration file";
+      call.GetOutput().SignalError(HttpStatus_403_Forbidden);
+      return;
+    }
+
+    std::string result;
+    std::string command;
+    call.BodyToString(command);
+
+    {
+      LuaScripting::Lock lock(context.GetLuaScripting());
+      lock.GetLua().Execute(result, command);
+    }
+
+    call.GetOutput().AnswerBuffer(result, MimeType_PlainText);
+  }
+
+  template <bool UTC>
+  static void GetNowIsoString(RestApiGetCall& call)
+  {
+    call.GetOutput().AnswerBuffer(SystemToolbox::GetNowIsoString(UTC), MimeType_PlainText);
+  }
+
+
+  static void GetDicomConformanceStatement(RestApiGetCall& call)
+  {
+    std::string statement;
+    GetFileResource(statement, ServerResources::DICOM_CONFORMANCE_STATEMENT);
+    call.GetOutput().AnswerBuffer(statement, MimeType_PlainText);
+  }
+
+
+  static void GetDefaultEncoding(RestApiGetCall& call)
+  {
+    Encoding encoding = GetDefaultDicomEncoding();
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
+  }
+
+
+  static void SetDefaultEncoding(RestApiPutCall& call)
+  {
+    std::string body;
+    call.BodyToString(body);
+
+    Encoding encoding = StringToEncoding(body.c_str());
+
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().SetDefaultEncoding(encoding);
+    }
+
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), MimeType_PlainText);
+  }
+
+
+  
+  // Plugins information ------------------------------------------------------
+
+  static void ListPlugins(RestApiGetCall& call)
+  {
+    Json::Value v = Json::arrayValue;
+
+    v.append("explorer.js");
+
+    if (OrthancRestApi::GetContext(call).HasPlugins())
+    {
+#if ORTHANC_ENABLE_PLUGINS == 1
+      std::list<std::string> plugins;
+      OrthancRestApi::GetContext(call).GetPlugins().GetManager().ListPlugins(plugins);
+
+      for (std::list<std::string>::const_iterator 
+             it = plugins.begin(); it != plugins.end(); ++it)
+      {
+        v.append(*it);
+      }
+#endif
+    }
+
+    call.GetOutput().AnswerJson(v);
+  }
+
+
+  static void GetPlugin(RestApiGetCall& call)
+  {
+    if (!OrthancRestApi::GetContext(call).HasPlugins())
+    {
+      return;
+    }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    const PluginsManager& manager = OrthancRestApi::GetContext(call).GetPlugins().GetManager();
+    std::string id = call.GetUriComponent("id", "");
+
+    if (manager.HasPlugin(id))
+    {
+      Json::Value v = Json::objectValue;
+      v["ID"] = id;
+      v["Version"] = manager.GetPluginVersion(id);
+
+      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
+      const char *c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_RootUri);
+      if (c != NULL)
+      {
+        std::string root = c;
+        if (!root.empty())
+        {
+          // Turn the root URI into a URI relative to "/app/explorer.js"
+          if (root[0] == '/')
+          {
+            root = ".." + root;
+          }
+
+          v["RootUri"] = root;
+        }
+      }
+
+      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_Description);
+      if (c != NULL)
+      {
+        v["Description"] = c;
+      }
+
+      c = plugins.GetProperty(id.c_str(), _OrthancPluginProperty_OrthancExplorer);
+      v["ExtendsOrthancExplorer"] = (c != NULL);
+
+      call.GetOutput().AnswerJson(v);
+    }
+#endif
+  }
+
+
+  static void GetOrthancExplorerPlugins(RestApiGetCall& call)
+  {
+    std::string s = "// Extensions to Orthanc Explorer by the registered plugins\n\n";
+
+    if (OrthancRestApi::GetContext(call).HasPlugins())
+    {
+#if ORTHANC_ENABLE_PLUGINS == 1
+      const OrthancPlugins& plugins = OrthancRestApi::GetContext(call).GetPlugins();
+      const PluginsManager& manager = plugins.GetManager();
+
+      std::list<std::string> lst;
+      manager.ListPlugins(lst);
+
+      for (std::list<std::string>::const_iterator
+             it = lst.begin(); it != lst.end(); ++it)
+      {
+        const char* tmp = plugins.GetProperty(it->c_str(), _OrthancPluginProperty_OrthancExplorer);
+        if (tmp != NULL)
+        {
+          s += "/**\n * From plugin: " + *it + " (version " + manager.GetPluginVersion(*it) + ")\n **/\n\n";
+          s += std::string(tmp) + "\n\n";
+        }
+      }
+#endif
+    }
+
+    call.GetOutput().AnswerBuffer(s, MimeType_JavaScript);
+  }
+
+
+
+
+  // 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);
+    }
+  }
+
+
+  static void GetJobOutput(RestApiGetCall& call)
+  {
+    std::string job = call.GetUriComponent("id", "");
+    std::string key = call.GetUriComponent("key", "");
+
+    std::string value;
+    MimeType mime;
+    
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().
+        GetRegistry().GetJobOutput(value, mime, job, key))
+    {
+      call.GetOutput().AnswerBuffer(value, mime);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InexistentItem,
+                             "Job has no such output: " + key);
+    }
+  }
+
+
+  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("{}", MimeType_Json);
+    }
+  }
+
+  
+  static void GetMetricsPrometheus(RestApiGetCall& call)
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancRestApi::GetContext(call).GetPlugins().RefreshMetrics();
+#endif
+
+    static const float MEGA_BYTES = 1024 * 1024;
+
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+
+    unsigned int jobsPending, jobsRunning, jobsSuccess, jobsFailed;
+    context.GetJobsEngine().GetRegistry().GetStatistics(jobsPending, jobsRunning, jobsSuccess, jobsFailed);
+
+    MetricsRegistry& registry = context.GetMetricsRegistry();
+    registry.SetValue("orthanc_disk_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
+    registry.SetValue("orthanc_uncompressed_size_mb", static_cast<float>(diskSize) / MEGA_BYTES);
+    registry.SetValue("orthanc_count_patients", static_cast<unsigned int>(countPatients));
+    registry.SetValue("orthanc_count_studies", static_cast<unsigned int>(countStudies));
+    registry.SetValue("orthanc_count_series", static_cast<unsigned int>(countSeries));
+    registry.SetValue("orthanc_count_instances", static_cast<unsigned int>(countInstances));
+    registry.SetValue("orthanc_jobs_pending", jobsPending);
+    registry.SetValue("orthanc_jobs_running", jobsRunning);
+    registry.SetValue("orthanc_jobs_completed", jobsSuccess + jobsFailed);
+    registry.SetValue("orthanc_jobs_success", jobsSuccess);
+    registry.SetValue("orthanc_jobs_failed", jobsFailed);
+    
+    std::string s;
+    registry.ExportPrometheusText(s);
+
+    call.GetOutput().AnswerBuffer(s, MimeType_PrometheusText);
+  }
+
+
+  static void GetMetricsEnabled(RestApiGetCall& call)
+  {
+    bool enabled = OrthancRestApi::GetContext(call).GetMetricsRegistry().IsEnabled();
+    call.GetOutput().AnswerBuffer(enabled ? "1" : "0", MimeType_PlainText);
+  }
+
+
+  static void PutMetricsEnabled(RestApiPutCall& call)
+  {
+    bool enabled;
+
+    std::string body;
+    call.BodyToString(body);
+
+    if (body == "1")
+    {
+      enabled = true;
+    }
+    else if (body == "0")
+    {
+      enabled = false;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "The HTTP body must be 0 or 1, but found: " + body);
+    }
+
+    // Success
+    OrthancRestApi::GetContext(call).GetMetricsRegistry().SetEnabled(enabled);
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  static void GetLogLevel(RestApiGetCall& call)
+  {
+    std::string s;
+    
+    if (Logging::IsTraceLevelEnabled())
+    {
+      s = LOG_LEVEL_TRACE;
+    }
+    else if (Logging::IsInfoLevelEnabled())
+    {
+      s = LOG_LEVEL_VERBOSE;
+    }
+    else
+    {
+      s = LOG_LEVEL_DEFAULT;
+    }
+    
+    call.GetOutput().AnswerBuffer(s, MimeType_PlainText);
+  }
+
+
+  static void PutLogLevel(RestApiPutCall& call)
+  {
+    std::string body;
+    call.BodyToString(body);
+
+    if (body == LOG_LEVEL_DEFAULT)
+    {
+      Logging::EnableInfoLevel(false);
+      Logging::EnableTraceLevel(false);
+    }
+    else if (body == LOG_LEVEL_VERBOSE)
+    {
+      Logging::EnableInfoLevel(true);
+      Logging::EnableTraceLevel(false);
+    }
+    else if (body == LOG_LEVEL_TRACE)
+    {
+      Logging::EnableInfoLevel(true);
+      Logging::EnableTraceLevel(true);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "The log level must be one of the following values: \"" +
+                             std::string(LOG_LEVEL_DEFAULT) + "\", \"" +
+                             std::string(LOG_LEVEL_VERBOSE) + "\", of \"" +
+                             std::string(LOG_LEVEL_TRACE) + "\"");
+    }
+
+    // Success
+    LOG(WARNING) << "REST API call has switched the log level to: " << body;
+    call.GetOutput().AnswerBuffer("", MimeType_PlainText);
+  }
+
+
+  void OrthancRestApi::RegisterSystem()
+  {
+    Register("/", ServeRoot);
+    Register("/system", GetSystemInformation);
+    Register("/statistics", GetStatistics);
+    Register("/tools/generate-uid", GenerateUid);
+    Register("/tools/execute-script", ExecuteScript);
+    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("/tools/metrics", GetMetricsEnabled);
+    Register("/tools/metrics", PutMetricsEnabled);
+    Register("/tools/metrics-prometheus", GetMetricsPrometheus);
+    Register("/tools/log-level", GetLogLevel);
+    Register("/tools/log-level", PutLogLevel);
+
+    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>);
+    Register("/jobs/{id}/{key}", GetJobOutput);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/PrecompiledHeadersServer.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,34 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/PrecompiledHeadersServer.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,42 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/PrecompiledHeaders.h"
+
+#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
+
+#include <boost/thread.hpp>
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/QueryRetrieveHandler.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,171 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "QueryRetrieveHandler.h"
+
+#include "OrthancConfiguration.h"
+
+#include "../Core/DicomNetworking/DicomControlUserConnection.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/Logging.h"
+#include "LuaScripting.h"
+#include "ServerContext.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);
+    }
+  }
+
+
+  void QueryRetrieveHandler::Invalidate()
+  {
+    done_ = false;
+    answers_.Clear();
+  }
+
+
+  void QueryRetrieveHandler::Run()
+  {
+    if (!done_)
+    {
+      // Firstly, fix the content of the query for specific manufacturers
+      DicomMap fixed;
+      fixed.Assign(query_);
+
+      // Secondly, possibly fix the query with the user-provider Lua callback
+      FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); 
+
+      {
+        DicomAssociationParameters params(localAet_, modality_);
+        DicomControlUserConnection connection(params);
+        connection.Find(answers_, level_, fixed, findNormalized_);
+      }
+
+      done_ = true;
+    }
+  }
+
+
+  QueryRetrieveHandler::QueryRetrieveHandler(ServerContext& context) : 
+    context_(context),
+    localAet_(context.GetDefaultLocalApplicationEntityTitle()),
+    done_(false),
+    level_(ResourceType_Study),
+    answers_(false),
+    findNormalized_(true)
+  {
+  }
+
+
+  void QueryRetrieveHandler::SetModality(const std::string& symbolicName)
+  {
+    Invalidate();
+    modalityName_ = symbolicName;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      lock.GetConfiguration().GetDicomModalityUsingSymbolicName(modality_, symbolicName);
+    }
+  }
+
+
+  void QueryRetrieveHandler::SetLevel(ResourceType level)
+  {
+    Invalidate();
+    level_ = level;
+  }
+
+
+  void QueryRetrieveHandler::SetQuery(const DicomTag& tag,
+                                      const std::string& value)
+  {
+    Invalidate();
+    query_.SetValue(tag, value, false);
+  }
+
+
+  void QueryRetrieveHandler::CopyStringTag(const DicomMap& from,
+                                           const DicomTag& tag)
+  {
+    const DicomValue* value = from.TestAndGetValue(tag);
+
+    if (value == NULL ||
+        value->IsNull() ||
+        value->IsBinary())
+    {
+      throw OrthancException(ErrorCode_InexistentTag);
+    }
+    else
+    {
+      SetQuery(tag, value->GetContent());
+    }
+  }
+
+
+  size_t QueryRetrieveHandler::GetAnswersCount()
+  {
+    Run();
+    return answers_.GetSize();
+  }
+
+
+  void QueryRetrieveHandler::GetAnswer(DicomMap& target,
+                                       size_t i)
+  {
+    Run();
+    answers_.GetAnswer(i).ExtractDicomSummary(target);
+  }
+
+  
+  void QueryRetrieveHandler::SetFindNormalized(bool normalized)
+  {
+    Invalidate();
+    findNormalized_ = normalized;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/QueryRetrieveHandler.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,110 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomNetworking/DicomFindAnswers.h"
+#include "../Core/DicomNetworking/RemoteModalityParameters.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class QueryRetrieveHandler : public IDynamicObject
+  {
+  private:
+    ServerContext&             context_;
+    std::string                localAet_;
+    bool                       done_;
+    RemoteModalityParameters   modality_;
+    ResourceType               level_;
+    DicomMap                   query_;
+    DicomFindAnswers           answers_;
+    std::string                modalityName_;
+    bool                       findNormalized_;
+
+    void Invalidate();
+
+  public:
+    QueryRetrieveHandler(ServerContext& context);
+
+    void SetModality(const std::string& symbolicName);
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return modality_;
+    }
+
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    const std::string& GetModalitySymbolicName() const
+    {
+      return modalityName_;
+    }
+
+    void SetLevel(ResourceType level);
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetQuery(const DicomTag& tag,
+                  const std::string& value);
+
+    const DicomMap& GetQuery() const
+    {
+      return query_;
+    }
+
+    void CopyStringTag(const DicomMap& from,
+                       const DicomTag& tag);
+
+    void Run();
+
+    size_t GetAnswersCount();
+
+    void GetAnswer(DicomMap& target,
+                   size_t i);
+
+    bool IsFindNormalized() const
+    {
+      return findNormalized_;
+    }
+
+    void SetFindNormalized(bool normalized);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,245 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DatabaseConstraint.h"
+
+#include "../../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  namespace Plugins
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancPluginResourceType Convert(ResourceType type)
+    {
+      switch (type)
+      {
+        case ResourceType_Patient:
+          return OrthancPluginResourceType_Patient;
+
+        case ResourceType_Study:
+          return OrthancPluginResourceType_Study;
+
+        case ResourceType_Series:
+          return OrthancPluginResourceType_Series;
+
+        case ResourceType_Instance:
+          return OrthancPluginResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    ResourceType Convert(OrthancPluginResourceType type)
+    {
+      switch (type)
+      {
+        case OrthancPluginResourceType_Patient:
+          return ResourceType_Patient;
+
+        case OrthancPluginResourceType_Study:
+          return ResourceType_Study;
+
+        case OrthancPluginResourceType_Series:
+          return ResourceType_Series;
+
+        case OrthancPluginResourceType_Instance:
+          return ResourceType_Instance;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    OrthancPluginConstraintType Convert(ConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case ConstraintType_Equal:
+          return OrthancPluginConstraintType_Equal;
+
+        case ConstraintType_GreaterOrEqual:
+          return OrthancPluginConstraintType_GreaterOrEqual;
+
+        case ConstraintType_SmallerOrEqual:
+          return OrthancPluginConstraintType_SmallerOrEqual;
+
+        case ConstraintType_Wildcard:
+          return OrthancPluginConstraintType_Wildcard;
+
+        case ConstraintType_List:
+          return OrthancPluginConstraintType_List;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif    
+
+    
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    ConstraintType Convert(OrthancPluginConstraintType constraint)
+    {
+      switch (constraint)
+      {
+        case OrthancPluginConstraintType_Equal:
+          return ConstraintType_Equal;
+
+        case OrthancPluginConstraintType_GreaterOrEqual:
+          return ConstraintType_GreaterOrEqual;
+
+        case OrthancPluginConstraintType_SmallerOrEqual:
+          return ConstraintType_SmallerOrEqual;
+
+        case OrthancPluginConstraintType_Wildcard:
+          return ConstraintType_Wildcard;
+
+        case OrthancPluginConstraintType_List:
+          return ConstraintType_List;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+#endif
+  }
+
+  DatabaseConstraint::DatabaseConstraint(ResourceType level,
+                                         const DicomTag& tag,
+                                         bool isIdentifier,
+                                         ConstraintType type,
+                                         const std::vector<std::string>& values,
+                                         bool caseSensitive,
+                                         bool mandatory) :
+    level_(level),
+    tag_(tag),
+    isIdentifier_(isIdentifier),
+    constraintType_(type),
+    values_(values),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    if (type != ConstraintType_List &&
+        values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }      
+
+    
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  DatabaseConstraint::DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint) :
+    level_(Plugins::Convert(constraint.level)),
+    tag_(constraint.tagGroup, constraint.tagElement),
+    isIdentifier_(constraint.isIdentifierTag),
+    constraintType_(Plugins::Convert(constraint.type)),
+    caseSensitive_(constraint.isCaseSensitive),
+    mandatory_(constraint.isMandatory)
+  {
+    if (constraintType_ != ConstraintType_List &&
+        constraint.valuesCount != 1)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    values_.resize(constraint.valuesCount);
+
+    for (uint32_t i = 0; i < constraint.valuesCount; i++)
+    {
+      assert(constraint.values[i] != NULL);
+      values_[i].assign(constraint.values[i]);
+    }
+  }
+#endif
+    
+
+  const std::string& DatabaseConstraint::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return values_[index];
+    }
+  }
+
+
+  const std::string& DatabaseConstraint::GetSingleValue() const
+  {
+    if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return values_[0];
+    }
+  }
+
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+  void DatabaseConstraint::EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                                            std::vector<const char*>& tmpValues) const
+  {
+    memset(&constraint, 0, sizeof(constraint));
+    
+    tmpValues.resize(values_.size());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      tmpValues[i] = values_[i].c_str();
+    }
+
+    constraint.level = Plugins::Convert(level_);
+    constraint.tagGroup = tag_.GetGroup();
+    constraint.tagElement = tag_.GetElement();
+    constraint.isIdentifierTag = isIdentifier_;
+    constraint.isCaseSensitive = caseSensitive_;
+    constraint.isMandatory = mandatory_;
+    constraint.type = Plugins::Convert(constraintType_);
+    constraint.valuesCount = values_.size();
+    constraint.values = (tmpValues.empty() ? NULL : &tmpValues[0]);
+  }
+#endif    
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseConstraint.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,144 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../ServerEnumerations.h"
+
+#define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 0
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+#  include <orthanc/OrthancCDatabasePlugin.h>
+#  if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)      // Macro introduced in 1.3.1
+#    if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 2)
+#      undef  ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT
+#      define ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT 1
+#    endif
+#  endif
+#endif
+
+namespace Orthanc
+{
+  namespace Plugins
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancPluginResourceType Convert(ResourceType type);
+#endif
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    ResourceType Convert(OrthancPluginResourceType type);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    OrthancPluginConstraintType Convert(ConstraintType constraint);
+#endif
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    ConstraintType Convert(OrthancPluginConstraintType constraint);
+#endif
+  }
+
+
+  // This class is also used by the "orthanc-databases" project
+  class DatabaseConstraint
+  {
+  private:
+    ResourceType              level_;
+    DicomTag                  tag_;
+    bool                      isIdentifier_;
+    ConstraintType            constraintType_;
+    std::vector<std::string>  values_;
+    bool                      caseSensitive_;
+    bool                      mandatory_;
+
+  public:
+    DatabaseConstraint(ResourceType level,
+                       const DicomTag& tag,
+                       bool isIdentifier,
+                       ConstraintType type,
+                       const std::vector<std::string>& values,
+                       bool caseSensitive,
+                       bool mandatory);
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    DatabaseConstraint(const OrthancPluginDatabaseConstraint& constraint);
+#endif
+    
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    bool IsIdentifier() const
+    {
+      return isIdentifier_;
+    }
+
+    ConstraintType GetConstraintType() const
+    {
+      return constraintType_;
+    }
+
+    size_t GetValuesCount() const
+    {
+      return values_.size();
+    }
+
+    const std::string& GetValue(size_t index) const;
+
+    const std::string& GetSingleValue() const;
+
+    bool IsCaseSensitive() const
+    {
+      return caseSensitive_;
+    }
+
+    bool IsMandatory() const
+    {
+      return mandatory_;
+    }
+
+    bool IsMatch(const DicomMap& dicom) const;
+
+#if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
+    void EncodeForPlugins(OrthancPluginDatabaseConstraint& constraint,
+                          std::vector<const char*>& tmpValues) const;
+#endif    
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseLookup.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,316 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DatabaseLookup.h"
+
+#include "../ServerToolbox.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  DatabaseLookup::~DatabaseLookup()
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      delete constraints_[i];
+    }
+  }
+
+
+  const DicomTagConstraint& DatabaseLookup::GetConstraint(size_t index) const
+  {
+    if (index >= constraints_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(constraints_[index] != NULL);
+      return *constraints_[index];
+    }
+  }
+
+
+  void DatabaseLookup::AddConstraint(DicomTagConstraint* constraint)
+  {
+    if (constraint == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      constraints_.push_back(constraint);
+    }
+  }
+
+
+  bool DatabaseLookup::IsMatch(const DicomMap& value) const
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      if (!constraints_[i]->IsMatch(value))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool DatabaseLookup::IsMatch(DcmItem& item,
+                               Encoding encoding,
+                               bool hasCodeExtensions) const
+  {
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+
+      const bool isOptionalConstraint = !constraints_[i]->IsMandatory();
+      const DcmTagKey tag = ToDcmtkBridge::Convert(constraints_[i]->GetTag());
+
+      DcmElement* element = NULL;
+      if (!item.findAndGetElement(tag, element).good())
+      {
+        return isOptionalConstraint;
+      }
+
+      if (element == NULL)
+      {
+        return false;
+      }
+
+      std::set<DicomTag> ignoreTagLength;
+      std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                        (*element, DicomToJsonFlags_None, 
+                                         0, encoding, hasCodeExtensions, ignoreTagLength));
+
+      // WARNING: Also modify "HierarchicalMatcher::Setup()" if modifying this code
+      if (value.get() == NULL ||
+          value->IsNull())
+      {
+        return isOptionalConstraint;        
+      }
+      else if (value->IsBinary() ||
+               !constraints_[i]->IsMatch(value->GetContent()))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  void DatabaseLookup::AddDicomConstraintInternal(const DicomTag& tag,
+                                                  ValueRepresentation vr,
+                                                  const std::string& dicomQuery,
+                                                  bool caseSensitive,
+                                                  bool mandatoryTag)
+  {
+    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);
+
+      if (!lower.empty())
+      {
+        AddConstraint(new DicomTagConstraint
+                      (tag, ConstraintType_GreaterOrEqual, lower, caseSensitive, mandatoryTag));
+      }
+
+      if (!upper.empty())
+      {
+        AddConstraint(new DicomTagConstraint
+                      (tag, ConstraintType_SmallerOrEqual, upper, caseSensitive, mandatoryTag));
+      }
+    }
+    else if (tag == DICOM_TAG_MODALITIES_IN_STUDY ||
+             dicomQuery.find('\\') != std::string::npos)
+    {
+      DicomTag fixedTag(tag);
+
+      if (tag == DICOM_TAG_MODALITIES_IN_STUDY)
+      {
+        // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+        // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+        fixedTag = DICOM_TAG_MODALITY;
+      }
+
+      std::unique_ptr<DicomTagConstraint> constraint
+        (new DicomTagConstraint(fixedTag, ConstraintType_List, caseSensitive, mandatoryTag));
+
+      std::vector<std::string> items;
+      Toolbox::TokenizeString(items, dicomQuery, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        constraint->AddValue(items[i]);
+      }
+
+      AddConstraint(constraint.release());
+    }
+    else if (
+      /**
+       * New test in Orthanc 1.6.0: Wild card matching is only allowed
+       * for a subset of value representations: AE, CS, LO, LT, PN,
+       * SH, ST, UC, UR, UT.
+       * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part04/sect_C.2.2.2.4.html
+       **/
+      (vr == ValueRepresentation_ApplicationEntity ||    // AE
+       vr == ValueRepresentation_CodeString ||           // CS
+       vr == ValueRepresentation_LongString ||           // LO
+       vr == ValueRepresentation_LongText ||             // LT
+       vr == ValueRepresentation_PersonName ||           // PN
+       vr == ValueRepresentation_ShortString ||          // SH
+       vr == ValueRepresentation_ShortText ||            // ST
+       vr == ValueRepresentation_UnlimitedCharacters ||  // UC
+       vr == ValueRepresentation_UniversalResource ||    // UR
+       vr == ValueRepresentation_UnlimitedText           // UT
+        ) &&
+      (dicomQuery.find('*') != std::string::npos ||
+       dicomQuery.find('?') != std::string::npos))
+    {
+      AddConstraint(new DicomTagConstraint
+                    (tag, ConstraintType_Wildcard, dicomQuery, caseSensitive, mandatoryTag));
+    }
+    else
+    {
+      AddConstraint(new DicomTagConstraint
+                    (tag, ConstraintType_Equal, dicomQuery, caseSensitive, mandatoryTag));
+    }
+  }
+
+
+  void DatabaseLookup::AddDicomConstraint(const DicomTag& tag,
+                                          const std::string& dicomQuery,
+                                          bool caseSensitivePN,
+                                          bool mandatoryTag)
+  {
+    ValueRepresentation vr = FromDcmtkBridge::LookupValueRepresentation(tag);
+
+    if (vr == ValueRepresentation_Sequence)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    /**
+     * DICOM specifies that searches must always be case sensitive,
+     * except for tags with a PN value representation. For PN, Orthanc
+     * uses the configuration option "CaseSensitivePN" to decide
+     * whether matching is case-sensitive or case-insensitive.
+     *
+     * 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
+     **/
+    
+    if (vr == ValueRepresentation_PersonName)
+    {
+      AddDicomConstraintInternal(tag, vr, dicomQuery, caseSensitivePN, mandatoryTag);
+    }
+    else
+    {
+      AddDicomConstraintInternal(tag, vr, dicomQuery, true /* case sensitive */, mandatoryTag);
+    }
+  }
+
+
+  void DatabaseLookup::AddRestConstraint(const DicomTag& tag,
+                                         const std::string& dicomQuery,
+                                         bool caseSensitive,
+                                         bool mandatoryTag)
+  {
+    AddDicomConstraintInternal(tag, FromDcmtkBridge::LookupValueRepresentation(tag),
+                               dicomQuery, caseSensitive, mandatoryTag);
+  }
+
+
+  bool DatabaseLookup::HasOnlyMainDicomTags() const
+  {
+    std::set<DicomTag> mainTags;
+    DicomMap::GetMainDicomTags(mainTags);
+
+    for (size_t i = 0; i < constraints_.size(); i++)
+    {
+      assert(constraints_[i] != NULL);
+      
+      if (mainTags.find(constraints_[i]->GetTag()) == mainTags.end())
+      {
+        // This is not a main DICOM tag
+        return false;
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DatabaseLookup.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomTagConstraint.h"
+
+class DcmItem;
+
+namespace Orthanc
+{
+  class DatabaseLookup : public boost::noncopyable
+  {
+  private:
+    std::vector<DicomTagConstraint*>  constraints_;
+
+    void AddDicomConstraintInternal(const DicomTag& tag,
+                                    ValueRepresentation vr,
+                                    const std::string& dicomQuery,
+                                    bool caseSensitive,
+                                    bool mandatoryTag);
+
+    void AddConstraint(DicomTagConstraint* constraint);  // Takes ownership
+
+  public:
+    DatabaseLookup()
+    {
+    }
+
+    ~DatabaseLookup();
+
+    void Reserve(size_t n)
+    {
+      constraints_.reserve(n);
+    }
+
+    size_t GetConstraintsCount() const
+    {
+      return constraints_.size();
+    }
+
+    const DicomTagConstraint& GetConstraint(size_t index) const;
+
+    bool IsMatch(const DicomMap& value) const;
+
+    bool IsMatch(DcmItem& item,
+                 Encoding encoding,
+                 bool hasCodeExtensions) const;
+
+    void AddDicomConstraint(const DicomTag& tag,
+                            const std::string& dicomQuery,
+                            bool caseSensitivePN,
+                            bool mandatoryTag);
+
+    void AddRestConstraint(const DicomTag& tag,
+                           const std::string& dicomQuery,
+                           bool caseSensitive,
+                           bool mandatoryTag);
+
+    bool HasOnlyMainDicomTags() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,385 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomTagConstraint.h"
+
+#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0
+#  include "../ServerToolbox.h"
+#endif
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+#include "DatabaseConstraint.h"
+
+#include <boost/regex.hpp>
+
+namespace Orthanc
+{
+  class DicomTagConstraint::NormalizedString : public boost::noncopyable
+  {
+  private:
+    const std::string&  source_;
+    bool                caseSensitive_;
+    std::string         upper_;
+
+  public:
+    NormalizedString(const std::string& source,
+                     bool caseSensitive) :
+      source_(source),
+      caseSensitive_(caseSensitive)
+    {
+      if (!caseSensitive_)
+      {
+        upper_ = Toolbox::ToUpperCaseWithAccents(source);
+      }
+    }
+
+    const std::string& GetValue() const
+    {
+      if (caseSensitive_)
+      {
+        return source_;
+      }
+      else
+      {
+        return upper_;
+      }
+    }
+  };
+
+
+  class DicomTagConstraint::RegularExpression : public boost::noncopyable
+  {
+  private:
+    boost::regex  regex_;
+
+  public:
+    RegularExpression(const std::string& source,
+                      bool caseSensitive)
+    {
+      NormalizedString normalized(source, caseSensitive);
+      regex_ = boost::regex(Toolbox::WildcardToRegularExpression(normalized.GetValue()));
+    }
+
+    const boost::regex& GetValue() const
+    {
+      return regex_;
+    }
+  };
+
+
+  void DicomTagConstraint::AssignSingleValue(const std::string& value)
+  {
+    if (constraintType_ != ConstraintType_Wildcard &&
+        (value.find('*') != std::string::npos ||
+         value.find('?') != std::string::npos))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Wildcards are not allowed on tag " + tag_.Format());
+    }
+
+    if (constraintType_ == ConstraintType_Equal ||
+        constraintType_ == ConstraintType_SmallerOrEqual ||
+        constraintType_ == ConstraintType_GreaterOrEqual ||
+        constraintType_ == ConstraintType_Wildcard)
+    {
+      values_.clear();
+      values_.insert(value);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
+                                         ConstraintType type,
+                                         const std::string& value,
+                                         bool caseSensitive,
+                                         bool mandatory) :
+    tag_(tag),
+    constraintType_(type),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    AssignSingleValue(value);
+  }
+
+
+  DicomTagConstraint::DicomTagConstraint(const DicomTag& tag,
+                                         ConstraintType type,
+                                         bool caseSensitive,
+                                         bool mandatory) :
+    tag_(tag),
+    constraintType_(type),
+    caseSensitive_(caseSensitive),
+    mandatory_(mandatory)
+  {
+    if (type != ConstraintType_List)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  DicomTagConstraint::DicomTagConstraint(const DatabaseConstraint& constraint) :
+    tag_(constraint.GetTag()),
+    constraintType_(constraint.GetConstraintType()),
+    caseSensitive_(constraint.IsCaseSensitive()),
+    mandatory_(constraint.IsMandatory())
+  {
+#if defined(ORTHANC_ENABLE_LUA) && ORTHANC_ENABLE_LUA != 0
+    assert(constraint.IsIdentifier() ==
+           ServerToolbox::IsIdentifier(constraint.GetTag(), constraint.GetLevel()));
+#endif
+    
+    if (constraint.IsIdentifier())
+    {
+      // This conversion is only available for main DICOM tags, not for identifers
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    
+    if (constraintType_ == ConstraintType_List)
+    {
+      for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+      {
+        AddValue(constraint.GetValue(i));
+      }
+    }
+    else
+    {
+      AssignSingleValue(constraint.GetSingleValue());
+    }
+  }
+
+
+  void DicomTagConstraint::AddValue(const std::string& value)
+  {
+    if (constraintType_ != ConstraintType_List)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else
+    {
+      values_.insert(value);
+    }
+  }
+
+
+  const std::string& DicomTagConstraint::GetValue() const
+  {
+    if (constraintType_ == ConstraintType_List)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+    else if (values_.size() != 1)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      return *values_.begin();
+    }
+  }
+
+
+  bool DicomTagConstraint::IsMatch(const std::string& value)
+  {
+    NormalizedString source(value, caseSensitive_);
+
+    switch (constraintType_)
+    {
+      case ConstraintType_Equal:
+      {
+        NormalizedString reference(GetValue(), caseSensitive_);
+        return source.GetValue() == reference.GetValue();
+      }
+
+      case ConstraintType_SmallerOrEqual:
+      {
+        NormalizedString reference(GetValue(), caseSensitive_);
+        return source.GetValue() <= reference.GetValue();
+      }
+
+      case ConstraintType_GreaterOrEqual:
+      {
+        NormalizedString reference(GetValue(), caseSensitive_);
+        return source.GetValue() >= reference.GetValue();
+      }
+
+      case ConstraintType_Wildcard:
+      {
+        if (regex_.get() == NULL)
+        {
+          regex_.reset(new RegularExpression(GetValue(), caseSensitive_));
+        }
+
+        return boost::regex_match(source.GetValue(), regex_->GetValue());
+      }
+
+      case ConstraintType_List:
+      {
+        for (std::set<std::string>::const_iterator
+               it = values_.begin(); it != values_.end(); ++it)
+        {
+          NormalizedString reference(*it, caseSensitive_);
+          if (source.GetValue() == reference.GetValue())
+          {
+            return true;
+          }
+        }
+
+        return false;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  bool DicomTagConstraint::IsMatch(const DicomMap& value)
+  {
+    const DicomValue* tmp = value.TestAndGetValue(tag_);
+
+    if (tmp == NULL ||
+        tmp->IsNull())
+    {
+      if (mandatory_)
+      {
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+    else if (tmp->IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      return IsMatch(tmp->GetContent());
+    }
+  }
+
+
+  std::string DicomTagConstraint::Format() const
+  {
+    switch (constraintType_)
+    {
+      case ConstraintType_Equal:
+        return tag_.Format() + " == " + GetValue();
+
+      case ConstraintType_SmallerOrEqual:
+        return tag_.Format() + " <= " + GetValue();
+
+      case ConstraintType_GreaterOrEqual:
+        return tag_.Format() + " >= " + GetValue();
+
+      case ConstraintType_Wildcard:
+        return tag_.Format() + " ~~ " + GetValue();
+
+      case ConstraintType_List:
+      {
+        std::string s = tag_.Format() + " IN [ ";
+
+        bool first = true;
+        for (std::set<std::string>::const_iterator
+               it = values_.begin(); it != values_.end(); ++it)
+        {
+          if (first)
+          {
+            first = false;
+          }
+          else
+          {
+            s += ", ";
+          }
+
+          s += *it;
+        }
+
+        return s + "]";
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  DatabaseConstraint DicomTagConstraint::ConvertToDatabaseConstraint(ResourceType level,
+                                                                     DicomTagType tagType) const
+  {
+    bool isIdentifier, caseSensitive;
+    
+    switch (tagType)
+    {
+      case DicomTagType_Identifier:
+        isIdentifier = true;
+        caseSensitive = true;
+        break;
+
+      case DicomTagType_Main:
+        isIdentifier = false;
+        caseSensitive = IsCaseSensitive();
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    std::vector<std::string> values;
+    values.reserve(values_.size());
+      
+    for (std::set<std::string>::const_iterator
+           it = values_.begin(); it != values_.end(); ++it)
+    {
+      if (isIdentifier)
+      {
+        values.push_back(ServerToolbox::NormalizeIdentifier(*it));
+      }
+      else
+      {
+        values.push_back(*it);
+      }
+    }
+
+    return DatabaseConstraint(level, tag_, isIdentifier, constraintType_,
+                              values, caseSensitive, mandatory_);
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/DicomTagConstraint.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,118 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DatabaseConstraint.h"
+
+#include <boost/shared_ptr.hpp>
+
+namespace Orthanc
+{
+  class DicomTagConstraint : public boost::noncopyable
+  {
+  private:
+    class NormalizedString;
+    class RegularExpression;
+
+    DicomTag                tag_;
+    ConstraintType          constraintType_;
+    std::set<std::string>   values_;
+    bool                    caseSensitive_;
+    bool                    mandatory_;
+
+    boost::shared_ptr<RegularExpression>  regex_;
+
+    void AssignSingleValue(const std::string& value);
+
+  public:
+    DicomTagConstraint(const DicomTag& tag,
+                       ConstraintType type,
+                       const std::string& value,
+                       bool caseSensitive,
+                       bool mandatory);
+
+    // For list search
+    DicomTagConstraint(const DicomTag& tag,
+                       ConstraintType type,
+                       bool caseSensitive,
+                       bool mandatory);
+
+    DicomTagConstraint(const DatabaseConstraint& constraint);
+
+    const DicomTag& GetTag() const
+    {
+      return tag_;
+    }
+
+    ConstraintType GetConstraintType() const
+    {
+      return constraintType_;
+    }
+    
+    bool IsCaseSensitive() const
+    {
+      return caseSensitive_;
+    }
+
+    void SetCaseSensitive(bool caseSensitive)
+    {
+      caseSensitive_ = caseSensitive;
+    }
+
+    bool IsMandatory() const
+    {
+      return mandatory_;
+    }
+
+    void AddValue(const std::string& value);
+
+    const std::string& GetValue() const;
+
+    const std::set<std::string>& GetValues() const
+    {
+      return values_;
+    }
+
+    bool IsMatch(const std::string& value);
+
+    bool IsMatch(const DicomMap& value);
+
+    std::string Format() const;
+
+    DatabaseConstraint ConvertToDatabaseConstraint(ResourceType level,
+                                                   DicomTagType tagType) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancConfiguration.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+namespace Orthanc
+{
+  HierarchicalMatcher::HierarchicalMatcher(ParsedDicomFile& query)
+  {
+    bool caseSensitivePN;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      caseSensitivePN = lock.GetConfiguration().GetBooleanParameter("CaseSensitivePN", false);
+    }
+
+    bool hasCodeExtensions;
+    Encoding encoding = query.DetectEncoding(hasCodeExtensions);
+    Setup(*query.GetDcmtkObject().getDataset(), caseSensitivePN, encoding, hasCodeExtensions);
+  }
+
+
+  HierarchicalMatcher::~HierarchicalMatcher()
+  {
+    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,
+                                  bool hasCodeExtensions)
+  {
+    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;
+      }
+
+      if (flatTags_.find(tag) != flatTags_.end() ||
+          sequences_.find(tag) != sequences_.end())
+      {
+        // A constraint already exists on this tag
+        throw OrthancException(ErrorCode_BadRequest);        
+      }
+
+      if (FromDcmtkBridge::LookupValueRepresentation(tag) == 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, hasCodeExtensions);
+        }
+        else
+        {
+          throw OrthancException(ErrorCode_BadRequest);        
+        }
+      }
+      else
+      {
+        flatTags_.insert(tag);
+
+        std::set<DicomTag> ignoreTagLength;
+        std::unique_ptr<DicomValue> value(FromDcmtkBridge::ConvertLeafElement
+                                          (*element, DicomToJsonFlags_None, 
+                                           0, encoding, hasCodeExtensions, ignoreTagLength));
+
+        // WARNING: Also modify "DatabaseLookup::IsMatch()" if modifying this code
+        if (value.get() == NULL ||
+            value->IsNull())
+        {
+          // This is an universal constraint
+        }
+        else 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.";
+          }
+        }
+        else if (value->GetContent().empty())
+        {
+          // This is an universal matcher
+        }
+        else
+        {
+          flatConstraints_.AddDicomConstraint
+            (tag, value->GetContent(), caseSensitivePN, true /* mandatory */);
+        }
+      }
+    }
+  }
+
+
+  std::string HierarchicalMatcher::Format(const std::string& prefix) const
+  {
+    std::string s;
+
+    std::set<DicomTag> tags;
+    for (size_t i = 0; i < flatConstraints_.GetConstraintsCount(); i++)
+    {
+      const DicomTagConstraint& c = flatConstraints_.GetConstraint(i);
+
+      s += c.Format() + "\n";
+      tags.insert(c.GetTag());
+    }
+    
+    // Loop over the universal constraints
+    for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
+         it != flatTags_.end(); ++it)
+    {
+      if (tags.find(*it) == tags.end())
+      {
+        s += prefix + it->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
+  {
+    bool hasCodeExtensions;
+    Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+    
+    return MatchInternal(*dicom.GetDcmtkObject().getDataset(),
+                         encoding, hasCodeExtensions);
+  }
+
+
+  bool HierarchicalMatcher::MatchInternal(DcmItem& item,
+                                          Encoding encoding,
+                                          bool hasCodeExtensions) const
+  {
+    if (!flatConstraints_.IsMatch(item, encoding, hasCodeExtensions))
+    {
+      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, hasCodeExtensions))
+          {
+            match = true;
+            break;
+          }
+        }
+
+        if (!match)
+        {
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+
+  DcmDataset* HierarchicalMatcher::ExtractInternal(DcmItem& source,
+                                                   Encoding encoding,
+                                                   bool hasCodeExtensions) const
+  {
+    std::unique_ptr<DcmDataset> target(new DcmDataset);
+
+    for (std::set<DicomTag>::const_iterator it = flatTags_.begin();
+         it != flatTags_.end(); ++it)
+    {
+      DcmTagKey tag = ToDcmtkBridge::Convert(*it);
+      
+      DcmElement* element = NULL;
+      if (source.findAndGetElement(tag, element).good() &&
+          element != NULL)
+      {
+        if (it->IsPrivate())
+        {
+          throw OrthancException(ErrorCode_NotImplemented,
+                                 "Not applicable to private tags: " + it->Format());
+        }
+        
+        std::unique_ptr<DcmElement> cloned(FromDcmtkBridge::CreateElementForTag(*it, "" /* no private creator */));
+        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::unique_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, hasCodeExtensions))  // 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::unique_ptr<DcmDataset> child(it->second->ExtractInternal(*sequence->getItem(i), encoding, hasCodeExtensions));
+            cloned->append(new DcmItem(*child));
+          }
+        }
+
+        target->insert(cloned.release());
+      }
+    }
+
+    return target.release();
+  }
+
+
+  ParsedDicomFile* HierarchicalMatcher::Extract(ParsedDicomFile& dicom) const
+  {
+    bool hasCodeExtensions;
+    Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+    
+    std::unique_ptr<DcmDataset> dataset(ExtractInternal(*dicom.GetDcmtkObject().getDataset(),
+                                                        encoding, hasCodeExtensions));
+
+    std::unique_ptr<ParsedDicomFile> result(new ParsedDicomFile(*dataset));
+    result->SetEncoding(encoding);
+
+    return result.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/HierarchicalMatcher.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DatabaseLookup.h"
+#include "../../Core/DicomParsing/ParsedDicomFile.h"
+
+class DcmItem;
+
+namespace Orthanc
+{
+  class HierarchicalMatcher : public boost::noncopyable
+  {
+  private:
+    typedef std::map<DicomTag, HierarchicalMatcher*>  Sequences;
+
+    std::set<DicomTag>  flatTags_;
+    DatabaseLookup      flatConstraints_;
+    Sequences           sequences_;
+
+    void Setup(DcmItem& query,
+               bool caseSensitivePN,
+               Encoding encoding,
+               bool hasCodeExtensions);
+
+    HierarchicalMatcher(DcmItem& query,
+                        bool caseSensitivePN,
+                        Encoding encoding,
+                        bool hasCodeExtensions)
+    {
+      Setup(query, caseSensitivePN, encoding, hasCodeExtensions);
+    }
+
+    bool MatchInternal(DcmItem& dicom,
+                       Encoding encoding,
+                       bool hasCodeExtensions) const;
+
+    DcmDataset* ExtractInternal(DcmItem& dicom,
+                                Encoding encoding,
+                                bool hasCodeExtensions) 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/Sources/Search/ISqlLookupFormatter.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,343 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ISqlLookupFormatter.h"
+
+#include "../../Core/OrthancException.h"
+#include "DatabaseConstraint.h"
+
+namespace Orthanc
+{
+  static std::string FormatLevel(ResourceType level)
+  {
+    switch (level)
+    {
+      case ResourceType_Patient:
+        return "patients";
+        
+      case ResourceType_Study:
+        return "studies";
+        
+      case ResourceType_Series:
+        return "series";
+        
+      case ResourceType_Instance:
+        return "instances";
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }      
+  
+
+  static bool FormatComparison(std::string& target,
+                               ISqlLookupFormatter& formatter,
+                               const DatabaseConstraint& constraint,
+                               size_t index)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    std::string comparison;
+    
+    switch (constraint.GetConstraintType())
+    {
+      case ConstraintType_Equal:
+      case ConstraintType_SmallerOrEqual:
+      case ConstraintType_GreaterOrEqual:
+      {
+        std::string op;
+        switch (constraint.GetConstraintType())
+        {
+          case ConstraintType_Equal:
+            op = "=";
+            break;
+          
+          case ConstraintType_SmallerOrEqual:
+            op = "<=";
+            break;
+          
+          case ConstraintType_GreaterOrEqual:
+            op = ">=";
+            break;
+          
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        std::string parameter = formatter.GenerateParameter(constraint.GetSingleValue());
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = tag + ".value " + op + " " + parameter;
+        }
+        else
+        {
+          comparison = "lower(" + tag + ".value) " + op + " lower(" + parameter + ")";
+        }
+
+        break;
+      }
+
+      case ConstraintType_List:
+      {
+        for (size_t i = 0; i < constraint.GetValuesCount(); i++)
+        {
+          if (!comparison.empty())
+          {
+            comparison += ", ";
+          }
+            
+          std::string parameter = formatter.GenerateParameter(constraint.GetValue(i));
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison += parameter;
+          }
+          else
+          {
+            comparison += "lower(" + parameter + ")";
+          }
+        }
+
+        if (constraint.IsCaseSensitive())
+        {
+          comparison = tag + ".value IN (" + comparison + ")";
+        }
+        else
+        {
+          comparison = "lower(" +  tag + ".value) IN (" + comparison + ")";
+        }
+            
+        break;
+      }
+
+      case ConstraintType_Wildcard:
+      {
+        const std::string value = constraint.GetSingleValue();
+
+        if (value == "*")
+        {
+          if (!constraint.IsMandatory())
+          {
+            // Universal constraint on an optional tag, ignore it
+            return false;
+          }
+        }
+        else
+        {
+          std::string escaped;
+          escaped.reserve(value.size());
+
+          for (size_t i = 0; i < value.size(); i++)
+          {
+            if (value[i] == '*')
+            {
+              escaped += "%";
+            }
+            else if (value[i] == '?')
+            {
+              escaped += "_";
+            }
+            else if (value[i] == '%')
+            {
+              escaped += "\\%";
+            }
+            else if (value[i] == '_')
+            {
+              escaped += "\\_";
+            }
+            else if (value[i] == '\\')
+            {
+              escaped += "\\\\";
+            }
+            else
+            {
+              escaped += value[i];
+            }               
+          }
+
+          std::string parameter = formatter.GenerateParameter(escaped);
+
+          if (constraint.IsCaseSensitive())
+          {
+            comparison = (tag + ".value LIKE " + parameter + " " +
+                          formatter.FormatWildcardEscape());
+          }
+          else
+          {
+            comparison = ("lower(" + tag + ".value) LIKE lower(" +
+                          parameter + ") " + formatter.FormatWildcardEscape());
+          }
+        }
+          
+        break;
+      }
+
+      default:
+        return false;
+    }
+
+    if (constraint.IsMandatory())
+    {
+      target = comparison;
+    }
+    else if (comparison.empty())
+    {
+      target = tag + ".value IS NULL";
+    }
+    else
+    {
+      target = tag + ".value IS NULL OR " + comparison;
+    }
+
+    return true;
+  }
+
+
+  static void FormatJoin(std::string& target,
+                         const DatabaseConstraint& constraint,
+                         size_t index)
+  {
+    std::string tag = "t" + boost::lexical_cast<std::string>(index);
+
+    if (constraint.IsMandatory())
+    {
+      target = " INNER JOIN ";
+    }
+    else
+    {
+      target = " LEFT JOIN ";
+    }
+
+    if (constraint.IsIdentifier())
+    {
+      target += "DicomIdentifiers ";
+    }
+    else
+    {
+      target += "MainDicomTags ";
+    }
+
+    target += (tag + " ON " + tag + ".id = " + FormatLevel(constraint.GetLevel()) +
+               ".internalId AND " + tag + ".tagGroup = " +
+               boost::lexical_cast<std::string>(constraint.GetTag().GetGroup()) +
+               " AND " + tag + ".tagElement = " +
+               boost::lexical_cast<std::string>(constraint.GetTag().GetElement()));
+  }
+  
+
+  void ISqlLookupFormatter::Apply(std::string& sql,
+                                  ISqlLookupFormatter& formatter,
+                                  const std::vector<DatabaseConstraint>& lookup,
+                                  ResourceType queryLevel,
+                                  size_t limit)
+  {
+    assert(ResourceType_Patient < ResourceType_Study &&
+           ResourceType_Study < ResourceType_Series &&
+           ResourceType_Series < ResourceType_Instance);
+    
+    ResourceType upperLevel = queryLevel;
+    ResourceType lowerLevel = queryLevel;
+
+    for (size_t i = 0; i < lookup.size(); i++)
+    {
+      ResourceType level = lookup[i].GetLevel();
+
+      if (level < upperLevel)
+      {
+        upperLevel = level;
+      }
+
+      if (level > lowerLevel)
+      {
+        lowerLevel = level;
+      }
+    }
+    
+    assert(upperLevel <= queryLevel &&
+           queryLevel <= lowerLevel);
+
+    std::string joins, comparisons;
+
+    size_t count = 0;
+    
+    for (size_t i = 0; i < lookup.size(); i++)
+    {
+      std::string comparison;
+      
+      if (FormatComparison(comparison, formatter, lookup[i], count))
+      {
+        std::string join;
+        FormatJoin(join, lookup[i], count);
+        joins += join;
+
+        if (!comparison.empty())
+        {
+          comparisons += " AND " + comparison;
+        }
+        
+        count ++;
+      }
+    }
+
+    sql = ("SELECT " +
+           FormatLevel(queryLevel) + ".publicId, " +
+           FormatLevel(queryLevel) + ".internalId" +
+           " FROM Resources AS " + FormatLevel(queryLevel));
+
+    for (int level = queryLevel - 1; level >= upperLevel; level--)
+    {
+      sql += (" INNER JOIN Resources " +
+              FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+              FormatLevel(static_cast<ResourceType>(level)) + ".internalId=" +
+              FormatLevel(static_cast<ResourceType>(level + 1)) + ".parentId");
+    }
+      
+    for (int level = queryLevel + 1; level <= lowerLevel; level++)
+    {
+      sql += (" INNER JOIN Resources " +
+              FormatLevel(static_cast<ResourceType>(level)) + " ON " +
+              FormatLevel(static_cast<ResourceType>(level - 1)) + ".internalId=" +
+              FormatLevel(static_cast<ResourceType>(level)) + ".parentId");
+    }
+      
+    sql += (joins + " WHERE " + FormatLevel(queryLevel) + ".resourceType = " +
+            formatter.FormatResourceType(queryLevel) + comparisons);
+
+    if (limit != 0)
+    {
+      sql += " LIMIT " + boost::lexical_cast<std::string>(limit);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/Search/ISqlLookupFormatter.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Enumerations.h"
+
+#include <boost/noncopyable.hpp>
+#include <vector>
+
+namespace Orthanc
+{
+  class DatabaseConstraint;
+  
+  // This class is also used by the "orthanc-databases" project
+  class ISqlLookupFormatter : public boost::noncopyable
+  {
+  public:
+    virtual ~ISqlLookupFormatter()
+    {
+    }
+
+    virtual std::string GenerateParameter(const std::string& value) = 0;
+
+    virtual std::string FormatResourceType(ResourceType level) = 0;
+
+    virtual std::string FormatWildcardEscape() = 0;
+
+    static void Apply(std::string& sql,
+                      ISqlLookupFormatter& formatter,
+                      const std::vector<DatabaseConstraint>& lookup,
+                      ResourceType queryLevel,
+                      size_t limit);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1373 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ServerContext.h"
+
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../Core/Cache/SharedArchive.h"
+#include "../Core/DicomParsing/DcmtkTranscoder.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/FileStorage/StorageAccessor.h"
+#include "../Core/HttpServer/FilesystemHttpSender.h"
+#include "../Core/HttpServer/HttpStreamTranscoder.h"
+#include "../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../Core/Logging.h"
+#include "../Core/MetricsRegistry.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+
+#include "OrthancConfiguration.h"
+#include "OrthancRestApi/OrthancRestApi.h"
+#include "Search/DatabaseLookup.h"
+#include "ServerJobs/OrthancJobUnserializer.h"
+#include "ServerToolbox.h"
+#include "StorageCommitmentReports.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+
+
+
+#define ENABLE_DICOM_CACHE  1
+
+static const size_t DICOM_CACHE_SIZE = 2;
+
+/**
+ * IMPORTANT: We make the assumption that the same instance of
+ * FileStorage can be accessed from multiple threads. This seems OK
+ * since the filesystem implements the required locking mechanisms,
+ * but maybe a read-writer lock on the "FileStorage" could be
+ * useful. Conversely, "ServerIndex" already implements mutex-based
+ * locking.
+ **/
+
+namespace Orthanc
+{
+  void ServerContext::ChangeThread(ServerContext* that,
+                                   unsigned int sleepDelay)
+  {
+    while (!that->done_)
+    {
+      std::unique_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay));
+        
+      if (obj.get() != NULL)
+      {
+        const ServerIndexChange& change = dynamic_cast<const ServerIndexChange&>(*obj.get());
+
+        boost::shared_lock<boost::shared_mutex> lock(that->listenersMutex_);
+        for (ServerListeners::iterator it = that->listeners_.begin(); 
+             it != that->listeners_.end(); ++it)
+        {
+          try
+          {
+            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()
+                       << " (code " << e.GetErrorCode() << ")";
+          }
+        }
+      }
+    }
+  }
+
+
+  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);
+    plugins_->SignalJobSubmitted(jobId);
+  }
+  
+
+  void ServerContext::SignalJobSuccess(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    mainLua_.SignalJobSuccess(jobId);
+    plugins_->SignalJobSuccess(jobId);
+  }
+
+  
+  void ServerContext::SignalJobFailure(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    mainLua_.SignalJobFailure(jobId);
+    plugins_->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(WARNING) << "Cannot unserialize the jobs engine, starting anyway: " << e.What();
+        }
+      }
+      else
+      {
+        LOG(INFO) << "The last execution of Orthanc has archived no job";
+      }
+    }
+    else
+    {
+      LOG(INFO) << "Not reloading the jobs from the last execution of Orthanc";
+    }
+
+    jobsEngine_.GetRegistry().SetObserver(*this);
+    jobsEngine_.Start();
+    isJobsEngineUnserialized_ = true;
+
+    saveJobsThread_ = boost::thread(SaveJobsThread, this, (unitTesting ? 20 : 100));
+  }
+
+
+  void ServerContext::SaveJobsEngine()
+  {
+    if (saveJobs_)
+    {
+      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,
+                               bool unitTesting,
+                               size_t maxCompletedJobs) :
+    index_(*this, database, (unitTesting ? 20 : 500)),
+    area_(area),
+    compressionEnabled_(false),
+    storeMD5_(true),
+    provider_(*this),
+    dicomCache_(provider_, DICOM_CACHE_SIZE),
+    mainLua_(*this),
+    filterLua_(*this),
+    luaListener_(*this),
+    jobsEngine_(maxCompletedJobs),
+#if ORTHANC_ENABLE_PLUGINS == 1
+    plugins_(NULL),
+#endif
+    done_(false),
+    haveJobsChanged_(false),
+    isJobsEngineUnserialized_(false),
+    metricsRegistry_(new MetricsRegistry),
+    isHttpServerSecure_(true),
+    isExecuteLuaEnabled_(false),
+    overwriteInstances_(false),
+    dcmtkTranscoder_(new DcmtkTranscoder),
+    isIngestTranscoding_(false)
+  {
+    try
+    {
+      unsigned int lossyQuality;
+
+      {
+        OrthancConfiguration::ReaderLock lock;
+
+        queryRetrieveArchive_.reset(
+          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("QueryRetrieveSize", 100)));
+        mediaArchive_.reset(
+          new SharedArchive(lock.GetConfiguration().GetUnsignedIntegerParameter("MediaArchiveSize", 1)));
+        defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC");
+        jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2));
+        saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true);
+        metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true));
+
+        // New configuration options in Orthanc 1.5.1
+        findStorageAccessMode_ = StringToFindStorageAccessMode(lock.GetConfiguration().GetStringParameter("StorageAccessOnFind", "Always"));
+        limitFindInstances_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0);
+        limitFindResults_ = lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0);
+
+        // New configuration option in Orthanc 1.6.0
+        storageCommitmentReports_.reset(new StorageCommitmentReports(lock.GetConfiguration().GetUnsignedIntegerParameter("StorageCommitmentReportsSize", 100)));
+
+        // New options in Orthanc 1.7.0
+        transcodeDicomProtocol_ = lock.GetConfiguration().GetBooleanParameter("TranscodeDicomProtocol", true);
+        builtinDecoderTranscoderOrder_ = StringToBuiltinDecoderTranscoderOrder(lock.GetConfiguration().GetStringParameter("BuiltinDecoderTranscoderOrder", "After"));
+        lossyQuality = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomLossyTranscodingQuality", 90);
+
+        std::string s;
+        if (lock.GetConfiguration().LookupStringParameter(s, "IngestTranscoding"))
+        {
+          if (LookupTransferSyntax(ingestTransferSyntax_, s))
+          {
+            isIngestTranscoding_ = true;
+            LOG(WARNING) << "Incoming DICOM instances will automatically be transcoded to "
+                         << "transfer syntax: " << GetTransferSyntaxUid(ingestTransferSyntax_);
+          }
+          else
+          {
+            throw OrthancException(ErrorCode_ParameterOutOfRange,
+                                   "Unknown transfer syntax for ingest transcoding: " + s);
+          }
+        }
+        else
+        {
+          isIngestTranscoding_ = false;
+          LOG(INFO) << "Automated transcoding of incoming DICOM instances is disabled";
+        }
+      }
+
+      jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
+
+      listeners_.push_back(ServerListener(luaListener_, "Lua"));
+      changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
+    
+      dynamic_cast<DcmtkTranscoder&>(*dcmtkTranscoder_).SetLossyQuality(lossyQuality);
+    }
+    catch (OrthancException&)
+    {
+      Stop();
+      throw;
+    }
+  }
+
+
+  
+  ServerContext::~ServerContext()
+  {
+    if (!done_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ServerContext::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+
+  void ServerContext::Stop()
+  {
+    if (!done_)
+    {
+      {
+        boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
+        listeners_.clear();
+      }
+
+      done_ = true;
+
+      if (changeThread_.joinable())
+      {
+        changeThread_.join();
+      }
+
+      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!
+      jobsEngine_.Stop();
+      index_.Stop();
+    }
+  }
+
+
+  void ServerContext::SetCompressionEnabled(bool enabled)
+  {
+    if (enabled)
+      LOG(WARNING) << "Disk compression is enabled";
+    else
+      LOG(WARNING) << "Disk compression is disabled";
+
+    compressionEnabled_ = enabled;
+  }
+
+
+  void ServerContext::RemoveFile(const std::string& fileUuid,
+                                 FileContentType type)
+  {
+    StorageAccessor accessor(area_, GetMetricsRegistry());
+    accessor.Remove(fileUuid, type);
+  }
+
+
+  StoreStatus ServerContext::StoreAfterTranscoding(std::string& resultPublicId,
+                                                   DicomInstanceToStore& dicom,
+                                                   StoreInstanceMode mode)
+  {
+    bool overwrite;
+    switch (mode)
+    {
+      case StoreInstanceMode_Default:
+        overwrite = overwriteInstances_;
+        break;
+        
+      case StoreInstanceMode_OverwriteDuplicate:
+        overwrite = true;
+        break;
+        
+      case StoreInstanceMode_IgnoreDuplicate:
+        overwrite = false;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }    
+    
+    try
+    {
+      MetricsRegistry::Timer timer(GetMetricsRegistry(), "orthanc_store_dicom_duration_ms");
+      StorageAccessor accessor(area_, GetMetricsRegistry());
+
+      resultPublicId = dicom.GetHasher().HashInstance();
+
+      Json::Value simplifiedTags;
+      ServerToolbox::SimplifyTags(simplifiedTags, dicom.GetJson(), DicomToJsonFormat_Human);
+
+      // Test if the instance must be filtered out
+      bool accepted = true;
+
+      {
+        boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
+
+        for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
+        {
+          try
+          {
+            if (!it->GetListener().FilterIncomingInstance(dicom, simplifiedTags))
+            {
+              accepted = false;
+              break;
+            }
+          }
+          catch (OrthancException& e)
+          {
+            LOG(ERROR) << "Error in the " << it->GetDescription() 
+                       << " callback while receiving an instance: " << e.What()
+                       << " (code " << e.GetErrorCode() << ")";
+            throw;
+          }
+        }
+      }
+
+      if (!accepted)
+      {
+        LOG(INFO) << "An incoming instance has been discarded by the filter";
+        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);
+
+      FileInfo dicomInfo = accessor.Write(dicom.GetBufferData(), dicom.GetBufferSize(), 
+                                          FileContentType_Dicom, compression, storeMD5_);
+      FileInfo jsonInfo = accessor.Write(dicom.GetJson().toStyledString(), 
+                                         FileContentType_DicomAsJson, compression, storeMD5_);
+
+      ServerIndex::Attachments attachments;
+      attachments.push_back(dicomInfo);
+      attachments.push_back(jsonInfo);
+
+      typedef std::map<MetadataType, std::string>  InstanceMetadata;
+      InstanceMetadata  instanceMetadata;
+      StoreStatus status = index_.Store(
+        instanceMetadata, dicom, attachments, overwrite);
+
+      // Only keep the metadata for the "instance" level
+      dicom.GetMetadata().clear();
+
+      for (InstanceMetadata::const_iterator it = instanceMetadata.begin();
+           it != instanceMetadata.end(); ++it)
+      {
+        dicom.GetMetadata().insert(std::make_pair(std::make_pair(ResourceType_Instance, it->first),
+                                                  it->second));
+      }
+            
+      if (status != StoreStatus_Success)
+      {
+        accessor.Remove(dicomInfo);
+        accessor.Remove(jsonInfo);
+      }
+
+      switch (status)
+      {
+        case StoreStatus_Success:
+          LOG(INFO) << "New instance stored";
+          break;
+
+        case StoreStatus_AlreadyStored:
+          LOG(INFO) << "Already stored";
+          break;
+
+        case StoreStatus_Failure:
+          LOG(ERROR) << "Store failure";
+          break;
+
+        default:
+          // This should never happen
+          break;
+      }
+
+      if (status == StoreStatus_Success ||
+          status == StoreStatus_AlreadyStored)
+      {
+        boost::shared_lock<boost::shared_mutex> lock(listenersMutex_);
+
+        for (ServerListeners::iterator it = listeners_.begin(); it != listeners_.end(); ++it)
+        {
+          try
+          {
+            it->GetListener().SignalStoredInstance(resultPublicId, dicom, simplifiedTags);
+          }
+          catch (OrthancException& e)
+          {
+            LOG(ERROR) << "Error in the " << it->GetDescription() 
+                       << " callback while receiving an instance: " << e.What()
+                       << " (code " << e.GetErrorCode() << ")";
+          }
+        }
+      }
+
+      return status;
+    }
+    catch (OrthancException& e)
+    {
+      if (e.GetErrorCode() == ErrorCode_InexistentTag)
+      {
+        dicom.GetSummary().LogMissingTagsForStore();
+      }
+
+      throw;
+    }
+  }
+
+
+  StoreStatus ServerContext::Store(std::string& resultPublicId,
+                                   DicomInstanceToStore& dicom,
+                                   StoreInstanceMode mode)
+  {
+    if (!isIngestTranscoding_)
+    {
+      // No automated transcoding. This was the only path in Orthanc <= 1.6.1.
+      return StoreAfterTranscoding(resultPublicId, dicom, mode);
+    }
+    else
+    {
+      // Automated transcoding of incoming DICOM files
+
+      DicomTransferSyntax sourceSyntax;
+      if (!FromDcmtkBridge::LookupOrthancTransferSyntax(
+            sourceSyntax, dicom.GetParsedDicomFile().GetDcmtkObject()) ||
+          sourceSyntax == ingestTransferSyntax_)
+      {
+        // No transcoding
+        return StoreAfterTranscoding(resultPublicId, dicom, mode);
+      }
+      else
+      {      
+        std::set<DicomTransferSyntax> syntaxes;
+        syntaxes.insert(ingestTransferSyntax_);
+
+        IDicomTranscoder::DicomImage source;
+        source.SetExternalBuffer(dicom.GetBufferData(), dicom.GetBufferSize());
+
+        IDicomTranscoder::DicomImage transcoded;
+        if (Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+        {
+          std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
+
+          DicomInstanceToStore toStore;
+          toStore.SetParsedDicomFile(*tmp);
+          toStore.SetOrigin(dicom.GetOrigin());
+
+          StoreStatus ok = StoreAfterTranscoding(resultPublicId, toStore, mode);
+          assert(resultPublicId == tmp->GetHasher().HashInstance());
+
+          return ok;
+        }
+        else
+        {
+          // Cannot transcode => store the original file
+          return StoreAfterTranscoding(resultPublicId, dicom, mode);
+        }
+      }
+    }
+  }
+
+  
+  void ServerContext::AnswerAttachment(RestApiOutput& output,
+                                       const std::string& resourceId,
+                                       FileContentType content)
+  {
+    FileInfo attachment;
+    if (!index_.LookupAttachment(attachment, resourceId, content))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    StorageAccessor accessor(area_, GetMetricsRegistry());
+    accessor.AnswerFile(output, attachment, GetFileContentMime(content));
+  }
+
+
+  void ServerContext::ChangeAttachmentCompression(const std::string& resourceId,
+                                                  FileContentType attachmentType,
+                                                  CompressionType compression)
+  {
+    LOG(INFO) << "Changing compression type for attachment "
+              << EnumerationToString(attachmentType) 
+              << " of resource " << resourceId << " to " 
+              << compression; 
+
+    FileInfo attachment;
+    if (!index_.LookupAttachment(attachment, resourceId, attachmentType))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (attachment.GetCompressionType() == compression)
+    {
+      // Nothing to do
+      return;
+    }
+
+    std::string content;
+
+    StorageAccessor accessor(area_, GetMetricsRegistry());
+    accessor.Read(content, attachment);
+
+    FileInfo modified = accessor.Write(content.empty() ? NULL : content.c_str(),
+                                       content.size(), attachmentType, compression, storeMD5_);
+
+    try
+    {
+      StoreStatus status = index_.AddAttachment(modified, resourceId);
+      if (status != StoreStatus_Success)
+      {
+        accessor.Remove(modified);
+        throw OrthancException(ErrorCode_Database);
+      }
+    }
+    catch (OrthancException&)
+    {
+      accessor.Remove(modified);
+      throw;
+    }    
+  }
+
+
+  void ServerContext::ReadDicomAsJsonInternal(std::string& result,
+                                              const std::string& instancePublicId)
+  {
+    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);
+
+      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()))
+      {
+        throw OrthancException(ErrorCode_InternalError,
+                               "Cannot associate the DICOM-as-JSON summary to instance: " + instancePublicId);
+      }
+    }
+  }
+
+
+  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))
+    {
+      throw OrthancException(ErrorCode_InternalError,
+                             "Unable to read attachment " + EnumerationToString(content) +
+                             " of instance " + instancePublicId);
+    }
+
+    assert(attachment.GetContentType() == content);
+
+    if (uncompressIfNeeded)
+    {
+      ReadAttachment(result, attachment);
+    }
+    else
+    {
+      // Do not uncompress the content of the storage area, return the
+      // raw data
+      StorageAccessor accessor(area_, GetMetricsRegistry());
+      accessor.ReadRaw(result, attachment);
+    }
+  }
+
+
+  void ServerContext::ReadAttachment(std::string& result,
+                                     const FileInfo& attachment)
+  {
+    // This will decompress the attachment
+    StorageAccessor accessor(area_, GetMetricsRegistry());
+    accessor.Read(result, attachment);
+  }
+
+
+  IDynamicObject* ServerContext::DicomCacheProvider::Provide(const std::string& instancePublicId)
+  {
+    std::string content;
+    context_.ReadDicom(content, instancePublicId);
+    return new ParsedDicomFile(content);
+  }
+
+
+  ServerContext::DicomCacheLocker::DicomCacheLocker(ServerContext& that,
+                                                    const std::string& instancePublicId) : 
+    that_(that),
+    lock_(that_.dicomCacheMutex_)
+  {
+#if ENABLE_DICOM_CACHE == 0
+    static std::unique_ptr<IDynamicObject> p;
+    p.reset(that_.provider_.Provide(instancePublicId));
+    dicom_ = dynamic_cast<ParsedDicomFile*>(p.get());
+#else
+    dicom_ = &dynamic_cast<ParsedDicomFile&>(that_.dicomCache_.Access(instancePublicId));
+#endif
+  }
+
+
+  ServerContext::DicomCacheLocker::~DicomCacheLocker()
+  {
+  }
+
+
+  void ServerContext::SetStoreMD5ForAttachments(bool storeMD5)
+  {
+    LOG(INFO) << "Storing MD5 for attachments: " << (storeMD5 ? "yes" : "no");
+    storeMD5_ = storeMD5;
+  }
+
+
+  bool ServerContext::AddAttachment(const std::string& resourceId,
+                                    FileContentType attachmentType,
+                                    const void* data,
+                                    size_t size)
+  {
+    LOG(INFO) << "Adding attachment " << EnumerationToString(attachmentType) << " to resource " << resourceId;
+    
+    // TODO Should we use "gzip" instead?
+    CompressionType compression = (compressionEnabled_ ? CompressionType_ZlibWithSize : CompressionType_None);
+
+    StorageAccessor accessor(area_, GetMetricsRegistry());
+    FileInfo attachment = accessor.Write(data, size, attachmentType, compression, storeMD5_);
+
+    StoreStatus status = index_.AddAttachment(attachment, resourceId);
+    if (status != StoreStatus_Success)
+    {
+      accessor.Remove(attachment);
+      return false;
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+
+  bool ServerContext::DeleteResource(Json::Value& target,
+                                     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);
+  }
+
+
+  void ServerContext::SignalChange(const ServerIndexChange& change)
+  {
+    pendingChanges_.Enqueue(change.Clone());
+  }
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  void ServerContext::SetPlugins(OrthancPlugins& plugins)
+  {
+    boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
+
+    plugins_ = &plugins;
+
+    // TODO REFACTOR THIS
+    listeners_.clear();
+    listeners_.push_back(ServerListener(luaListener_, "Lua"));
+    listeners_.push_back(ServerListener(plugins, "plugin"));
+  }
+
+
+  void ServerContext::ResetPlugins()
+  {
+    boost::unique_lock<boost::shared_mutex> lock(listenersMutex_);
+
+    plugins_ = NULL;
+
+    // TODO REFACTOR THIS
+    listeners_.clear();
+    listeners_.push_back(ServerListener(luaListener_, "Lua"));
+  }
+
+
+  const OrthancPlugins& ServerContext::GetPlugins() const
+  {
+    if (HasPlugins())
+    {
+      return *plugins_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  OrthancPlugins& ServerContext::GetPlugins()
+  {
+    if (HasPlugins())
+    {
+      return *plugins_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+#endif
+
+
+  bool ServerContext::HasPlugins() const
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    return (plugins_ != NULL);
+#else
+    return false;
+#endif
+  }
+
+
+  void ServerContext::Apply(ILookupVisitor& visitor,
+                            const DatabaseLookup& lookup,
+                            ResourceType queryLevel,
+                            size_t since,
+                            size_t limit)
+  {
+    unsigned int databaseLimit = (queryLevel == ResourceType_Instance ?
+                                  limitFindInstances_ : limitFindResults_);
+      
+    std::vector<std::string> resources, instances;
+
+    {
+      const size_t lookupLimit = (databaseLimit == 0 ? 0 : databaseLimit + 1);      
+      GetIndex().ApplyLookupResources(resources, &instances, lookup, queryLevel, lookupLimit);
+    }
+
+    bool complete = (databaseLimit == 0 ||
+                     resources.size() <= databaseLimit);
+
+    LOG(INFO) << "Number of candidate resources after fast DB filtering on main DICOM tags: " << resources.size();
+
+    /**
+     * "resources" contains the Orthanc ID of the resource at level
+     * "queryLevel", "instances" contains one the Orthanc ID of one
+     * sample instance from this resource.
+     **/
+    assert(resources.size() == instances.size());
+
+    size_t countResults = 0;
+    size_t skipped = 0;
+
+    const bool isDicomAsJsonNeeded = visitor.IsDicomAsJsonNeeded();
+    
+    for (size_t i = 0; i < instances.size(); i++)
+    {
+      // Optimization in Orthanc 1.5.1 - Don't read the full JSON from
+      // the disk if only "main DICOM tags" are to be returned
+
+      std::unique_ptr<Json::Value> dicomAsJson;
+
+      bool hasOnlyMainDicomTags;
+      DicomMap dicom;
+      
+      if (findStorageAccessMode_ == FindStorageAccessMode_DatabaseOnly ||
+          findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer ||
+          lookup.HasOnlyMainDicomTags())
+      {
+        // Case (1): The main DICOM tags, as stored in the database,
+        // are sufficient to look for match
+
+        DicomMap tmp;
+        if (!GetIndex().GetAllMainDicomTags(tmp, instances[i]))
+        {
+          // The instance has been removed during the execution of the
+          // lookup, ignore it
+          continue;
+        }
+
+#if 1
+        // New in Orthanc 1.6.0: Only keep the main DICOM tags at the
+        // level of interest for the query
+        switch (queryLevel)
+        {
+          // WARNING: Don't reorder cases below, and don't add "break"
+          case ResourceType_Instance:
+            dicom.MergeMainDicomTags(tmp, ResourceType_Instance);
+
+          case ResourceType_Series:
+            dicom.MergeMainDicomTags(tmp, ResourceType_Series);
+
+          case ResourceType_Study:
+            dicom.MergeMainDicomTags(tmp, ResourceType_Study);
+            
+          case ResourceType_Patient:
+            dicom.MergeMainDicomTags(tmp, ResourceType_Patient);
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+
+        // Special case of the "Modality" at the study level, in order
+        // to deal with C-FIND on "ModalitiesInStudy" (0008,0061).
+        // Check out integration test "test_rest_modalities_in_study".
+        if (queryLevel == ResourceType_Study)
+        {
+          dicom.CopyTagIfExists(tmp, DICOM_TAG_MODALITY);
+        }
+#else
+        dicom.Assign(tmp);  // This emulates Orthanc <= 1.5.8
+#endif
+        
+        hasOnlyMainDicomTags = true;
+      }
+      else
+      {
+        // Case (2): Need to read the "DICOM-as-JSON" attachment from
+        // the storage area
+        dicomAsJson.reset(new Json::Value);
+        ReadDicomAsJson(*dicomAsJson, instances[i]);
+
+        dicom.FromDicomAsJson(*dicomAsJson);
+
+        // This map contains the entire JSON, i.e. more than the main DICOM tags
+        hasOnlyMainDicomTags = false;   
+      }
+      
+      if (lookup.IsMatch(dicom))
+      {
+        if (skipped < since)
+        {
+          skipped++;
+        }
+        else if (limit != 0 &&
+                 countResults >= limit)
+        {
+          // Too many results, don't mark as complete
+          complete = false;
+          break;
+        }
+        else
+        {
+          if ((findStorageAccessMode_ == FindStorageAccessMode_DiskOnLookupAndAnswer ||
+               findStorageAccessMode_ == FindStorageAccessMode_DiskOnAnswer) &&
+              dicomAsJson.get() == NULL &&
+              isDicomAsJsonNeeded)
+          {
+            dicomAsJson.reset(new Json::Value);
+            ReadDicomAsJson(*dicomAsJson, instances[i]);
+          }
+
+          if (hasOnlyMainDicomTags)
+          {
+            // This is Case (1): The variable "dicom" only contains the main DICOM tags
+            visitor.Visit(resources[i], instances[i], dicom, dicomAsJson.get());
+          }
+          else
+          {
+            // Remove the non-main DICOM tags from "dicom" if Case (2)
+            // was used, for consistency with Case (1)
+
+            DicomMap mainDicomTags;
+            mainDicomTags.ExtractMainDicomTags(dicom);
+            visitor.Visit(resources[i], instances[i], mainDicomTags, dicomAsJson.get());            
+          }
+            
+          countResults ++;
+        }
+      }
+    }
+
+    if (complete)
+    {
+      visitor.MarkAsComplete();
+    }
+
+    LOG(INFO) << "Number of matching resources: " << countResults;
+  }
+
+
+  bool ServerContext::LookupOrReconstructMetadata(std::string& target,
+                                                  const std::string& publicId,
+                                                  MetadataType metadata)
+  {
+    // This is a backwards-compatibility function, that can
+    // reconstruct metadata that were not generated by an older
+    // release of Orthanc
+
+    if (metadata == MetadataType_Instance_SopClassUid ||
+        metadata == MetadataType_Instance_TransferSyntax)
+    {
+      if (index_.LookupMetadata(target, publicId, metadata))
+      {
+        return true;
+      }
+      else
+      {
+        // These metadata are mandatory in DICOM instances, and were
+        // introduced in Orthanc 1.2.0. The fact that
+        // "LookupMetadata()" has failed indicates that this database
+        // comes from an older release of Orthanc.
+        
+        DicomTag tag(0, 0);
+      
+        switch (metadata)
+        {
+          case MetadataType_Instance_SopClassUid:
+            tag = DICOM_TAG_SOP_CLASS_UID;
+            break;
+
+          case MetadataType_Instance_TransferSyntax:
+            tag = DICOM_TAG_TRANSFER_SYNTAX_UID;
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      
+        Json::Value dicomAsJson;
+        ReadDicomAsJson(dicomAsJson, publicId);
+
+        DicomMap tags;
+        tags.FromDicomAsJson(dicomAsJson);
+
+        const DicomValue* value = tags.TestAndGetValue(tag);
+
+        if (value != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          target = value->GetContent();
+
+          // Store for reuse
+          index_.SetMetadata(publicId, metadata, target);
+          return true;
+        }
+        else
+        {
+          // Should never happen
+          return false;
+        }
+      }
+    }
+    else
+    {
+      // No backward
+      return index_.LookupMetadata(target, publicId, metadata);
+    }
+  }
+
+
+  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);
+    }
+  }
+
+
+  void ServerContext::SignalUpdatedModalities()
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins())
+    {
+      GetPlugins().SignalUpdatedModalities();
+    }
+#endif
+  }
+
+   
+  void ServerContext::SignalUpdatedPeers()
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins())
+    {
+      GetPlugins().SignalUpdatedPeers();
+    }
+#endif
+  }
+
+
+  IStorageCommitmentFactory::ILookupHandler*
+  ServerContext::CreateStorageCommitment(const std::string& jobId,
+                                         const std::string& transactionUid,
+                                         const std::vector<std::string>& sopClassUids,
+                                         const std::vector<std::string>& sopInstanceUids,
+                                         const std::string& remoteAet,
+                                         const std::string& calledAet)
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins())
+    {
+      return GetPlugins().CreateStorageCommitment(
+        jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet);
+    }
+#endif
+
+    return NULL;
+  }
+
+
+  ImageAccessor* ServerContext::DecodeDicomFrame(const std::string& publicId,
+                                                 unsigned int frameIndex)
+  {
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      // Use Orthanc's built-in decoder, using the cache to speed-up
+      // things on multi-frame images
+      ServerContext::DicomCacheLocker locker(*this, publicId);        
+      std::unique_ptr<ImageAccessor> decoded(
+        DicomImageDecoder::Decode(locker.GetDicom(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+    }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins() &&
+        GetPlugins().HasCustomImageDecoder())
+    {
+      // TODO: Store the raw buffer in the DicomCacheLocker
+      std::string dicomContent;
+      ReadDicom(dicomContent, publicId);
+      std::unique_ptr<ImageAccessor> decoded(
+        GetPlugins().Decode(dicomContent.c_str(), dicomContent.size(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+      {
+        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
+                  << "fallback to the built-in DCMTK decoder";
+      }
+    }
+#endif
+
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    {
+      ServerContext::DicomCacheLocker locker(*this, publicId);        
+      return DicomImageDecoder::Decode(locker.GetDicom(), frameIndex);
+    }
+    else
+    {
+      return NULL;  // Built-in decoder is disabled
+    }
+  }
+
+
+  ImageAccessor* ServerContext::DecodeDicomFrame(const DicomInstanceToStore& dicom,
+                                                 unsigned int frameIndex)
+  {
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      std::unique_ptr<ImageAccessor> decoded(
+        DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+    }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins() &&
+        GetPlugins().HasCustomImageDecoder())
+    {
+      std::unique_ptr<ImageAccessor> decoded(
+        GetPlugins().Decode(dicom.GetBufferData(), dicom.GetBufferSize(), frameIndex));
+      if (decoded.get() != NULL)
+      {
+        return decoded.release();
+      }
+      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+      {
+        LOG(INFO) << "The installed image decoding plugins cannot handle an image, "
+                  << "fallback to the built-in DCMTK decoder";
+      }
+    }
+#endif
+
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    {
+      return DicomImageDecoder::Decode(dicom.GetParsedDicomFile(), frameIndex);
+    }
+    else
+    {
+      return NULL;
+    }
+  }
+
+
+  ImageAccessor* ServerContext::DecodeDicomFrame(const void* dicom,
+                                                 size_t size,
+                                                 unsigned int frameIndex)
+  {
+    DicomInstanceToStore instance;
+    instance.SetBuffer(dicom, size);
+    return DecodeDicomFrame(instance, frameIndex);
+  }
+  
+
+  void ServerContext::StoreWithTranscoding(std::string& sopClassUid,
+                                           std::string& sopInstanceUid,
+                                           DicomStoreUserConnection& connection,
+                                           const std::string& dicom,
+                                           bool hasMoveOriginator,
+                                           const std::string& moveOriginatorAet,
+                                           uint16_t moveOriginatorId)
+  {
+    const void* data = dicom.empty() ? NULL : dicom.c_str();
+    
+    if (!transcodeDicomProtocol_ ||
+        !connection.GetParameters().GetRemoteModality().IsTranscodingAllowed())
+    {
+      connection.Store(sopClassUid, sopInstanceUid, data, dicom.size(),
+                       hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
+    }
+    else
+    {
+      connection.Transcode(sopClassUid, sopInstanceUid, *this, data, dicom.size(),
+                           hasMoveOriginator, moveOriginatorAet, moveOriginatorId);
+    }
+  }
+
+
+  bool ServerContext::Transcode(DicomImage& target,
+                                DicomImage& source /* in, "GetParsed()" possibly modified */,
+                                const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                                bool allowNewSopInstanceUid)
+  {
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_Before)
+    {
+      if (dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      {
+        return true;
+      }
+    }
+      
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (HasPlugins() &&
+        GetPlugins().HasCustomTranscoder())
+    {
+      if (GetPlugins().Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid))
+      {
+        return true;
+      }
+      else if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+      {
+        LOG(INFO) << "The installed transcoding plugins cannot handle an image, "
+                  << "fallback to the built-in DCMTK transcoder";
+      }
+    }
+#endif
+
+    if (builtinDecoderTranscoderOrder_ == BuiltinDecoderTranscoderOrder_After)
+    {
+      return dcmtkTranscoder_->Transcode(target, source, allowedSyntaxes, allowNewSopInstanceUid);
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerContext.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,496 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "IServerListener.h"
+#include "LuaScripting.h"
+#include "OrthancHttpHandler.h"
+#include "ServerIndex.h"
+#include "ServerJobs/IStorageCommitmentFactory.h"
+
+#include "../Core/Cache/MemoryCache.h"
+#include "../Core/DicomParsing/IDicomTranscoder.h"
+
+
+namespace Orthanc
+{
+  class DicomInstanceToStore;
+  class IStorageArea;
+  class JobsEngine;
+  class MetricsRegistry;
+  class OrthancPlugins;
+  class ParsedDicomFile;
+  class RestApiOutput;
+  class SetOfInstancesJob;
+  class SharedArchive;
+  class SharedMessageQueue;
+  class StorageCommitmentReports;
+  
+  
+  /**
+   * This class is responsible for maintaining the storage area on the
+   * filesystem (including compression), as well as the index of the
+   * DICOM store. It implements the required locking mechanisms.
+   **/
+  class ServerContext :
+    public IStorageCommitmentFactory,
+    public IDicomTranscoder,
+    private JobsRegistry::IObserver
+  {
+  public:
+    class ILookupVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~ILookupVisitor()
+      {
+      }
+
+      virtual bool IsDicomAsJsonNeeded() const = 0;
+      
+      virtual void MarkAsComplete() = 0;
+
+      virtual void Visit(const std::string& publicId,
+                         const std::string& instanceId,
+                         const DicomMap& mainDicomTags,
+                         const Json::Value* dicomAsJson) = 0;
+    };
+    
+    
+  private:
+    class LuaServerListener : public IServerListener
+    {
+    private:
+      ServerContext& context_;
+
+    public:
+      LuaServerListener(ServerContext& context) :
+        context_(context)
+      {
+      }
+
+      virtual void SignalStoredInstance(const std::string& publicId,
+                                        const 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 Deprecated::ICachePageProvider  // TODO
+    {
+    private:
+      ServerContext& context_;
+
+    public:
+      DicomCacheProvider(ServerContext& context) : context_(context)
+      {
+      }
+      
+      virtual IDynamicObject* Provide(const std::string& id);
+    };
+
+    class ServerListener
+    {
+    private:
+      IServerListener *listener_;
+      std::string      description_;
+
+    public:
+      ServerListener(IServerListener& listener,
+                     const std::string& description) :
+        listener_(&listener),
+        description_(description)
+      {
+      }
+
+      IServerListener& GetListener()
+      {
+        return *listener_;
+      }
+
+      const std::string& GetDescription()
+      {
+        return description_;
+      }
+    };
+
+    typedef std::list<ServerListener>  ServerListeners;
+
+
+    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) ORTHANC_OVERRIDE;
+
+    virtual void SignalJobSuccess(const std::string& jobId) ORTHANC_OVERRIDE;
+
+    virtual void SignalJobFailure(const std::string& jobId) ORTHANC_OVERRIDE;
+
+    ServerIndex index_;
+    IStorageArea& area_;
+
+    bool compressionEnabled_;
+    bool storeMD5_;
+    
+    DicomCacheProvider provider_;
+    boost::mutex dicomCacheMutex_;
+    Deprecated::MemoryCache dicomCache_;  // TODO
+
+    LuaScripting mainLua_;
+    LuaScripting filterLua_;
+    LuaServerListener  luaListener_;
+    std::unique_ptr<SharedArchive>  mediaArchive_;
+    
+    // The "JobsEngine" must be *after* "LuaScripting", as
+    // "LuaScripting" embeds "LuaJobManager" that registers as an
+    // observer to "SequenceOfOperationsJob", whose lifetime
+    // corresponds to that of "JobsEngine". It must also be after
+    // "mediaArchive_", as jobs might access this archive.
+    JobsEngine jobsEngine_;
+    
+#if ORTHANC_ENABLE_PLUGINS == 1
+    OrthancPlugins* plugins_;
+#endif
+
+    ServerListeners listeners_;
+    boost::shared_mutex listenersMutex_;
+
+    bool done_;
+    bool haveJobsChanged_;
+    bool isJobsEngineUnserialized_;
+    SharedMessageQueue  pendingChanges_;
+    boost::thread  changeThread_;
+    boost::thread  saveJobsThread_;
+        
+    std::unique_ptr<SharedArchive>  queryRetrieveArchive_;
+    std::string defaultLocalAet_;
+    OrthancHttpHandler  httpHandler_;
+    bool saveJobs_;
+    FindStorageAccessMode findStorageAccessMode_;
+    unsigned int limitFindInstances_;
+    unsigned int limitFindResults_;
+
+    std::unique_ptr<MetricsRegistry>  metricsRegistry_;
+    bool isHttpServerSecure_;
+    bool isExecuteLuaEnabled_;
+    bool overwriteInstances_;
+
+    std::unique_ptr<StorageCommitmentReports>  storageCommitmentReports_;
+
+    bool transcodeDicomProtocol_;
+    std::unique_ptr<IDicomTranscoder>  dcmtkTranscoder_;
+    BuiltinDecoderTranscoderOrder builtinDecoderTranscoderOrder_;
+    bool isIngestTranscoding_;
+    DicomTransferSyntax ingestTransferSyntax_;
+
+    StoreStatus StoreAfterTranscoding(std::string& resultPublicId,
+                                      DicomInstanceToStore& dicom,
+                                      StoreInstanceMode mode);
+
+  public:
+    class DicomCacheLocker : public boost::noncopyable
+    {
+    private:
+      ServerContext& that_;
+      ParsedDicomFile *dicom_;
+      boost::mutex::scoped_lock lock_;
+
+    public:
+      DicomCacheLocker(ServerContext& that,
+                       const std::string& instancePublicId);
+
+      ~DicomCacheLocker();
+
+      ParsedDicomFile& GetDicom()
+      {
+        return *dicom_;
+      }
+    };
+
+    ServerContext(IDatabaseWrapper& database,
+                  IStorageArea& area,
+                  bool unitTesting,
+                  size_t maxCompletedJobs);
+
+    ~ServerContext();
+
+    void SetupJobsEngine(bool unitTesting,
+                         bool loadJobsFromDatabase);
+
+    ServerIndex& GetIndex()
+    {
+      return index_;
+    }
+
+    void SetCompressionEnabled(bool enabled);
+
+    bool IsCompressionEnabled() const
+    {
+      return compressionEnabled_;
+    }
+
+    void RemoveFile(const std::string& fileUuid,
+                    FileContentType type);
+
+    bool AddAttachment(const std::string& resourceId,
+                       FileContentType attachmentType,
+                       const void* data,
+                       size_t size);
+
+    StoreStatus Store(std::string& resultPublicId,
+                      DicomInstanceToStore& dicom,
+                      StoreInstanceMode mode);
+
+    void AnswerAttachment(RestApiOutput& output,
+                          const std::string& resourceId,
+                          FileContentType content);
+
+    void ChangeAttachmentCompression(const std::string& resourceId,
+                                     FileContentType attachmentType,
+                                     CompressionType compression);
+
+    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 ReadAttachment(std::string& result,
+                        const std::string& instancePublicId,
+                        FileContentType content,
+                        bool uncompressIfNeeded);
+    
+    void ReadAttachment(std::string& result,
+                        const FileInfo& attachment);
+
+    void SetStoreMD5ForAttachments(bool storeMD5);
+
+    bool IsStoreMD5ForAttachments() const
+    {
+      return storeMD5_;
+    }
+
+    JobsEngine& GetJobsEngine()
+    {
+      return jobsEngine_;
+    }
+
+    bool DeleteResource(Json::Value& target,
+                        const std::string& uuid,
+                        ResourceType expectedType);
+
+    void SignalChange(const ServerIndexChange& change);
+
+    SharedArchive& GetQueryRetrieveArchive()
+    {
+      return *queryRetrieveArchive_;
+    }
+
+    SharedArchive& GetMediaArchive()
+    {
+      return *mediaArchive_;
+    }
+
+    const std::string& GetDefaultLocalApplicationEntityTitle() const
+    {
+      return defaultLocalAet_;
+    }
+
+    LuaScripting& GetLuaScripting()
+    {
+      return mainLua_;
+    }
+
+    OrthancHttpHandler& GetHttpHandler()
+    {
+      return httpHandler_;
+    }
+
+    void Stop();
+
+    void Apply(ILookupVisitor& visitor,
+               const DatabaseLookup& lookup,
+               ResourceType queryLevel,
+               size_t since,
+               size_t limit);
+
+    bool LookupOrReconstructMetadata(std::string& target,
+                                     const std::string& publicId,
+                                     MetadataType type);
+
+
+    /**
+     * Management of the plugins
+     **/
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+    void SetPlugins(OrthancPlugins& plugins);
+
+    void ResetPlugins();
+
+    const OrthancPlugins& GetPlugins() const;
+
+    OrthancPlugins& GetPlugins();
+#endif
+
+    bool HasPlugins() const;
+
+    void AddChildInstances(SetOfInstancesJob& job,
+                           const std::string& publicId);
+
+    void SignalUpdatedModalities();
+
+    void SignalUpdatedPeers();
+
+    MetricsRegistry& GetMetricsRegistry()
+    {
+      return *metricsRegistry_;
+    }
+
+    void SetHttpServerSecure(bool isSecure)
+    {
+      isHttpServerSecure_ = isSecure;
+    }
+
+    bool IsHttpServerSecure() const
+    {
+      return isHttpServerSecure_;
+    }
+
+    void SetExecuteLuaEnabled(bool enabled)
+    {
+      isExecuteLuaEnabled_ = enabled;
+    }
+
+    bool IsExecuteLuaEnabled() const
+    {
+      return isExecuteLuaEnabled_;
+    }
+
+    void SetOverwriteInstances(bool overwrite)
+    {
+      overwriteInstances_ = overwrite;
+    }
+    
+    bool IsOverwriteInstances() const
+    {
+      return overwriteInstances_;
+    }
+    
+    virtual IStorageCommitmentFactory::ILookupHandler*
+    CreateStorageCommitment(const std::string& jobId,
+                            const std::string& transactionUid,
+                            const std::vector<std::string>& sopClassUids,
+                            const std::vector<std::string>& sopInstanceUids,
+                            const std::string& remoteAet,
+                            const std::string& calledAet) ORTHANC_OVERRIDE;
+
+    StorageCommitmentReports& GetStorageCommitmentReports()
+    {
+      return *storageCommitmentReports_;
+    }
+
+    ImageAccessor* DecodeDicomFrame(const std::string& publicId,
+                                    unsigned int frameIndex);
+
+    ImageAccessor* DecodeDicomFrame(const DicomInstanceToStore& dicom,
+                                    unsigned int frameIndex);
+
+    ImageAccessor* DecodeDicomFrame(const void* dicom,
+                                    size_t size,
+                                    unsigned int frameIndex);
+    
+    void StoreWithTranscoding(std::string& sopClassUid,
+                              std::string& sopInstanceUid,
+                              DicomStoreUserConnection& connection,
+                              const std::string& dicom,
+                              bool hasMoveOriginator,
+                              const std::string& moveOriginatorAet,
+                              uint16_t moveOriginatorId);
+
+    // This method can be used even if the global option
+    // "TranscodeDicomProtocol" is set to "false"
+    virtual bool Transcode(DicomImage& target,
+                           DicomImage& source /* in, "GetParsed()" possibly modified */,
+                           const std::set<DicomTransferSyntax>& allowedSyntaxes,
+                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
+
+    bool IsTranscodeDicomProtocol() const
+    {
+      return transcodeDicomProtocol_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerEnumerations.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,376 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ServerEnumerations.h"
+
+#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 EnumerationDictionary<MetadataType> dictMetadataType_;
+  static EnumerationDictionary<FileContentType> dictContentType_;
+  static MimeTypes  mimeTypes_;
+
+  void InitializeServerEnumerations()
+  {
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+
+    dictMetadataType_.Clear();
+    dictContentType_.Clear();
+    
+    dictMetadataType_.Add(MetadataType_Instance_IndexInSeries, "IndexInSeries");
+    dictMetadataType_.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate");
+    dictMetadataType_.Add(MetadataType_Instance_RemoteAet, "RemoteAET");
+    dictMetadataType_.Add(MetadataType_Series_ExpectedNumberOfInstances, "ExpectedNumberOfInstances");
+    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");
+  }
+
+  void RegisterUserMetadata(int metadata,
+                            const std::string& name)
+  {
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+
+    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);
+    }
+
+    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)
+  {
+    // This function MUST return a "std::string" and not "const
+    // char*", as the result is not a static string
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+    return dictMetadataType_.Translate(type);
+  }
+
+  MetadataType StringToMetadata(const std::string& str)
+  {
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+    return dictMetadataType_.Translate(str);
+  }
+
+  void RegisterUserContentType(int contentType,
+                               const std::string& name,
+                               const std::string& mime)
+  {
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+
+    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);
+    }
+
+    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)
+  {
+    // This function MUST return a "std::string" and not "const
+    // char*", as the result is not a static string
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+    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 EnumerationToString(MimeType_Dicom);
+
+      case FileContentType_DicomAsJson:
+        return MIME_JSON_UTF8;
+
+      default:
+        return EnumerationToString(MimeType_Binary);
+    }
+  }
+
+  FileContentType StringToContentType(const std::string& str)
+  {
+    boost::mutex::scoped_lock lock(enumerationsMutex_);
+    return dictContentType_.Translate(str);
+  }
+
+
+  FindStorageAccessMode StringToFindStorageAccessMode(const std::string& value)
+  {
+    if (value == "Always")
+    {
+      return FindStorageAccessMode_DiskOnLookupAndAnswer;
+    }
+    else if (value == "Never")
+    {
+      return FindStorageAccessMode_DatabaseOnly;
+    }
+    else if (value == "Answers")
+    {
+      return FindStorageAccessMode_DiskOnAnswer;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Configuration option \"StorageAccessOnFind\" "
+                             "should be \"Always\", \"Never\" or \"Answers\": " + value);
+    }    
+  }
+
+
+  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& value)
+  {
+    if (value == "Before")
+    {
+      return BuiltinDecoderTranscoderOrder_Before;
+    }
+    else if (value == "After")
+    {
+      return BuiltinDecoderTranscoderOrder_After;
+    }
+    else if (value == "Disabled")
+    {
+      return BuiltinDecoderTranscoderOrder_Disabled;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Configuration option \"BuiltinDecoderTranscoderOrder\" "
+                             "should be \"After\", \"Before\" or \"Disabled\": " + value);
+    }    
+  }
+  
+
+  std::string GetBasePath(ResourceType type,
+                          const std::string& publicId)
+  {
+    switch (type)
+    {
+      case ResourceType_Patient:
+        return "/patients/" + publicId;
+
+      case ResourceType_Study:
+        return "/studies/" + publicId;
+
+      case ResourceType_Series:
+        return "/series/" + publicId;
+
+      case ResourceType_Instance:
+        return "/instances/" + publicId;
+      
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  const char* EnumerationToString(SeriesStatus status)
+  {
+    switch (status)
+    {
+      case SeriesStatus_Complete:
+        return "Complete";
+
+      case SeriesStatus_Missing:
+        return "Missing";
+
+      case SeriesStatus_Inconsistent:
+        return "Inconsistent";
+
+      case SeriesStatus_Unknown:
+        return "Unknown";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  const char* EnumerationToString(StoreStatus status)
+  {
+    switch (status)
+    {
+      case StoreStatus_Success:
+        return "Success";
+
+      case StoreStatus_AlreadyStored:
+        return "AlreadyStored";
+
+      case StoreStatus_Failure:
+        return "Failure";
+
+      case StoreStatus_FilteredOut:
+        return "FilteredOut";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  const char* EnumerationToString(ChangeType type)
+  {
+    switch (type)
+    {
+      case ChangeType_CompletedSeries:
+        return "CompletedSeries";
+
+      case ChangeType_NewInstance:
+        return "NewInstance";
+
+      case ChangeType_NewPatient:
+        return "NewPatient";
+
+      case ChangeType_NewSeries:
+        return "NewSeries";
+
+      case ChangeType_NewStudy:
+        return "NewStudy";
+
+      case ChangeType_AnonymizedStudy:
+        return "AnonymizedStudy";
+
+      case ChangeType_AnonymizedSeries:
+        return "AnonymizedSeries";
+
+      case ChangeType_ModifiedStudy:
+        return "ModifiedStudy";
+
+      case ChangeType_ModifiedSeries:
+        return "ModifiedSeries";
+
+      case ChangeType_AnonymizedPatient:
+        return "AnonymizedPatient";
+
+      case ChangeType_ModifiedPatient:
+        return "ModifiedPatient";
+
+      case ChangeType_StablePatient:
+        return "StablePatient";
+
+      case ChangeType_StableStudy:
+        return "StableStudy";
+
+      case ChangeType_StableSeries:
+        return "StableSeries";
+
+      case ChangeType_Deleted:
+        return "Deleted";
+
+      case ChangeType_NewChildInstance:
+        return "NewChildInstance";
+
+      case ChangeType_UpdatedAttachment:
+        return "UpdatedAttachment";
+
+      case ChangeType_UpdatedMetadata:
+        return "UpdatedMetadata";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  
+  bool IsUserMetadata(MetadataType metadata)
+  {
+    return (metadata >= MetadataType_StartUser &&
+            metadata <= MetadataType_EndUser);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerEnumerations.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <map>
+
+#include "../Core/Enumerations.h"
+#include "../Core/DicomFormat/DicomTag.h"
+
+namespace Orthanc
+{
+  enum SeriesStatus
+  {
+    SeriesStatus_Complete,
+    SeriesStatus_Missing,
+    SeriesStatus_Inconsistent,
+    SeriesStatus_Unknown
+  };
+
+  enum StoreStatus
+  {
+    StoreStatus_Success,
+    StoreStatus_AlreadyStored,
+    StoreStatus_Failure,
+    StoreStatus_FilteredOut     // Removed by NewInstanceFilter
+  };
+
+  enum DicomTagType
+  {
+    DicomTagType_Identifier,   // Tag that whose value is stored and indexed in the DB
+    DicomTagType_Main,         // Tag that is stored in the DB (but not indexed)
+    DicomTagType_Generic       // Tag that is only stored in the JSON files
+  };
+
+  enum ConstraintType
+  {
+    ConstraintType_Equal,
+    ConstraintType_SmallerOrEqual,
+    ConstraintType_GreaterOrEqual,
+    ConstraintType_Wildcard,
+    ConstraintType_List
+  };
+
+  namespace Compatibility
+  {
+    enum IdentifierConstraintType
+    {
+      IdentifierConstraintType_Equal,
+      IdentifierConstraintType_SmallerOrEqual,
+      IdentifierConstraintType_GreaterOrEqual,
+      IdentifierConstraintType_Wildcard        /* Case sensitive, "*" or "?" are the only allowed wildcards */
+    };
+  }
+
+  enum FindStorageAccessMode
+  {
+    FindStorageAccessMode_DatabaseOnly,
+    FindStorageAccessMode_DiskOnAnswer,
+    FindStorageAccessMode_DiskOnLookupAndAnswer
+  };
+
+  enum StoreInstanceMode
+  {
+    StoreInstanceMode_Default,
+    StoreInstanceMode_OverwriteDuplicate,
+    StoreInstanceMode_IgnoreDuplicate
+  };
+
+
+  /**
+   * WARNING: Do not change the explicit values in the enumerations
+   * below this point. This would result in incompatible databases
+   * between versions of Orthanc!
+   **/
+
+  enum GlobalProperty
+  {
+    GlobalProperty_DatabaseSchemaVersion = 1,   // Unused in the Orthanc core as of Orthanc 0.9.5
+    GlobalProperty_FlushSleep = 2,
+    GlobalProperty_AnonymizationSequence = 3,
+    GlobalProperty_JobsRegistry = 5,
+    GlobalProperty_GetTotalSizeIsFast = 6,      // New in Orthanc 1.5.2
+    GlobalProperty_Modalities = 20,             // New in Orthanc 1.5.0
+    GlobalProperty_Peers = 21,                  // New in Orthanc 1.5.0
+
+    // 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
+  {
+    MetadataType_Instance_IndexInSeries = 1,
+    MetadataType_Instance_ReceptionDate = 2,
+    MetadataType_Instance_RemoteAet = 3,
+    MetadataType_Series_ExpectedNumberOfInstances = 4,
+    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,
+    MetadataType_EndUser = 65535
+  };
+
+  enum ChangeType
+  {
+    ChangeType_CompletedSeries = 1,
+    ChangeType_NewInstance = 2,
+    ChangeType_NewPatient = 3,
+    ChangeType_NewSeries = 4,
+    ChangeType_NewStudy = 5,
+    ChangeType_AnonymizedStudy = 6,
+    ChangeType_AnonymizedSeries = 7,
+    ChangeType_ModifiedStudy = 8,
+    ChangeType_ModifiedSeries = 9,
+    ChangeType_AnonymizedPatient = 10,
+    ChangeType_ModifiedPatient = 11,
+    ChangeType_StablePatient = 12,
+    ChangeType_StableStudy = 13,
+    ChangeType_StableSeries = 14,
+    ChangeType_UpdatedAttachment = 15,
+    ChangeType_UpdatedMetadata = 16,
+
+    ChangeType_INTERNAL_LastLogged = 4095,
+
+    // The changes below this point are not logged into the database
+    ChangeType_Deleted = 4096,
+    ChangeType_NewChildInstance = 4097
+  };
+
+  enum BuiltinDecoderTranscoderOrder
+  {
+    BuiltinDecoderTranscoderOrder_Before,
+    BuiltinDecoderTranscoderOrder_After,
+    BuiltinDecoderTranscoderOrder_Disabled
+  };
+
+
+
+  void InitializeServerEnumerations();
+
+  void RegisterUserMetadata(int metadata,
+                            const std::string& name);
+
+  MetadataType StringToMetadata(const std::string& str);
+
+  std::string EnumerationToString(MetadataType type);
+
+  void RegisterUserContentType(int contentType,
+                               const std::string& name,
+                               const std::string& mime);
+
+  FileContentType StringToContentType(const std::string& str);
+
+  FindStorageAccessMode StringToFindStorageAccessMode(const std::string& str);
+
+  BuiltinDecoderTranscoderOrder StringToBuiltinDecoderTranscoderOrder(const std::string& str);
+
+  std::string EnumerationToString(FileContentType type);
+
+  std::string GetFileContentMime(FileContentType type);
+
+  std::string GetBasePath(ResourceType type,
+                          const std::string& publicId);
+
+  const char* EnumerationToString(SeriesStatus status);
+
+  const char* EnumerationToString(StoreStatus status);
+
+  const char* EnumerationToString(ChangeType type);
+
+  bool IsUserMetadata(MetadataType type);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerIndex.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2597 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ServerIndex.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
+
+#include "Database/ResourcesContent.h"
+#include "DicomInstanceToStore.h"
+#include "OrthancConfiguration.h"
+#include "Search/DatabaseLookup.h"
+#include "Search/DicomTagConstraint.h"
+#include "ServerContext.h"
+#include "ServerIndexChange.h"
+#include "ServerToolbox.h"
+
+#include <boost/lexical_cast.hpp>
+#include <stdio.h>
+
+static const uint64_t MEGA_BYTES = 1024 * 1024;
+
+namespace Orthanc
+{
+  static void CopyListToVector(std::vector<std::string>& target,
+                               const std::list<std::string>& source)
+  {
+    target.resize(source.size());
+
+    size_t pos = 0;
+    
+    for (std::list<std::string>::const_iterator
+           it = source.begin(); it != source.end(); ++it)
+    {
+      target[pos] = *it;
+      pos ++;
+    }      
+  }
+
+  
+  class ServerIndex::Listener : public IDatabaseListener
+  {
+  private:
+    struct FileToRemove
+    {
+    private:
+      std::string  uuid_;
+      FileContentType  type_;
+
+    public:
+      FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), 
+                                           type_(info.GetContentType())
+      {
+      }
+
+      const std::string& GetUuid() const
+      {
+        return uuid_;
+      }
+
+      FileContentType GetContentType() const 
+      {
+        return type_;
+      }
+    };
+
+    ServerContext& context_;
+    bool hasRemainingLevel_;
+    ResourceType remainingType_;
+    std::string remainingPublicId_;
+    std::list<FileToRemove> pendingFilesToRemove_;
+    std::list<ServerIndexChange> pendingChanges_;
+    uint64_t sizeOfFilesToRemove_;
+    bool insideTransaction_;
+
+    void Reset()
+    {
+      sizeOfFilesToRemove_ = 0;
+      hasRemainingLevel_ = false;
+      pendingFilesToRemove_.clear();
+      pendingChanges_.clear();
+    }
+
+  public:
+    Listener(ServerContext& context) : context_(context),
+                                       insideTransaction_(false)      
+    {
+      Reset();
+      assert(ResourceType_Patient < ResourceType_Study &&
+             ResourceType_Study < ResourceType_Series &&
+             ResourceType_Series < ResourceType_Instance);
+    }
+
+    void StartTransaction()
+    {
+      Reset();
+      insideTransaction_ = true;
+    }
+
+    void EndTransaction()
+    {
+      insideTransaction_ = false;
+    }
+
+    uint64_t GetSizeOfFilesToRemove()
+    {
+      return sizeOfFilesToRemove_;
+    }
+
+    void CommitFilesToRemove()
+    {
+      for (std::list<FileToRemove>::const_iterator 
+             it = pendingFilesToRemove_.begin();
+           it != pendingFilesToRemove_.end(); ++it)
+      {
+        try
+        {
+          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Unable to remove an attachment from the storage area: "
+                     << it->GetUuid() << " (type: " << EnumerationToString(it->GetContentType()) << ")";
+        }
+      }
+    }
+
+    void CommitChanges()
+    {
+      for (std::list<ServerIndexChange>::const_iterator 
+             it = pendingChanges_.begin(); 
+           it != pendingChanges_.end(); ++it)
+      {
+        context_.SignalChange(*it);
+      }
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType parentType,
+                                         const std::string& publicId)
+    {
+      VLOG(1) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")";
+
+      if (hasRemainingLevel_)
+      {
+        if (parentType < remainingType_)
+        {
+          remainingType_ = parentType;
+          remainingPublicId_ = publicId;
+        }
+      }
+      else
+      {
+        hasRemainingLevel_ = true;
+        remainingType_ = parentType;
+        remainingPublicId_ = publicId;
+      }        
+    }
+
+    virtual void SignalFileDeleted(const FileInfo& info)
+    {
+      assert(Toolbox::IsUuid(info.GetUuid()));
+      pendingFilesToRemove_.push_back(FileToRemove(info));
+      sizeOfFilesToRemove_ += info.GetCompressedSize();
+    }
+
+    virtual void SignalChange(const ServerIndexChange& change)
+    {
+      VLOG(1) << "Change related to resource " << change.GetPublicId() << " of type " 
+              << EnumerationToString(change.GetResourceType()) << ": " 
+              << EnumerationToString(change.GetChangeType());
+
+      if (insideTransaction_)
+      {
+        pendingChanges_.push_back(change);
+      }
+      else
+      {
+        context_.SignalChange(change);
+      }
+    }
+
+    bool HasRemainingLevel() const
+    {
+      return hasRemainingLevel_;
+    }
+
+    ResourceType GetRemainingType() const
+    {
+      assert(HasRemainingLevel());
+      return remainingType_;
+    }
+
+    const std::string& GetRemainingPublicId() const
+    {
+      assert(HasRemainingLevel());
+      return remainingPublicId_;
+    }                                 
+  };
+
+
+  class ServerIndex::Transaction
+  {
+  private:
+    ServerIndex& index_;
+    std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_;
+    bool isCommitted_;
+
+  public:
+    Transaction(ServerIndex& index) : 
+      index_(index),
+      isCommitted_(false)
+    {
+      transaction_.reset(index_.db_.StartTransaction());
+      transaction_->Begin();
+
+      index_.listener_->StartTransaction();
+    }
+
+    ~Transaction()
+    {
+      index_.listener_->EndTransaction();
+
+      if (!isCommitted_)
+      {
+        transaction_->Rollback();
+      }
+    }
+
+    void Commit(uint64_t sizeOfAddedFiles)
+    {
+      if (!isCommitted_)
+      {
+        int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) -
+                         static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove()));
+
+        transaction_->Commit(delta);
+
+        // We can remove the files once the SQLite transaction has
+        // been successfully committed. Some files might have to be
+        // deleted because of recycling.
+        index_.listener_->CommitFilesToRemove();
+
+        // Send all the pending changes to the Orthanc plugins
+        index_.listener_->CommitChanges();
+
+        isCommitted_ = true;
+      }
+    }
+  };
+
+
+  class ServerIndex::UnstableResourcePayload
+  {
+  private:
+    ResourceType type_;
+    std::string publicId_;
+    boost::posix_time::ptime time_;
+
+  public:
+    UnstableResourcePayload() : type_(ResourceType_Instance)
+    {
+    }
+
+    UnstableResourcePayload(Orthanc::ResourceType type,
+                            const std::string& publicId) : 
+      type_(type),
+      publicId_(publicId)
+    {
+      time_ = boost::posix_time::second_clock::local_time();
+    }
+
+    unsigned int GetAge() const
+    {
+      return (boost::posix_time::second_clock::local_time() - time_).total_seconds();
+    }
+
+    ResourceType GetResourceType() const
+    {
+      return type_;
+    }
+    
+    const std::string& GetPublicId() const
+    {
+      return publicId_;
+    }
+  };
+
+
+  class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+      
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+
+    Registry  registry_;
+      
+    void LoadTags(ResourceType level)
+    {
+      {
+        const DicomTag* tags = NULL;
+        size_t size;
+  
+        ServerToolbox::LoadIdentifiers(tags, size, level);
+  
+        for (size_t i = 0; i < size; i++)
+        {
+          if (registry_.find(tags[i]) == registry_.end())
+          {
+            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+          }
+          else
+          {
+            // These patient-level tags are copied in the study level
+            assert(level == ResourceType_Study &&
+                   (tags[i] == DICOM_TAG_PATIENT_ID ||
+                    tags[i] == DICOM_TAG_PATIENT_NAME ||
+                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+          }
+        }
+      }
+
+      {
+        std::set<DicomTag> tags;
+        DicomMap::GetMainDicomTags(tags, level);
+
+        for (std::set<DicomTag>::const_iterator
+               tag = tags.begin(); tag != tags.end(); ++tag)
+        {
+          if (registry_.find(*tag) == registry_.end())
+          {
+            registry_[*tag] = TagInfo(level, DicomTagType_Main);
+          }
+        }
+      }
+    }
+
+  public:
+    MainDicomTagsRegistry()
+    {
+      LoadTags(ResourceType_Patient);
+      LoadTags(ResourceType_Study);
+      LoadTags(ResourceType_Series);
+      LoadTags(ResourceType_Instance); 
+    }
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const
+    {
+      Registry::const_iterator it = registry_.find(tag);
+
+      if (it == registry_.end())
+      {
+        // Default values
+        level = ResourceType_Instance;
+        type = DicomTagType_Generic;
+      }
+      else
+      {
+        level = it->second.GetLevel();
+        type = it->second.GetType();
+      }
+    }
+  };
+
+
+  bool ServerIndex::DeleteResource(Json::Value& target,
+                                   const std::string& uuid,
+                                   ResourceType expectedType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction t(*this);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, uuid) ||
+        expectedType != type)
+    {
+      return false;
+    }
+      
+    db_.DeleteResource(id);
+
+    if (listener_->HasRemainingLevel())
+    {
+      ResourceType type = listener_->GetRemainingType();
+      const std::string& uuid = listener_->GetRemainingPublicId();
+
+      target["RemainingAncestor"] = Json::Value(Json::objectValue);
+      target["RemainingAncestor"]["Path"] = GetBasePath(type, uuid);
+      target["RemainingAncestor"]["Type"] = EnumerationToString(type);
+      target["RemainingAncestor"]["ID"] = uuid;
+    }
+    else
+    {
+      target["RemainingAncestor"] = Json::nullValue;
+    }
+
+    t.Commit(0);
+
+    return true;
+  }
+
+
+  void ServerIndex::FlushThread(ServerIndex* that,
+                                unsigned int threadSleep)
+  {
+    // By default, wait for 10 seconds before flushing
+    unsigned int sleep = 10;
+
+    try
+    {
+      boost::mutex::scoped_lock lock(that->mutex_);
+      std::string sleepString;
+
+      if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) &&
+          Toolbox::IsInteger(sleepString))
+      {
+        sleep = boost::lexical_cast<unsigned int>(sleepString);
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")";
+
+    unsigned int count = 0;
+
+    while (!that->done_)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
+      count++;
+      if (count < sleep)
+      {
+        continue;
+      }
+
+      Logging::Flush();
+
+      boost::mutex::scoped_lock lock(that->mutex_);
+
+      try
+      {
+        that->db_.FlushToDisk();
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
+      }
+          
+      count = 0;
+    }
+
+    LOG(INFO) << "Stopping the database flushing thread";
+  }
+
+
+  static bool ComputeExpectedNumberOfInstances(int64_t& target,
+                                               const DicomMap& dicomSummary)
+  {
+    try
+    {
+      const DicomValue* value;
+      const DicomValue* value2;
+          
+      if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary() &&
+          (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL &&
+          !value2->IsNull() &&
+          !value2->IsBinary())
+      {
+        // Patch for series with temporal positions thanks to Will Ryder
+        int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent());
+        int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
+        target = imagesInAcquisition * countTemporalPositions;
+        return (target > 0);
+      }
+
+      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL &&
+               !value->IsNull() &&
+               !value->IsBinary() &&
+               (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL &&
+               !value2->IsBinary() &&
+               !value2->IsNull())
+      {
+        // Support of Cardio-PET images
+        int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent());
+        int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
+        target = numberOfSlices * numberOfTimeSlices;
+        return (target > 0);
+      }
+
+      else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL &&
+               !value->IsNull() &&
+               !value->IsBinary())
+      {
+        target = boost::lexical_cast<int64_t>(value->GetContent());
+        return (target > 0);
+      }
+    }
+    catch (OrthancException&)
+    {
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    return false;
+  }
+
+
+
+
+  static bool LookupStringMetadata(std::string& result,
+                                   const std::map<MetadataType, std::string>& metadata,
+                                   MetadataType type)
+  {
+    std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
+
+    if (found == metadata.end())
+    {
+      return false;
+    }
+    else
+    {
+      result = found->second;
+      return true;
+    }
+  }
+
+
+  static bool LookupIntegerMetadata(int64_t& result,
+                                    const std::map<MetadataType, std::string>& metadata,
+                                    MetadataType type)
+  {
+    std::string s;
+    if (!LookupStringMetadata(s, metadata, type))
+    {
+      return false;
+    }
+
+    try
+    {
+      result = boost::lexical_cast<int64_t>(s);
+      return true;
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      return false;
+    }
+  }
+
+
+  void ServerIndex::LogChange(int64_t internalId,
+                              ChangeType changeType,
+                              ResourceType resourceType,
+                              const std::string& publicId)
+  {
+    ServerIndexChange change(changeType, resourceType, publicId);
+
+    if (changeType <= ChangeType_INTERNAL_LastLogged)
+    {
+      db_.LogChange(internalId, change);
+    }
+
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+  }
+
+
+  uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property)
+  {
+    std::string oldValue;
+
+    if (db_.LookupGlobalProperty(oldValue, property))
+    {
+      uint64_t oldNumber;
+
+      try
+      {
+        oldNumber = boost::lexical_cast<uint64_t>(oldValue);
+        db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1));
+        return oldNumber + 1;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      // Initialize the sequence at "1"
+      db_.SetGlobalProperty(property, "1");
+      return 1;
+    }
+  }
+
+
+
+  ServerIndex::ServerIndex(ServerContext& context,
+                           IDatabaseWrapper& db,
+                           unsigned int threadSleep) : 
+    done_(false),
+    db_(db),
+    maximumStorageSize_(0),
+    maximumPatients_(0),
+    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
+  {
+    listener_.reset(new Listener(context));
+    db_.SetListener(*listener_);
+
+    // Initial recycling if the parameters have changed since the last
+    // execution of Orthanc
+    StandaloneRecycling();
+
+    if (db.HasFlushToDisk())
+    {
+      flushThread_ = boost::thread(FlushThread, this, threadSleep);
+    }
+
+    unstableResourcesMonitorThread_ = boost::thread
+      (UnstableResourcesMonitorThread, this, threadSleep);
+  }
+
+
+
+  ServerIndex::~ServerIndex()
+  {
+    if (!done_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ServerIndex::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+
+
+  void ServerIndex::Stop()
+  {
+    if (!done_)
+    {
+      done_ = true;
+
+      if (db_.HasFlushToDisk() &&
+          flushThread_.joinable())
+      {
+        flushThread_.join();
+      }
+
+      if (unstableResourcesMonitorThread_.joinable())
+      {
+        unstableResourcesMonitorThread_.join();
+      }
+    }
+  }
+
+
+  static void SetInstanceMetadata(ResourcesContent& content,
+                                  std::map<MetadataType, std::string>& instanceMetadata,
+                                  int64_t instance,
+                                  MetadataType metadata,
+                                  const std::string& value)
+  {
+    content.AddMetadata(instance, metadata, value);
+    instanceMetadata[metadata] = value;
+  }
+
+
+  void ServerIndex::SignalNewResource(ChangeType changeType,
+                                      ResourceType level,
+                                      const std::string& publicId,
+                                      int64_t internalId)
+  {
+    ServerIndexChange change(changeType, level, publicId);
+    db_.LogChange(internalId, change);
+    
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+  }
+
+  
+  StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
+                                 DicomInstanceToStore& instanceToStore,
+                                 const Attachments& attachments,
+                                 bool overwrite)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    const DicomMap& dicomSummary = instanceToStore.GetSummary();
+    const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata();
+
+    int64_t expectedInstances;
+    const bool hasExpectedInstances =
+      ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary);
+    
+    instanceMetadata.clear();
+
+    const std::string hashPatient = instanceToStore.GetHasher().HashPatient();
+    const std::string hashStudy = instanceToStore.GetHasher().HashStudy();
+    const std::string hashSeries = instanceToStore.GetHasher().HashSeries();
+    const std::string hashInstance = instanceToStore.GetHasher().HashInstance();
+
+    try
+    {
+      Transaction t(*this);
+
+      IDatabaseWrapper::CreateInstanceResult status;
+      int64_t instanceId;
+
+      // Check whether this instance is already stored
+      if (!db_.CreateInstance(status, instanceId, hashPatient,
+                              hashStudy, hashSeries, hashInstance))
+      {
+        // The instance already exists
+        
+        if (overwrite)
+        {
+          // Overwrite the old instance
+          LOG(INFO) << "Overwriting instance: " << hashInstance;
+          db_.DeleteResource(instanceId);
+
+          // Re-create the instance, now that the old one is removed
+          if (!db_.CreateInstance(status, instanceId, hashPatient,
+                                  hashStudy, hashSeries, hashInstance))
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+        else
+        {
+          // Do nothing if the instance already exists and overwriting is disabled
+          db_.GetAllMetadata(instanceMetadata, instanceId);
+          return StoreStatus_AlreadyStored;
+        }
+      }
+
+
+      // Warn about the creation of new resources. The order must be
+      // from instance to patient.
+
+      // NB: In theory, could be sped up by grouping the underlying
+      // calls to "db_.LogChange()". However, this would only have an
+      // impact when new patient/study/series get created, which
+      // occurs far less often that creating new instances. The
+      // positive impact looks marginal in practice.
+      SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId);
+
+      if (status.isNewSeries_)
+      {
+        SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_);
+      }
+      
+      if (status.isNewStudy_)
+      {
+        SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_);
+      }
+      
+      if (status.isNewPatient_)
+      {
+        SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_);
+      }
+      
+      
+      // Ensure there is enough room in the storage for the new instance
+      uint64_t instanceSize = 0;
+      for (Attachments::const_iterator it = attachments.begin();
+           it != attachments.end(); ++it)
+      {
+        instanceSize += it->GetCompressedSize();
+      }
+
+      Recycle(instanceSize, hashPatient /* don't consider the current patient for recycling */);
+      
+     
+      // Attach the files to the newly created instance
+      for (Attachments::const_iterator it = attachments.begin();
+           it != attachments.end(); ++it)
+      {
+        db_.AddAttachment(instanceId, *it);
+      }
+
+      
+      {
+        ResourcesContent content;
+      
+        // Populate the tags of the newly-created resources
+
+        content.AddResource(instanceId, ResourceType_Instance, dicomSummary);
+
+        if (status.isNewSeries_)
+        {
+          content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary);
+        }
+
+        if (status.isNewStudy_)
+        {
+          content.AddResource(status.studyId_, ResourceType_Study, dicomSummary);
+        }
+
+        if (status.isNewPatient_)
+        {
+          content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary);
+        }
+
+
+        // Attach the user-specified metadata
+
+        for (MetadataMap::const_iterator 
+               it = metadata.begin(); it != metadata.end(); ++it)
+        {
+          switch (it->first.first)
+          {
+            case ResourceType_Patient:
+              content.AddMetadata(status.patientId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Study:
+              content.AddMetadata(status.studyId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Series:
+              content.AddMetadata(status.seriesId_, it->first.second, it->second);
+              break;
+
+            case ResourceType_Instance:
+              SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                  it->first.second, it->second);
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_ParameterOutOfRange);
+          }
+        }
+
+        
+        // Attach the auto-computed metadata for the patient/study/series levels
+        std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
+        content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
+        content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
+        content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
+
+        if (status.isNewSeries_ &&
+            hasExpectedInstances)
+        {
+          content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
+                              boost::lexical_cast<std::string>(expectedInstances));
+        }
+
+        
+        // Attach the auto-computed metadata for the instance level,
+        // reflecting these additions into the input metadata map
+        SetInstanceMetadata(content, instanceMetadata, instanceId,
+                            MetadataType_Instance_ReceptionDate, now);
+        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_RemoteAet,
+                            instanceToStore.GetOrigin().GetRemoteAetC());
+        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, 
+                            EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
+
+
+        {
+          std::string s;
+
+          if (instanceToStore.LookupTransferSyntax(s))
+          {
+            // New in Orthanc 1.2.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_TransferSyntax, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupRemoteIp(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_RemoteIp, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupCalledAet(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_CalledAet, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupHttpUsername(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_HttpUsername, s);
+          }
+        }
+
+        
+        const DicomValue* value;
+        if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+            !value->IsNull() &&
+            !value->IsBinary())
+        {
+          SetInstanceMetadata(content, instanceMetadata, instanceId,
+                              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())
+          {
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_IndexInSeries, value->GetContent());
+          }
+        }
+
+        
+        db_.SetResourcesContent(content);
+      }
+
+  
+      // Check whether the series of this new instance is now completed
+      int64_t expectedNumberOfInstances;
+      if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary))
+      {
+        SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
+        if (seriesStatus == SeriesStatus_Complete)
+        {
+          LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
+        }
+      }
+      
+
+      // Mark the parent resources of this instance as unstable
+      MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries);
+      MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy);
+      MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient);
+
+      t.Commit(instanceSize);
+
+      return StoreStatus_Success;
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+    }
+
+    return StoreStatus_Failure;
+  }
+
+
+  void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize,
+                                        /* out */ uint64_t& uncompressedSize,
+                                        /* out */ uint64_t& countPatients, 
+                                        /* out */ uint64_t& countStudies, 
+                                        /* out */ uint64_t& countSeries, 
+                                        /* out */ uint64_t& countInstances)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    diskSize = db_.GetTotalCompressedSize();
+    uncompressedSize = db_.GetTotalUncompressedSize();
+    countPatients = db_.GetResourceCount(ResourceType_Patient);
+    countStudies = db_.GetResourceCount(ResourceType_Study);
+    countSeries = db_.GetResourceCount(ResourceType_Series);
+    countInstances = db_.GetResourceCount(ResourceType_Instance);
+  }
+
+  
+  SeriesStatus ServerIndex::GetSeriesStatus(int64_t id,
+                                            int64_t expectedNumberOfInstances)
+  {
+    std::list<std::string> values;
+    db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
+
+    std::set<int64_t> instances;
+
+    for (std::list<std::string>::const_iterator
+           it = values.begin(); it != values.end(); ++it)
+    {
+      int64_t index;
+
+      try
+      {
+        index = boost::lexical_cast<int64_t>(*it);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return SeriesStatus_Unknown;
+      }
+      
+      if (!(index > 0 && index <= expectedNumberOfInstances))
+      {
+        // Out-of-range instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      if (instances.find(index) != instances.end())
+      {
+        // Twice the same instance index
+        return SeriesStatus_Inconsistent;
+      }
+
+      instances.insert(index);
+    }
+
+    if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances)
+    {
+      return SeriesStatus_Complete;
+    }
+    else
+    {
+      return SeriesStatus_Missing;
+    }
+  }
+
+
+  void ServerIndex::MainDicomTagsToJson(Json::Value& target,
+                                        int64_t resourceId,
+                                        ResourceType resourceType)
+  {
+    DicomMap tags;
+    db_.GetMainDicomTags(tags, resourceId);
+
+    if (resourceType == ResourceType_Study)
+    {
+      DicomMap t1, t2;
+      tags.ExtractStudyInformation(t1);
+      tags.ExtractPatientInformation(t2);
+
+      target["MainDicomTags"] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true);
+
+      target["PatientMainDicomTags"] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true);
+    }
+    else
+    {
+      target["MainDicomTags"] = Json::objectValue;
+      FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true);
+    }
+  }
+
+  
+  bool ServerIndex::LookupResource(Json::Value& result,
+                                   const std::string& publicId,
+                                   ResourceType expectedType)
+  {
+    result = Json::objectValue;
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    std::string parent;
+    if (!db_.LookupResourceAndParent(id, type, parent, publicId) ||
+        type != expectedType)
+    {
+      return false;
+    }
+
+    // Set information about the parent resource (if it exists)
+    if (type == ResourceType_Patient)
+    {
+      if (!parent.empty())
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+    }
+    else
+    {
+      if (parent.empty())
+      {
+        throw OrthancException(ErrorCode_DatabasePlugin);
+      }
+
+      switch (type)
+      {
+        case ResourceType_Study:
+          result["ParentPatient"] = parent;
+          break;
+
+        case ResourceType_Series:
+          result["ParentStudy"] = parent;
+          break;
+
+        case ResourceType_Instance:
+          result["ParentSeries"] = parent;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    // List the children resources
+    std::list<std::string> children;
+    db_.GetChildrenPublicId(children, id);
+
+    if (type != ResourceType_Instance)
+    {
+      Json::Value c = Json::arrayValue;
+
+      for (std::list<std::string>::const_iterator
+             it = children.begin(); it != children.end(); ++it)
+      {
+        c.append(*it);
+      }
+
+      switch (type)
+      {
+        case ResourceType_Patient:
+          result["Studies"] = c;
+          break;
+
+        case ResourceType_Study:
+          result["Series"] = c;
+          break;
+
+        case ResourceType_Series:
+          result["Instances"] = c;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    // Extract the metadata
+    std::map<MetadataType, std::string> metadata;
+    db_.GetAllMetadata(metadata, id);
+
+    // Set the resource type
+    switch (type)
+    {
+      case ResourceType_Patient:
+        result["Type"] = "Patient";
+        break;
+
+      case ResourceType_Study:
+        result["Type"] = "Study";
+        break;
+
+      case ResourceType_Series:
+      {
+        result["Type"] = "Series";
+
+        int64_t i;
+        if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
+        {
+          result["ExpectedNumberOfInstances"] = static_cast<int>(i);
+          result["Status"] = EnumerationToString(GetSeriesStatus(id, i));
+        }
+        else
+        {
+          result["ExpectedNumberOfInstances"] = Json::nullValue;
+          result["Status"] = EnumerationToString(SeriesStatus_Unknown);
+        }
+
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        result["Type"] = "Instance";
+
+        FileInfo attachment;
+        if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
+        result["FileUuid"] = attachment.GetUuid();
+
+        int64_t i;
+        if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
+        {
+          result["IndexInSeries"] = static_cast<int>(i);
+        }
+        else
+        {
+          result["IndexInSeries"] = Json::nullValue;
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Record the remaining information
+    result["ID"] = publicId;
+    MainDicomTagsToJson(result, id, type);
+
+    std::string tmp;
+
+    if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
+    {
+      result["AnonymizedFrom"] = tmp;
+    }
+
+    if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
+    {
+      result["ModifiedFrom"] = tmp;
+    }
+
+    if (type == ResourceType_Patient ||
+        type == ResourceType_Study ||
+        type == ResourceType_Series)
+    {
+      result["IsStable"] = !unstableResources_.Contains(id);
+
+      if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
+      {
+        result["LastUpdate"] = tmp;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool ServerIndex::LookupAttachment(FileInfo& attachment,
+                                     const std::string& instanceUuid,
+                                     FileContentType contentType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, instanceUuid))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (db_.LookupAttachment(attachment, id, contentType))
+    {
+      assert(attachment.GetContentType() == contentType);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+  void ServerIndex::GetAllUuids(std::list<std::string>& target,
+                                ResourceType resourceType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.GetAllPublicIds(target, resourceType);
+  }
+
+
+  void ServerIndex::GetAllUuids(std::list<std::string>& target,
+                                ResourceType resourceType,
+                                size_t since,
+                                size_t limit)
+  {
+    if (limit == 0)
+    {
+      target.clear();
+      return;
+    }
+
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.GetAllPublicIds(target, resourceType, since, limit);
+  }
+
+
+  template <typename T>
+  static void FormatLog(Json::Value& target,
+                        const std::list<T>& log,
+                        const std::string& name,
+                        bool done,
+                        int64_t since,
+                        bool hasLast,
+                        int64_t last)
+  {
+    Json::Value items = Json::arrayValue;
+    for (typename std::list<T>::const_iterator
+           it = log.begin(); it != log.end(); ++it)
+    {
+      Json::Value item;
+      it->Format(item);
+      items.append(item);
+    }
+
+    target = Json::objectValue;
+    target[name] = items;
+    target["Done"] = done;
+
+    if (!hasLast)
+    {
+      // Best-effort guess of the last index in the sequence
+      if (log.empty())
+      {
+        last = since;
+      }
+      else
+      {
+        last = log.back().GetSeq();
+      }
+    }
+    
+    target["Last"] = static_cast<int>(last);
+  }
+
+
+  void ServerIndex::GetChanges(Json::Value& target,
+                               int64_t since,                               
+                               unsigned int maxResults)
+  {
+    std::list<ServerIndexChange> changes;
+    bool done;
+    bool hasLast = false;
+    int64_t last = 0;
+
+    {
+      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);
+      if (changes.empty())
+      {
+        last = db_.GetLastChangeIndex();
+        hasLast = true;
+      }
+      
+      transaction.Commit(0);
+    }
+
+    FormatLog(target, changes, "Changes", done, since, hasLast, last);
+  }
+
+
+  void ServerIndex::GetLastChange(Json::Value& target)
+  {
+    std::list<ServerIndexChange> changes;
+    bool hasLast = false;
+    int64_t last = 0;
+
+    {
+      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);
+      if (changes.empty())
+      {
+        last = db_.GetLastChangeIndex();
+        hasLast = true;
+      }
+
+      transaction.Commit(0);
+    }
+
+    FormatLog(target, changes, "Changes", true, 0, hasLast, last);
+  }
+
+
+  void ServerIndex::LogExportedResource(const std::string& publicId,
+                                        const std::string& remoteModality)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+
+    std::string patientId;
+    std::string studyInstanceUid;
+    std::string seriesInstanceUid;
+    std::string sopInstanceUid;
+
+    int64_t currentId = id;
+    ResourceType currentType = type;
+
+    // Iteratively go up inside the patient/study/series/instance hierarchy
+    bool done = false;
+    while (!done)
+    {
+      DicomMap map;
+      db_.GetMainDicomTags(map, currentId);
+
+      switch (currentType)
+      {
+        case ResourceType_Patient:
+          if (map.HasTag(DICOM_TAG_PATIENT_ID))
+          {
+            patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
+          }
+          done = true;
+          break;
+
+        case ResourceType_Study:
+          if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
+          {
+            studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
+          }
+          currentType = ResourceType_Patient;
+          break;
+
+        case ResourceType_Series:
+          if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
+          {
+            seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
+          }
+          currentType = ResourceType_Study;
+          break;
+
+        case ResourceType_Instance:
+          if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
+          {
+            sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
+          }
+          currentType = ResourceType_Series;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      // If we have not reached the Patient level, find the parent of
+      // the current resource
+      if (!done)
+      {
+        bool ok = db_.LookupParent(currentId, currentId);
+        assert(ok);
+      }
+    }
+
+    ExportedResource resource(-1, 
+                              type,
+                              publicId,
+                              remoteModality,
+                              SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
+                              patientId,
+                              studyInstanceUid,
+                              seriesInstanceUid,
+                              sopInstanceUid);
+
+    db_.LogExportedResource(resource);
+    transaction.Commit(0);
+  }
+
+
+  void ServerIndex::GetExportedResources(Json::Value& target,
+                                         int64_t since,
+                                         unsigned int maxResults)
+  {
+    std::list<ExportedResource> exported;
+    bool done;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetExportedResources(exported, done, since, maxResults);
+    }
+
+    FormatLog(target, exported, "Exports", done, since, false, -1);
+  }
+
+
+  void ServerIndex::GetLastExportedResource(Json::Value& target)
+  {
+    std::list<ExportedResource> exported;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetLastExportedResource(exported);
+    }
+
+    FormatLog(target, exported, "Exports", true, 0, false, -1);
+  }
+
+
+  bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize)
+  {
+    if (maximumStorageSize_ != 0)
+    {
+      assert(maximumStorageSize_ >= instanceSize);
+      
+      if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize))
+      {
+        return true;
+      }
+    }
+
+    if (maximumPatients_ != 0)
+    {
+      uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient);
+      if (patientCount > maximumPatients_)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  
+  void ServerIndex::Recycle(uint64_t instanceSize,
+                            const std::string& newPatientId)
+  {
+    if (!IsRecyclingNeeded(instanceSize))
+    {
+      return;
+    }
+
+    // Check whether other DICOM instances from this patient are
+    // already stored
+    int64_t patientToAvoid;
+    ResourceType type;
+    bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
+
+    if (hasPatientToAvoid && type != ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Iteratively select patient to remove until there is enough
+    // space in the DICOM store
+    int64_t patientToRecycle;
+    while (true)
+    {
+      // If other instances of this patient are already in the store,
+      // we must avoid to recycle them
+      bool ok = hasPatientToAvoid ?
+        db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
+        db_.SelectPatientToRecycle(patientToRecycle);
+        
+      if (!ok)
+      {
+        throw OrthancException(ErrorCode_FullStorage);
+      }
+      
+      VLOG(1) << "Recycling one patient";
+      db_.DeleteResource(patientToRecycle);
+
+      if (!IsRecyclingNeeded(instanceSize))
+      {
+        // OK, we're done
+        break;
+      }
+    }
+  }  
+
+  void ServerIndex::SetMaximumPatientCount(unsigned int count) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maximumPatients_ = count;
+
+    if (count == 0)
+    {
+      LOG(WARNING) << "No limit on the number of stored patients";
+    }
+    else
+    {
+      LOG(WARNING) << "At most " << count << " patients will be stored";
+    }
+
+    StandaloneRecycling();
+  }
+
+  void ServerIndex::SetMaximumStorageSize(uint64_t size) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maximumStorageSize_ = size;
+
+    if (size == 0)
+    {
+      LOG(WARNING) << "No limit on the size of the storage area";
+    }
+    else
+    {
+      LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
+    }
+
+    StandaloneRecycling();
+  }
+
+
+  void ServerIndex::StandaloneRecycling()
+  {
+    // WARNING: No mutex here, do not include this as a public method
+    Transaction t(*this);
+    Recycle(0, "");
+    t.Commit(0);
+  }
+
+
+  bool ServerIndex::IsProtectedPatient(const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return db_.IsProtectedPatient(id);
+  }
+     
+
+  void ServerIndex::SetProtectedPatient(const std::string& publicId,
+                                        bool isProtected)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    db_.SetProtectedPatient(id, isProtected);
+    transaction.Commit(0);
+
+    if (isProtected)
+      LOG(INFO) << "Patient " << publicId << " has been protected";
+    else
+      LOG(INFO) << "Patient " << publicId << " has been unprotected";
+  }
+
+
+  void ServerIndex::GetChildren(std::list<std::string>& result,
+                                const std::string& publicId)
+  {
+    result.clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t resource;
+    if (!db_.LookupResource(resource, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (type == ResourceType_Instance)
+    {
+      // An instance cannot have a child
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    std::list<int64_t> tmp;
+    db_.GetChildrenInternalId(tmp, resource);
+
+    for (std::list<int64_t>::const_iterator 
+           it = tmp.begin(); it != tmp.end(); ++it)
+    {
+      result.push_back(db_.GetPublicId(*it));
+    }
+  }
+
+
+  void ServerIndex::GetChildInstances(std::list<std::string>& result,
+                                      const std::string& publicId)
+  {
+    result.clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t top;
+    if (!db_.LookupResource(top, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (type == ResourceType_Instance)
+    {
+      // The resource is already an instance: Do not go down the hierarchy
+      result.push_back(publicId);
+      return;
+    }
+
+    std::stack<int64_t> toExplore;
+    toExplore.push(top);
+
+    std::list<int64_t> tmp;
+
+    while (!toExplore.empty())
+    {
+      // Get the internal ID of the current resource
+      int64_t resource = toExplore.top();
+      toExplore.pop();
+
+      if (db_.GetResourceType(resource) == ResourceType_Instance)
+      {
+        result.push_back(db_.GetPublicId(resource));
+      }
+      else
+      {
+        // Tag all the children of this resource as to be explored
+        db_.GetChildrenInternalId(tmp, resource);
+        for (std::list<int64_t>::const_iterator 
+               it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          toExplore.push(*it);
+        }
+      }
+    }
+  }
+
+
+  void ServerIndex::SetMetadata(const std::string& publicId,
+                                MetadataType type,
+                                const std::string& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.SetMetadata(id, type, value);
+
+    if (IsUserMetadata(type))
+    {
+      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
+    }
+
+    t.Commit(0);
+  }
+
+
+  void ServerIndex::DeleteMetadata(const std::string& publicId,
+                                   MetadataType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.DeleteMetadata(id, type);
+
+    if (IsUserMetadata(type))
+    {
+      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
+    }
+
+    t.Commit(0);
+  }
+
+
+  bool ServerIndex::LookupMetadata(std::string& target,
+                                   const std::string& publicId,
+                                   MetadataType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    return db_.LookupMetadata(target, id, type);
+  }
+
+
+  void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                   const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    return db_.GetAllMetadata(target, id);
+  }
+
+
+  void ServerIndex::ListAvailableAttachments(std::list<FileContentType>& target,
+                                             const std::string& publicId,
+                                             ResourceType expectedType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId) ||
+        expectedType != type)
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.ListAvailableAttachments(target, id);
+  }
+
+
+  bool ServerIndex::LookupParent(std::string& target,
+                                 const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    int64_t parentId;
+    if (db_.LookupParent(parentId, id))
+    {
+      target = db_.GetPublicId(parentId);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    uint64_t seq = IncrementGlobalSequenceInternal(sequence);
+    transaction.Commit(0);
+
+    return seq;
+  }
+
+
+
+  void ServerIndex::LogChange(ChangeType changeType,
+                              const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    LogChange(id, changeType, type, publicId);
+    transaction.Commit(0);
+  }
+
+
+  void ServerIndex::DeleteChanges()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction transaction(*this);
+    db_.ClearChanges();
+    transaction.Commit(0);
+  }
+
+  void ServerIndex::DeleteExportedResources()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction transaction(*this);
+    db_.ClearExportedResources();
+    transaction.Commit(0);
+  }
+
+
+  void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type,
+                                          /* out */ uint64_t& diskSize, 
+                                          /* out */ uint64_t& uncompressedSize, 
+                                          /* out */ unsigned int& countStudies, 
+                                          /* out */ unsigned int& countSeries, 
+                                          /* out */ unsigned int& countInstances, 
+                                          /* out */ uint64_t& dicomDiskSize, 
+                                          /* out */ uint64_t& dicomUncompressedSize, 
+                                          const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t top;
+    if (!db_.LookupResource(top, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    std::stack<int64_t> toExplore;
+    toExplore.push(top);
+
+    countInstances = 0;
+    countSeries = 0;
+    countStudies = 0;
+    diskSize = 0;
+    uncompressedSize = 0;
+    dicomDiskSize = 0;
+    dicomUncompressedSize = 0;
+
+    while (!toExplore.empty())
+    {
+      // Get the internal ID of the current resource
+      int64_t resource = toExplore.top();
+      toExplore.pop();
+
+      ResourceType thisType = db_.GetResourceType(resource);
+
+      std::list<FileContentType> f;
+      db_.ListAvailableAttachments(f, resource);
+
+      for (std::list<FileContentType>::const_iterator
+             it = f.begin(); it != f.end(); ++it)
+      {
+        FileInfo attachment;
+        if (db_.LookupAttachment(attachment, resource, *it))
+        {
+          if (attachment.GetContentType() == FileContentType_Dicom)
+          {
+            dicomDiskSize += attachment.GetCompressedSize();
+            dicomUncompressedSize += attachment.GetUncompressedSize();
+          }
+          
+          diskSize += attachment.GetCompressedSize();
+          uncompressedSize += attachment.GetUncompressedSize();
+        }
+      }
+
+      if (thisType == ResourceType_Instance)
+      {
+        countInstances++;
+      }
+      else
+      {
+        switch (thisType)
+        {
+          case ResourceType_Study:
+            countStudies++;
+            break;
+
+          case ResourceType_Series:
+            countSeries++;
+            break;
+
+          default:
+            break;
+        }
+
+        // Tag all the children of this resource as to be explored
+        std::list<int64_t> tmp;
+        db_.GetChildrenInternalId(tmp, resource);
+        for (std::list<int64_t>::const_iterator 
+               it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          toExplore.push(*it);
+        }
+      }
+    }
+
+    if (countStudies == 0)
+    {
+      countStudies = 1;
+    }
+
+    if (countSeries == 0)
+    {
+      countSeries = 1;
+    }
+  }
+
+
+  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
+                                                   unsigned int threadSleep)
+  {
+    int stableAge;
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60);
+    }
+
+    if (stableAge <= 0)
+    {
+      stableAge = 60;
+    }
+
+    LOG(INFO) << "Starting the monitor for stable resources (stable age = " << stableAge << ")";
+
+    while (!that->done_)
+    {
+      // Check for stable resources each few seconds
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
+
+      boost::mutex::scoped_lock lock(that->mutex_);
+
+      while (!that->unstableResources_.IsEmpty() &&
+             that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
+      {
+        // This DICOM resource has not received any new instance for
+        // some time. It can be considered as stable.
+          
+        UnstableResourcePayload payload;
+        int64_t id = that->unstableResources_.RemoveOldest(payload);
+
+        // Ensure that the resource is still existing before logging the change
+        if (that->db_.IsExistingResource(id))
+        {
+          switch (payload.GetResourceType())
+          {
+            case ResourceType_Patient:
+              that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId());
+              break;
+
+            case ResourceType_Study:
+              that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId());
+              break;
+
+            case ResourceType_Series:
+              that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId());
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+
+          //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id;
+        }
+      }
+    }
+
+    LOG(INFO) << "Closing the monitor thread for stable resources";
+  }
+  
+
+  void ServerIndex::MarkAsUnstable(int64_t id,
+                                   Orthanc::ResourceType type,
+                                   const std::string& publicId)
+  {
+    // WARNING: Before calling this method, "mutex_" must be locked.
+
+    assert(type == Orthanc::ResourceType_Patient ||
+           type == Orthanc::ResourceType_Study ||
+           type == Orthanc::ResourceType_Series);
+
+    UnstableResourcePayload payload(type, publicId);
+    unstableResources_.AddOrMakeMostRecent(id, payload);
+    //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
+
+    LogChange(id, ChangeType_NewChildInstance, type, publicId);
+  }
+
+
+
+  void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result,
+                                          ResourceType level,
+                                          const DicomTag& tag,
+                                          const std::string& value)
+  {
+    assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
+           (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
+           (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
+           (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
+           (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
+    
+    result.clear();
+
+    DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
+
+    std::vector<DatabaseConstraint> query;
+    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+
+    std::list<std::string> tmp;
+    
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupResources(tmp, NULL, query, level, 0);
+    }
+
+    CopyListToVector(result, tmp);
+  }
+
+
+  StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
+                                         const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction t(*this);
+
+    ResourceType resourceType;
+    int64_t resourceId;
+    if (!db_.LookupResource(resourceId, resourceType, publicId))
+    {
+      return StoreStatus_Failure;  // Inexistent resource
+    }
+
+    // Remove possible previous attachment
+    db_.DeleteAttachment(resourceId, attachment.GetContentType());
+
+    // Locate the patient of the target resource
+    int64_t patientId = resourceId;
+    for (;;)
+    {
+      int64_t parent;
+      if (db_.LookupParent(parent, patientId))
+      {
+        // We have not reached the patient level yet
+        patientId = parent;
+      }
+      else
+      {
+        // We have reached the patient level
+        break;
+      }
+    }
+
+    // Possibly apply the recycling mechanism while preserving this patient
+    assert(db_.GetResourceType(patientId) == ResourceType_Patient);
+    Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId));
+
+    db_.AddAttachment(resourceId, attachment);
+
+    if (IsUserContentType(attachment.GetContentType()))
+    {
+      LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId);
+    }
+
+    t.Commit(attachment.GetCompressedSize());
+
+    return StoreStatus_Success;
+  }
+
+
+  void ServerIndex::DeleteAttachment(const std::string& publicId,
+                                     FileContentType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.DeleteAttachment(id, type);
+
+    if (IsUserContentType(type))
+    {
+      LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId);
+    }
+
+    t.Commit(0);
+  }
+
+
+  void ServerIndex::SetGlobalProperty(GlobalProperty property,
+                                      const std::string& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction transaction(*this);
+    db_.SetGlobalProperty(property, value);
+    transaction.Commit(0);
+  }
+
+
+  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)
+  {
+    std::string value;
+
+    if (LookupGlobalProperty(value, property))
+    {
+      return value;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool ServerIndex::GetMainDicomTags(DicomMap& result,
+                                     const std::string& publicId,
+                                     ResourceType expectedType,
+                                     ResourceType levelOfInterest)
+  {
+    // Yes, the following test could be shortened, but we wish to make it as clear as possible
+    if (!(expectedType == ResourceType_Patient  && levelOfInterest == ResourceType_Patient) &&
+        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Patient) &&
+        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Study)   &&
+        !(expectedType == ResourceType_Series   && levelOfInterest == ResourceType_Series)  &&
+        !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance))
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    result.Clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != expectedType)
+    {
+      return false;
+    }
+
+    if (type == ResourceType_Study)
+    {
+      DicomMap tmp;
+      db_.GetMainDicomTags(tmp, id);
+
+      switch (levelOfInterest)
+      {
+        case ResourceType_Patient:
+          tmp.ExtractPatientInformation(result);
+          return true;
+
+        case ResourceType_Study:
+          tmp.ExtractStudyInformation(result);
+          return true;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      db_.GetMainDicomTags(result, id);
+      return true;
+    }    
+  }
+
+
+  bool ServerIndex::GetAllMainDicomTags(DicomMap& result,
+                                        const std::string& instancePublicId)
+  {
+    result.Clear();
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t instance;
+    ResourceType type;
+    if (!db_.LookupResource(instance, type, instancePublicId) ||
+        type != ResourceType_Instance)
+    {
+      return false;
+    }
+    else
+    {
+      DicomMap tmp;
+
+      db_.GetMainDicomTags(tmp, instance);
+      result.Merge(tmp);
+
+      int64_t series;
+      if (!db_.LookupParent(series, instance))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      tmp.Clear();
+      db_.GetMainDicomTags(tmp, series);
+      result.Merge(tmp);
+
+      int64_t study;
+      if (!db_.LookupParent(study, series))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      tmp.Clear();
+      db_.GetMainDicomTags(tmp, study);
+      result.Merge(tmp);
+
+#ifndef NDEBUG
+      {
+        // Sanity test to check that all the main DICOM tags from the
+        // patient level are copied at the study level
+        
+        int64_t patient;
+        if (!db_.LookupParent(patient, study))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        tmp.Clear();
+        db_.GetMainDicomTags(tmp, study);
+
+        std::set<DicomTag> patientTags;
+        tmp.GetTags(patientTags);
+
+        for (std::set<DicomTag>::const_iterator
+               it = patientTags.begin(); it != patientTags.end(); ++it)
+        {
+          assert(result.HasTag(*it));
+        }
+      }
+#endif
+      
+      return true;
+    }
+  }
+
+
+  bool ServerIndex::LookupResourceType(ResourceType& type,
+                                       const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t id;
+    return db_.LookupResource(id, type, publicId);
+  }
+
+
+  unsigned int ServerIndex::GetDatabaseVersion()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return db_.GetDatabaseVersion();
+  }
+
+
+  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);
+
+      {
+        ResourcesContent content;
+        content.AddResource(patient, ResourceType_Patient, summary);
+        content.AddResource(study, ResourceType_Study, summary);
+        content.AddResource(series, ResourceType_Series, summary);
+        content.AddResource(instance, ResourceType_Instance, summary);
+        db_.SetResourcesContent(content);
+      }
+
+      {
+        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() << "]";
+    }
+  }
+
+
+  void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                                    const DatabaseLookup& source,
+                                    ResourceType queryLevel) const
+  {
+    assert(mainDicomTagsRegistry_.get() != NULL);
+
+    target.clear();
+    target.reserve(source.GetConstraintsCount());
+
+    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
+    {
+      ResourceType level;
+      DicomTagType type;
+      
+      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
+
+      if (type == DicomTagType_Identifier ||
+          type == DicomTagType_Main)
+      {
+        // Use the fact that patient-level tags are copied at the study level
+        if (level == ResourceType_Patient &&
+            queryLevel != ResourceType_Patient)
+        {
+          level = ResourceType_Study;
+        }
+        
+        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
+      }
+    }
+  }
+
+
+  void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                         std::vector<std::string>* instancesId,
+                                         const DatabaseLookup& lookup,
+                                         ResourceType queryLevel,
+                                         size_t limit)
+  {
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, lookup, queryLevel);
+
+    std::list<std::string> resourcesList, instancesList;
+    
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (instancesId == NULL)
+      {
+        db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit);
+      }
+      else
+      {
+        db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit);
+      }
+    }
+
+    CopyListToVector(resourcesId, resourcesList);
+
+    if (instancesId != NULL)
+    { 
+      CopyListToVector(*instancesId, instancesList);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerIndex.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Cache/LeastRecentlyUsedIndex.h"
+#include "../Core/DicomFormat/DicomMap.h"
+
+#include "Database/IDatabaseWrapper.h"
+
+#include <boost/thread.hpp>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class DatabaseLookup;
+  class DicomInstanceToStore;
+  class ParsedDicomFile;
+  class ServerContext;
+
+  class ServerIndex : public boost::noncopyable
+  {
+  public:
+    typedef std::list<FileInfo> Attachments;
+    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
+
+  private:
+    class Listener;
+    class Transaction;
+    class UnstableResourcePayload;
+    class MainDicomTagsRegistry;
+
+    bool done_;
+    boost::mutex mutex_;
+    boost::thread flushThread_;
+    boost::thread unstableResourcesMonitorThread_;
+
+    std::unique_ptr<Listener> listener_;
+    IDatabaseWrapper& db_;
+    LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
+
+    uint64_t     maximumStorageSize_;
+    unsigned int maximumPatients_;
+    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
+
+    static void FlushThread(ServerIndex* that,
+                            unsigned int threadSleep);
+
+    static void UnstableResourcesMonitorThread(ServerIndex* that,
+                                               unsigned int threadSleep);
+
+    void MainDicomTagsToJson(Json::Value& result,
+                             int64_t resourceId,
+                             ResourceType resourceType);
+
+    bool IsRecyclingNeeded(uint64_t instanceSize);
+
+    void Recycle(uint64_t instanceSize,
+                 const std::string& newPatientId);
+
+    void StandaloneRecycling();
+
+    void MarkAsUnstable(int64_t id,
+                        Orthanc::ResourceType type,
+                        const std::string& publicId);
+
+    void LogChange(int64_t internalId,
+                   ChangeType changeType,
+                   ResourceType resourceType,
+                   const std::string& publicId);
+
+    void SignalNewResource(ChangeType changeType,
+                           ResourceType level,
+                           const std::string& publicId,
+                           int64_t internalId);
+
+    uint64_t IncrementGlobalSequenceInternal(GlobalProperty property);
+
+    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
+                         const DatabaseLookup& source,
+                         ResourceType level) const;
+
+    SeriesStatus GetSeriesStatus(int64_t id,
+                                 int64_t expectedNumberOfInstances);
+
+  public:
+    ServerIndex(ServerContext& context,
+                IDatabaseWrapper& database,
+                unsigned int threadSleep);
+
+    ~ServerIndex();
+
+    void Stop();
+
+    uint64_t GetMaximumStorageSize() const
+    {
+      return maximumStorageSize_;
+    }
+
+    uint64_t GetMaximumPatientCount() const
+    {
+      return maximumPatients_;
+    }
+
+    // "size == 0" means no limit on the storage size
+    void SetMaximumStorageSize(uint64_t size);
+
+    // "count == 0" means no limit on the number of patients
+    void SetMaximumPatientCount(unsigned int count);
+
+    StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
+                      DicomInstanceToStore& instance,
+                      const Attachments& attachments,
+                      bool overwrite);
+
+    void GetGlobalStatistics(/* out */ uint64_t& diskSize,
+                             /* out */ uint64_t& uncompressedSize,
+                             /* out */ uint64_t& countPatients, 
+                             /* out */ uint64_t& countStudies, 
+                             /* out */ uint64_t& countSeries, 
+                             /* out */ uint64_t& countInstances);
+
+    bool LookupResource(Json::Value& result,
+                        const std::string& publicId,
+                        ResourceType expectedType);
+
+    bool LookupAttachment(FileInfo& attachment,
+                          const std::string& instanceUuid,
+                          FileContentType contentType);
+
+    void GetAllUuids(std::list<std::string>& target,
+                     ResourceType resourceType);
+
+    void GetAllUuids(std::list<std::string>& target,
+                     ResourceType resourceType,
+                     size_t since,
+                     size_t limit);
+
+    bool DeleteResource(Json::Value& target /* out */,
+                        const std::string& uuid,
+                        ResourceType expectedType);
+
+    void GetChanges(Json::Value& target,
+                    int64_t since,
+                    unsigned int maxResults);
+
+    void GetLastChange(Json::Value& target);
+
+    void LogExportedResource(const std::string& publicId,
+                             const std::string& remoteModality);
+
+    void GetExportedResources(Json::Value& target,
+                              int64_t since,
+                              unsigned int maxResults);
+
+    void GetLastExportedResource(Json::Value& target);
+
+    bool IsProtectedPatient(const std::string& publicId);
+
+    void SetProtectedPatient(const std::string& publicId,
+                             bool isProtected);
+
+    void GetChildren(std::list<std::string>& result,
+                     const std::string& publicId);
+
+    void GetChildInstances(std::list<std::string>& result,
+                           const std::string& publicId);
+
+    void SetMetadata(const std::string& publicId,
+                     MetadataType type,
+                     const std::string& value);
+
+    void DeleteMetadata(const std::string& publicId,
+                        MetadataType type);
+
+    void GetAllMetadata(std::map<MetadataType, std::string>& target,
+                        const std::string& publicId);
+
+    bool LookupMetadata(std::string& target,
+                        const std::string& publicId,
+                        MetadataType type);
+
+    void ListAvailableAttachments(std::list<FileContentType>& target,
+                                  const std::string& publicId,
+                                  ResourceType expectedType);
+
+    bool LookupParent(std::string& target,
+                      const std::string& publicId);
+
+    uint64_t IncrementGlobalSequence(GlobalProperty sequence);
+
+    void LogChange(ChangeType changeType,
+                   const std::string& publicId);
+
+    void DeleteChanges();
+
+    void DeleteExportedResources();
+
+    void GetResourceStatistics(/* out */ ResourceType& type,
+                               /* out */ uint64_t& diskSize, 
+                               /* out */ uint64_t& uncompressedSize, 
+                               /* out */ unsigned int& countStudies, 
+                               /* out */ unsigned int& countSeries, 
+                               /* out */ unsigned int& countInstances, 
+                               /* out */ uint64_t& dicomDiskSize, 
+                               /* out */ uint64_t& dicomUncompressedSize, 
+                               const std::string& publicId);
+
+    void LookupIdentifierExact(std::vector<std::string>& result,
+                               ResourceType level,
+                               const DicomTag& tag,
+                               const std::string& value);
+
+    StoreStatus AddAttachment(const FileInfo& attachment,
+                              const std::string& publicId);
+
+    void DeleteAttachment(const std::string& publicId,
+                          FileContentType type);
+
+    void SetGlobalProperty(GlobalProperty property,
+                           const std::string& value);
+
+    bool LookupGlobalProperty(std::string& value,
+                              GlobalProperty property);
+
+    std::string GetGlobalProperty(GlobalProperty property,
+                                  const std::string& defaultValue);
+
+    bool GetMainDicomTags(DicomMap& result,
+                          const std::string& publicId,
+                          ResourceType expectedType,
+                          ResourceType levelOfInterest);
+
+    // Only applicable at the instance level
+    bool GetAllMainDicomTags(DicomMap& result,
+                             const std::string& instancePublicId);
+
+    bool LookupResourceType(ResourceType& type,
+                            const std::string& publicId);
+
+    unsigned int GetDatabaseVersion();
+
+    bool LookupParent(std::string& target,
+                      const std::string& publicId,
+                      ResourceType parentType);
+
+    void ReconstructInstance(ParsedDicomFile& dicom);
+
+    void ApplyLookupResources(std::vector<std::string>& resourcesId,
+                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
+                              const DatabaseLookup& lookup,
+                              ResourceType queryLevel,
+                              size_t limit);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerIndexChange.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,129 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/IDynamicObject.h"
+#include "../Core/SystemToolbox.h"
+
+#include <string>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  struct ServerIndexChange : public IDynamicObject
+  {
+  private:
+    int64_t      seq_;
+    ChangeType   changeType_;
+    ResourceType resourceType_;
+    std::string  publicId_;
+    std::string  date_;
+
+  public:
+    ServerIndexChange(ChangeType changeType,
+                      ResourceType resourceType,
+                      const std::string& publicId) :
+      seq_(-1),
+      changeType_(changeType),
+      resourceType_(resourceType),
+      publicId_(publicId),
+      date_(SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */))
+    {
+    }
+
+    ServerIndexChange(int64_t seq,
+                      ChangeType changeType,
+                      ResourceType resourceType,
+                      const std::string& publicId,
+                      const std::string& date) :
+      seq_(seq),
+      changeType_(changeType),
+      resourceType_(resourceType),
+      publicId_(publicId),
+      date_(date)
+    {
+    }
+
+    ServerIndexChange(const ServerIndexChange& other) 
+    : seq_(other.seq_),
+      changeType_(other.changeType_),
+      resourceType_(other.resourceType_),
+      publicId_(other.publicId_),
+      date_(other.date_)
+    {
+    }
+
+    ServerIndexChange* Clone() const
+    {
+      return new ServerIndexChange(*this);
+    }
+
+    int64_t  GetSeq() const
+    {
+      return seq_;
+    }
+
+    ChangeType  GetChangeType() const
+    {
+      return changeType_;
+    }
+
+    ResourceType  GetResourceType() const
+    {
+      return resourceType_;
+    }
+
+    const std::string&  GetPublicId() const
+    {
+      return publicId_;
+    }
+
+    const std::string& GetDate() const
+    {
+      return date_;
+    }
+
+    void Format(Json::Value& item) const
+    {
+      item = Json::objectValue;
+      item["Seq"] = static_cast<int>(seq_);
+      item["ChangeType"] = EnumerationToString(changeType_);
+      item["ResourceType"] = EnumerationToString(resourceType_);
+      item["ID"] = publicId_;
+      item["Path"] = GetBasePath(resourceType_, publicId_);
+      item["Date"] = date_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1114 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Cache/SharedArchive.h"
+#include "../../Core/Compression/HierarchicalZipWriter.h"
+#include "../../Core/DicomParsing/DicomDirWriter.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../OrthancConfiguration.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;
+
+static const char* const MEDIA_IMAGES_FOLDER = "IMAGES"; 
+static const char* const KEY_DESCRIPTION = "Description";
+static const char* const KEY_INSTANCES_COUNT = "InstancesCount";
+static const char* const KEY_UNCOMPRESSED_SIZE_MB = "UncompressedSizeMB";
+static const char* const KEY_TRANSCODE = "Transcode";
+
+
+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::unique_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::unique_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,
+                 bool transcode,
+                 DicomTransferSyntax transferSyntax) 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());
+
+            bool transcodeSuccess = false;
+
+            std::unique_ptr<ParsedDicomFile> parsed;
+            
+            if (transcode)
+            {
+              // New in Orthanc 1.7.0
+              std::set<DicomTransferSyntax> syntaxes;
+              syntaxes.insert(transferSyntax);
+
+              IDicomTranscoder::DicomImage source, transcoded;
+              source.SetExternalBuffer(content);
+
+              if (context.Transcode(transcoded, source, syntaxes, true /* allow new SOP instance UID */))
+              {
+                writer.Write(transcoded.GetBufferData(), transcoded.GetBufferSize());
+
+                if (dicomDir != NULL)
+                {
+                  std::unique_ptr<ParsedDicomFile> tmp(transcoded.ReleaseAsParsedDicomFile());
+                  dicomDir->Add(dicomDirFolder, filename_, *tmp);
+                }
+                
+                transcodeSuccess = true;
+              }
+              else
+              {
+                LOG(INFO) << "Cannot transcode instance " << instanceId_
+                          << " to transfer syntax: " << GetTransferSyntaxUid(transferSyntax);
+              }
+            }
+
+            if (!transcodeSuccess)
+            {
+              writer.Write(content);
+
+              if (dicomDir != NULL)
+              {
+                if (parsed.get() == NULL)
+                {
+                  parsed.reset(new ParsedDicomFile(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,
+                       bool transcode,
+                       DicomTransferSyntax transferSyntax) const
+    {
+      if (index >= commands_.size())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder, transcode, transferSyntax);
+    }
+      
+  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_;
+    }
+
+    // "media" flavor (with DICOMDIR)
+    void Apply(HierarchicalZipWriter& writer,
+               ServerContext& context,
+               size_t index,
+               DicomDirWriter& dicomDir,
+               const std::string& dicomDirFolder,
+               bool transcode,
+               DicomTransferSyntax transferSyntax) const
+    {
+      ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder, transcode, transferSyntax);
+    }
+
+    // "archive" flavor (without DICOMDIR)
+    void Apply(HierarchicalZipWriter& writer,
+               ServerContext& context,
+               size_t index,
+               bool transcode,
+               DicomTransferSyntax transferSyntax) const
+    {
+      ApplyInternal(writer, context, index, NULL, "", transcode, transferSyntax);
+    }
+      
+    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::unique_ptr<HierarchicalZipWriter>  zip_;
+    std::unique_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,
+                 bool transcode,
+                 DicomTransferSyntax transferSyntax)
+    {
+      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, transcode, transferSyntax);
+        }
+        else
+        {
+          assert(dicomDir_.get() == NULL);
+          commands_.Apply(*zip_, context_, index, transcode, transferSyntax);
+        }
+      }
+    }
+
+    unsigned int GetInstancesCount() const
+    {
+      return commands_.GetInstancesCount();
+    }
+
+    uint64_t GetUncompressedSize() const
+    {
+      return commands_.GetUncompressedSize();
+    }
+  };
+
+
+  ArchiveJob::ArchiveJob(ServerContext& context,
+                         bool isMedia,
+                         bool enableExtendedSopClass) :
+    context_(context),
+    archive_(new ArchiveIndex(ResourceType_Patient)),  // root
+    isMedia_(isMedia),
+    enableExtendedSopClass_(enableExtendedSopClass),
+    currentStep_(0),
+    instancesCount_(0),
+    uncompressedSize_(0),
+    transcode_(false),
+    transferSyntax_(DicomTransferSyntax_LittleEndianImplicit)
+  {
+  }
+
+  
+  ArchiveJob::~ArchiveJob()
+  {
+    if (!mediaArchiveId_.empty())
+    {
+      context_.GetMediaArchive().Remove(mediaArchiveId_);
+    }
+  }
+
+
+  void ArchiveJob::SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& target)
+  {
+    if (target.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (writer_.get() != NULL ||  // Already started
+             synchronousTarget_.get() != NULL ||
+             asynchronousTarget_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      synchronousTarget_ = target;
+    }
+  }
+
+
+  void ArchiveJob::SetDescription(const std::string& description)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      description_ = description;
+    }
+  }
+
+  
+  void ArchiveJob::AddResource(const std::string& publicId)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      ResourceIdentifiers resource(context_.GetIndex(), publicId);
+      archive_->Add(context_.GetIndex(), resource);
+    }
+  }
+
+
+  void ArchiveJob::SetTranscode(DicomTransferSyntax transferSyntax)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = true;
+      transferSyntax_ = transferSyntax;
+    }
+  }
+
+  
+  void ArchiveJob::Reset()
+  {
+    throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                           "Cannot resubmit the creation of an archive");
+  }
+
+  
+  void ArchiveJob::Start()
+  {
+    TemporaryFile* target = NULL;
+    
+    if (synchronousTarget_.get() == NULL)
+    {
+      {
+        OrthancConfiguration::ReaderLock lock;
+        asynchronousTarget_.reset(lock.GetConfiguration().CreateTemporaryFile());
+      }
+
+      target = asynchronousTarget_.get();
+    }
+    else
+    {
+      target = synchronousTarget_.get();
+    }
+
+    assert(target != NULL);
+    target->Touch();  // Make sure we can write to the temporary file
+    
+    if (writer_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    writer_.reset(new ZipWriterIterator(*target, context_, *archive_,
+                                        isMedia_, enableExtendedSopClass_));
+
+    instancesCount_ = writer_->GetInstancesCount();
+    uncompressedSize_ = writer_->GetUncompressedSize();
+  }
+
+
+
+  namespace
+  {
+    class DynamicTemporaryFile : public IDynamicObject
+    {
+    private:
+      std::unique_ptr<TemporaryFile>   file_;
+
+    public:
+      DynamicTemporaryFile(TemporaryFile* f) : file_(f)
+      {
+        if (f == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+      }
+
+      const TemporaryFile& GetFile() const
+      {
+        assert(file_.get() != NULL);
+        return *file_;
+      }
+    };
+  }
+  
+
+  void ArchiveJob::FinalizeTarget()
+  {
+    writer_.reset();  // Flush all the results
+
+    if (asynchronousTarget_.get() != NULL)
+    {
+      // Asynchronous behavior: Move the resulting file into the media archive
+      mediaArchiveId_ = context_.GetMediaArchive().Add(
+        new DynamicTemporaryFile(asynchronousTarget_.release()));
+    }
+  }
+    
+
+  JobStepResult ArchiveJob::Step(const std::string& jobId)
+  {
+    assert(writer_.get() != NULL);
+
+    if (synchronousTarget_.get() != NULL &&
+        synchronousTarget_.unique())
+    {
+      LOG(WARNING) << "A client has disconnected while creating an archive";
+      return JobStepResult::Failure(ErrorCode_NetworkProtocol,
+                                    "A client has disconnected while creating an archive");
+    }
+        
+    if (writer_->GetStepsCount() == 0)
+    {
+      FinalizeTarget();
+      return JobStepResult::Success();
+    }
+    else
+    {
+      writer_->RunStep(currentStep_, transcode_, transferSyntax_);
+
+      currentStep_ ++;
+
+      if (currentStep_ == writer_->GetStepsCount())
+      {
+        FinalizeTarget();
+        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 = Json::objectValue;
+    value[KEY_DESCRIPTION] = description_;
+    value[KEY_INSTANCES_COUNT] = instancesCount_;
+    value[KEY_UNCOMPRESSED_SIZE_MB] =
+      static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
+
+    if (transcode_)
+    {
+      value[KEY_TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
+    }
+  }
+
+
+  bool ArchiveJob::GetOutput(std::string& output,
+                             MimeType& mime,
+                             const std::string& key)
+  {   
+    if (key == "archive" &&
+        !mediaArchiveId_.empty())
+    {
+      SharedArchive::Accessor accessor(context_.GetMediaArchive(), mediaArchiveId_);
+
+      if (accessor.IsValid())
+      {
+        const DynamicTemporaryFile& f = dynamic_cast<DynamicTemporaryFile&>(accessor.GetItem());
+        f.GetFile().Read(output);
+        mime = MimeType_Zip;
+        return true;
+      }
+      else
+      {
+        return false;
+      }
+    }    
+    else
+    {
+      return false;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../Core/JobsEngine/IJob.h"
+#include "../../Core/TemporaryFile.h"
+
+#include <boost/shared_ptr.hpp>
+#include <stdint.h>
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class ArchiveJob : public IJob
+  {
+  private:
+    class ArchiveIndex;
+    class ArchiveIndexVisitor;
+    class IArchiveVisitor;
+    class MediaIndexVisitor;
+    class ResourceIdentifiers;
+    class ZipCommands;
+    class ZipWriterIterator;
+    
+    boost::shared_ptr<TemporaryFile>      synchronousTarget_;
+    std::unique_ptr<TemporaryFile>        asynchronousTarget_;
+    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_;
+    std::string                           mediaArchiveId_;
+
+    // New in Orthanc 1.7.0
+    bool                 transcode_;
+    DicomTransferSyntax  transferSyntax_;
+    
+    void FinalizeTarget();
+    
+  public:
+    ArchiveJob(ServerContext& context,
+               bool isMedia,
+               bool enableExtendedSopClass);
+    
+    virtual ~ArchiveJob();
+    
+    void SetSynchronousTarget(boost::shared_ptr<TemporaryFile>& synchronousTarget);
+
+    void SetDescription(const std::string& description);
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    void AddResource(const std::string& publicId);
+
+    void SetTranscode(DicomTransferSyntax transferSyntax);
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    virtual void Start() ORTHANC_OVERRIDE;
+
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual float GetProgress() ORTHANC_OVERRIDE;
+
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE;
+    
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      return false;  // Cannot serialize this kind of job
+    }
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "CleaningInstancesJob.h"
+
+#include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+
+
+namespace Orthanc
+{
+  bool CleaningInstancesJob::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;
+  }
+
+  
+  void CleaningInstancesJob::SetKeepSource(bool keep)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    keepSource_ = keep;
+  }
+
+
+  static const char* KEEP_SOURCE = "KeepSource";
+
+
+  CleaningInstancesJob::CleaningInstancesJob(ServerContext& context,
+                                             const Json::Value& serialized,
+                                             bool defaultKeepSource) :
+    SetOfInstancesJob(serialized),  // (*)
+    context_(context)
+  {
+    if (!HasTrailingStep())
+    {
+      // Should have been set by (*)
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (serialized.isMember(KEEP_SOURCE))
+    {
+      keepSource_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SOURCE);
+    }
+    else
+    {
+      keepSource_ = defaultKeepSource;
+    }
+  }
+
+  
+  bool CleaningInstancesJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[KEEP_SOURCE] = keepSource_;
+      return true;
+    }
+  }
+
+
+  void CleaningInstancesJob::Start()
+  {
+    if (!HasTrailingStep())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "AddTrailingStep() should have been called before submitting the job");
+    }
+
+    SetOfInstancesJob::Start();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/CleaningInstancesJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class CleaningInstancesJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&  context_;
+    bool            keepSource_;
+    
+  protected:
+    virtual bool HandleTrailingStep();
+    
+  public:
+    CleaningInstancesJob(ServerContext& context,
+                         bool keepSource) :
+      context_(context),
+      keepSource_(keepSource)
+    {
+    }
+
+    CleaningInstancesJob(ServerContext& context,
+                         const Json::Value& serialized,
+                         bool defaultKeepSource);
+
+    ServerContext& GetContext() const
+    {
+      return context_;
+    }
+    
+    bool IsKeepSource() const
+    {
+      return keepSource_;
+    }
+    
+    void SetKeepSource(bool keep);
+
+    virtual bool Serialize(Json::Value& target);
+
+    virtual void Start();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../Core/DicomNetworking/DicomAssociation.h"
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+#include "../StorageCommitmentReports.h"
+
+
+namespace Orthanc
+{
+  void DicomModalityStoreJob::OpenConnection()
+  {
+    if (connection_.get() == NULL)
+    {
+      connection_.reset(new DicomStoreUserConnection(parameters_));
+    }
+  }
+
+
+  bool DicomModalityStoreJob::HandleInstance(const std::string& instance)
+  {
+    assert(IsStarted());
+    OpenConnection();
+
+    LOG(INFO) << "Sending instance " << instance << " to modality \"" 
+              << parameters_.GetRemoteModality().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;
+    }
+    
+    std::string sopClassUid, sopInstanceUid;
+    context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, *connection_, dicom,
+                                  HasMoveOriginator(), moveOriginatorAet_, moveOriginatorId_);
+
+    if (storageCommitment_)
+    {
+      sopClassUids_.push_back(sopClassUid);
+      sopInstanceUids_.push_back(sopInstanceUid);
+
+      if (sopClassUids_.size() != sopInstanceUids_.size() ||
+          sopClassUids_.size() > GetInstancesCount())
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+      
+      if (sopClassUids_.size() == GetInstancesCount())
+      {
+        assert(IsStarted());
+        connection_.reset(NULL);
+        
+        const std::string& remoteAet = parameters_.GetRemoteModality().GetApplicationEntityTitle();
+        
+        LOG(INFO) << "Sending storage commitment request to modality: " << remoteAet;
+
+        // Create a "pending" storage commitment report BEFORE the
+        // actual SCU call in order to avoid race conditions
+        context_.GetStorageCommitmentReports().Store(
+          transactionUid_, new StorageCommitmentReports::Report(remoteAet));
+        
+        std::vector<std::string> a(sopClassUids_.begin(), sopClassUids_.end());
+        std::vector<std::string> b(sopInstanceUids_.begin(), sopInstanceUids_.end());
+
+        DicomAssociation::RequestStorageCommitment(parameters_, transactionUid_, a, b);
+      }
+    }
+
+    //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+    return true;
+  }
+    
+
+  bool DicomModalityStoreJob::HandleTrailingStep()
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+
+  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
+    context_(context),
+    moveOriginatorId_(0),      // By default, not a C-MOVE
+    storageCommitment_(false)  // By default, no storage commitment
+  {
+    ResetStorageCommitment();
+  }
+
+
+  void DicomModalityStoreJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetLocalApplicationEntityTitle(aet);
+    }
+  }
+
+
+  void DicomModalityStoreJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetRemoteModality(remote);
+    }
+  }
+
+    
+  void DicomModalityStoreJob::SetTimeout(uint32_t seconds)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetTimeout(seconds);
+    }
+  }
+
+
+  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::ResetStorageCommitment()
+  {
+    if (storageCommitment_)
+    {
+      transactionUid_ = Toolbox::GenerateDicomPrivateUniqueIdentifier();
+      sopClassUids_.clear();
+      sopInstanceUids_.clear();
+    }
+  }
+  
+
+  void DicomModalityStoreJob::Reset()
+  {
+    SetOfInstancesJob::Reset();
+
+    /**
+     * "After the N-EVENT-REPORT has been sent, the Transaction UID is
+     * no longer active and shall not be reused for other
+     * transactions." => Need to reset the transaction UID here
+     * http://dicom.nema.org/medical/dicom/2019a/output/chtml/part04/sect_J.3.3.html
+     **/
+    ResetStorageCommitment();
+  }
+  
+
+  void DicomModalityStoreJob::EnableStorageCommitment(bool enabled)
+  {
+    storageCommitment_ = enabled;
+    ResetStorageCommitment();
+  }
+  
+
+  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+    
+    value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
+    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
+
+    if (HasMoveOriginator())
+    {
+      value["MoveOriginatorAET"] = GetMoveOriginatorAet();
+      value["MoveOriginatorID"] = GetMoveOriginatorId();
+    }
+
+    if (storageCommitment_)
+    {
+      value["StorageCommitmentTransactionUID"] = transactionUid_;
+    }
+  }
+
+
+  static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
+  static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
+  static const char* STORAGE_COMMITMENT = "StorageCommitment";
+  
+
+  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
+                                               const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
+    moveOriginatorId_ = static_cast<uint16_t>
+      (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
+    EnableStorageCommitment(SerializationToolbox::ReadBoolean(serialized, STORAGE_COMMITMENT));
+
+    parameters_ = DicomAssociationParameters::UnserializeJob(serialized);
+  }
+
+
+  bool DicomModalityStoreJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      parameters_.SerializeJob(target);
+      target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
+      target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
+      target[STORAGE_COMMITMENT] = storageCommitment_;
+      return true;
+    }
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomModalityStoreJob.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,119 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../../Core/DicomNetworking/DicomStoreUserConnection.h"
+
+#include <list>
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class DicomModalityStoreJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&                             context_;
+    DicomAssociationParameters                 parameters_;
+    std::string                                moveOriginatorAet_;
+    uint16_t                                   moveOriginatorId_;
+    std::unique_ptr<DicomStoreUserConnection>  connection_;
+    bool                                       storageCommitment_;
+
+    // For storage commitment
+    std::string             transactionUid_;
+    std::list<std::string>  sopInstanceUids_;
+    std::list<std::string>  sopClassUids_;
+
+    void OpenConnection();
+
+    void ResetStorageCommitment();
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE;
+    
+    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE;
+
+  public:
+    DicomModalityStoreJob(ServerContext& context);
+
+    DicomModalityStoreJob(ServerContext& context,
+                          const Json::Value& serialized);
+
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+
+    void SetLocalAet(const std::string& aet);
+
+    void SetRemoteModality(const RemoteModalityParameters& remote);
+
+    void SetTimeout(uint32_t seconds);
+
+    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) ORTHANC_OVERRIDE;
+
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    {
+      target = "DicomModalityStore";
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+
+    virtual void Reset() ORTHANC_OVERRIDE;
+
+    void EnableStorageCommitment(bool enabled);
+
+    bool HasStorageCommitment() const
+    {
+      return storageCommitment_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+#include "../ServerContext.h"
+
+static const char* const LOCAL_AET = "LocalAet";
+static const char* const TARGET_AET = "TargetAet";
+static const char* const REMOTE = "Remote";
+static const char* const QUERY = "Query";
+static const char* const TIMEOUT = "Timeout";
+
+namespace Orthanc
+{
+  class DicomMoveScuJob::Command : public SetOfCommandsJob::ICommand
+  {
+  private:
+    DicomMoveScuJob&           that_;
+    std::unique_ptr<DicomMap>  findAnswer_;
+
+  public:
+    Command(DicomMoveScuJob& that,
+            const DicomMap&  findAnswer) :
+      that_(that),
+      findAnswer_(findAnswer.Clone())
+    {
+    }
+
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Retrieve(*findAnswer_);
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      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 DicomControlUserConnection(parameters_));
+    }
+    
+    connection_->Move(targetAet_, findAnswer);
+  }
+
+
+  static void AddTagIfString(Json::Value& target,
+                             const DicomMap& answer,
+                             const DicomTag& tag)
+  {
+    const DicomValue* value = answer.TestAndGetValue(tag);
+    if (value != NULL &&
+        !value->IsNull() &&
+        !value->IsBinary())
+    {
+      target[tag.Format()] = value->GetContent();
+    }
+  }
+  
+
+  void DicomMoveScuJob::AddFindAnswer(const DicomMap& answer)
+  {
+    assert(query_.type() == Json::arrayValue);
+
+    // Copy the identifiers tags, if they exist
+    Json::Value item = Json::objectValue;
+    AddTagIfString(item, answer, DICOM_TAG_QUERY_RETRIEVE_LEVEL);
+    AddTagIfString(item, answer, DICOM_TAG_PATIENT_ID);
+    AddTagIfString(item, answer, DICOM_TAG_STUDY_INSTANCE_UID);
+    AddTagIfString(item, answer, DICOM_TAG_SERIES_INSTANCE_UID);
+    AddTagIfString(item, answer, DICOM_TAG_SOP_INSTANCE_UID);
+    AddTagIfString(item, answer, DICOM_TAG_ACCESSION_NUMBER);
+    query_.append(item);
+    
+    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
+    {
+      parameters_.SetLocalApplicationEntityTitle(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
+    {
+      parameters_.SetRemoteModality(remote);
+    }
+  }
+
+
+  void DicomMoveScuJob::SetTimeout(uint32_t seconds)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      parameters_.SetTimeout(seconds);
+    }
+  }
+
+  
+  void DicomMoveScuJob::Stop(JobStopReason reason)
+  {
+    connection_.reset();
+  }
+  
+
+  void DicomMoveScuJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+
+    value["LocalAet"] = parameters_.GetLocalApplicationEntityTitle();
+    value["RemoteAet"] = parameters_.GetRemoteModality().GetApplicationEntityTitle();
+    value["Query"] = query_;
+  }
+
+
+  DicomMoveScuJob::DicomMoveScuJob(ServerContext& context,
+                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context),
+    query_(Json::arrayValue)
+  {
+    parameters_ = DicomAssociationParameters::UnserializeJob(serialized);
+    targetAet_ = SerializationToolbox::ReadString(serialized, TARGET_AET);
+
+    if (serialized.isMember(QUERY) &&
+        serialized[QUERY].type() == Json::arrayValue)
+    {
+      query_ = serialized[QUERY];
+    }
+  }
+
+  
+  bool DicomMoveScuJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      parameters_.SerializeJob(target);
+      target[TARGET_AET] = targetAet_;
+      target[QUERY] = query_;
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/DicomMoveScuJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../Core/DicomNetworking/DicomControlUserConnection.h"
+#include "../../Core/JobsEngine/SetOfCommandsJob.h"
+
+#include "../QueryRetrieveHandler.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class DicomMoveScuJob : public SetOfCommandsJob
+  {
+  private:
+    class Command;
+    class Unserializer;
+    
+    ServerContext&              context_;
+    DicomAssociationParameters  parameters_;
+    std::string                 targetAet_;
+    Json::Value                 query_;
+
+    std::unique_ptr<DicomControlUserConnection>  connection_;
+    
+    void Retrieve(const DicomMap& findAnswer);
+    
+  public:
+    DicomMoveScuJob(ServerContext& context) :
+      context_(context),
+      query_(Json::arrayValue)
+    {
+    }
+
+    DicomMoveScuJob(ServerContext& context,
+                    const Json::Value& serialized);
+
+    void AddFindAnswer(const DicomMap& answer);
+    
+    void AddFindAnswer(QueryRetrieveHandler& query,
+                       size_t i);
+
+    const DicomAssociationParameters& GetParameters() const
+    {
+      return parameters_;
+    }
+    
+    void SetLocalAet(const std::string& aet);
+
+    void SetRemoteModality(const RemoteModalityParameters& remote);
+
+    void SetTimeout(uint32_t timeout);
+
+    const std::string& GetTargetAet() const
+    {
+      return targetAet_;
+    }
+    
+    void SetTargetAet(const std::string& aet);
+
+    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/Sources/ServerJobs/IStorageCommitmentFactory.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <vector>
+
+namespace Orthanc
+{
+  class IStorageCommitmentFactory : public boost::noncopyable
+  {
+  public:
+    class ILookupHandler : public boost::noncopyable
+    {
+    public:
+      virtual ~ILookupHandler()
+      {
+      }
+
+      virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid) = 0;
+    };
+
+    virtual ~IStorageCommitmentFactory()
+    {
+    }
+
+    virtual ILookupHandler* CreateStorageCommitment(const std::string& jobId,
+                                                    const std::string& transactionUid,
+                                                    const std::vector<std::string>& sopClassUids,
+                                                    const std::vector<std::string>& sopInstanceUids,
+                                                    const std::string& remoteAet,
+                                                    const std::string& calledAet) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/LuaJobManager.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,280 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancConfiguration.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)
+  {
+    unsigned int dicomTimeout;
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      dicomTimeout = lock.GetConfiguration().GetUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+    }
+
+    connectionManager_.SetInactivityTimeout(dicomTimeout * 1000);  // Milliseconds expected
+    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_->Register(that_);
+      that_.currentJob_->SetDescription("Lua");
+
+      {
+        jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
+        jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_);
+      }
+    }
+
+    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(ServerContext& context,
+                                                   const std::string& localAet,
+                                                   const RemoteModalityParameters& modality)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new StoreScuOperation(
+                                    context, that_.connectionManager_, 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/Sources/ServerJobs/LuaJobManager.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomNetworking/TimeoutDicomConnectionManager.h"
+#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_;
+    TimeoutDicomConnectionManager  connectionManager_;
+
+    virtual void SignalDone(const SequenceOfOperationsJob& job);
+
+  public:
+    LuaJobManager();
+
+    void SetMaxOperationsPerJob(size_t count);
+
+    void SetPriority(int priority);
+
+    void SetTrailingOperationTimeout(unsigned int timeout);
+
+    void AwakeTrailingSleep();
+
+    TimeoutDicomConnectionManager& GetDicomConnectionManager()
+    {
+      return connectionManager_;
+    }
+
+    class Lock : public boost::noncopyable
+    {
+    private:
+      LuaJobManager&                                  that_;
+      boost::mutex::scoped_lock                       lock_;
+      JobsEngine&                                     engine_;
+      std::unique_ptr<SequenceOfOperationsJob::Lock>  jobLock_;
+      bool                                            isNewJob_;
+
+    public:
+      Lock(LuaJobManager& that,
+           JobsEngine& engine);
+
+      ~Lock();
+
+      size_t AddLogOperation();
+
+      size_t AddDeleteResourceOperation(ServerContext& context);
+
+      size_t AddStoreScuOperation(ServerContext& context,
+                                  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/Sources/ServerJobs/MergeStudyJob.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+#include "../ServerContext.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;
+    GetContext().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_)
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot merge a study into the same study: " + study);
+    }
+    else
+    {
+      std::list<std::string> series;
+      GetContext().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)
+  {
+    if (!HasTrailingStep())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "AddTrailingStep() should have been called after AddSourceXXX()");
+    }
+    
+    /**
+     * Retrieve the DICOM instance to be modified
+     **/
+    
+    std::unique_ptr<ParsedDicomFile> modified;
+
+    try
+    {
+      ServerContext::DicomCacheLocker locker(GetContext(), 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);
+
+    // Fix since Orthanc 1.5.8: Assign new "SOPInstanceUID", as the instance has been modified
+    modified->ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(origin_);
+    toStore.SetParsedDicomFile(*modified);
+
+    std::string modifiedInstance;
+    if (GetContext().Store(modifiedInstance, toStore,
+                       StoreInstanceMode_Default) != StoreStatus_Success)
+    {
+      LOG(ERROR) << "Error while storing a modified instance " << instance;
+      return false;
+    }
+
+    return true;
+  }
+
+  
+  MergeStudyJob::MergeStudyJob(ServerContext& context,
+                               const std::string& targetStudy) :
+    CleaningInstancesJob(context, false /* by default, remove source instances */),
+    targetStudy_(targetStudy)
+  {
+    /**
+     * Check the validity of the input ID
+     **/
+    
+    ResourceType type;
+
+    if (!GetContext().GetIndex().LookupResourceType(type, targetStudy) ||
+        type != ResourceType_Study)
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot merge into an unknown study: " + targetStudy);
+    }
+
+
+    /**
+     * 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;
+    GetContext().GetIndex().GetChildInstances(instances, targetStudy);
+    
+    if (instances.empty())
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    DicomMap dicom;
+
+    {
+      ServerContext::DicomCacheLocker locker(GetContext(), 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 (!GetContext().GetIndex().LookupResourceType(level, studyOrSeries))
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot find this resource: " + studyOrSeries);
+    }
+    else
+    {
+      switch (level)
+      {
+        case ResourceType_Study:
+          AddSourceStudyInternal(studyOrSeries);
+          break;
+          
+        case ResourceType_Series:
+          AddSourceSeries(studyOrSeries);
+          break;
+          
+        default:
+          throw OrthancException(ErrorCode_UnknownResource,
+                                 "This resource is neither a study, nor a series: " +
+                                 studyOrSeries + " is a " +
+                                 std::string(EnumerationToString(level)));
+      }
+    }    
+  }
+  
+
+  void MergeStudyJob::AddSourceSeries(const std::string& series)
+  {
+    std::string parent;
+
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study))
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "This resource is not a series: " + series);
+    }
+    else if (parent == targetStudy_)
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot merge series " + series +
+                             " into its parent study " + targetStudy_);
+    }
+    else
+    {
+      AddSourceSeriesInternal(series);
+    }    
+  }
+
+
+  void MergeStudyJob::AddSourceStudy(const std::string& study)
+  {
+    ResourceType actualLevel;
+
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (!GetContext().GetIndex().LookupResourceType(actualLevel, study) ||
+             actualLevel != ResourceType_Study)
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "This resource is not a study: " + study);
+    }
+    else
+    {
+      AddSourceStudyInternal(study);
+    }    
+  }
+
+
+  void MergeStudyJob::GetPublicContent(Json::Value& value)
+  {
+    CleaningInstancesJob::GetPublicContent(value);
+    value["TargetStudy"] = targetStudy_;
+  }
+
+
+  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) :
+    CleaningInstancesJob(context, serialized,
+                         false /* by default, remove source instances */)  // (*)
+  {
+    if (!HasTrailingStep())
+    {
+      // Should have been set by (*)
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    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 (!CleaningInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      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/Sources/ServerJobs/MergeStudyJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../DicomInstanceOrigin.h"
+#include "CleaningInstancesJob.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class MergeStudyJob : public CleaningInstancesJob
+  {
+  private:
+    typedef std::map<std::string, std::string>  SeriesUidMap;
+    typedef std::map<DicomTag, std::string>     Replacements;
+    
+    
+    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);
+
+  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);
+
+    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/Sources/ServerJobs/Operations/DeleteResourceOperation.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../ServerContext.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void DeleteResourceOperation::Apply(JobOperationValues& outputs,
+                                      const JobOperationValue& input)
+  {
+    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/Sources/ServerJobs/Operations/DeleteResourceOperation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class DeleteResourceOperation : public IJobOperation
+  {
+  private:
+    ServerContext&  context_;
+
+  public:
+    DeleteResourceOperation(ServerContext& context) :
+    context_(context)
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input);
+
+    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/Sources/ServerJobs/Operations/DicomInstanceOperationValue.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,45 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "DicomInstanceOperationValue.h"
+
+#include "../../ServerContext.h"
+
+namespace Orthanc
+{
+  void DicomInstanceOperationValue::ReadDicom(std::string& dicom) const
+  {
+    context_.ReadDicom(dicom, id_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/Operations/DicomInstanceOperationValue.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  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;
+
+    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/Sources/ServerJobs/Operations/ModifyInstanceOperation.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../ServerContext.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)
+  {
+    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::unique_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, StoreInstanceMode_Default);
+
+      // 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/Sources/ServerJobs/Operations/ModifyInstanceOperation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../../Core/DicomParsing/DicomModification.h"
+#include "../../../Core/JobsEngine/Operations/IJobOperation.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class ModifyInstanceOperation : public IJobOperation
+  {
+  private:
+    ServerContext&                      context_;
+    RequestOrigin                       origin_;
+    std::unique_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);
+
+    virtual void Serialize(Json::Value& target) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/Operations/StorePeerOperation.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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)
+  {
+    // 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/Sources/ServerJobs/Operations/StorePeerOperation.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,65 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/Operations/StoreScuOperation.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../ServerContext.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+#include "../../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  void StoreScuOperation::Apply(JobOperationValues& outputs,
+                                const JobOperationValue& input)
+  {
+    TimeoutDicomConnectionManager::Lock lock(connectionManager_, localAet_, modality_);
+    
+    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);
+
+      std::string sopClassUid, sopInstanceUid;  // Unused
+      context_.StoreWithTranscoding(sopClassUid, sopInstanceUid, lock.GetConnection(), dicom,
+                                    false /* Not a C-MOVE */, "", 0);
+    }
+    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(ServerContext& context,
+                                       TimeoutDicomConnectionManager& connectionManager,
+                                       const Json::Value& serialized) :
+    context_(context),
+    connectionManager_(connectionManager)
+  {
+    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/Sources/ServerJobs/Operations/StoreScuOperation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/TimeoutDicomConnectionManager.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class StoreScuOperation : public IJobOperation
+  {
+  private:
+    ServerContext&                  context_;
+    TimeoutDicomConnectionManager&  connectionManager_;
+    std::string                     localAet_;
+    RemoteModalityParameters        modality_;
+    
+  public:
+    StoreScuOperation(ServerContext& context,
+                      TimeoutDicomConnectionManager& connectionManager,
+                      const std::string& localAet,
+                      const RemoteModalityParameters& modality) :
+      context_(context),
+      connectionManager_(connectionManager),
+      localAet_(localAet),
+      modality_(modality)
+    {
+    }
+
+    StoreScuOperation(ServerContext& context,
+                      TimeoutDicomConnectionManager& connectionManager,
+                      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);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/Operations/SystemCallOperation.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,170 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+#include "../../../Core/SystemToolbox.h"
+#include "../../OrthancConfiguration.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)
+  {
+    std::vector<std::string> arguments = preArguments_;
+
+    arguments.reserve(arguments.size() + postArguments_.size() + 1);
+
+    std::unique_ptr<TemporaryFile> tmp;
+    
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_DicomInstance:
+      {
+        const DicomInstanceOperationValue& instance =
+          dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+        std::string dicom;
+        instance.ReadDicom(dicom);
+
+        {
+          OrthancConfiguration::ReaderLock lock;
+          tmp.reset(lock.GetConfiguration().CreateTemporaryFile());
+        }
+
+        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/Sources/ServerJobs/Operations/SystemCallOperation.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/OrthancJobUnserializer.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,156 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../../Plugins/Engine/OrthancPlugins.h"
+#include "../ServerContext.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 "MergeStudyJob.h"
+#include "OrthancPeerStoreJob.h"
+#include "ResourceModificationJob.h"
+#include "SplitStudyJob.h"
+#include "StorageCommitmentScpJob.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::unique_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 if (type == "StorageCommitmentScp")
+    {
+      return new StorageCommitmentScpJob(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(
+        context_, context_.GetLuaScripting().GetDicomConnectionManager(), 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/Sources/ServerJobs/OrthancJobUnserializer.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/GenericJobUnserializer.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  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/Sources/ServerJobs/OrthancPeerStoreJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,245 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+#include "../../Core/SerializationToolbox.h"
+#include "../ServerContext.h"
+
+#include <dcmtk/dcmdata/dcfilefo.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
+    {
+      if (transcode_)
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, instance);
+
+        std::set<DicomTransferSyntax> syntaxes;
+        syntaxes.insert(transferSyntax_);
+        
+        IDicomTranscoder::DicomImage source, transcoded;
+        source.SetExternalBuffer(dicom);
+
+        if (context_.Transcode(transcoded, source, syntaxes, true))
+        {
+          client_->GetBody().assign(reinterpret_cast<const char*>(transcoded.GetBufferData()),
+                                    transcoded.GetBufferSize());
+        }
+        else
+        {
+          client_->GetBody().swap(dicom);
+        }
+      }
+      else
+      {
+        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;
+    }
+  }
+
+
+  DicomTransferSyntax OrthancPeerStoreJob::GetTransferSyntax() const
+  {
+    if (transcode_)
+    {
+      return transferSyntax_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  void OrthancPeerStoreJob::SetTranscode(DicomTransferSyntax syntax)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = true;
+      transferSyntax_ = syntax;
+    }    
+  }
+  
+
+  void OrthancPeerStoreJob::SetTranscode(const std::string& transferSyntaxUid)
+  {
+    DicomTransferSyntax s;
+    if (LookupTransferSyntax(s, transferSyntaxUid))
+    {
+      SetTranscode(s);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Unknown transfer syntax UID: " + transferSyntaxUid);
+    }
+  }
+
+
+  void OrthancPeerStoreJob::ClearTranscode()
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = false;
+    }
+  }
+
+
+  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;
+    
+    if (transcode_)
+    {
+      value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
+    }
+  }
+
+
+  static const char* PEER = "Peer";
+  static const char* TRANSCODE = "Transcode";
+
+  OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context,
+                                           const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    assert(serialized.type() == Json::objectValue);
+    peer_ = WebServiceParameters(serialized[PEER]);
+
+    if (serialized.isMember(TRANSCODE))
+    {
+      SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
+    }
+    else
+    {
+      transcode_ = false;
+    }
+  }
+
+
+  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      assert(target.type() == Json::objectValue);
+      peer_.Serialize(target[PEER],
+                      true /* force advanced format */,
+                      true /* include passwords */);
+
+      if (transcode_)
+      {
+        target[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
+      }
+      
+      return true;
+    }
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/OrthancPeerStoreJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../../Core/HttpClient.h"
+
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class OrthancPeerStoreJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&               context_;
+    WebServiceParameters         peer_;
+    std::unique_ptr<HttpClient>  client_;
+    bool                         transcode_;
+    DicomTransferSyntax          transferSyntax_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+    virtual bool HandleTrailingStep();
+
+  public:
+    OrthancPeerStoreJob(ServerContext& context) :
+      context_(context),
+      transcode_(false)
+    {
+    }
+
+    OrthancPeerStoreJob(ServerContext& context,
+                        const Json::Value& serialize);
+
+    void SetPeer(const WebServiceParameters& peer);
+
+    const WebServiceParameters& GetPeer() const
+    {
+      return peer_;
+    }
+
+    bool IsTranscode() const
+    {
+      return transcode_;
+    }
+
+    DicomTransferSyntax GetTransferSyntax() const;
+
+    void SetTranscode(DicomTransferSyntax syntax);
+
+    void SetTranscode(const std::string& transferSyntaxUid);
+
+    void ClearTranscode();
+
+    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/Sources/ServerJobs/ResourceModificationJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,460 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+#include "../ServerContext.h"
+
+#include <dcmtk/dcmdata/dcfilefo.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <cassert>
+
+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)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "No modification was provided for this job");
+    }
+
+      
+    LOG(INFO) << "Modifying instance in a job: " << instance;
+
+
+    /**
+     * Retrieve the original instance from the DICOM cache.
+     **/
+    
+    std::unique_ptr<DicomInstanceHasher> originalHasher;
+    std::unique_ptr<ParsedDicomFile> modified;
+
+    try
+    {
+      ServerContext::DicomCacheLocker locker(GetContext(), instance);
+      ParsedDicomFile& original = locker.GetDicom();
+
+      originalHasher.reset(new DicomInstanceHasher(original.GetHasher()));
+      modified.reset(original.Clone(true));
+    }
+    catch (OrthancException& e)
+    {
+      LOG(WARNING) << "An error occurred while executing a Modification job on instance " << instance << ": " << e.GetDetails();
+      return false;
+    }
+
+
+    /**
+     * Compute the resulting DICOM instance.
+     **/
+
+    modification_->Apply(*modified);
+
+    const std::string modifiedUid = IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject());
+    
+    if (transcode_)
+    {
+      std::set<DicomTransferSyntax> syntaxes;
+      syntaxes.insert(transferSyntax_);
+
+      IDicomTranscoder::DicomImage source;
+      source.AcquireParsed(*modified);  // "modified" is invalid below this point
+      
+      IDicomTranscoder::DicomImage transcoded;
+      if (GetContext().Transcode(transcoded, source, syntaxes, true))
+      {
+        modified.reset(transcoded.ReleaseAsParsedDicomFile());
+
+        // Fix the SOP instance UID in order the preserve the
+        // references between instance UIDs in the DICOM hierarchy
+        // (the UID might have changed in the case of lossy transcoding)
+        if (modified.get() == NULL ||
+            modified->GetDcmtkObject().getDataset() == NULL ||
+            !modified->GetDcmtkObject().getDataset()->putAndInsertString(
+              DCM_SOPInstanceUID, modifiedUid.c_str(), OFTrue /* replace */).good())
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+      else
+      {
+        LOG(WARNING) << "Cannot transcode instance, keeping original transfer syntax: " << instance;
+        modified.reset(source.ReleaseAsParsedDicomFile());
+      }
+    }
+
+    assert(modifiedUid == IDicomTranscoder::GetSopInstanceUid(modified->GetDcmtkObject()));
+
+    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 (GetContext().Store(modifiedInstance, toStore,
+                           StoreInstanceMode_Default) != StoreStatus_Success)
+    {
+      throw OrthancException(ErrorCode_CannotStoreInstance,
+                             "Error while storing a modified instance " + instance);
+    }
+
+    /**
+     * The assertion below will fail if automated transcoding to a
+     * lossy transfer syntax is enabled in the Orthanc core, and if
+     * the source instance is not in this transfer syntax.
+     **/
+    // assert(modifiedInstance == modifiedHasher.HashInstance());
+
+    output_->Update(modifiedHasher);
+
+    return true;
+  }
+
+
+  ResourceModificationJob::ResourceModificationJob(ServerContext& context) :
+    CleaningInstancesJob(context, true /* by default, keep source */),
+    modification_(new DicomModification),
+    isAnonymization_(false),
+    transcode_(false)
+  {
+  }
+
+
+  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_;
+    }
+  }
+
+
+  DicomTransferSyntax ResourceModificationJob::GetTransferSyntax() const
+  {
+    if (transcode_)
+    {
+      return transferSyntax_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+  
+
+  void ResourceModificationJob::SetTranscode(DicomTransferSyntax syntax)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = true;
+      transferSyntax_ = syntax;
+    }    
+  }
+
+
+  void ResourceModificationJob::SetTranscode(const std::string& transferSyntaxUid)
+  {
+    DicomTransferSyntax s;
+    if (LookupTransferSyntax(s, transferSyntaxUid))
+    {
+      SetTranscode(s);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat,
+                             "Unknown transfer syntax UID: " + transferSyntaxUid);
+    }
+  }
+
+
+  void ResourceModificationJob::ClearTranscode()
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      transcode_ = false;
+    }
+  }
+
+
+  void ResourceModificationJob::GetPublicContent(Json::Value& value)
+  {
+    CleaningInstancesJob::GetPublicContent(value);
+
+    value["IsAnonymization"] = isAnonymization_;
+
+    if (output_.get() != NULL)
+    {
+      output_->Format(value);
+    }
+
+    if (transcode_)
+    {
+      value["Transcode"] = GetTransferSyntaxUid(transferSyntax_);
+    }
+  }
+
+
+  static const char* MODIFICATION = "Modification";
+  static const char* ORIGIN = "Origin";
+  static const char* IS_ANONYMIZATION = "IsAnonymization";
+  static const char* TRANSCODE = "Transcode";
+  
+
+  ResourceModificationJob::ResourceModificationJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    CleaningInstancesJob(context, serialized, true /* by default, keep source */)
+  {
+    assert(serialized.type() == Json::objectValue);
+
+    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
+    origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
+    modification_.reset(new DicomModification(serialized[MODIFICATION]));
+
+    if (serialized.isMember(TRANSCODE))
+    {
+      SetTranscode(SerializationToolbox::ReadString(serialized, TRANSCODE));
+    }
+    else
+    {
+      transcode_ = false;
+    }
+  }
+  
+  bool ResourceModificationJob::Serialize(Json::Value& value)
+  {
+    if (!CleaningInstancesJob::Serialize(value))
+    {
+      return false;
+    }
+    else
+    {
+      assert(value.type() == Json::objectValue);
+      
+      value[IS_ANONYMIZATION] = isAnonymization_;
+
+      if (transcode_)
+      {
+        value[TRANSCODE] = GetTransferSyntaxUid(transferSyntax_);
+      }
+      
+      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/Sources/ServerJobs/ResourceModificationJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../DicomInstanceOrigin.h"
+#include "CleaningInstancesJob.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class ResourceModificationJob : public CleaningInstancesJob
+  {
+  private:
+    class Output;
+    
+    std::unique_ptr<DicomModification>  modification_;
+    boost::shared_ptr<Output>           output_;
+    bool                                isAnonymization_;
+    DicomInstanceOrigin                 origin_;
+    bool                                transcode_;
+    DicomTransferSyntax                 transferSyntax_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+  public:
+    ResourceModificationJob(ServerContext& context);
+
+    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_;
+    }
+
+    bool IsTranscode() const
+    {
+      return transcode_;
+    }
+
+    DicomTransferSyntax GetTransferSyntax() const;
+
+    void SetTranscode(DicomTransferSyntax syntax);
+
+    void SetTranscode(const std::string& transferSyntaxUid);
+
+    void ClearTranscode();
+
+    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/Sources/ServerJobs/SplitStudyJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,351 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+#include "../ServerContext.h"
+
+
+namespace Orthanc
+{
+  void SplitStudyJob::CheckAllowedTag(const DicomTag& tag) const
+  {
+    if (allowedTags_.find(tag) == allowedTags_.end())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange,
+                             "Cannot modify the following tag while splitting a study "
+                             "(not in the patient/study modules): " +
+                             FromDcmtkBridge::GetTagName(tag, "") +
+                             " (" + tag.Format() + ")");
+    }
+  }
+
+  
+  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)
+  {
+    if (!HasTrailingStep())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls,
+                             "AddTrailingStep() should have been called after AddSourceSeries()");
+    }
+    
+    /**
+     * Retrieve the DICOM instance to be modified
+     **/
+    
+    std::unique_ptr<ParsedDicomFile> modified;
+
+    try
+    {
+      ServerContext::DicomCacheLocker locker(GetContext(), 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);
+
+    // Fix since Orthanc 1.5.8: Assign new "SOPInstanceUID", as the instance has been modified
+    modified->ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Instance));
+
+    if (targetStudy_.empty())
+    {
+      targetStudy_ = modified->GetHasher().HashStudy();
+    }
+    
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(origin_);
+    toStore.SetParsedDicomFile(*modified);
+
+    std::string modifiedInstance;
+    if (GetContext().Store(modifiedInstance, toStore,
+                           StoreInstanceMode_Default) != StoreStatus_Success)
+    {
+      LOG(ERROR) << "Error while storing a modified instance " << instance;
+      return false;
+    }
+
+    return true;
+  }
+
+  
+  SplitStudyJob::SplitStudyJob(ServerContext& context,
+                               const std::string& sourceStudy) :
+    CleaningInstancesJob(context, false /* by default, remove source instances */),
+    sourceStudy_(sourceStudy),
+    targetStudyUid_(FromDcmtkBridge::GenerateUniqueIdentifier(ResourceType_Study))
+  {
+    Setup();
+    
+    ResourceType type;
+    
+    if (!GetContext().GetIndex().LookupResourceType(type, sourceStudy) ||
+        type != ResourceType_Study)
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "Cannot split unknown study " + sourceStudy);
+    }
+  }
+  
+
+  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 (!GetContext().GetIndex().LookupParent(parent, series, ResourceType_Study) ||
+             parent != sourceStudy_)
+    {
+      throw OrthancException(ErrorCode_UnknownResource,
+                             "This series does not belong to the study to be split: " + series);
+    }
+    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;
+      GetContext().GetIndex().GetChildren(instances, series);
+
+      for (std::list<std::string>::const_iterator
+             it = instances.begin(); it != instances.end(); ++it)
+      {
+        AddInstance(*it);
+      }
+    }    
+  }
+
+
+  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)
+  {
+    CleaningInstancesJob::GetPublicContent(value);
+
+    if (!targetStudy_.empty())
+    {
+      value["TargetStudy"] = targetStudy_;
+    }
+    
+    value["TargetStudyUID"] = targetStudyUid_;
+  }
+
+
+  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) :
+    CleaningInstancesJob(context, serialized,
+                         false /* by default, remove source instances */)  // (*)
+  {
+    if (!HasTrailingStep())
+    {
+      // Should have been set by (*)
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    Setup();
+
+    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 (!CleaningInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      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/Sources/ServerJobs/SplitStudyJob.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,129 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/DicomTag.h"
+#include "../DicomInstanceOrigin.h"
+#include "CleaningInstancesJob.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class SplitStudyJob : public CleaningInstancesJob
+  {
+  private:
+    typedef std::map<std::string, std::string>  SeriesUidMap;
+    typedef std::map<DicomTag, std::string>     Replacements;
+    
+    
+    std::set<DicomTag>     allowedTags_;
+    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);
+
+  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 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);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,455 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "StorageCommitmentScpJob.h"
+
+#include "../../Core/DicomNetworking/DicomAssociation.h"
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
+#include "../OrthancConfiguration.h"
+#include "../ServerContext.h"
+
+
+static const char* ANSWER = "Answer";
+static const char* CALLED_AET = "CalledAet";
+static const char* INDEX = "Index";
+static const char* LOOKUP = "Lookup";
+static const char* REMOTE_MODALITY = "RemoteModality";
+static const char* SETUP = "Setup";
+static const char* SOP_CLASS_UIDS = "SopClassUids";
+static const char* SOP_INSTANCE_UIDS = "SopInstanceUids";
+static const char* TRANSACTION_UID = "TransactionUid";
+static const char* TYPE = "Type";
+
+
+
+namespace Orthanc
+{
+  class StorageCommitmentScpJob::StorageCommitmentCommand : public SetOfCommandsJob::ICommand
+  {
+  public:
+    virtual CommandType GetType() const = 0;
+  };
+
+  
+  class StorageCommitmentScpJob::SetupCommand : public StorageCommitmentCommand
+  {
+  private:
+    StorageCommitmentScpJob&  that_;
+
+  public:
+    SetupCommand(StorageCommitmentScpJob& that) :
+      that_(that)
+    {
+    }
+
+    virtual CommandType GetType() const ORTHANC_OVERRIDE
+    {
+      return CommandType_Setup;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Setup(jobId);
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[TYPE] = SETUP;
+    }
+  };
+
+
+  class StorageCommitmentScpJob::LookupCommand : public StorageCommitmentCommand
+  {
+  private:
+    StorageCommitmentScpJob&        that_;
+    size_t                          index_;
+    bool                            hasFailureReason_;
+    StorageCommitmentFailureReason  failureReason_;
+
+  public:
+    LookupCommand(StorageCommitmentScpJob&  that,
+                  size_t index) :
+      that_(that),
+      index_(index),
+      hasFailureReason_(false)
+    {
+    }
+
+    virtual CommandType GetType() const ORTHANC_OVERRIDE
+    {
+      return CommandType_Lookup;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      failureReason_ = that_.Lookup(index_);
+      hasFailureReason_ = true;
+      return true;
+    }
+
+    size_t GetIndex() const
+    {
+      return index_;
+    }
+
+    StorageCommitmentFailureReason GetFailureReason() const
+    {
+      if (hasFailureReason_)
+      {
+        return failureReason_;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[TYPE] = LOOKUP;
+      target[INDEX] = static_cast<unsigned int>(index_);
+    }
+  };
+
+  
+  class StorageCommitmentScpJob::AnswerCommand : public StorageCommitmentCommand
+  {
+  private:
+    StorageCommitmentScpJob&  that_;
+
+  public:
+    AnswerCommand(StorageCommitmentScpJob& that) :
+      that_(that)
+    {
+      if (that_.ready_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        that_.ready_ = true;
+      }
+    }
+
+    virtual CommandType GetType() const ORTHANC_OVERRIDE
+    {
+      return CommandType_Answer;
+    }
+    
+    virtual bool Execute(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      that_.Answer();
+      return true;
+    }
+
+    virtual void Serialize(Json::Value& target) const ORTHANC_OVERRIDE
+    {
+      target = Json::objectValue;
+      target[TYPE] = ANSWER;
+    }
+  };
+    
+
+  class StorageCommitmentScpJob::Unserializer : public SetOfCommandsJob::ICommandUnserializer
+  {
+  private:
+    StorageCommitmentScpJob&  that_;
+
+  public:
+    Unserializer(StorageCommitmentScpJob& that) :
+      that_(that)
+    {
+      that_.ready_ = false;
+    }
+
+    virtual ICommand* Unserialize(const Json::Value& source) const
+    {
+      const std::string type = SerializationToolbox::ReadString(source, TYPE);
+
+      if (type == SETUP)
+      {
+        return new SetupCommand(that_);
+      }
+      else if (type == LOOKUP)
+      {
+        return new LookupCommand(that_, SerializationToolbox::ReadUnsignedInteger(source, INDEX));
+      }
+      else if (type == ANSWER)
+      {
+        return new AnswerCommand(that_);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+  };
+
+
+  void StorageCommitmentScpJob::CheckInvariants()
+  {
+    const size_t n = GetCommandsCount();
+
+    if (n <= 1)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    for (size_t i = 0; i < n; i++)
+    {
+      const CommandType type = dynamic_cast<const StorageCommitmentCommand&>(GetCommand(i)).GetType();
+      
+      if ((i == 0 && type != CommandType_Setup) ||
+          (i >= 1 && i < n - 1 && type != CommandType_Lookup) ||
+          (i == n - 1 && type != CommandType_Answer))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (type == CommandType_Lookup)
+      {
+        const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i));
+        if (lookup.GetIndex() != i - 1)
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    }
+  }
+    
+
+  void StorageCommitmentScpJob::Setup(const std::string& jobId)
+  {
+    CheckInvariants();
+
+    const std::string& remoteAet = remoteModality_.GetApplicationEntityTitle();
+    lookupHandler_.reset(context_.CreateStorageCommitment(jobId, transactionUid_, sopClassUids_,
+                                                          sopInstanceUids_, remoteAet, calledAet_));
+  }
+
+
+  StorageCommitmentFailureReason StorageCommitmentScpJob::Lookup(size_t index)
+  {
+#ifndef NDEBUG
+    CheckInvariants();
+#endif
+
+    if (index >= sopClassUids_.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else if (lookupHandler_.get() != NULL)
+    {
+      return lookupHandler_->Lookup(sopClassUids_[index], sopInstanceUids_[index]);
+    }
+    else
+    {
+      // This is the default implementation of Orthanc (if no storage
+      // commitment plugin is installed)
+      bool success = false;
+      StorageCommitmentFailureReason reason =
+        StorageCommitmentFailureReason_NoSuchObjectInstance /* 0x0112 == 274 */;
+      
+      try
+      {
+        std::vector<std::string> orthancId;
+        context_.GetIndex().LookupIdentifierExact(orthancId, ResourceType_Instance, DICOM_TAG_SOP_INSTANCE_UID, sopInstanceUids_[index]);
+
+        if (orthancId.size() == 1)
+        {
+          std::string a, b;
+
+          // Make sure that the DICOM file can be re-read by DCMTK
+          // from the file storage, and that the actual SOP
+          // class/instance UIDs do match
+          ServerContext::DicomCacheLocker locker(context_, orthancId[0]);
+          if (locker.GetDicom().GetTagValue(a, DICOM_TAG_SOP_CLASS_UID) &&
+              locker.GetDicom().GetTagValue(b, DICOM_TAG_SOP_INSTANCE_UID) &&
+              b == sopInstanceUids_[index])
+          {
+            if (a == sopClassUids_[index])
+            {
+              success = true;
+              reason = StorageCommitmentFailureReason_Success;
+            }
+            else
+            {
+              // Mismatch in the SOP class UID
+              reason = StorageCommitmentFailureReason_ClassInstanceConflict /* 0x0119 */;
+            }
+          }
+        }
+      }
+      catch (OrthancException&)
+      {
+      }
+
+      LOG(INFO) << "  Storage commitment SCP job: " << (success ? "Success" : "Failure")
+                << " while looking for " << sopClassUids_[index] << " / " << sopInstanceUids_[index];
+
+      return reason;
+    }
+  }
+  
+  
+  void StorageCommitmentScpJob::Answer()
+  {   
+    CheckInvariants();
+    LOG(INFO) << "  Storage commitment SCP job: Sending answer";
+
+    std::vector<StorageCommitmentFailureReason> failureReasons;
+    failureReasons.reserve(sopClassUids_.size());
+
+    for (size_t i = 1; i < GetCommandsCount() - 1; i++)
+    {
+      const LookupCommand& lookup = dynamic_cast<const LookupCommand&>(GetCommand(i));
+      failureReasons.push_back(lookup.GetFailureReason());
+    }
+
+    if (failureReasons.size() != sopClassUids_.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    DicomAssociationParameters parameters(calledAet_, remoteModality_);
+    DicomAssociation::ReportStorageCommitment(
+      parameters, transactionUid_, sopClassUids_, sopInstanceUids_, failureReasons);
+  }
+    
+
+  StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context,
+                                                   const std::string& transactionUid,
+                                                   const std::string& remoteAet,
+                                                   const std::string& calledAet) :
+    context_(context),
+    ready_(false),
+    transactionUid_(transactionUid),
+    calledAet_(calledAet)
+  {
+    {
+      OrthancConfiguration::ReaderLock lock;
+      if (!lock.GetConfiguration().LookupDicomModalityUsingAETitle(remoteModality_, remoteAet))
+      {
+        throw OrthancException(ErrorCode_InexistentItem,
+                               "Unknown remote modality for storage commitment SCP: " + remoteAet);
+      }
+    }
+
+    AddCommand(new SetupCommand(*this));
+  }
+    
+
+  void StorageCommitmentScpJob::Reserve(size_t size)
+  {
+    if (ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      sopClassUids_.reserve(size);
+      sopInstanceUids_.reserve(size);
+    }
+  }
+    
+
+  void StorageCommitmentScpJob::AddInstance(const std::string& sopClassUid,
+                                            const std::string& sopInstanceUid)
+  {
+    if (ready_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      assert(sopClassUids_.size() == sopInstanceUids_.size());
+      AddCommand(new LookupCommand(*this, sopClassUids_.size()));
+      sopClassUids_.push_back(sopClassUid);
+      sopInstanceUids_.push_back(sopInstanceUid);
+    }
+  }
+    
+
+  void StorageCommitmentScpJob::MarkAsReady()
+  {
+    AddCommand(new AnswerCommand(*this));
+  }
+
+
+  void StorageCommitmentScpJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfCommandsJob::GetPublicContent(value);
+      
+    value["CalledAet"] = calledAet_;
+    value["RemoteAet"] = remoteModality_.GetApplicationEntityTitle();
+    value["TransactionUid"] = transactionUid_;
+  }
+
+
+  StorageCommitmentScpJob::StorageCommitmentScpJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfCommandsJob(new Unserializer(*this), serialized),
+    context_(context)
+  {
+    transactionUid_ = SerializationToolbox::ReadString(serialized, TRANSACTION_UID);
+    remoteModality_ = RemoteModalityParameters(serialized[REMOTE_MODALITY]);
+    calledAet_ = SerializationToolbox::ReadString(serialized, CALLED_AET);
+    SerializationToolbox::ReadArrayOfStrings(sopClassUids_, serialized, SOP_CLASS_UIDS);
+    SerializationToolbox::ReadArrayOfStrings(sopInstanceUids_, serialized, SOP_INSTANCE_UIDS);
+  }
+  
+
+  bool StorageCommitmentScpJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfCommandsJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[TRANSACTION_UID] = transactionUid_;
+      remoteModality_.Serialize(target[REMOTE_MODALITY], true /* force advanced format */);
+      target[CALLED_AET] = calledAet_;
+      SerializationToolbox::WriteArrayOfStrings(target, sopClassUids_, SOP_CLASS_UIDS);
+      SerializationToolbox::WriteArrayOfStrings(target, sopInstanceUids_, SOP_INSTANCE_UIDS);
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerJobs/StorageCommitmentScpJob.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Compatibility.h"
+#include "../../Core/DicomNetworking/RemoteModalityParameters.h"
+#include "../../Core/JobsEngine/SetOfCommandsJob.h"
+#include "IStorageCommitmentFactory.h"
+
+#include <memory>
+#include <vector>
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class StorageCommitmentScpJob : public SetOfCommandsJob
+  {
+  private:
+    enum CommandType
+    {
+      CommandType_Setup,
+      CommandType_Lookup,
+      CommandType_Answer
+    };
+    
+    class StorageCommitmentCommand;
+    class SetupCommand;
+    class LookupCommand;
+    class AnswerCommand;
+    class Unserializer;
+
+    ServerContext&            context_;
+    bool                      ready_;
+    std::string               transactionUid_;
+    RemoteModalityParameters  remoteModality_;
+    std::string               calledAet_;
+    std::vector<std::string>  sopClassUids_;
+    std::vector<std::string>  sopInstanceUids_;
+
+    std::unique_ptr<IStorageCommitmentFactory::ILookupHandler>  lookupHandler_;
+
+    void CheckInvariants();
+    
+    void Setup(const std::string& jobId);
+    
+    StorageCommitmentFailureReason Lookup(size_t index);
+    
+    void Answer();
+    
+  public:
+    StorageCommitmentScpJob(ServerContext& context,
+                            const std::string& transactionUid,
+                            const std::string& remoteAet,
+                            const std::string& calledAet);
+
+    StorageCommitmentScpJob(ServerContext& context,
+                            const Json::Value& serialized);
+
+    void Reserve(size_t size);
+    
+    void AddInstance(const std::string& sopClassUid,
+                     const std::string& sopInstanceUid);
+
+    void MarkAsReady();
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
+    {
+      target = "StorageCommitmentScp";
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
+
+    virtual bool Serialize(Json::Value& target) ORTHANC_OVERRIDE;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,455 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "ServerToolbox.h"
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/FileStorage/StorageAccessor.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+#include "Database/IDatabaseWrapper.h"
+#include "Database/ResourcesContent.h"
+#include "ServerContext.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  static const DicomTag PATIENT_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_PATIENT_ID,
+    DICOM_TAG_PATIENT_NAME,
+    DICOM_TAG_PATIENT_BIRTH_DATE
+  };
+
+  static const DicomTag STUDY_IDENTIFIERS[] = 
+  {
+    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 SERIES_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_SERIES_INSTANCE_UID
+  };
+
+  static const DicomTag INSTANCE_IDENTIFIERS[] = 
+  {
+    DICOM_TAG_SOP_INSTANCE_UID
+  };
+
+
+  static void StoreMainDicomTagsInternal(ResourcesContent& target,
+                                         int64_t resource,
+                                         const DicomMap& tags)
+  {
+    DicomArray flattened(tags);
+
+    for (size_t i = 0; i < flattened.GetSize(); i++)
+    {
+      const DicomElement& element = flattened.GetElement(i);
+      const DicomTag& tag = element.GetTag();
+      const DicomValue& value = element.GetValue();
+      if (!value.IsNull() && 
+          !value.IsBinary())
+      {
+        target.AddMainDicomTag(resource, tag, element.GetValue().GetContent());
+      }
+    }
+  }
+
+
+  static void StoreIdentifiers(ResourcesContent& target,
+                               int64_t resource,
+                               ResourceType level,
+                               const DicomMap& map)
+  {
+    const DicomTag* tags;
+    size_t size;
+
+    ServerToolbox::LoadIdentifiers(tags, size, level);
+
+    for (size_t i = 0; i < size; i++)
+    {
+      // The identifiers tags are a subset of the main DICOM tags
+      assert(DicomMap::IsMainDicomTag(tags[i]));
+        
+      const DicomValue* value = map.TestAndGetValue(tags[i]);
+      if (value != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        std::string s = ServerToolbox::NormalizeIdentifier(value->GetContent());
+        target.AddIdentifierTag(resource, tags[i], s);
+      }
+    }
+  }
+
+
+  void ResourcesContent::AddResource(int64_t resource,
+                                     ResourceType level,
+                                     const DicomMap& dicomSummary)
+  {
+    StoreIdentifiers(*this, resource, level, dicomSummary);
+
+    DicomMap tags;
+
+    switch (level)
+    {
+      case ResourceType_Patient:
+        dicomSummary.ExtractPatientInformation(tags);
+        break;
+
+      case ResourceType_Study:
+        // Duplicate the patient tags at the study level (new in Orthanc 0.9.5 - db v6)
+        dicomSummary.ExtractPatientInformation(tags);
+        StoreMainDicomTagsInternal(*this, resource, tags);
+
+        dicomSummary.ExtractStudyInformation(tags);
+        break;
+
+      case ResourceType_Series:
+        dicomSummary.ExtractSeriesInformation(tags);
+        break;
+
+      case ResourceType_Instance:
+        dicomSummary.ExtractInstanceInformation(tags);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    StoreMainDicomTagsInternal(*this, resource, tags);
+  }
+
+
+  namespace ServerToolbox
+  {
+    void SimplifyTags(Json::Value& target,
+                      const Json::Value& source,
+                      DicomToJsonFormat format)
+    {
+      if (!source.isObject())
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      }
+
+      target = Json::objectValue;
+      Json::Value::Members members = source.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const Json::Value& v = source[members[i]];
+        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();
+        }
+        else if (type == "TooLong" ||
+                 type == "Null")
+        {
+          target[name] = Json::nullValue;
+        }
+        else if (type == "Sequence")
+        {
+          const Json::Value& array = v["Value"];
+          assert(array.isArray());
+
+          Json::Value children = Json::arrayValue;
+          for (Json::Value::ArrayIndex i = 0; i < array.size(); i++)
+          {
+            Json::Value c;
+            SimplifyTags(c, array[i], format);
+            children.append(c);
+          }
+
+          target[name] = children;
+        }
+        else
+        {
+          assert(0);
+        }
+      }
+    }
+
+
+    bool FindOneChildInstance(int64_t& result,
+                              IDatabaseWrapper& database,
+                              int64_t resource,
+                              ResourceType type)
+    {
+      for (;;)
+      {
+        if (type == ResourceType_Instance)
+        {
+          result = resource;
+          return true;
+        }
+
+        std::list<int64_t> children;
+        database.GetChildrenInternalId(children, resource);
+        if (children.empty())
+        {
+          return false;
+        }
+
+        resource = children.front();
+        type = GetChildResourceType(type);    
+      }
+    }
+
+
+    void ReconstructMainDicomTags(IDatabaseWrapper& database,
+                                  IStorageArea& storageArea,
+                                  ResourceType level)
+    {
+      // 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)
+      {
+        case ResourceType_Patient:
+          plural = "patients";
+          break;
+
+        case ResourceType_Study:
+          plural = "studies";
+          break;
+
+        case ResourceType_Series:
+          plural = "series";
+          break;
+
+        case ResourceType_Instance:
+          plural = "instances";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      LOG(WARNING) << "Upgrade: Reconstructing the main DICOM tags of all the " << plural << "...";
+
+      std::list<std::string> resources;
+      database.GetAllPublicIds(resources, level);
+
+      for (std::list<std::string>::const_iterator
+             it = resources.begin(); it != resources.end(); ++it)
+      {
+        // Locate the resource and one of its child instances
+        int64_t resource, instance;
+        ResourceType tmp;
+
+        if (!database.LookupResource(resource, tmp, *it) ||
+            tmp != level ||
+            !FindOneChildInstance(instance, database, resource, level))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot find an instance for " +
+                                 std::string(EnumerationToString(level)) +
+                                 " with identifier " + *it);
+        }
+
+        // Get the DICOM file attached to some instances in the resource
+        FileInfo attachment;
+        if (!database.LookupAttachment(attachment, instance, FileContentType_Dicom))
+        {
+          throw OrthancException(ErrorCode_InternalError,
+                                 "Cannot retrieve the DICOM file associated with instance " +
+                                 database.GetPublicId(instance));
+        }
+
+        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);
+
+          ResourcesContent tags;
+          tags.AddResource(resource, level, dicomSummary);
+          database.SetResourcesContent(tags);
+        }
+        catch (OrthancException&)
+        {
+          LOG(ERROR) << "Cannot decode the DICOM file with UUID " << attachment.GetUuid()
+                     << " associated with instance " << database.GetPublicId(instance);
+          throw;
+        }
+      }
+    }
+
+
+    void LoadIdentifiers(const DicomTag*& tags,
+                         size_t& size,
+                         ResourceType level)
+    {
+      switch (level)
+      {
+        case ResourceType_Patient:
+          tags = PATIENT_IDENTIFIERS;
+          size = sizeof(PATIENT_IDENTIFIERS) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Study:
+          tags = STUDY_IDENTIFIERS;
+          size = sizeof(STUDY_IDENTIFIERS) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Series:
+          tags = SERIES_IDENTIFIERS;
+          size = sizeof(SERIES_IDENTIFIERS) / sizeof(DicomTag);
+          break;
+
+        case ResourceType_Instance:
+          tags = INSTANCE_IDENTIFIERS;
+          size = sizeof(INSTANCE_IDENTIFIERS) / sizeof(DicomTag);
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+    }
+
+
+    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;
+
+      LoadIdentifiers(tags, size, level);
+
+      for (size_t i = 0; i < size; i++)
+      {
+        if (tag == tags[i])
+        {
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+    
+    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());
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerToolbox.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 <json/json.h>
+#include <boost/noncopyable.hpp>
+#include <list>
+
+namespace Orthanc
+{
+  class ServerContext;
+  class IDatabaseWrapper;
+  class IStorageArea;
+
+  namespace ServerToolbox
+  {
+    void SimplifyTags(Json::Value& target,
+                      const Json::Value& source,
+                      DicomToJsonFormat format);
+
+    bool FindOneChildInstance(int64_t& result,
+                              IDatabaseWrapper& database,
+                              int64_t resource,
+                              ResourceType type);
+
+    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);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/SliceOrdering.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,491 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "SliceOrdering.h"
+
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
+#include "ServerEnumerations.h"
+#include "ServerIndex.h"
+
+#include <algorithm>
+#include <boost/lexical_cast.hpp>
+#include <boost/noncopyable.hpp>
+
+
+namespace Orthanc
+{
+  static bool TokenizeVector(std::vector<float>& result,
+                             const std::string& value,
+                             unsigned int expectedSize)
+  {
+    std::vector<std::string> tokens;
+    Toolbox::TokenizeString(tokens, value, '\\');
+
+    if (tokens.size() != expectedSize)
+    {
+      return false;
+    }
+
+    result.resize(tokens.size());
+
+    for (size_t i = 0; i < tokens.size(); i++)
+    {
+      try
+      {
+        result[i] = boost::lexical_cast<float>(tokens[i]);
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+
+  static bool TokenizeVector(std::vector<float>& result,
+                             const DicomMap& map,
+                             const DicomTag& tag,
+                             unsigned int expectedSize)
+  {
+    const DicomValue* value = map.TestAndGetValue(tag);
+
+    if (value == NULL ||
+        value->IsNull() ||
+        value->IsBinary())
+    {
+      return false;
+    }
+    else
+    {
+      return TokenizeVector(result, value->GetContent(), expectedSize);
+    }
+  }
+
+
+  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_;
+
+  public:
+    Instance(ServerIndex& index,
+             const std::string& instanceId) :
+      instanceId_(instanceId),
+      framesCount_(1)
+    {
+      DicomMap instance;
+      if (!index.GetMainDicomTags(instance, instanceId, ResourceType_Instance, ResourceType_Instance))
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+
+      const DicomValue* frames = instance.TestAndGetValue(DICOM_TAG_NUMBER_OF_FRAMES);
+      if (frames != NULL &&
+          !frames->IsNull() &&
+          !frames->IsBinary())
+      {
+        try
+        {
+          framesCount_ = boost::lexical_cast<unsigned int>(frames->GetContent());
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+      }
+      
+      std::vector<float> tmp;
+      hasPosition_ = TokenizeVector(tmp, instance, DICOM_TAG_IMAGE_POSITION_PATIENT, 3);
+
+      if (hasPosition_)
+      {
+        position_[0] = tmp[0];
+        position_[1] = tmp[1];
+        position_[2] = tmp[2];
+      }
+
+      hasNormal_ = ComputeNormal(normal_, instance);
+
+      std::string s;
+      hasIndexInSeries_ = false;
+
+      try
+      {
+        if (index.LookupMetadata(s, instanceId, MetadataType_Instance_IndexInSeries))
+        {
+          indexInSeries_ = boost::lexical_cast<size_t>(s);
+          hasIndexInSeries_ = true;
+        }
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+      }
+    }
+
+    const std::string& GetIdentifier() const
+    {
+      return instanceId_;
+    }
+
+    bool HasPosition() const
+    {
+      return hasPosition_;
+    }
+
+    float ComputeRelativePosition(const Vector& normal) const
+    {
+      assert(HasPosition());
+      return (normal[0] * position_[0] + 
+              normal[1] * position_[1] +
+              normal[2] * position_[2]);
+    }
+
+    bool HasIndexInSeries() const
+    {
+      return hasIndexInSeries_;
+    }
+    
+    size_t GetIndexInSeries() const
+    {
+      assert(HasIndexInSeries());
+      return indexInSeries_;
+    }
+
+    unsigned int GetFramesCount() const
+    {
+      return framesCount_;
+    }
+
+    bool HasNormal() const
+    {
+      return hasNormal_;
+    }
+
+    const Vector& GetNormal() const
+    {
+      assert(hasNormal_);
+      return normal_;
+    }
+  };
+
+
+  class SliceOrdering::PositionComparator
+  {
+  private:
+    const Vector&  normal_;
+
+  public:
+    PositionComparator(const Vector& normal) : normal_(normal)
+    {
+    }
+    
+    int operator() (const Instance* a,
+                    const Instance* b) const
+    {
+      return a->ComputeRelativePosition(normal_) < b->ComputeRelativePosition(normal_);
+    }
+  };
+
+
+  bool SliceOrdering::IndexInSeriesComparator(const SliceOrdering::Instance* a,
+                                              const SliceOrdering::Instance* b)
+  {
+    return a->GetIndexInSeries() < b->GetIndexInSeries();
+  }  
+
+
+  void SliceOrdering::ComputeNormal()
+  {
+    DicomMap series;
+    if (!index_.GetMainDicomTags(series, seriesId_, ResourceType_Series, ResourceType_Series))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    hasNormal_ = ComputeNormal(normal_, series);
+  }
+
+
+  void SliceOrdering::CreateInstances()
+  {
+    std::list<std::string> instancesId;
+    index_.GetChildren(instancesId, seriesId_);
+
+    instances_.reserve(instancesId.size());
+    for (std::list<std::string>::const_iterator
+           it = instancesId.begin(); it != instancesId.end(); ++it)
+    {
+      instances_.push_back(new Instance(index_, *it));
+    }
+  }
+  
+
+  bool SliceOrdering::SortUsingPositions()
+  {
+    if (instances_.size() <= 1)
+    {
+      // One single instance: It is sorted by default
+      return true;
+    }
+
+    if (!hasNormal_)
+    {
+      return false;
+    }
+
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      assert(instances_[i] != NULL);
+
+      if (!instances_[i]->HasPosition() ||
+          (instances_[i]->HasNormal() &&
+           !IsParallelOrOpposite(instances_[i]->GetNormal(), normal_)))
+      {
+        return false;
+      }
+    }
+
+    PositionComparator comparator(normal_);
+    std::sort(instances_.begin(), instances_.end(), comparator);
+
+    float a = instances_[0]->ComputeRelativePosition(normal_);
+    for (size_t i = 1; i < instances_.size(); i++)
+    {
+      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;
+    }
+
+    // This is a 3D volume
+    isVolume_ = true;
+    return true;
+  }
+
+
+  bool SliceOrdering::SortUsingIndexInSeries()
+  {
+    if (instances_.size() <= 1)
+    {
+      // One single instance: It is sorted by default
+      return true;
+    }
+
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      assert(instances_[i] != NULL);
+      if (!instances_[i]->HasIndexInSeries())
+      {
+        return false;
+      }
+    }
+
+    std::sort(instances_.begin(), instances_.end(), IndexInSeriesComparator);
+    
+    for (size_t i = 1; i < instances_.size(); i++)
+    {
+      if (instances_[i - 1]->GetIndexInSeries() == instances_[i]->GetIndexInSeries())
+      {
+        // The current "IndexInSeries" occurs 2 times: Not a proper ordering
+        LOG(WARNING) << "This series contains 2 slices with the same index, trying to display it anyway";
+        break;
+      }
+    }
+
+    return true;
+  }
+
+
+  SliceOrdering::SliceOrdering(ServerIndex& index,
+                               const std::string& seriesId) :
+    index_(index),
+    seriesId_(seriesId),
+    isVolume_(false)
+  {
+    ComputeNormal();
+    CreateInstances();
+
+    if (!SortUsingPositions() &&
+        !SortUsingIndexInSeries())
+    {
+      throw OrthancException(ErrorCode_CannotOrderSlices,
+                             "Unable to order the slices of series " + seriesId);
+    }
+  }
+
+
+  SliceOrdering::~SliceOrdering()
+  {
+    for (std::vector<Instance*>::iterator
+           it = instances_.begin(); it != instances_.end(); ++it)
+    {
+      if (*it != NULL)
+      {
+        delete *it;
+      }
+    }
+  }
+
+
+  const std::string& SliceOrdering::GetInstanceId(size_t index) const
+  {
+    if (index >= instances_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return instances_[index]->GetIdentifier();
+    }
+  }
+
+
+  unsigned int SliceOrdering::GetFramesCount(size_t index) const
+  {
+    if (index >= instances_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return instances_[index]->GetFramesCount();
+    }
+  }
+
+
+  void SliceOrdering::Format(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["Type"] = (isVolume_ ? "Volume" : "Sequence");
+    
+    Json::Value tmp = Json::arrayValue;
+    for (size_t i = 0; i < GetInstancesCount(); i++)
+    {
+      tmp.append(GetBasePath(ResourceType_Instance, GetInstanceId(i)) + "/file");
+    }
+
+    result["Dicom"] = tmp;
+
+    Json::Value slicesShort = Json::arrayValue;
+
+    tmp.clear();
+    for (size_t i = 0; i < GetInstancesCount(); i++)
+    {
+      std::string base = GetBasePath(ResourceType_Instance, GetInstanceId(i));
+      for (size_t j = 0; j < GetFramesCount(i); j++)
+      {
+        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;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/SliceOrdering.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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"
+
+namespace Orthanc
+{
+  class ServerIndex;
+  
+  class SliceOrdering
+  {
+  private:
+    typedef float Vector[3];
+
+    struct Instance;
+    class  PositionComparator;
+
+    ServerIndex&             index_;
+    std::string              seriesId_;
+    bool                     hasNormal_;
+    Vector                   normal_;
+    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);
+
+    void ComputeNormal();
+
+    void CreateInstances();
+
+    bool SortUsingPositions();
+
+    bool SortUsingIndexInSeries();
+
+  public:
+    SliceOrdering(ServerIndex& index,
+                  const std::string& seriesId);
+
+    ~SliceOrdering();
+
+    size_t  GetInstancesCount() const
+    {
+      return instances_.size();
+    }
+
+    const std::string& GetInstanceId(size_t index) const;
+
+    unsigned int GetFramesCount(size_t index) const;
+
+    void Format(Json::Value& result) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/StorageCommitmentReports.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,272 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "StorageCommitmentReports.h"
+
+#include "../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void StorageCommitmentReports::Report::MarkAsComplete()
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      isComplete_ = true;
+    }
+  }
+
+  void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid)
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      Success success;
+      success.sopClassUid_ = sopClassUid;
+      success.sopInstanceUid_ = sopInstanceUid;
+      success_.push_back(success);
+    }
+  }
+
+  void StorageCommitmentReports::Report::AddFailure(const std::string& sopClassUid,
+                                                    const std::string& sopInstanceUid,
+                                                    StorageCommitmentFailureReason reason)
+  {
+    if (isComplete_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      Failure failure;
+      failure.sopClassUid_ = sopClassUid;
+      failure.sopInstanceUid_ = sopInstanceUid;
+      failure.reason_ = reason;
+      failures_.push_back(failure);
+    }
+  }
+
+  
+  StorageCommitmentReports::Report::Status StorageCommitmentReports::Report::GetStatus() const
+  {
+    if (!isComplete_)
+    {
+      return Status_Pending;
+    }
+    else if (failures_.empty())
+    {
+      return Status_Success;
+    }
+    else
+    {
+      return Status_Failure;
+    }
+  }
+
+
+  void StorageCommitmentReports::Report::Format(Json::Value& json) const
+  {
+    static const char* const FIELD_STATUS = "Status";
+    static const char* const FIELD_SOP_CLASS_UID = "SOPClassUID";
+    static const char* const FIELD_SOP_INSTANCE_UID = "SOPInstanceUID";
+    static const char* const FIELD_FAILURE_REASON = "FailureReason";
+    static const char* const FIELD_DESCRIPTION = "Description";
+    static const char* const FIELD_REMOTE_AET = "RemoteAET";
+    static const char* const FIELD_SUCCESS = "Success";
+    static const char* const FIELD_FAILURES = "Failures";
+
+    
+    json = Json::objectValue;
+    json[FIELD_REMOTE_AET] = remoteAet_;
+
+    bool pending;
+    
+    switch (GetStatus())
+    {
+      case Status_Pending:
+        json[FIELD_STATUS] = "Pending";
+        pending = true;
+        break;
+
+      case Status_Success:
+        json[FIELD_STATUS] = "Success";
+        pending = false;
+        break;
+
+      case Status_Failure:
+        json[FIELD_STATUS] = "Failure";
+        pending = false;
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (!pending)
+    {
+      {
+        Json::Value success = Json::arrayValue;
+        for (std::list<Success>::const_iterator
+               it = success_.begin(); it != success_.end(); ++it)
+        {
+          Json::Value item = Json::objectValue;
+          item[FIELD_SOP_CLASS_UID] = it->sopClassUid_;
+          item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_;
+          success.append(item);
+        }
+
+        json[FIELD_SUCCESS] = success;
+      }
+
+      {
+        Json::Value failures = Json::arrayValue;
+        for (std::list<Failure>::const_iterator
+               it = failures_.begin(); it != failures_.end(); ++it)
+        {
+          Json::Value item = Json::objectValue;
+          item[FIELD_SOP_CLASS_UID] = it->sopClassUid_;
+          item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_;
+          item[FIELD_FAILURE_REASON] = it->reason_;
+          item[FIELD_DESCRIPTION] = EnumerationToString(it->reason_);
+          failures.append(item);
+        }
+
+        json[FIELD_FAILURES] = failures;
+      }
+    }
+  }
+
+
+  void StorageCommitmentReports::Report::GetSuccessSopInstanceUids(
+    std::vector<std::string>& target) const
+  {
+    target.clear();
+    target.reserve(success_.size());
+
+    for (std::list<Success>::const_iterator
+           it = success_.begin(); it != success_.end(); ++it)
+    {
+      target.push_back(it->sopInstanceUid_);
+    }
+  }
+
+
+  StorageCommitmentReports::~StorageCommitmentReports()
+  {
+    while (!content_.IsEmpty())
+    {
+      Report* report = NULL;
+      content_.RemoveOldest(report);
+
+      assert(report != NULL);
+      delete report;
+    }
+  }
+
+  
+  void StorageCommitmentReports::Store(const std::string& transactionUid,
+                                       Report* report)
+  {
+    std::unique_ptr<Report> protection(report);
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    {
+      Report* previous = NULL;
+      if (content_.Contains(transactionUid, previous))
+      {
+        assert(previous != NULL);
+        delete previous;
+
+        content_.Invalidate(transactionUid);
+      }
+    }
+
+    assert(maxSize_ == 0 ||
+           content_.GetSize() <= maxSize_);
+
+    if (maxSize_ != 0 &&
+        content_.GetSize() == maxSize_)
+    {
+      assert(!content_.IsEmpty());
+      
+      Report* oldest = NULL;
+      content_.RemoveOldest(oldest);
+
+      assert(oldest != NULL);
+      delete oldest;
+    }
+
+    assert(maxSize_ == 0 ||
+           content_.GetSize() < maxSize_);
+
+    content_.Add(transactionUid, protection.release());
+  }
+
+
+  StorageCommitmentReports::Accessor::Accessor(StorageCommitmentReports& that,
+                                               const std::string& transactionUid) :
+    lock_(that.mutex_),
+    transactionUid_(transactionUid)
+  {
+    if (that.content_.Contains(transactionUid, report_))
+    {
+      that.content_.MakeMostRecent(transactionUid);
+    }
+    else
+    {
+      report_ = NULL;
+    }
+  }
+
+  const StorageCommitmentReports::Report&
+  StorageCommitmentReports::Accessor::GetReport() const
+  {
+    if (report_ == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *report_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/StorageCommitmentReports.h	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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/Cache/LeastRecentlyUsedIndex.h"
+
+namespace Orthanc
+{
+  class StorageCommitmentReports
+  {
+  public:
+    class Report : public boost::noncopyable
+    {
+    public:
+      enum Status
+      {
+        Status_Success,
+        Status_Failure,
+        Status_Pending
+      };
+      
+    private:
+      struct Success
+      {
+        std::string  sopClassUid_;
+        std::string  sopInstanceUid_;
+      };
+      
+      struct Failure
+      {
+        std::string  sopClassUid_;
+        std::string  sopInstanceUid_;
+        StorageCommitmentFailureReason  reason_;
+      };
+      
+      bool                isComplete_;
+      std::list<Success>  success_;
+      std::list<Failure>  failures_;
+      std::string         remoteAet_;
+
+    public:
+      Report(const std::string& remoteAet) :
+        isComplete_(false),
+        remoteAet_(remoteAet)
+      {
+      }
+
+      const std::string& GetRemoteAet() const
+      {
+        return remoteAet_;
+      }
+
+      void MarkAsComplete();
+
+      void AddSuccess(const std::string& sopClassUid,
+                      const std::string& sopInstanceUid);
+
+      void AddFailure(const std::string& sopClassUid,
+                      const std::string& sopInstanceUid,
+                      StorageCommitmentFailureReason reason);
+
+      Status GetStatus() const;
+
+      void Format(Json::Value& json) const;
+
+      void GetSuccessSopInstanceUids(std::vector<std::string>& target) const;
+    };
+
+  private:
+    typedef LeastRecentlyUsedIndex<std::string, Report*>  Content;
+    
+    boost::mutex   mutex_;
+    Content        content_;
+    size_t         maxSize_;
+
+  public:
+    StorageCommitmentReports(size_t maxSize) :
+      maxSize_(maxSize)
+    {
+    }
+
+    ~StorageCommitmentReports();
+
+    size_t GetMaxSize() const
+    {
+      return maxSize_;
+    }
+
+    void Store(const std::string& transactionUid,
+               Report* report); // Takes ownership
+
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      std::string                transactionUid_;
+      Report                    *report_;
+
+    public:
+      Accessor(StorageCommitmentReports& that,
+               const std::string& transactionUid);
+
+      const std::string& GetTransactionUid() const
+      {
+        return transactionUid_;
+      }
+
+      bool IsValid() const
+      {
+        return report_ != NULL;
+      }
+
+      const Report& GetReport() const;
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/main.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1718 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "OrthancRestApi/OrthancRestApi.h"
+
+#include <boost/algorithm/string/predicate.hpp>
+
+#include "../Core/Compatibility.h"
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomNetworking/DicomAssociationParameters.h"
+#include "../Core/DicomNetworking/DicomServer.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/HttpServer/FilesystemHttpHandler.h"
+#include "../Core/HttpServer/HttpServer.h"
+#include "../Core/Logging.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+#include "EmbeddedResourceHttpHandler.h"
+#include "OrthancConfiguration.h"
+#include "OrthancFindRequestHandler.h"
+#include "OrthancGetRequestHandler.h"
+#include "OrthancInitialization.h"
+#include "OrthancMoveRequestHandler.h"
+#include "ServerContext.h"
+#include "ServerJobs/StorageCommitmentScpJob.h"
+#include "ServerToolbox.h"
+#include "StorageCommitmentReports.h"
+
+using namespace Orthanc;
+
+
+class OrthancStoreRequestHandler : public IStoreRequestHandler
+{
+private:
+  ServerContext& context_;
+
+public:
+  OrthancStoreRequestHandler(ServerContext& context) :
+    context_(context)
+  {
+  }
+
+
+  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) 
+  {
+    if (dicomFile.size() > 0)
+    {
+      DicomInstanceToStore toStore;
+      toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol
+                        (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
+      toStore.SetBuffer(dicomFile.c_str(), dicomFile.size());
+      toStore.SetSummary(dicomSummary);
+      toStore.SetJson(dicomJson);
+
+      std::string id;
+      context_.Store(id, toStore, StoreInstanceMode_Default);
+    }
+  }
+};
+
+
+
+class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler
+{
+private:
+  ServerContext& context_;
+  
+public:
+  OrthancStorageCommitmentRequestHandler(ServerContext& context) :
+    context_(context)
+  {
+  }
+
+  virtual void HandleRequest(const std::string& transactionUid,
+                             const std::vector<std::string>& referencedSopClassUids,
+                             const std::vector<std::string>& referencedSopInstanceUids,
+                             const std::string& remoteIp,
+                             const std::string& remoteAet,
+                             const std::string& calledAet)
+  {
+    if (referencedSopClassUids.size() != referencedSopInstanceUids.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    std::unique_ptr<StorageCommitmentScpJob> job(
+      new StorageCommitmentScpJob(context_, transactionUid, remoteAet, calledAet));
+
+    for (size_t i = 0; i < referencedSopClassUids.size(); i++)
+    {
+      job->AddInstance(referencedSopClassUids[i], referencedSopInstanceUids[i]);
+    }
+
+    job->MarkAsReady();
+
+    context_.GetJobsEngine().GetRegistry().Submit(job.release(), 0 /* default priority */);
+  }
+
+  virtual void HandleReport(const std::string& transactionUid,
+                            const std::vector<std::string>& successSopClassUids,
+                            const std::vector<std::string>& successSopInstanceUids,
+                            const std::vector<std::string>& failedSopClassUids,
+                            const std::vector<std::string>& failedSopInstanceUids,
+                            const std::vector<StorageCommitmentFailureReason>& failureReasons,
+                            const std::string& remoteIp,
+                            const std::string& remoteAet,
+                            const std::string& calledAet)
+  {
+    if (successSopClassUids.size() != successSopInstanceUids.size() ||
+        failedSopClassUids.size() != failedSopInstanceUids.size() ||
+        failedSopClassUids.size() != failureReasons.size())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    std::unique_ptr<StorageCommitmentReports::Report> report(
+      new StorageCommitmentReports::Report(remoteAet));
+
+    for (size_t i = 0; i < successSopClassUids.size(); i++)
+    {
+      report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]);
+    }
+
+    for (size_t i = 0; i < failedSopClassUids.size(); i++)
+    {
+      report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]);
+    }
+
+    report->MarkAsComplete();
+
+    context_.GetStorageCommitmentReports().Store(transactionUid, report.release());
+  }
+};
+
+
+
+class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities
+{
+public:
+  virtual bool IsSameAETitle(const std::string& aet1,
+                             const std::string& aet2) 
+  {
+    OrthancConfiguration::ReaderLock lock;
+    return lock.GetConfiguration().IsSameAETitle(aet1, aet2);
+  }
+
+  virtual bool LookupAETitle(RemoteModalityParameters& modality,
+                             const std::string& aet) 
+  {
+    OrthancConfiguration::ReaderLock lock;
+    return lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, aet);
+  }
+};
+
+
+class MyDicomServerFactory : 
+  public IStoreRequestHandlerFactory,
+  public IFindRequestHandlerFactory, 
+  public IMoveRequestHandlerFactory,
+  public IGetRequestHandlerFactory,
+  public IStorageCommitmentRequestHandlerFactory
+{
+private:
+  ServerContext& context_;
+
+public:
+  MyDicomServerFactory(ServerContext& context) : context_(context)
+  {
+  }
+
+  virtual IStoreRequestHandler* ConstructStoreRequestHandler()
+  {
+    return new OrthancStoreRequestHandler(context_);
+  }
+
+  virtual IFindRequestHandler* ConstructFindRequestHandler()
+  {
+    std::unique_ptr<OrthancFindRequestHandler> result(new OrthancFindRequestHandler(context_));
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0));
+      result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0));
+    }
+
+    if (result->GetMaxResults() == 0)
+    {
+      LOG(INFO) << "No limit on the number of C-FIND results at the Patient, Study and Series levels";
+    }
+    else
+    {
+      LOG(INFO) << "Maximum " << result->GetMaxResults() 
+                << " results for C-FIND queries at the Patient, Study and Series levels";
+    }
+
+    if (result->GetMaxInstances() == 0)
+    {
+      LOG(INFO) << "No limit on the number of C-FIND results at the Instance level";
+    }
+    else
+    {
+      LOG(INFO) << "Maximum " << result->GetMaxInstances() 
+                << " instances will be returned for C-FIND queries at the Instance level";
+    }
+
+    return result.release();
+  }
+
+  virtual IMoveRequestHandler* ConstructMoveRequestHandler()
+  {
+    return new OrthancMoveRequestHandler(context_);
+  }
+  
+  virtual IGetRequestHandler* ConstructGetRequestHandler()
+  {
+    return new OrthancGetRequestHandler(context_);
+  }
+  
+  virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler()
+  {
+    return new OrthancStorageCommitmentRequestHandler(context_);
+  }
+  
+
+  void Done()
+  {
+  }
+};
+
+
+class OrthancApplicationEntityFilter : public IApplicationEntityFilter
+{
+private:
+  ServerContext&  context_;
+  bool            alwaysAllowEcho_;
+  bool            alwaysAllowStore_;
+
+public:
+  OrthancApplicationEntityFilter(ServerContext& context) :
+    context_(context)
+  {
+    OrthancConfiguration::ReaderLock lock;
+    alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
+    alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
+  }
+
+  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;
+
+    if (alwaysAllowEcho_ ||
+        alwaysAllowStore_)
+    {
+      return true;
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+      return lock.GetConfiguration().IsKnownAETitle(remoteAet, remoteIp);
+    }
+  }
+
+  virtual bool IsAllowedRequest(const std::string& remoteIp,
+                                const std::string& remoteAet,
+                                const std::string& calledAet,
+                                DicomRequestType type)
+  {
+    LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
+              << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
+    
+    if (type == DicomRequestType_Echo &&
+        alwaysAllowEcho_)
+    {
+      // Incoming C-Echo requests are always accepted, even from unknown AET
+      return true;
+    }
+    else if (type == DicomRequestType_Store &&
+             alwaysAllowStore_)
+    {
+      // Incoming C-Store requests are always accepted, even from unknown AET
+      return true;
+    }
+    else
+    {
+      OrthancConfiguration::ReaderLock lock;
+
+      RemoteModalityParameters modality;
+      if (lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, remoteAet))
+      {
+        return modality.IsRequestAllowed(type);
+      }
+      else
+      {
+        return false;
+      }
+    }
+  }
+
+  virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
+                                       const std::string& remoteAet,
+                                       const std::string& calledAet,
+                                       TransferSyntax syntax)
+  {
+    std::string configuration;
+
+    switch (syntax)
+    {
+      case TransferSyntax_Deflated:
+        configuration = "DeflatedTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Jpeg:
+        configuration = "JpegTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Jpeg2000:
+        configuration = "Jpeg2000TransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_JpegLossless:
+        configuration = "JpegLosslessTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Jpip:
+        configuration = "JpipTransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Mpeg2:
+        configuration = "Mpeg2TransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Mpeg4:
+        configuration = "Mpeg4TransferSyntaxAccepted";
+        break;
+
+      case TransferSyntax_Rle:
+        configuration = "RleTransferSyntaxAccepted";
+        break;
+
+      default: 
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    {
+      std::string name = "Is" + configuration;
+
+      LuaScripting::Lock lock(context_.GetLuaScripting());
+      
+      if (lock.GetLua().IsExistingFunction(name.c_str()))
+      {
+        LuaFunctionCall call(lock.GetLua(), name.c_str());
+        call.PushString(remoteAet);
+        call.PushString(remoteIp);
+        call.PushString(calledAet);
+        return call.ExecutePredicate();
+      }
+    }
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      return lock.GetConfiguration().GetBooleanParameter(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();
+      }
+    }
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      return lock.GetConfiguration().GetBooleanParameter(configuration, false);
+    }
+  }
+};
+
+
+class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
+{
+private:
+  ServerContext&   context_;
+  OrthancPlugins*  plugins_;
+
+public:
+  MyIncomingHttpRequestFilter(ServerContext& context,
+                              OrthancPlugins* plugins) : 
+    context_(context),
+    plugins_(plugins)
+  {
+  }
+
+  virtual bool IsAllowed(HttpMethod method,
+                         const char* uri,
+                         const char* ip,
+                         const char* username,
+                         const IHttpHandler::Arguments& httpHeaders,
+                         const IHttpHandler::GetArguments& getArguments)
+  {
+#if ORTHANC_ENABLE_PLUGINS == 1
+    if (plugins_ != NULL &&
+        !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments))
+    {
+      return false;
+    }
+#endif
+
+    static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
+
+    LuaScripting::Lock lock(context_.GetLuaScripting());
+
+    // Test if the instance must be filtered out
+    if (lock.GetLua().IsExistingFunction(HTTP_FILTER))
+    {
+      LuaFunctionCall call(lock.GetLua(), HTTP_FILTER);
+
+      switch (method)
+      {
+        case HttpMethod_Get:
+          call.PushString("GET");
+          break;
+
+        case HttpMethod_Put:
+          call.PushString("PUT");
+          break;
+
+        case HttpMethod_Post:
+          call.PushString("POST");
+          break;
+
+        case HttpMethod_Delete:
+          call.PushString("DELETE");
+          break;
+
+        default:
+          return true;
+      }
+
+      call.PushString(uri);
+      call.PushString(ip);
+      call.PushString(username);
+      call.PushStringMap(httpHeaders);
+
+      if (!call.ExecutePredicate())
+      {
+        LOG(INFO) << "An incoming HTTP request has been discarded by the filter";
+        return false;
+      }
+    }
+
+    return true;
+  }
+};
+
+
+
+class MyHttpExceptionFormatter : public IHttpExceptionFormatter
+{
+private:
+  bool             describeErrors_;
+  OrthancPlugins*  plugins_;
+
+public:
+  MyHttpExceptionFormatter(bool describeErrors,
+                           OrthancPlugins* plugins) :
+    describeErrors_(describeErrors),
+    plugins_(plugins)
+  {
+  }
+
+  virtual void Format(HttpOutput& output,
+                      const OrthancException& exception,
+                      HttpMethod method,
+                      const char* uri)
+  {
+    {
+      bool isPlugin = false;
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (plugins_ != NULL)
+      {
+        plugins_->GetErrorDictionary().LogError(exception.GetErrorCode(), true);
+        isPlugin = true;
+      }
+#endif
+
+      if (!isPlugin)
+      {
+        LOG(ERROR) << "Exception in the HTTP handler: " << exception.What();
+      }
+    }      
+
+    Json::Value message = Json::objectValue;
+    ErrorCode errorCode = exception.GetErrorCode();
+    HttpStatus httpStatus = exception.GetHttpStatus();
+
+    {
+      bool isPlugin = false;
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (plugins_ != NULL &&
+          plugins_->GetErrorDictionary().Format(message, httpStatus, exception))
+      {
+        errorCode = ErrorCode_Plugin;
+        isPlugin = true;
+      }
+#endif
+
+      if (!isPlugin)
+      {
+        message["Message"] = exception.What();
+      }
+    }
+
+    if (!describeErrors_)
+    {
+      output.SendStatus(httpStatus);
+    }
+    else
+    {
+      message["Method"] = EnumerationToString(method);
+      message["Uri"] = uri;
+      message["HttpError"] = EnumerationToString(httpStatus);
+      message["HttpStatus"] = httpStatus;
+      message["OrthancError"] = EnumerationToString(errorCode);
+      message["OrthancStatus"] = errorCode;
+
+      if (exception.HasDetails())
+      {
+        message["Details"] = exception.GetDetails();
+      }
+
+      std::string info = message.toStyledString();
+      output.SendStatus(httpStatus, info);
+    }
+  }
+};
+
+
+
+static void PrintHelp(const char* path)
+{
+  std::cout 
+    << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl
+    << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl
+    << std::endl
+    << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl
+    << "case of a directory, all the JSON files it contains will be merged. " << std::endl
+    << "If no configuration path is given on the command line, a set of default " << std::endl
+    << "parameters is used. Please refer to the Orthanc homepage for the full " << std::endl
+    << "instructions about how to use Orthanc <http://www.orthanc-server.com/>." << std::endl
+    << std::endl
+    << "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(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
+    << "\t\t\t(if file is \"-\", dumps to stdout)" << std::endl
+    << "  --errors\t\tprint the supported error codes and exit" << std::endl
+    << "  --verbose\t\tbe verbose in logs" << std::endl
+    << "  --trace\t\thighest verbosity in logs (for debug)" << std::endl
+    << "  --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
+    << "   0 if success," << std::endl
+#if defined(_WIN32)
+    << "!= 0 if error (use the --errors option to get the list of possible errors)." << std::endl
+#else
+    << "  -1 if error (have a look at the logs)." << std::endl
+#endif
+    << std::endl;
+}
+
+
+static void PrintVersion(const char* path)
+{
+  std::cout
+    << path << " " << ORTHANC_VERSION << std::endl
+    << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
+    << "Copyright (C) 2017-2020 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
+    << std::endl
+    << "Written by Sebastien Jodogne <s.jodogne@orthanc-labs.com>" << std::endl;
+}
+
+
+static void PrintErrorCode(ErrorCode code, const char* description)
+{
+  std::cout 
+    << std::right << std::setw(16) 
+    << static_cast<int>(code)
+    << "   " << description << std::endl;
+}
+
+
+static void PrintErrors(const char* path)
+{
+  std::cout
+    << path << " " << ORTHANC_VERSION << std::endl
+    << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." 
+    << std::endl << std::endl
+    << "List of error codes that could be returned by Orthanc:" 
+    << std::endl << std::endl;
+
+  // The content of the following brackets is automatically generated
+  // by the "GenerateErrorCodes.py" script
+  {
+    PrintErrorCode(ErrorCode_InternalError, "Internal error");
+    PrintErrorCode(ErrorCode_Success, "Success");
+    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, "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");
+    PrintErrorCode(ErrorCode_BadRequest, "Bad request");
+    PrintErrorCode(ErrorCode_NetworkProtocol, "Error in the network protocol");
+    PrintErrorCode(ErrorCode_SystemCommand, "Error while calling a system command");
+    PrintErrorCode(ErrorCode_Database, "Error with the database engine");
+    PrintErrorCode(ErrorCode_UriSyntax, "Badly formatted URI");
+    PrintErrorCode(ErrorCode_InexistentFile, "Inexistent file");
+    PrintErrorCode(ErrorCode_CannotWriteFile, "Cannot write to file");
+    PrintErrorCode(ErrorCode_BadFileFormat, "Bad file format");
+    PrintErrorCode(ErrorCode_Timeout, "Timeout");
+    PrintErrorCode(ErrorCode_UnknownResource, "Unknown resource");
+    PrintErrorCode(ErrorCode_IncompatibleDatabaseVersion, "Incompatible version of the database");
+    PrintErrorCode(ErrorCode_FullStorage, "The file storage is full");
+    PrintErrorCode(ErrorCode_CorruptedFile, "Corrupted file (e.g. inconsistent MD5 hash)");
+    PrintErrorCode(ErrorCode_InexistentTag, "Inexistent tag");
+    PrintErrorCode(ErrorCode_ReadOnly, "Cannot modify a read-only data structure");
+    PrintErrorCode(ErrorCode_IncompatibleImageFormat, "Incompatible format of the images");
+    PrintErrorCode(ErrorCode_IncompatibleImageSize, "Incompatible size of the images");
+    PrintErrorCode(ErrorCode_SharedLibrary, "Error while using a shared library (plugin)");
+    PrintErrorCode(ErrorCode_UnknownPluginService, "Plugin invoking an unknown service");
+    PrintErrorCode(ErrorCode_UnknownDicomTag, "Unknown DICOM tag");
+    PrintErrorCode(ErrorCode_BadJson, "Cannot parse a JSON document");
+    PrintErrorCode(ErrorCode_Unauthorized, "Bad credentials were provided to an HTTP request");
+    PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file");
+    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_BadGeometry, "Geometry error encountered in Stone");
+    PrintErrorCode(ErrorCode_SslInitialization, "Cannot initialize SSL encryption, check out your certificates");
+    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");
+    PrintErrorCode(ErrorCode_SQLiteStatementAlreadyUsed, "SQLite: This cached statement is already being referred to");
+    PrintErrorCode(ErrorCode_SQLiteExecute, "SQLite: Cannot execute a command");
+    PrintErrorCode(ErrorCode_SQLiteRollbackWithoutTransaction, "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)");
+    PrintErrorCode(ErrorCode_SQLiteCommitWithoutTransaction, "SQLite: Committing a nonexistent transaction");
+    PrintErrorCode(ErrorCode_SQLiteRegisterFunction, "SQLite: Unable to register a function");
+    PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
+    PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
+    PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
+    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
+    PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
+    PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
+    PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
+    PrintErrorCode(ErrorCode_SQLiteTransactionBegin, "SQLite: Cannot start a transaction");
+    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 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");
+    PrintErrorCode(ErrorCode_MakeDirectory, "Cannot create a directory");
+    PrintErrorCode(ErrorCode_BadApplicationEntityTitle, "An application entity title (AET) cannot be empty or be longer than 16 characters");
+    PrintErrorCode(ErrorCode_NoCFindHandler, "No request handler factory for DICOM C-FIND SCP");
+    PrintErrorCode(ErrorCode_NoCMoveHandler, "No request handler factory for DICOM C-MOVE SCP");
+    PrintErrorCode(ErrorCode_NoCStoreHandler, "No request handler factory for DICOM C-STORE SCP");
+    PrintErrorCode(ErrorCode_NoApplicationEntityFilter, "No application entity filter");
+    PrintErrorCode(ErrorCode_NoSopClassOrInstance, "DicomUserConnection: Unable to find the SOP class and instance");
+    PrintErrorCode(ErrorCode_NoPresentationContext, "DicomUserConnection: No acceptable presentation context for modality");
+    PrintErrorCode(ErrorCode_DicomFindUnavailable, "DicomUserConnection: The C-FIND command is not supported by the remote SCP");
+    PrintErrorCode(ErrorCode_DicomMoveUnavailable, "DicomUserConnection: The C-MOVE command is not supported by the remote SCP");
+    PrintErrorCode(ErrorCode_CannotStoreInstance, "Cannot store an instance");
+    PrintErrorCode(ErrorCode_CreateDicomNotString, "Only string values are supported when creating DICOM instances");
+    PrintErrorCode(ErrorCode_CreateDicomOverrideTag, "Trying to override a value inherited from a parent module");
+    PrintErrorCode(ErrorCode_CreateDicomUseContent, "Use \"Content\" to inject an image into a new DICOM instance");
+    PrintErrorCode(ErrorCode_CreateDicomNoPayload, "No payload is present for one instance in the series");
+    PrintErrorCode(ErrorCode_CreateDicomUseDataUriScheme, "The payload of the DICOM instance must be specified according to Data URI scheme");
+    PrintErrorCode(ErrorCode_CreateDicomBadParent, "Trying to attach a new DICOM instance to an inexistent resource");
+    PrintErrorCode(ErrorCode_CreateDicomParentIsInstance, "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)");
+    PrintErrorCode(ErrorCode_CreateDicomParentEncoding, "Unable to get the encoding of the parent resource");
+    PrintErrorCode(ErrorCode_UnknownModality, "Unknown modality");
+    PrintErrorCode(ErrorCode_BadJobOrdering, "Bad ordering of filters in a job");
+    PrintErrorCode(ErrorCode_JsonToLuaTable, "Cannot convert the given JSON object to a Lua table");
+    PrintErrorCode(ErrorCode_CannotCreateLua, "Cannot create the Lua context");
+    PrintErrorCode(ErrorCode_CannotExecuteLua, "Cannot execute a Lua command");
+    PrintErrorCode(ErrorCode_LuaAlreadyExecuted, "Arguments cannot be pushed after the Lua function is executed");
+    PrintErrorCode(ErrorCode_LuaBadOutput, "The Lua function does not give the expected number of outputs");
+    PrintErrorCode(ErrorCode_NotLuaPredicate, "The Lua function is not a predicate (only true/false outputs allowed)");
+    PrintErrorCode(ErrorCode_LuaReturnsNoString, "The Lua function does not return a string");
+    PrintErrorCode(ErrorCode_StorageAreaAlreadyRegistered, "Another plugin has already registered a custom storage area");
+    PrintErrorCode(ErrorCode_DatabaseBackendAlreadyRegistered, "Another plugin has already registered a custom database back-end");
+    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");
+    PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
+    PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
+    PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
+  }
+
+  std::cout << std::endl;
+}
+
+
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+static void LoadPlugins(OrthancPlugins& plugins)
+{
+  std::list<std::string> path;
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+    lock.GetConfiguration().GetListOfStringsParameter(path, "Plugins");
+  }
+
+  for (std::list<std::string>::const_iterator
+         it = path.begin(); it != path.end(); ++it)
+  {
+    std::string path;
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      path = lock.GetConfiguration().InterpretStringParameterAsPath(*it);
+    }
+
+    LOG(WARNING) << "Loading plugin(s) from: " << path;
+    plugins.GetManager().RegisterPlugin(path);
+  }  
+}
+#endif
+
+
+
+// Returns "true" if restart is required
+static bool WaitForExit(ServerContext& context,
+                        OrthancRestApi& restApi)
+{
+  LOG(WARNING) << "Orthanc has started";
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  if (context.HasPlugins())
+  {
+    context.GetPlugins().SignalOrthancStarted();
+  }
+#endif
+
+  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
+
+      OrthancConfiguration::ReaderLock lock;
+      if (lock.GetConfiguration().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 on the disk";
+        Logging::Flush();
+        continue;
+      }
+    }
+    else
+    {
+      break;
+    }
+  }
+
+  context.GetLuaScripting().Execute("Finalize");
+  context.GetLuaScripting().Stop();
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  if (context.HasPlugins())
+  {
+    context.GetPlugins().SignalOrthancStopped();
+  }
+#endif
+
+  if (restart)
+  {
+    LOG(WARNING) << "Reset request received, restarting Orthanc";
+  }
+
+  // We're done
+  LOG(WARNING) << "Orthanc is stopping";
+
+  return restart;
+}
+
+
+
+static bool StartHttpServer(ServerContext& context,
+                            OrthancRestApi& restApi,
+                            OrthancPlugins* plugins)
+{
+  bool httpServerEnabled;
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+    httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true);
+  }
+
+  if (!httpServerEnabled)
+  {
+    LOG(WARNING) << "The HTTP server is disabled";
+    return WaitForExit(context, restApi);
+  }
+  else
+  {
+    MyIncomingHttpRequestFilter httpFilter(context, plugins);
+    HttpServer httpServer;
+    bool httpDescribeErrors;
+
+#if ORTHANC_ENABLE_MONGOOSE == 1
+    const bool defaultKeepAlive = false;
+#elif ORTHANC_ENABLE_CIVETWEB == 1
+    const bool defaultKeepAlive = true;
+#else
+#  error "Either Mongoose or Civetweb must be enabled to compile this file"
+#endif
+  
+    {
+      OrthancConfiguration::ReaderLock lock;
+      
+      httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
+  
+      // HTTP server
+      httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50));
+      httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
+      httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
+      httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
+      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true));
+      httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true));
+      httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30));
+
+      // Let's assume that the HTTP server is secure
+      context.SetHttpServerSecure(true);
+
+      bool authenticationEnabled;
+      if (lock.GetConfiguration().LookupBooleanParameter(authenticationEnabled, "AuthenticationEnabled"))
+      {
+        httpServer.SetAuthenticationEnabled(authenticationEnabled);
+
+        if (httpServer.IsRemoteAccessAllowed() &&
+            !authenticationEnabled)
+        {
+          LOG(WARNING) << "====> Remote access is enabled while user authentication is explicitly disabled, "
+                       << "your setup is POSSIBLY INSECURE <====";
+          context.SetHttpServerSecure(false);
+        }
+      }
+      else if (httpServer.IsRemoteAccessAllowed())
+      {
+        // Starting with Orthanc 1.5.8, it is impossible to enable
+        // remote access without having explicitly disabled user
+        // authentication.
+        LOG(WARNING) << "Remote access is allowed but \"AuthenticationEnabled\" is not in the configuration, "
+                     << "automatically enabling HTTP authentication for security";          
+        httpServer.SetAuthenticationEnabled(true);
+      }
+      else
+      {
+        // If Orthanc only listens on the localhost, it is OK to have
+        // "AuthenticationEnabled" disabled
+        httpServer.SetAuthenticationEnabled(false);
+      }
+
+      bool hasUsers = lock.GetConfiguration().SetupRegisteredUsers(httpServer);
+
+      if (httpServer.IsAuthenticationEnabled() &&
+          !hasUsers)
+      {
+        if (httpServer.IsRemoteAccessAllowed())
+        {
+          /**
+           * Starting with Orthanc 1.5.8, if no user is explicitly
+           * defined while remote access is allowed, we create a
+           * default user, and Orthanc Explorer shows a warning
+           * message about an "Insecure setup". This convention is
+           * used in Docker images "jodogne/orthanc",
+           * "jodogne/orthanc-plugins" and "osimis/orthanc".
+           **/
+          LOG(WARNING) << "====> HTTP authentication is enabled, but no user is declared. "
+                       << "Creating a default user: Review your configuration option \"RegisteredUsers\". "
+                       << "Your setup is INSECURE <====";
+
+          context.SetHttpServerSecure(false);
+
+          // This is the username/password of the default user in Orthanc.
+          httpServer.RegisterUser("orthanc", "orthanc");
+        }
+        else
+        {
+          LOG(WARNING) << "HTTP authentication is enabled, but no user is declared, "
+                       << "check the value of configuration option \"RegisteredUsers\"";
+        }
+      }
+      
+      if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false))
+      {
+        std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath(
+          lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem"));
+        httpServer.SetSslEnabled(true);
+        httpServer.SetSslCertificate(certificate.c_str());
+      }
+      else
+      {
+        httpServer.SetSslEnabled(false);
+      }
+
+      if (lock.GetConfiguration().GetBooleanParameter("ExecuteLuaEnabled", false))
+      {
+        context.SetExecuteLuaEnabled(true);
+        LOG(WARNING) << "====> Remote LUA script execution is enabled.  Review your configuration option \"ExecuteLuaEnabled\". "
+                     << "Your setup is POSSIBLY INSECURE <====";
+      }
+      else
+      {
+        context.SetExecuteLuaEnabled(false);
+        LOG(WARNING) << "Remote LUA script execution is disabled";
+      }
+    }
+
+    MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
+        
+    httpServer.SetIncomingHttpRequestFilter(httpFilter);
+    httpServer.SetHttpExceptionFormatter(exceptionFormatter);
+    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();
+  
+    bool restart = WaitForExit(context, restApi);
+
+    httpServer.Stop();
+    LOG(WARNING) << "    HTTP server has stopped";
+
+    return restart;
+  }
+}
+
+
+static bool StartDicomServer(ServerContext& context,
+                             OrthancRestApi& restApi,
+                             OrthancPlugins* plugins)
+{
+  bool dicomServerEnabled;
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+    dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true);
+  }
+
+  if (!dicomServerEnabled)
+  {
+    LOG(WARNING) << "The DICOM server is disabled";
+    return StartHttpServer(context, restApi, plugins);
+  }
+  else
+  {
+    MyDicomServerFactory serverFactory(context);
+    OrthancApplicationEntityFilter dicomFilter(context);
+    ModalitiesFromConfiguration modalities;
+  
+    // Setup the DICOM server  
+    DicomServer dicomServer;
+    dicomServer.SetRemoteModalities(modalities);
+    dicomServer.SetStoreRequestHandlerFactory(serverFactory);
+    dicomServer.SetMoveRequestHandlerFactory(serverFactory);
+    dicomServer.SetGetRequestHandlerFactory(serverFactory);
+    dicomServer.SetFindRequestHandlerFactory(serverFactory);
+    dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
+
+    {
+      OrthancConfiguration::ReaderLock lock;
+      dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
+      dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
+      dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242));
+      dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"));
+    }
+
+#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.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 with AET " << dicomServer.GetApplicationEntityTitle() 
+                 << " on port: " << dicomServer.GetPortNumber();
+
+    bool restart = false;
+    ErrorCode error = ErrorCode_Success;
+
+    try
+    {
+      restart = StartHttpServer(context, restApi, plugins);
+    }
+    catch (OrthancException& e)
+    {
+      error = e.GetErrorCode();
+    }
+
+    dicomServer.Stop();
+    LOG(WARNING) << "    DICOM server has stopped";
+
+    serverFactory.Done();
+
+    if (error != ErrorCode_Success)
+    {
+      throw OrthancException(error);
+    }
+
+    return restart;
+  }
+}
+
+
+static bool ConfigureHttpHandler(ServerContext& context,
+                                 OrthancPlugins *plugins,
+                                 bool loadJobsFromDatabase)
+{
+#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)
+  {
+    assert(context.HasPlugins());
+    context.GetHttpHandler().Register(*plugins, false);
+  }
+#endif
+  
+  // Secondly, apply the "static resources" layer
+#if ORTHANC_STANDALONE == 1
+  EmbeddedResourceHttpHandler staticResources("/app", ServerResources::ORTHANC_EXPLORER);
+#else
+  FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer");
+#endif
+
+  context.GetHttpHandler().Register(staticResources, false);
+
+  // Thirdly, consider the built-in REST API of Orthanc
+  OrthancRestApi restApi(context);
+  context.GetHttpHandler().Register(restApi, true);
+
+  context.SetupJobsEngine(false /* not running unit tests */, loadJobsFromDatabase);
+
+  bool restart = StartDicomServer(context, restApi, plugins);
+
+  context.Stop();
+
+  return restart;
+}
+
+
+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)
+  {
+    LOG(WARNING) << "No upgrade is needed, start Orthanc without the \"--upgrade\" argument";
+    return;
+  }
+
+  if (currentVersion > ORTHANC_DATABASE_VERSION)
+  {
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                           "The version of the database schema (" +
+                           boost::lexical_cast<std::string>(currentVersion) +
+                           ") is too recent for this version of Orthanc. Please upgrade Orthanc.");
+  }
+
+  LOG(WARNING) << "Upgrading the database from schema version "
+               << currentVersion << " to " << ORTHANC_DATABASE_VERSION;
+
+  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)
+  {
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                           "The database schema was not properly upgraded, it is still at version " +
+                           boost::lexical_cast<std::string>(currentVersion));
+  }
+  else
+  {
+    LOG(WARNING) << "The database schema was successfully upgraded, "
+                 << "you can now start Orthanc without the \"--upgrade\" argument";
+  }
+}
+
+
+
+namespace
+{
+  class ServerContextConfigurator : public boost::noncopyable
+  {
+  private:
+    ServerContext&   context_;
+    OrthancPlugins*  plugins_;
+
+  public:
+    ServerContextConfigurator(ServerContext& context,
+                              OrthancPlugins* plugins) :
+      context_(context),
+      plugins_(plugins)
+    {
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().SetServerIndex(context.GetIndex());
+      }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (plugins_ != NULL)
+      {
+        plugins_->SetServerContext(context_);
+        context_.SetPlugins(*plugins_);
+      }
+#endif
+    }
+
+    ~ServerContextConfigurator()
+    {
+      {
+        OrthancConfiguration::WriterLock lock;
+        lock.GetConfiguration().ResetServerIndex();
+      }
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+      if (plugins_ != NULL)
+      {
+        plugins_->ResetServerContext();
+        context_.ResetPlugins();
+      }
+#endif
+    }
+  };
+}
+
+
+static bool ConfigureServerContext(IDatabaseWrapper& database,
+                                   IStorageArea& storageArea,
+                                   OrthancPlugins *plugins,
+                                   bool loadJobsFromDatabase)
+{
+  size_t maxCompletedJobs;
+  
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    // 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(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
+                             lock.GetConfiguration().InterpretStringParameterAsPath
+                             (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "")));
+    HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
+
+    // The value "0" below makes the class HttpClient use its default
+    // value (DEFAULT_HTTP_TIMEOUT = 60 seconds in Orthanc 1.5.7)
+    HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0));
+    
+    HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", ""));
+    
+    DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10));
+
+    maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10);
+
+    if (maxCompletedJobs == 0)
+    {
+      LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended";
+    }
+  }
+  
+  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
+
+  {
+    OrthancConfiguration::ReaderLock lock;
+
+    context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
+    context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
+
+    // New option in Orthanc 1.4.2
+    context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
+
+    try
+    {
+      context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
+    }
+    catch (...)
+    {
+      context.GetIndex().SetMaximumPatientCount(0);
+    }
+
+    try
+    {
+      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
+      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
+    }
+    catch (...)
+    {
+      context.GetIndex().SetMaximumStorageSize(0);
+    }
+  }
+
+  {
+    ServerContextConfigurator configurator(context, plugins);
+
+    {
+      OrthancConfiguration::WriterLock lock;
+      lock.GetConfiguration().LoadModalitiesAndPeers();
+    }
+
+    return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
+  }
+}
+
+
+static bool ConfigureDatabase(IDatabaseWrapper& database,
+                              IStorageArea& storageArea,
+                              OrthancPlugins *plugins,
+                              bool upgradeDatabase,
+                              bool loadJobsFromDatabase)
+{
+  database.Open();
+
+  unsigned int currentVersion = database.GetDatabaseVersion();
+
+  if (upgradeDatabase)
+  {
+    UpgradeDatabase(database, storageArea);
+    return false;  // Stop and don't restart Orthanc (cf. issue 29)
+  }
+  else if (currentVersion != ORTHANC_DATABASE_VERSION)
+  {
+    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
+                           "The database schema must be upgraded from version " +
+                           boost::lexical_cast<std::string>(currentVersion) + " to " +
+                           boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
+                           ": Please run Orthanc with the \"--upgrade\" argument");
+  }
+
+  bool success = ConfigureServerContext
+    (database, storageArea, plugins, loadJobsFromDatabase);
+
+  database.Close();
+
+  return success;
+}
+
+
+static bool ConfigurePlugins(int argc, 
+                             char* argv[],
+                             bool upgradeDatabase,
+                             bool loadJobsFromDatabase)
+{
+  std::unique_ptr<IDatabaseWrapper>  databasePtr;
+  std::unique_ptr<IStorageArea>  storage;
+
+#if ORTHANC_ENABLE_PLUGINS == 1
+  OrthancPlugins plugins;
+  plugins.SetCommandLineArguments(argc, argv);
+  LoadPlugins(plugins);
+
+  IDatabaseWrapper* database = NULL;
+  if (plugins.HasDatabaseBackend())
+  {
+    LOG(WARNING) << "Using a custom database from plugins";
+    database = &plugins.GetDatabaseBackend();
+  }
+  else
+  {
+    databasePtr.reset(CreateDatabaseWrapper());
+    database = databasePtr.get();
+  }
+
+  if (plugins.HasStorageArea())
+  {
+    LOG(WARNING) << "Using a custom storage area from plugins";
+    storage.reset(plugins.CreateStorageArea());
+  }
+  else
+  {
+    storage.reset(CreateStorageArea());
+  }
+
+  assert(database != NULL);
+  assert(storage.get() != NULL);
+
+  return ConfigureDatabase(*database, *storage, &plugins,
+                           upgradeDatabase, loadJobsFromDatabase);
+
+#elif ORTHANC_ENABLE_PLUGINS == 0
+  // The plugins are disabled
+
+  databasePtr.reset(CreateDatabaseWrapper());
+  storage.reset(CreateStorageArea());
+
+  assert(databasePtr.get() != NULL);
+  assert(storage.get() != NULL);
+
+  return ConfigureDatabase(*databasePtr, *storage, NULL,
+                           upgradeDatabase, loadJobsFromDatabase);
+
+#else
+#  error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
+#endif
+}
+
+
+static bool StartOrthanc(int argc, 
+                         char* argv[],
+                         bool upgradeDatabase,
+                         bool loadJobsFromDatabase)
+{
+  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;
+}
+
+
+int main(int argc, char* argv[]) 
+{
+  Logging::Initialize();
+
+  bool upgradeDatabase = false;
+  bool loadJobsFromDatabase = true;
+  const char* configurationFile = NULL;
+
+
+  /**
+   * Parse the command-line options.
+   **/ 
+
+  for (int i = 1; i < argc; i++)
+  {
+    std::string argument(argv[i]); 
+
+    if (argument.empty())
+    {
+      // Ignore empty arguments
+    }
+    else if (argument[0] != '-')
+    {
+      if (configurationFile != NULL)
+      {
+        LOG(ERROR) << "More than one configuration path were provided on the command line, aborting";
+        return -1;
+      }
+      else
+      {
+        // Use the first argument that does not start with a "-" as
+        // the configuration file
+
+        // TODO WHAT IS THE ENCODING?
+        configurationFile = argv[i];
+      }
+    }
+    else if (argument == "--errors")
+    {
+      PrintErrors(argv[0]);
+      return 0;
+    }
+    else if (argument == "--help")
+    {
+      PrintHelp(argv[0]);
+      return 0;
+    }
+    else if (argument == "--version")
+    {
+      PrintVersion(argv[0]);
+      return 0;
+    }
+    else if (argument == "--verbose")
+    {
+      Logging::EnableInfoLevel(true);
+    }
+    else if (argument == "--trace")
+    {
+      Logging::EnableTraceLevel(true);
+    }
+    else if (boost::starts_with(argument, "--logdir="))
+    {
+      // TODO WHAT IS THE ENCODING?
+      std::string directory = argument.substr(9);
+
+      try
+      {
+        Logging::SetTargetFolder(directory);
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "The directory where to store the log files (" 
+                   << directory << ") is inexistent, aborting.";
+        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")
+    {
+      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, ServerResources::CONFIGURATION_SAMPLE);
+
+#if defined(_WIN32)
+      // Replace UNIX newlines with DOS newlines 
+      boost::replace_all(configurationSample, "\n", "\r\n");
+#endif
+
+      std::string target = argument.substr(9);
+
+      try
+      {
+        if (target == "-")
+        {
+          // New in 1.5.8: Print to stdout
+          std::cout << configurationSample;
+        }
+        else
+        {
+          SystemToolbox::WriteFile(configurationSample, target);
+        }
+        return 0;
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
+        return -1;
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument;
+    }
+  }
+
+
+  /**
+   * Launch Orthanc.
+   **/
+
+  {
+    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());
+
+    std::string s = "Architecture: ";
+    if (sizeof(void*) == 4)
+    {
+      s += "32-bit, ";
+    }
+    else if (sizeof(void*) == 8)
+    {
+      s += "64-bit, ";
+    }
+    else
+    {
+      s += "unsupported pointer size, ";
+    }
+
+    switch (Toolbox::DetectEndianness())
+    {
+      case Endianness_Little:
+        s += "little endian";
+        break;
+      
+      case Endianness_Big:
+        s += "big endian";
+        break;
+      
+      default:
+        s += "unsupported endianness";
+        break;
+    }
+    
+    LOG(INFO) << s;
+  }
+
+  int status = 0;
+  try
+  {
+    for (;;)
+    {
+      OrthancInitialize(configurationFile);
+
+      bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase);
+      if (restart)
+      {
+        OrthancFinalize();
+        LOG(WARNING) << "Logging system is resetting";
+        Logging::Reset();
+      }
+      else
+      {
+        break;
+      }
+    }
+  }
+  catch (const OrthancException& e)
+  {
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ")";
+#if defined(_WIN32)
+    if (e.GetErrorCode() >= ErrorCode_START_PLUGINS)
+    {
+      status = static_cast<int>(ErrorCode_Plugin);
+    }
+    else
+    {
+      status = static_cast<int>(e.GetErrorCode());
+    }
+
+#else
+    status = -1;
+#endif
+  }
+  catch (const std::exception& e) 
+  {
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.what() << "]";
+    status = -1;
+  }
+  catch (const std::string& s) 
+  {
+    LOG(ERROR) << "Uncaught exception, stopping now: [" << s << "]";
+    status = -1;
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Native exception, stopping now. Check your plugins, if any.";
+    status = -1;
+  }
+
+  LOG(WARNING) << "Orthanc has stopped";
+
+  OrthancFinalize();
+
+  return status;
+}
--- a/OrthancServer/StorageCommitmentReports.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,272 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "StorageCommitmentReports.h"
-
-#include "../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  void StorageCommitmentReports::Report::MarkAsComplete()
-  {
-    if (isComplete_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      isComplete_ = true;
-    }
-  }
-
-  void StorageCommitmentReports::Report::AddSuccess(const std::string& sopClassUid,
-                                                    const std::string& sopInstanceUid)
-  {
-    if (isComplete_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      Success success;
-      success.sopClassUid_ = sopClassUid;
-      success.sopInstanceUid_ = sopInstanceUid;
-      success_.push_back(success);
-    }
-  }
-
-  void StorageCommitmentReports::Report::AddFailure(const std::string& sopClassUid,
-                                                    const std::string& sopInstanceUid,
-                                                    StorageCommitmentFailureReason reason)
-  {
-    if (isComplete_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      Failure failure;
-      failure.sopClassUid_ = sopClassUid;
-      failure.sopInstanceUid_ = sopInstanceUid;
-      failure.reason_ = reason;
-      failures_.push_back(failure);
-    }
-  }
-
-  
-  StorageCommitmentReports::Report::Status StorageCommitmentReports::Report::GetStatus() const
-  {
-    if (!isComplete_)
-    {
-      return Status_Pending;
-    }
-    else if (failures_.empty())
-    {
-      return Status_Success;
-    }
-    else
-    {
-      return Status_Failure;
-    }
-  }
-
-
-  void StorageCommitmentReports::Report::Format(Json::Value& json) const
-  {
-    static const char* const FIELD_STATUS = "Status";
-    static const char* const FIELD_SOP_CLASS_UID = "SOPClassUID";
-    static const char* const FIELD_SOP_INSTANCE_UID = "SOPInstanceUID";
-    static const char* const FIELD_FAILURE_REASON = "FailureReason";
-    static const char* const FIELD_DESCRIPTION = "Description";
-    static const char* const FIELD_REMOTE_AET = "RemoteAET";
-    static const char* const FIELD_SUCCESS = "Success";
-    static const char* const FIELD_FAILURES = "Failures";
-
-    
-    json = Json::objectValue;
-    json[FIELD_REMOTE_AET] = remoteAet_;
-
-    bool pending;
-    
-    switch (GetStatus())
-    {
-      case Status_Pending:
-        json[FIELD_STATUS] = "Pending";
-        pending = true;
-        break;
-
-      case Status_Success:
-        json[FIELD_STATUS] = "Success";
-        pending = false;
-        break;
-
-      case Status_Failure:
-        json[FIELD_STATUS] = "Failure";
-        pending = false;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (!pending)
-    {
-      {
-        Json::Value success = Json::arrayValue;
-        for (std::list<Success>::const_iterator
-               it = success_.begin(); it != success_.end(); ++it)
-        {
-          Json::Value item = Json::objectValue;
-          item[FIELD_SOP_CLASS_UID] = it->sopClassUid_;
-          item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_;
-          success.append(item);
-        }
-
-        json[FIELD_SUCCESS] = success;
-      }
-
-      {
-        Json::Value failures = Json::arrayValue;
-        for (std::list<Failure>::const_iterator
-               it = failures_.begin(); it != failures_.end(); ++it)
-        {
-          Json::Value item = Json::objectValue;
-          item[FIELD_SOP_CLASS_UID] = it->sopClassUid_;
-          item[FIELD_SOP_INSTANCE_UID] = it->sopInstanceUid_;
-          item[FIELD_FAILURE_REASON] = it->reason_;
-          item[FIELD_DESCRIPTION] = EnumerationToString(it->reason_);
-          failures.append(item);
-        }
-
-        json[FIELD_FAILURES] = failures;
-      }
-    }
-  }
-
-
-  void StorageCommitmentReports::Report::GetSuccessSopInstanceUids(
-    std::vector<std::string>& target) const
-  {
-    target.clear();
-    target.reserve(success_.size());
-
-    for (std::list<Success>::const_iterator
-           it = success_.begin(); it != success_.end(); ++it)
-    {
-      target.push_back(it->sopInstanceUid_);
-    }
-  }
-
-
-  StorageCommitmentReports::~StorageCommitmentReports()
-  {
-    while (!content_.IsEmpty())
-    {
-      Report* report = NULL;
-      content_.RemoveOldest(report);
-
-      assert(report != NULL);
-      delete report;
-    }
-  }
-
-  
-  void StorageCommitmentReports::Store(const std::string& transactionUid,
-                                       Report* report)
-  {
-    std::unique_ptr<Report> protection(report);
-    
-    boost::mutex::scoped_lock lock(mutex_);
-
-    {
-      Report* previous = NULL;
-      if (content_.Contains(transactionUid, previous))
-      {
-        assert(previous != NULL);
-        delete previous;
-
-        content_.Invalidate(transactionUid);
-      }
-    }
-
-    assert(maxSize_ == 0 ||
-           content_.GetSize() <= maxSize_);
-
-    if (maxSize_ != 0 &&
-        content_.GetSize() == maxSize_)
-    {
-      assert(!content_.IsEmpty());
-      
-      Report* oldest = NULL;
-      content_.RemoveOldest(oldest);
-
-      assert(oldest != NULL);
-      delete oldest;
-    }
-
-    assert(maxSize_ == 0 ||
-           content_.GetSize() < maxSize_);
-
-    content_.Add(transactionUid, protection.release());
-  }
-
-
-  StorageCommitmentReports::Accessor::Accessor(StorageCommitmentReports& that,
-                                               const std::string& transactionUid) :
-    lock_(that.mutex_),
-    transactionUid_(transactionUid)
-  {
-    if (that.content_.Contains(transactionUid, report_))
-    {
-      that.content_.MakeMostRecent(transactionUid);
-    }
-    else
-    {
-      report_ = NULL;
-    }
-  }
-
-  const StorageCommitmentReports::Report&
-  StorageCommitmentReports::Accessor::GetReport() const
-  {
-    if (report_ == NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return *report_;
-    }
-  }
-}
--- a/OrthancServer/StorageCommitmentReports.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Cache/LeastRecentlyUsedIndex.h"
-
-namespace Orthanc
-{
-  class StorageCommitmentReports
-  {
-  public:
-    class Report : public boost::noncopyable
-    {
-    public:
-      enum Status
-      {
-        Status_Success,
-        Status_Failure,
-        Status_Pending
-      };
-      
-    private:
-      struct Success
-      {
-        std::string  sopClassUid_;
-        std::string  sopInstanceUid_;
-      };
-      
-      struct Failure
-      {
-        std::string  sopClassUid_;
-        std::string  sopInstanceUid_;
-        StorageCommitmentFailureReason  reason_;
-      };
-      
-      bool                isComplete_;
-      std::list<Success>  success_;
-      std::list<Failure>  failures_;
-      std::string         remoteAet_;
-
-    public:
-      Report(const std::string& remoteAet) :
-        isComplete_(false),
-        remoteAet_(remoteAet)
-      {
-      }
-
-      const std::string& GetRemoteAet() const
-      {
-        return remoteAet_;
-      }
-
-      void MarkAsComplete();
-
-      void AddSuccess(const std::string& sopClassUid,
-                      const std::string& sopInstanceUid);
-
-      void AddFailure(const std::string& sopClassUid,
-                      const std::string& sopInstanceUid,
-                      StorageCommitmentFailureReason reason);
-
-      Status GetStatus() const;
-
-      void Format(Json::Value& json) const;
-
-      void GetSuccessSopInstanceUids(std::vector<std::string>& target) const;
-    };
-
-  private:
-    typedef LeastRecentlyUsedIndex<std::string, Report*>  Content;
-    
-    boost::mutex   mutex_;
-    Content        content_;
-    size_t         maxSize_;
-
-  public:
-    StorageCommitmentReports(size_t maxSize) :
-      maxSize_(maxSize)
-    {
-    }
-
-    ~StorageCommitmentReports();
-
-    size_t GetMaxSize() const
-    {
-      return maxSize_;
-    }
-
-    void Store(const std::string& transactionUid,
-               Report* report); // Takes ownership
-
-    class Accessor : public boost::noncopyable
-    {
-    private:
-      boost::mutex::scoped_lock  lock_;
-      std::string                transactionUid_;
-      Report                    *report_;
-
-    public:
-      Accessor(StorageCommitmentReports& that,
-               const std::string& transactionUid);
-
-      const std::string& GetTransactionUid() const
-      {
-        return transactionUid_;
-      }
-
-      bool IsValid() const
-      {
-        return report_ != NULL;
-      }
-
-      const Report& GetReport() const;
-    };
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/BitbucketCACertificates.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,25 @@
+#define BITBUCKET_CERTIFICATES  \
+"-----BEGIN CERTIFICATE-----\n"  \
+"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n"  \
+"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"  \
+"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n"  \
+"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n"  \
+"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n"  \
+"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n"  \
+"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n"  \
+"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n"  \
+"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n"  \
+"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n"  \
+"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n"  \
+"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n"  \
+"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n"  \
+"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n"  \
+"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n"  \
+"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n"  \
+"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n"  \
+"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n"  \
+"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n"  \
+"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n"  \
+"+OkuE6N36B9K\n"  \
+"-----END CERTIFICATE-----\n"  \
+"\n" 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/DatabaseLookupTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,279 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../OrthancServer/Search/DatabaseLookup.h"
+#include "../Core/OrthancException.h"
+
+using namespace Orthanc;
+
+
+TEST(DatabaseLookup, SingleConstraint)
+{
+  {
+    ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, 
+                                        "HEL*LO", true, true), OrthancException);
+    ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal,
+                                        "HEL?LO", true, true), OrthancException);
+    ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal,
+                                        true, true), OrthancException);
+
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true, true);
+    ASSERT_TRUE(tag.IsMatch("HELLO"));
+    ASSERT_FALSE(tag.IsMatch("hello"));
+
+    ASSERT_TRUE(tag.IsCaseSensitive());
+    ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType());
+
+    DicomMap m;
+    ASSERT_FALSE(tag.IsMatch(m));
+    m.SetNullValue(DICOM_TAG_PATIENT_NAME);
+    ASSERT_FALSE(tag.IsMatch(m));
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", true /* binary */);
+    ASSERT_FALSE(tag.IsMatch(m));
+    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false /* string */);
+    ASSERT_TRUE(tag.IsMatch(m));
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false, true);
+    ASSERT_TRUE(tag.IsMatch("HELLO"));
+    ASSERT_TRUE(tag.IsMatch("hello"));
+
+    ASSERT_EQ("HELlo", tag.GetValue());
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true, true);
+    ASSERT_TRUE(tag.IsMatch("HELLO"));
+    ASSERT_TRUE(tag.IsMatch("HELLLLLO"));
+    ASSERT_TRUE(tag.IsMatch("HELxO"));
+    ASSERT_FALSE(tag.IsMatch("hello"));
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false, true);
+    ASSERT_TRUE(tag.IsMatch("HELLO"));
+    ASSERT_TRUE(tag.IsMatch("HELLLLLO"));
+    ASSERT_TRUE(tag.IsMatch("HELxO"));
+    ASSERT_TRUE(tag.IsMatch("hello"));
+
+    ASSERT_FALSE(tag.IsCaseSensitive());
+    ASSERT_EQ(ConstraintType_Wildcard, tag.GetConstraintType());
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true, true);
+    ASSERT_TRUE(tag.IsMatch("120"));
+    ASSERT_TRUE(tag.IsMatch("123"));
+    ASSERT_FALSE(tag.IsMatch("124"));
+    ASSERT_TRUE(tag.IsMandatory());
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true, false);
+    ASSERT_FALSE(tag.IsMatch("122"));
+    ASSERT_TRUE(tag.IsMatch("123"));
+    ASSERT_TRUE(tag.IsMatch("124"));
+    ASSERT_FALSE(tag.IsMandatory());
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true, true);
+    ASSERT_FALSE(tag.IsMatch("CT"));
+    ASSERT_FALSE(tag.IsMatch("MR"));
+
+    tag.AddValue("CT");
+    ASSERT_TRUE(tag.IsMatch("CT"));
+    ASSERT_FALSE(tag.IsMatch("MR"));
+
+    tag.AddValue("MR");
+    ASSERT_TRUE(tag.IsMatch("CT"));
+    ASSERT_TRUE(tag.IsMatch("MR"));
+    ASSERT_FALSE(tag.IsMatch("ct"));
+    ASSERT_FALSE(tag.IsMatch("mr"));
+
+    ASSERT_THROW(tag.GetValue(), OrthancException);
+    ASSERT_EQ(2u, tag.GetValues().size());
+  }
+
+  {
+    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false, true);
+
+    tag.AddValue("ct");
+    tag.AddValue("mr");
+
+    ASSERT_TRUE(tag.IsMatch("CT"));
+    ASSERT_TRUE(tag.IsMatch("MR"));
+    ASSERT_TRUE(tag.IsMatch("ct"));
+    ASSERT_TRUE(tag.IsMatch("mr"));
+  }
+}
+
+
+
+TEST(DatabaseLookup, FromDicom)
+{
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, true);
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
+    ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue());
+    ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false, true);
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+
+    // This is *not* a PN VR => "false" above is *not* used
+    ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true, true);
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false, true);
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+
+    // This is a PN VR => "false" above is used
+    ASSERT_FALSE(lookup.GetConstraint(0).IsCaseSensitive());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true);
+
+    // This is not a data VR
+    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false, true);
+
+    // This is a data VR => range is effective
+    ASSERT_EQ(2u, lookup.GetConstraintsCount());
+
+    ASSERT_TRUE(lookup.GetConstraint(0).GetConstraintType() != lookup.GetConstraint(1).GetConstraintType());
+
+    for (size_t i = 0; i < 2; i++)
+    {
+      ASSERT_TRUE(lookup.GetConstraint(i).GetConstraintType() == ConstraintType_SmallerOrEqual ||
+                  lookup.GetConstraint(i).GetConstraintType() == ConstraintType_GreaterOrEqual);
+    }
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true);
+
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType());
+    ASSERT_EQ("2012", lookup.GetConstraint(0).GetValue());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false, true);
+
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE,  lookup.GetConstraint(0).GetTag());
+    ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetConstraintType());
+    ASSERT_EQ("2016", lookup.GetConstraint(0).GetValue());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false, true);
+
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(DICOM_TAG_MODALITY,  lookup.GetConstraint(0).GetTag());
+    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
+
+    const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
+    ASSERT_EQ(2u, values.size());
+    ASSERT_TRUE(values.find("CT") != values.end());
+    ASSERT_TRUE(values.find("MR") != values.end());
+    ASSERT_TRUE(values.find("nope") == values.end());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false, true);
+
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag());
+    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
+
+    const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
+    ASSERT_EQ(2u, values.size());
+    ASSERT_TRUE(values.find("CT") != values.end());
+    ASSERT_TRUE(values.find("MR") != values.end());
+    ASSERT_TRUE(values.find("nope") == values.end());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true);
+
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true);
+
+    ASSERT_EQ(1u, lookup.GetConstraintsCount());
+    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
+  }
+
+  {
+    DatabaseLookup lookup;
+    lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false, true);
+    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "TEST2", false, false);
+    ASSERT_TRUE(lookup.GetConstraint(0).IsMandatory());
+    ASSERT_FALSE(lookup.GetConstraint(1).IsMandatory());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/DicomMapTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,1048 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Compatibility.h"
+#include "../Core/OrthancException.h"
+#include "../Core/DicomFormat/DicomMap.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/DicomParsing/DicomWebJsonVisitor.h"
+
+#include "../OrthancServer/DicomInstanceToStore.h"
+
+#include <memory>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <dcmtk/dcmdata/dcvrat.h>
+
+using namespace Orthanc;
+
+TEST(DicomMap, MainTags)
+{
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
+  ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
+
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
+  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
+
+  std::set<DicomTag> s;
+  DicomMap::GetMainDicomTags(s);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Patient);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Study);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Series);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+
+  DicomMap::GetMainDicomTags(s, ResourceType_Instance);
+  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
+  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
+}
+
+
+TEST(DicomMap, Tags)
+{
+  std::set<DicomTag> s;
+
+  DicomMap m;
+  m.GetTags(s);
+  ASSERT_EQ(0u, s.size());
+
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(m.HasTag(0x0010, 0x0010));
+  m.SetValue(0x0010, 0x0010, "PatientName", false);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(m.HasTag(0x0010, 0x0010));
+
+  m.GetTags(s);
+  ASSERT_EQ(1u, s.size());
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
+
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID));
+  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID", false);
+  ASSERT_TRUE(m.HasTag(0x0010, 0x0020));
+  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2", false);
+  ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).GetContent());
+
+  m.GetTags(s);
+  ASSERT_EQ(2u, s.size());
+
+  m.Remove(DICOM_TAG_PATIENT_ID);
+  ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException);
+
+  m.GetTags(s);
+  ASSERT_EQ(1u, s.size());
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
+
+  std::unique_ptr<DicomMap> mm(m.Clone());
+  ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).GetContent());  
+
+  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());  
+
+  DicomValue v;
+  ASSERT_TRUE(v.IsNull());
+}
+
+
+TEST(DicomMap, FindTemplates)
+{
+  DicomMap m;
+
+  DicomMap::SetupFindPatientTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID));
+
+  DicomMap::SetupFindStudyTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID));
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER));
+
+  DicomMap::SetupFindSeriesTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID));
+
+  DicomMap::SetupFindInstanceTemplate(m);
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID));
+}
+
+
+
+
+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);
+  
+  // The main dicom tags are a subset of the module
+  for (std::set<DicomTag>::const_iterator it = main.begin(); it != main.end(); ++it)
+  {
+    bool ok = moduleTags.find(*it) != moduleTags.end();
+
+    // 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) ||  /* 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::GetTagName(*it, "")
+                << " not expected at level " << EnumerationToString(level) << std::endl;
+    }
+
+    EXPECT_TRUE(ok);
+  }
+}
+
+
+TEST(DicomMap, Modules)
+{
+  TestModule(ResourceType_Patient, DicomModule_Patient);
+  TestModule(ResourceType_Study, DicomModule_Study);
+  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;
+  unsigned int ui;
+  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.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+  ASSERT_TRUE(m.LookupStringValue(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.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+  ASSERT_EQ("42", s);
+  ASSERT_TRUE(m.LookupStringValue(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);
+
+
+  // "800\0" in US COLMUNS tag
+  m.SetValue(DICOM_TAG_COLUMNS, "800\0", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(ui));
+  ASSERT_EQ(800u, ui);
+  m.SetValue(DICOM_TAG_COLUMNS, "800", false);
+  ASSERT_TRUE(m.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(ui));
+  ASSERT_EQ(800u, ui);
+}
+
+
+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);
+  }
+}
+
+
+
+TEST(DicomMap, DicomAsJson)
+{
+  // This is a Latin-1 test string: "crane" with a circumflex accent
+  const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
+  std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char));
+
+  std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false);
+
+  ParsedDicomFile dicom(false);
+  dicom.SetEncoding(Encoding_Latin1);
+  dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Hello");
+  dicom.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, utf8);
+  dicom.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, std::string(ORTHANC_MAXIMUM_TAG_LENGTH, 'a'));
+  dicom.ReplacePlainString(DICOM_TAG_MANUFACTURER, std::string(ORTHANC_MAXIMUM_TAG_LENGTH + 1, 'a'));
+  dicom.ReplacePlainString(DICOM_TAG_PIXEL_DATA, "binary");
+  dicom.ReplacePlainString(DICOM_TAG_ROWS, "512");
+
+  DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
+  dataset.insertEmptyElement(DCM_StudyID, OFFalse);
+
+  {
+    std::unique_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence));
+
+    {
+      std::unique_ptr<DcmItem> item(new DcmItem);
+      item->putAndInsertString(DCM_ReferencedSOPInstanceUID, "nope", OFFalse);
+      ASSERT_TRUE(sequence->insert(item.release(), false, false).good());
+    }
+
+    ASSERT_TRUE(dataset.insert(sequence.release(), false, false).good());
+  }
+  
+                          
+  // Check re-encoding
+  DcmElement* element = NULL;
+  ASSERT_TRUE(dataset.findAndGetElement(DCM_StudyDescription, element).good() &&
+              element != NULL);
+
+  char* c = NULL;
+  ASSERT_TRUE(element != NULL &&
+              element->isLeaf() &&
+              element->isaString() &&
+              element->getString(c).good());
+  ASSERT_EQ(0, memcmp(c, raw, latin1.length()));
+
+  ASSERT_TRUE(dataset.findAndGetElement(DCM_Rows, element).good() &&
+              element != NULL &&
+              element->getTag().getEVR() == EVR_US);
+
+  DicomInstanceToStore toStore;
+  toStore.SetParsedDicomFile(dicom);
+
+  DicomMap m;
+  m.FromDicomAsJson(toStore.GetJson());
+
+  ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
+  
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
+  ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+  
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
+  ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
+  ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
+  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
+  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
+
+  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
+  ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH,
+            static_cast<int>(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()));
+
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
+  ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
+
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
+  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
+  ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
+
+  DicomArray a(m);
+  ASSERT_EQ(6u, a.GetSize());
+
+  
+  //dicom.SaveToFile("/tmp/test.dcm"); 
+  //std::cout << toStore.GetJson() << std::endl;
+  //a.Print(stdout);
+}
+
+
+
+TEST(DicomMap, ExtractMainDicomTags)
+{
+  DicomMap b;
+  b.SetValue(DICOM_TAG_PATIENT_NAME, "E", false);
+  ASSERT_TRUE(b.HasOnlyMainDicomTags());
+
+  {
+    DicomMap a;
+    a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
+    a.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "B", false);
+    a.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "C", false);
+    a.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "D", false);
+    a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false);
+    ASSERT_FALSE(a.HasOnlyMainDicomTags());
+    b.ExtractMainDicomTags(a);
+  }
+
+  ASSERT_EQ(4u, b.GetSize());
+  ASSERT_EQ("A", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+  ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+  ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent());
+  ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
+  ASSERT_FALSE(b.HasTag(DICOM_TAG_SLICE_THICKNESS));
+  ASSERT_TRUE(b.HasOnlyMainDicomTags());
+
+  b.SetValue(DICOM_TAG_PATIENT_NAME, "G", false);
+
+  {
+    DicomMap a;
+    a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
+    a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false);
+    ASSERT_FALSE(a.HasOnlyMainDicomTags());
+    b.Merge(a);
+  }
+
+  ASSERT_EQ(5u, b.GetSize());
+  ASSERT_EQ("G", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
+  ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
+  ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent());
+  ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
+  ASSERT_EQ("F", b.GetValue(DICOM_TAG_SLICE_THICKNESS).GetContent());
+  ASSERT_FALSE(b.HasOnlyMainDicomTags());
+}
+
+
+TEST(DicomMap, RemoveBinary)
+{
+  DicomMap b;
+  b.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
+  b.SetValue(DICOM_TAG_PATIENT_ID, "B", true);
+  b.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, DicomValue());  // NULL
+  b.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, DicomValue("C", false));
+  b.SetValue(DICOM_TAG_SOP_INSTANCE_UID, DicomValue("D", true));
+
+  b.RemoveBinaryTags();
+
+  std::string s;
+  ASSERT_EQ(2u, b.GetSize());
+  ASSERT_TRUE(b.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); ASSERT_EQ("A", s);
+  ASSERT_TRUE(b.LookupStringValue(s, DICOM_TAG_SERIES_INSTANCE_UID, false)); ASSERT_EQ("C", s);
+}
+
+
+
+TEST(DicomWebJson, Multiplicity)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.4.html
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "SB1^SB2^SB3^SB4^SB5");
+  dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1\\2.3\\4");
+  dicom.ReplacePlainString(DICOM_TAG_IMAGE_POSITION_PATIENT, "");
+
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  {
+    const Json::Value& tag = visitor.GetResult() ["00200037"];  // ImageOrientationPatient
+    const Json::Value& value = tag["Value"];
+  
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(2u, tag.getMemberNames().size());
+    ASSERT_EQ(3u, value.size());
+    ASSERT_EQ(Json::realValue, value[1].type());
+    ASSERT_FLOAT_EQ(1.0f, value[0].asFloat());
+    ASSERT_FLOAT_EQ(2.3f, value[1].asFloat());
+    ASSERT_FLOAT_EQ(4.0f, value[2].asFloat());
+  }
+
+  {
+    const Json::Value& tag = visitor.GetResult() ["00200032"];  // ImagePositionPatient
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(1u, tag.getMemberNames().size());
+  }
+
+  std::string xml;
+  visitor.FormatXml(xml);
+
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(3u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+    ASSERT_EQ("SB1^SB2^SB3^SB4^SB5", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_POSITION_PATIENT, false));
+    ASSERT_TRUE(s.empty());
+
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false));
+
+    std::vector<std::string> v;
+    Toolbox::TokenizeString(v, s, '\\');
+    ASSERT_FLOAT_EQ(1.0f, boost::lexical_cast<float>(v[0]));
+    ASSERT_FLOAT_EQ(2.3f, boost::lexical_cast<float>(v[1]));
+    ASSERT_FLOAT_EQ(4.0f, boost::lexical_cast<float>(v[2]));
+  }
+}
+
+
+TEST(DicomWebJson, NullValue)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.5.html
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1.5\\\\\\2.5");
+
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  {
+    const Json::Value& tag = visitor.GetResult() ["00200037"];
+    const Json::Value& value = tag["Value"];
+  
+    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
+    ASSERT_EQ(2u, tag.getMemberNames().size());
+    ASSERT_EQ(4u, value.size());
+    ASSERT_EQ(Json::realValue, value[0].type());
+    ASSERT_EQ(Json::nullValue, value[1].type());
+    ASSERT_EQ(Json::nullValue, value[2].type());
+    ASSERT_EQ(Json::realValue, value[3].type());
+    ASSERT_FLOAT_EQ(1.5f, value[0].asFloat());
+    ASSERT_FLOAT_EQ(2.5f, value[3].asFloat());
+  }
+
+  std::string xml;
+  visitor.FormatXml(xml);
+  
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(1u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false));
+
+    std::vector<std::string> v;
+    Toolbox::TokenizeString(v, s, '\\');
+    ASSERT_FLOAT_EQ(1.5f, boost::lexical_cast<float>(v[0]));
+    ASSERT_TRUE(v[1].empty());
+    ASSERT_TRUE(v[2].empty());
+    ASSERT_FLOAT_EQ(2.5f, boost::lexical_cast<float>(v[3]));
+  }
+}
+
+
+static void SetTagKey(ParsedDicomFile& dicom,
+                      const DicomTag& tag,
+                      const DicomTag& value)
+{
+  // This function emulates a call to function
+  // "dicom.GetDcmtkObject().getDataset()->putAndInsertTagKey(tag,
+  // value)" that was not available in DCMTK 3.6.0
+
+  std::unique_ptr<DcmAttributeTag> element(new DcmAttributeTag(ToDcmtkBridge::Convert(tag)));
+
+  DcmTagKey v = ToDcmtkBridge::Convert(value);
+  if (!element->putTagVal(v).good())
+  {
+    throw OrthancException(ErrorCode_InternalError);
+  }
+
+  dicom.GetDcmtkObject().getDataset()->insert(element.release());
+}
+
+
+TEST(DicomWebJson, ValueRepresentation)
+{
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DicomTag(0x0040, 0x0241), "AE");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x1010), "AS");
+  SetTagKey(dicom, DicomTag(0x0020, 0x9165), DicomTag(0x0010, 0x0020));
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0052), "CS");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0012), "DA");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x1020), "42");  // DS
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x002a), "DT");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x9431), "43");  // FL
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1163), "44");  // FD
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1160), "45");  // IS
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0070), "LO");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x4000), "LT");
+  dicom.ReplacePlainString(DicomTag(0x0028, 0x2000), "OB");
+  dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "3.14159");  // OD (other double)
+  dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "2.71828");  // OF (other float)
+  dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");  // OL (other long)
+  ASSERT_THROW(dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "O"), OrthancException);
+  dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "OWOW");
+  dicom.ReplacePlainString(DicomTag(0x0010, 0x0010), "PN");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0050), "SH");
+  dicom.ReplacePlainString(DicomTag(0x0018, 0x6020), "-15");  // SL
+  dicom.ReplacePlainString(DicomTag(0x0018, 0x9219), "-16");  // SS
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0081), "ST");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0013), "TM");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0119), "UC");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0016), "UI");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x1161), "128");  // UL
+  dicom.ReplacePlainString(DicomTag(0x4342, 0x1234), "UN");   // Inexistent tag
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0120), "UR");
+  dicom.ReplacePlainString(DicomTag(0x0008, 0x0301), "17");   // US
+  dicom.ReplacePlainString(DicomTag(0x0040, 0x0031), "UT");  
+
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  std::string s;
+
+  // The tag (0002,0002) is "Media Storage SOP Class UID" and is
+  // automatically copied by DCMTK from tag (0008,0016)
+  ASSERT_EQ("UI", visitor.GetResult() ["00020002"]["vr"].asString());
+  ASSERT_EQ("UI", visitor.GetResult() ["00020002"]["Value"][0].asString());
+  ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["vr"].asString());
+  ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["Value"][0].asString());
+  ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["vr"].asString());
+  ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["Value"][0].asString());
+  ASSERT_EQ("AT", visitor.GetResult() ["00209165"]["vr"].asString());
+  ASSERT_EQ("00100020", visitor.GetResult() ["00209165"]["Value"][0].asString());
+  ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["vr"].asString());
+  ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["Value"][0].asString());
+  ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["vr"].asString());
+  ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["Value"][0].asString());
+  ASSERT_EQ("DS", visitor.GetResult() ["00101020"]["vr"].asString());
+  ASSERT_FLOAT_EQ(42.0f, visitor.GetResult() ["00101020"]["Value"][0].asFloat());
+  ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["vr"].asString());
+  ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["Value"][0].asString());
+  ASSERT_EQ("FL", visitor.GetResult() ["00109431"]["vr"].asString());
+  ASSERT_FLOAT_EQ(43.0f, visitor.GetResult() ["00109431"]["Value"][0].asFloat());
+  ASSERT_EQ("FD", visitor.GetResult() ["00081163"]["vr"].asString());
+  ASSERT_FLOAT_EQ(44.0f, visitor.GetResult() ["00081163"]["Value"][0].asFloat());
+  ASSERT_EQ("IS", visitor.GetResult() ["00081160"]["vr"].asString());
+  ASSERT_FLOAT_EQ(45.0f, visitor.GetResult() ["00081160"]["Value"][0].asFloat());
+  ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["vr"].asString());
+  ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["Value"][0].asString());
+  ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["vr"].asString());
+  ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["Value"][0].asString());
+
+  ASSERT_EQ("OB", visitor.GetResult() ["00282000"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00282000"]["InlineBinary"].asString());
+  ASSERT_EQ("OB", s);
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("OD", visitor.GetResult() ["7FE00009"]["vr"].asString());
+  ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast<float>(visitor.GetResult() ["7FE00009"]["Value"][0].asString()));
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["7FE00009"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString());
+  ASSERT_EQ(8u, s.size()); // Because of padding
+  ASSERT_EQ(0, s[7]);
+  ASSERT_EQ("3.14159", s.substr(0, 7));
+#endif
+
+  ASSERT_EQ("OF", visitor.GetResult() ["00640009"]["vr"].asString());
+  ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(visitor.GetResult() ["00640009"]["Value"][0].asString()));
+
+#if DCMTK_VERSION_NUMBER < 361
+  ASSERT_EQ("UN", visitor.GetResult() ["00660040"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString());
+  ASSERT_EQ("46", s);
+#elif DCMTK_VERSION_NUMBER == 361
+  ASSERT_EQ("UL", visitor.GetResult() ["00660040"]["vr"].asString());
+  ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
+#else
+  ASSERT_EQ("OL", visitor.GetResult() ["00660040"]["vr"].asString());
+  ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
+#endif
+
+  ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00281201"]["InlineBinary"].asString());
+  ASSERT_EQ("OWOW", s);
+
+  ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["vr"].asString());
+  ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["Value"][0]["Alphabetic"].asString());
+
+  ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["vr"].asString());
+  ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["Value"][0].asString());
+
+  ASSERT_EQ("SL", visitor.GetResult() ["00186020"]["vr"].asString());
+  ASSERT_EQ(-15, visitor.GetResult() ["00186020"]["Value"][0].asInt());
+
+  ASSERT_EQ("SS", visitor.GetResult() ["00189219"]["vr"].asString());
+  ASSERT_EQ(-16, visitor.GetResult() ["00189219"]["Value"][0].asInt());
+
+  ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["vr"].asString());
+  ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["Value"][0].asString());
+
+  ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["vr"].asString());
+  ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["Value"][0].asString());
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["vr"].asString());
+  ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["Value"][0].asString());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["00080119"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080119"]["InlineBinary"].asString());
+  ASSERT_EQ("UC", s);
+#endif
+
+  ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["vr"].asString());
+  ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["Value"][0].asString());
+
+  ASSERT_EQ("UL", visitor.GetResult() ["00081161"]["vr"].asString());
+  ASSERT_EQ(128u, visitor.GetResult() ["00081161"]["Value"][0].asUInt());
+
+  ASSERT_EQ("UN", visitor.GetResult() ["43421234"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["43421234"]["InlineBinary"].asString());
+  ASSERT_EQ("UN", s);
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["vr"].asString());
+  ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["Value"][0].asString());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["00080120"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080120"]["InlineBinary"].asString());
+  ASSERT_EQ("UR", s);
+#endif
+
+#if DCMTK_VERSION_NUMBER >= 361
+  ASSERT_EQ("US", visitor.GetResult() ["00080301"]["vr"].asString());
+  ASSERT_EQ(17u, visitor.GetResult() ["00080301"]["Value"][0].asUInt());
+#else
+  ASSERT_EQ("UN", visitor.GetResult() ["00080301"]["vr"].asString());
+  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080301"]["InlineBinary"].asString());
+  ASSERT_EQ("17", s);
+#endif
+
+  ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["vr"].asString());
+  ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["Value"][0].asString());
+
+  std::string xml;
+  visitor.FormatXml(xml);
+  
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(31u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0002, 0x0002), false));  ASSERT_EQ("UI", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0241), false));  ASSERT_EQ("AE", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x1010), false));  ASSERT_EQ("AS", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0020, 0x9165), false));  ASSERT_EQ("00100020", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0052), false));  ASSERT_EQ("CS", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0012), false));  ASSERT_EQ("DA", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x1020), false));  ASSERT_EQ("42", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x002a), false));  ASSERT_EQ("DT", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x9431), false));  ASSERT_EQ("43", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1163), false));  ASSERT_EQ("44", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1160), false));  ASSERT_EQ("45", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0070), false));  ASSERT_EQ("LO", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x4000), false));  ASSERT_EQ("LT", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x2000), true));   ASSERT_EQ("OB", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true));
+
+#if DCMTK_VERSION_NUMBER >= 361
+    ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast<float>(s));
+#else
+    ASSERT_EQ(8u, s.size()); // Because of padding
+    ASSERT_EQ(0, s[7]);
+    ASSERT_EQ("3.14159", s.substr(0, 7));
+#endif
+
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true));
+    ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(s));
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x1201), true));   ASSERT_EQ("OWOW", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x0010), false));  ASSERT_EQ("PN", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0050), false));  ASSERT_EQ("SH", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x6020), false));  ASSERT_EQ("-15", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x9219), false));  ASSERT_EQ("-16", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0081), false));  ASSERT_EQ("ST", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0013), false));  ASSERT_EQ("TM", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0016), false));  ASSERT_EQ("UI", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1161), false));  ASSERT_EQ("128", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x4342, 0x1234), true));   ASSERT_EQ("UN", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0031), false));  ASSERT_EQ("UT", s);
+
+#if DCMTK_VERSION_NUMBER >= 361
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false));  ASSERT_EQ("46", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false));  ASSERT_EQ("UC", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false));  ASSERT_EQ("UR", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false));  ASSERT_EQ("17", s);
+#else
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true));  ASSERT_EQ("46", s);  // OL
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), true));  ASSERT_EQ("UC", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), true));  ASSERT_EQ("UR", s);
+    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), true));  ASSERT_EQ("17", s);  // US (but tag unknown to DCMTK 3.6.0)
+#endif
+    
+  }
+}
+
+
+TEST(DicomWebJson, Sequence)
+{
+  ParsedDicomFile dicom(false);
+  
+  {
+    std::unique_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence));
+
+    for (unsigned int i = 0; i < 3; i++)
+    {
+      std::unique_ptr<DcmItem> item(new DcmItem);
+      std::string s = "item" + boost::lexical_cast<std::string>(i);
+      item->putAndInsertString(DCM_ReferencedSOPInstanceUID, s.c_str(), OFFalse);
+      ASSERT_TRUE(sequence->insert(item.release(), false, false).good());
+    }
+
+    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->insert(sequence.release(), false, false).good());
+  }
+
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+
+  ASSERT_EQ("SQ", visitor.GetResult() ["00081115"]["vr"].asString());
+  ASSERT_EQ(3u, visitor.GetResult() ["00081115"]["Value"].size());
+
+  std::set<std::string> items;
+  
+  for (Json::Value::ArrayIndex i = 0; i < 3; i++)
+  {
+    ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i].size());
+    ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"].size());
+    ASSERT_EQ("UI", visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["vr"].asString());
+    items.insert(visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"][0].asString());
+  }
+
+  ASSERT_EQ(3u, items.size());
+  ASSERT_TRUE(items.find("item0") != items.end());
+  ASSERT_TRUE(items.find("item1") != items.end());
+  ASSERT_TRUE(items.find("item2") != items.end());
+
+  std::string xml;
+  visitor.FormatXml(xml);
+
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(0u, m.GetSize());  // Sequences are not handled by DicomMap
+  }
+}
+
+
+TEST(DicomWebJson, PixelSpacing)
+{
+  // Test related to locales: Make sure that decimal separator is
+  // correctly handled (dot "." vs comma ",")
+  ParsedDicomFile source(false);
+  source.ReplacePlainString(DICOM_TAG_PIXEL_SPACING, "1.5\\1.3");
+
+  DicomWebJsonVisitor visitor;
+  source.Apply(visitor);
+
+  DicomMap target;
+  target.FromDicomWeb(visitor.GetResult());
+
+  ASSERT_EQ("DS", visitor.GetResult() ["00280030"]["vr"].asString());
+  ASSERT_FLOAT_EQ(1.5f, visitor.GetResult() ["00280030"]["Value"][0].asFloat());
+  ASSERT_FLOAT_EQ(1.3f, visitor.GetResult() ["00280030"]["Value"][1].asFloat());
+
+  std::string s;
+  ASSERT_TRUE(target.LookupStringValue(s, DICOM_TAG_PIXEL_SPACING, false));
+  ASSERT_EQ(s, "1.5\\1.3");
+}
+
+
+TEST(DicomMap, MainTagNames)
+{
+  ASSERT_EQ(3, ResourceType_Instance - ResourceType_Patient);
+  
+  for (int i = ResourceType_Patient; i <= ResourceType_Instance; i++)
+  {
+    ResourceType level = static_cast<ResourceType>(i);
+
+    std::set<DicomTag> tags;
+    DicomMap::GetMainDicomTags(tags, level);
+
+    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
+    {
+      DicomMap a;
+      a.SetValue(*it, "TEST", false);
+
+      Json::Value json;
+      a.DumpMainDicomTags(json, level);
+
+      ASSERT_EQ(Json::objectValue, json.type());
+      ASSERT_EQ(1u, json.getMemberNames().size());
+
+      std::string name = json.getMemberNames() [0];
+      EXPECT_EQ(name, FromDcmtkBridge::GetTagName(*it, ""));
+
+      DicomMap b;
+      b.ParseMainDicomTags(json, level);
+
+      ASSERT_EQ(1u, b.GetSize());
+      ASSERT_EQ("TEST", b.GetStringValue(*it, "", false));
+
+      std::string main = it->GetMainTagsName();
+      if (!main.empty())
+      {
+        ASSERT_EQ(main, name);
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/FileStorageTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,190 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <ctype.h>
+
+#include "../Core/FileStorage/FilesystemStorage.h"
+#include "../Core/FileStorage/StorageAccessor.h"
+#include "../Core/HttpServer/BufferHttpSender.h"
+#include "../Core/HttpServer/FilesystemHttpSender.h"
+#include "../Core/Logging.h"
+#include "../Core/OrthancException.h"
+#include "../Core/Toolbox.h"
+#include "../OrthancServer/ServerIndex.h"
+
+using namespace Orthanc;
+
+
+static void StringToVector(std::vector<uint8_t>& v,
+                           const std::string& s)
+{
+  v.resize(s.size());
+  for (size_t i = 0; i < s.size(); i++)
+    v[i] = s[i];
+}
+
+
+TEST(FilesystemStorage, Basic)
+{
+  FilesystemStorage s("UnitTestsStorage");
+
+  std::string data = Toolbox::GenerateUuid();
+  std::string uid = Toolbox::GenerateUuid();
+  s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
+  std::string d;
+  s.Read(d, uid, FileContentType_Unknown);
+  ASSERT_EQ(d.size(), data.size());
+  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
+  ASSERT_EQ(s.GetSize(uid), data.size());
+}
+
+TEST(FilesystemStorage, Basic2)
+{
+  FilesystemStorage s("UnitTestsStorage");
+
+  std::vector<uint8_t> data;
+  StringToVector(data, Toolbox::GenerateUuid());
+  std::string uid = Toolbox::GenerateUuid();
+  s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
+  std::string d;
+  s.Read(d, uid, FileContentType_Unknown);
+  ASSERT_EQ(d.size(), data.size());
+  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
+  ASSERT_EQ(s.GetSize(uid), data.size());
+}
+
+TEST(FilesystemStorage, EndToEnd)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  s.Clear();
+
+  std::list<std::string> u;
+  for (unsigned int i = 0; i < 10; i++)
+  {
+    std::string t = Toolbox::GenerateUuid();
+    std::string uid = Toolbox::GenerateUuid();
+    s.Create(uid.c_str(), &t[0], t.size(), FileContentType_Unknown);
+    u.push_back(uid);
+  }
+
+  std::set<std::string> ss;
+  s.ListAllFiles(ss);
+  ASSERT_EQ(10u, ss.size());
+  
+  unsigned int c = 0;
+  for (std::list<std::string>::iterator
+         i = u.begin(); i != u.end(); i++, c++)
+  {
+    ASSERT_TRUE(ss.find(*i) != ss.end());
+    if (c < 5)
+      s.Remove(*i, FileContentType_Unknown);
+  }
+
+  s.ListAllFiles(ss);
+  ASSERT_EQ(5u, ss.size());
+
+  s.Clear();
+  s.ListAllFiles(ss);
+  ASSERT_EQ(0u, ss.size());
+}
+
+
+TEST(StorageAccessor, NoCompression)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  StorageAccessor accessor(s);
+
+  std::string data = "Hello world";
+  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
+  
+  std::string r;
+  accessor.Read(r, info);
+
+  ASSERT_EQ(data, r);
+  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
+  ASSERT_EQ(11u, info.GetUncompressedSize());
+  ASSERT_EQ(11u, info.GetCompressedSize());
+  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
+  ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5());
+  ASSERT_EQ(info.GetUncompressedMD5(), info.GetCompressedMD5());
+}
+
+
+TEST(StorageAccessor, Compression)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  StorageAccessor accessor(s);
+
+  std::string data = "Hello world";
+  FileInfo info = accessor.Write(data, FileContentType_DicomAsJson, CompressionType_ZlibWithSize, true);
+  
+  std::string r;
+  accessor.Read(r, info);
+
+  ASSERT_EQ(data, r);
+  ASSERT_EQ(CompressionType_ZlibWithSize, info.GetCompressionType());
+  ASSERT_EQ(11u, info.GetUncompressedSize());
+  ASSERT_EQ(FileContentType_DicomAsJson, info.GetContentType());
+  ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5());
+  ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5());
+}
+
+
+TEST(StorageAccessor, Mix)
+{
+  FilesystemStorage s("UnitTestsStorage");
+  StorageAccessor accessor(s);
+
+  std::string r;
+  std::string compressedData = "Hello";
+  std::string uncompressedData = "HelloWorld";
+
+  FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false);  
+  FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false);
+  
+  accessor.Read(r, compressedInfo);
+  ASSERT_EQ(compressedData, r);
+
+  accessor.Read(r, uncompressedInfo);
+  ASSERT_EQ(uncompressedData, r);
+  ASSERT_NE(compressedData, r);
+
+  /*
+  // This test is too slow on Windows
+  accessor.SetCompressionForNextOperations(CompressionType_ZlibWithSize);
+  ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException);
+  */
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/FromDcmtkTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2034 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Compatibility.h"
+#include "../Core/DicomNetworking/DicomFindAnswers.h"
+#include "../Core/DicomParsing/DicomModification.h"
+#include "../Core/DicomParsing/DicomWebJsonVisitor.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
+#include "../Core/DicomParsing/ToDcmtkBridge.h"
+#include "../Core/Endianness.h"
+#include "../Core/Images/Image.h"
+#include "../Core/Images/ImageBuffer.h"
+#include "../Core/Images/ImageProcessing.h"
+#include "../Core/Images/PngReader.h"
+#include "../Core/Images/PngWriter.h"
+#include "../Core/OrthancException.h"
+#include "../Core/SystemToolbox.h"
+#include "../OrthancServer/ServerToolbox.h"
+#include "../Plugins/Engine/PluginsEnumerations.h"
+#include "../Resources/EncodingTests.h"
+
+#include <dcmtk/dcmdata/dcelem.h>
+#include <dcmtk/dcmdata/dcdeftag.h>
+#include <boost/algorithm/string/predicate.hpp>
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
+
+using namespace Orthanc;
+
+TEST(DicomFormat, Tag)
+{
+  ASSERT_EQ("PatientName", FromDcmtkBridge::GetTagName(DicomTag(0x0010, 0x0010), ""));
+
+  DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
+  ASSERT_EQ(0x0008, t.GetGroup());
+  ASSERT_EQ(0x103E, t.GetElement());
+
+  t = FromDcmtkBridge::ParseTag("0020-e040");
+  ASSERT_EQ(0x0020, t.GetGroup());
+  ASSERT_EQ(0xe040, t.GetElement());
+
+  // Test ==() and !=() operators
+  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
+  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
+}
+
+
+TEST(DicomModification, Basic)
+{
+  DicomModification m;
+  m.SetupAnonymization(DicomVersion_2008);
+  //m.SetLevel(DicomRootLevel_Study);
+  //m.ReplacePlainString(DICOM_TAG_PATIENT_ID, "coucou");
+  //m.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
+
+  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::unique_ptr<ParsedDicomFile> f(o.Clone(false));
+    if (i > 4)
+      o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
+    m.Apply(*f);
+    f->SaveToFile(b);
+  }
+}
+
+
+TEST(DicomModification, Anonymization)
+{
+  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName"));
+
+  const DicomTag privateTag(0x0045, 0x1010);
+  const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
+  ASSERT_TRUE(privateTag.IsPrivate());
+  ASSERT_TRUE(privateTag2.IsPrivate());
+  ASSERT_EQ(0x0031, privateTag2.GetGroup());
+  ASSERT_EQ(0x1020, privateTag2.GetElement());
+
+  std::string s;
+  ParsedDicomFile o(true);
+  o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
+  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+  o.Insert(privateTag, "private tag", false, "OrthancCreator");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_STREQ("private tag", s.c_str());
+
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
+  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
+  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_STREQ("hello", s.c_str());
+  o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
+  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
+  ASSERT_STREQ("hello world", s.c_str());
+
+  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_FALSE(Toolbox::IsUuid(s));
+
+  DicomModification m;
+  m.SetupAnonymization(DicomVersion_2008);
+  m.Keep(privateTag);
+
+  m.Apply(o);
+
+  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_TRUE(Toolbox::IsUuid(s));
+  ASSERT_TRUE(o.GetTagValue(s, privateTag));
+  ASSERT_STREQ("private tag", s.c_str());
+  
+  m.SetupAnonymization(DicomVersion_2008);
+  m.Apply(o);
+  ASSERT_FALSE(o.GetTagValue(s, privateTag));
+}
+
+
+#include <dcmtk/dcmdata/dcuid.h>
+
+TEST(DicomModification, Png)
+{
+  // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image)
+  std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+
+  std::string m, cc;
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(m, cc, s));
+
+  ASSERT_EQ("image/png", m);
+
+  PngReader reader;
+  reader.ReadFromMemory(cc);
+
+  ASSERT_EQ(5u, reader.GetHeight());
+  ASSERT_EQ(5u, reader.GetWidth());
+  ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
+
+  ParsedDicomFile o(true);
+  o.EmbedContent(s);
+  o.SaveToFile("UnitTestsResults/png1.dcm");
+
+  // Red dot, without alpha channel
+  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII=";
+  o.EmbedContent(s);
+  o.SaveToFile("UnitTestsResults/png2.dcm");
+
+  // Check box in Graylevel8
+  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
+  o.EmbedContent(s);
+  //o.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
+  o.SaveToFile("UnitTestsResults/png3.dcm");
+
+
+  {
+    // Gradient in Graylevel16
+
+    ImageBuffer img;
+    img.SetWidth(256);
+    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*>(accessor.GetRow(y));
+      for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
+      {
+        *p = v;
+      }
+    }
+
+    o.EmbedImage(accessor);
+    o.SaveToFile("UnitTestsResults/png4.dcm");
+  }
+}
+
+
+TEST(FromDcmtkBridge, Encodings1)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    std::string source(testEncodingsEncoded[i]);
+    std::string expected(testEncodingsExpected[i]);
+    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false);
+    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+    EXPECT_EQ(expected, s);
+  }
+}
+
+
+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_Ascii, e);
+
+  // 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);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
+  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 166"));  ASSERT_EQ(Encoding_Thai, 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);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
+  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 166"));  ASSERT_EQ(Encoding_Thai, e);
+
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-4
+  ASSERT_TRUE(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_TRUE(GetDicomEncoding(e, "ISO 2022 IR 149"));   ASSERT_EQ(Encoding_Korean, e);
+  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 58"));    ASSERT_EQ(Encoding_SimplifiedChinese, e);
+
+  // 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, "GBK"));         ASSERT_EQ(Encoding_Chinese, e);
+}
+
+
+TEST(FromDcmtkBridge, Encodings3)
+{
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+    std::string dicom;
+
+    {
+      ParsedDicomFile f(true);
+      f.SetEncoding(testEncodings[i]);
+
+      std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
+      f.Insert(DICOM_TAG_PATIENT_NAME, s, false, "");
+      f.SaveToMemoryBuffer(dicom);
+    }
+
+    if (testEncodings[i] != Encoding_Windows1251)
+    {
+      ParsedDicomFile g(dicom);
+
+      if (testEncodings[i] != Encoding_Ascii)
+      {
+        bool hasCodeExtensions;
+        ASSERT_EQ(testEncodings[i], g.DetectEncoding(hasCodeExtensions));
+        ASSERT_FALSE(hasCodeExtensions);
+      }
+
+      std::string tag;
+      ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
+      ASSERT_EQ(std::string(testEncodingsExpected[i]), tag);
+    }
+  }
+}
+
+
+TEST(FromDcmtkBridge, ValueRepresentation)
+{
+  ASSERT_EQ(ValueRepresentation_PersonName, 
+            FromDcmtkBridge::LookupValueRepresentation(DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(ValueRepresentation_Date, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */));
+  ASSERT_EQ(ValueRepresentation_Time, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */));
+  ASSERT_EQ(ValueRepresentation_DateTime, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */));
+  ASSERT_EQ(ValueRepresentation_NotSupported, 
+            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0001, 0x0001) /* some private tag */));
+}
+
+
+TEST(FromDcmtkBridge, ValueRepresentationConversions)
+{
+#if ORTHANC_ENABLE_PLUGINS == 1
+  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));
+  }
+#endif
+}
+
+
+
+static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
+static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
+
+static void CreateSampleJson(Json::Value& a)
+{
+  {
+    Json::Value b = Json::objectValue;
+    b["PatientName"] = "Hello";
+    b["PatientID"] = "World";
+    b["StudyDescription"] = "Toto";
+    a.append(b);
+  }
+
+  {
+    Json::Value b = Json::objectValue;
+    b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y";  // echo -n "Hello2" | base64
+    b["PatientID"] = "World2";
+    a.append(b);
+  }
+}
+
+
+namespace Orthanc
+{
+  // 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)
+  {
+    std::unique_ptr<DcmElement> element;
+
+    {
+      Json::Value a;
+      a = "Hello";
+      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, ""));
+
+      Json::Value b;
+      std::set<DicomTag> ignoreTagLength;
+      ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
+
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, 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, false, ignoreTagLength);
+      ASSERT_TRUE(b["0010,0010"].isNull()); // "Hello" has more than 3 characters
+
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
+                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, 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, false, 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;
+      std::set<DicomTag> ignoreTagLength;
+      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, 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 b;
+        std::set<DicomTag> ignoreTagLength;
+        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
+                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, 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;
+
+        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, false, 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(true);
+
+  f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, "");
+  ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException);  // Already existing tag
+  f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
+  f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
+
+  std::string s;
+  ASSERT_TRUE(f.LookupTransferSyntax(s));
+  // The default transfer syntax depends on the OS endianness
+  ASSERT_TRUE(s == GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit) ||
+              s == GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit));
+
+  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, 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, 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, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, "");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
+  ASSERT_EQ(s, "Accession3");
+
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(s, "World");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
+  ASSERT_EQ(s, "Toto");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
+  ASSERT_EQ(s, "Toto");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
+  ASSERT_EQ(s, "Tata");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
+  ASSERT_EQ(s, "Tata");
+}
+
+
+
+
+TEST(ParsedDicomFile, InsertReplaceJson)
+{
+  ParsedDicomFile f(true);
+
+  Json::Value a;
+  CreateSampleJson(a);
+
+  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+  f.Remove(REFERENCED_STUDY_SEQUENCE);  // No effect
+  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
+  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+  ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException);
+  f.Remove(REFERENCED_STUDY_SEQUENCE);
+  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
+  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
+
+  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+  ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
+  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, "");
+  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, "");
+  ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
+
+  {
+    Json::Value b;
+    f.DatasetToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0);
+
+    Json::Value c;
+    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, 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));
+  ASSERT_EQ(s, a.asString());
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
+  ASSERT_EQ(s, a.asString());
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
+  ASSERT_EQ(s, "Tata");
+  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
+  ASSERT_EQ(s, "Tata");
+}
+
+
+TEST(ParsedDicomFile, JsonEncoding)
+{
+  ParsedDicomFile f(true);
+
+  for (unsigned int i = 0; i < testEncodingsCount; i++)
+  {
+    if (testEncodings[i] != Encoding_Windows1251)
+    {
+      //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
+      f.SetEncoding(testEncodings[i]);
+
+      if (testEncodings[i] != Encoding_Ascii)
+      {
+        bool hasCodeExtensions;
+        ASSERT_EQ(testEncodings[i], f.DetectEncoding(hasCodeExtensions));
+        ASSERT_FALSE(hasCodeExtensions);
+      }
+
+      Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
+      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, "");
+
+      Json::Value v;
+      f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
+      ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i]));
+    }
+  }
+}
+
+
+TEST(ParsedDicomFile, ToJsonFlags1)
+{
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator");
+  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, "");
+
+  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, "OrthancCreator");  // Odd group => private tag
+
+  Json::Value v;
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  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.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 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());
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7052,1000"));
+  ASSERT_TRUE(v.isMember("7050,1000"));
+  ASSERT_FALSE(v.isMember("7053,1000"));
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 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());
+  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.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  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());
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  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_IncludeBinary | 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());
+  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
+}
+
+
+TEST(ParsedDicomFile, ToJsonFlags2)
+{
+  ParsedDicomFile f(true);
+
+  {
+    // "ParsedDicomFile" uses Little Endian => 'B' (least significant
+    // byte) will be stored first in the memory buffer and in the
+    // file, then 'A'. Hence the expected "BA" value below.
+    Uint16 v[] = { 'A' * 256 + 'B', 0 };
+    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good());
+  }
+
+  Json::Value v;
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(5u, v.getMemberNames().size());
+  ASSERT_FALSE(v.isMember("7fe0,0010"));  
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7fe0,0010"));  
+  ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type());  
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7fe0,0010"));  
+  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());  
+  ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2));
+
+  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
+  ASSERT_EQ(Json::objectValue, v.type());
+  ASSERT_EQ(6u, v.getMemberNames().size());
+  ASSERT_TRUE(v.isMember("7fe0,0010"));  
+  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());
+  std::string mime, content;
+  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()));
+  ASSERT_EQ("application/octet-stream", mime);
+  ASSERT_EQ("BA", content.substr(0, 2));
+}
+
+
+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::unique_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::unique_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::unique_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::unique_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::unique_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::unique_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::unique_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, GetDefaultDicomEncoding(), false);
+    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
+    CheckEncoding(dicom, Encoding_Ascii);
+  }
+
+  {
+    DicomMap m;
+    ParsedDicomFile dicom(m, Encoding_Latin4, false);
+    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, GetDefaultDicomEncoding(), false);
+    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, false);
+    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], false);
+        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], false);
+          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], false);
+    
+        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], false);
+
+        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, false);
+      bool hasCodeExtensions;
+      ASSERT_EQ(Encoding_Utf8, dicom.DetectEncoding(hasCodeExtensions));
+      ASSERT_FALSE(hasCodeExtensions);
+      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.DetectEncoding(hasCodeExtensions));
+      ASSERT_FALSE(hasCodeExtensions);
+      
+      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 */, false);
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
+  }
+  
+  {
+    // 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 */, false);
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Japanese, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
+  }
+  
+  {
+    // 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, false),
+                 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, false),
+                 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 */, false);
+
+    bool hasCodeExtensions;
+    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
+    ASSERT_FALSE(hasCodeExtensions);
+  }
+}
+
+
+
+TEST(Toolbox, RemoveIso2022EscapeSequences)
+{
+  // +----------------------------------+
+  // | one-byte control messages        |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_oneByteControl[] = {
+    0x0f, 0x41, 
+    0x0e, 0x42, 
+    0x8e, 0x1b, 0x4e, 0x43, 
+    0x8f, 0x1b, 0x4f, 0x44,
+    0x8e, 0x1b, 0x4a, 0x45, 
+    0x8f, 0x1b, 0x4a, 0x46,
+    0x50, 0x51, 0x52, 0x00
+  };
+  
+  static const uint8_t iso2022_cstr_oneByteControl_ref[] = {
+    0x41,
+    0x42,
+    0x43,
+    0x44,
+    0x8e, 0x1b, 0x4a, 0x45, 
+    0x8f, 0x1b, 0x4a, 0x46,
+    0x50, 0x51, 0x52, 0x00
+  };
+
+  // +----------------------------------+
+  // | two-byte control messages        |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_twoByteControl[] = {
+    0x1b, 0x6e, 0x41,
+    0x1b, 0x6f, 0x42,
+    0x1b, 0x4e, 0x43,
+    0x1b, 0x4f, 0x44,
+    0x1b, 0x7e, 0x45,
+    0x1b, 0x7d, 0x46,
+    0x1b, 0x7c, 0x47, 0x00
+  };
+  
+  static const uint8_t iso2022_cstr_twoByteControl_ref[] = {
+    0x41,
+    0x42,
+    0x43,
+    0x44,
+    0x45,
+    0x46,
+    0x47, 0x00
+  };
+
+  // +----------------------------------+
+  // | various-length escape sequences  |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_escapeSequence[] = {
+    0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq)
+    0x1b, 0x50, 0x42, // ditto 
+    0x1b, 0x7f, 0x43, // ditto
+    0x1b, 0x21, 0x4a, 0x44, // this will match
+    0x1b, 0x20, 0x21, 0x2f, 0x40, 0x45, // this will match
+    0x1b, 0x20, 0x21, 0x2f, 0x2f, 0x40, 0x46, // this will match too
+    0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match!
+  };
+  
+  static const uint8_t iso2022_cstr_escapeSequence_ref[] = {
+    0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq)
+    0x1b, 0x50, 0x42, // ditto 
+    0x1b, 0x7f, 0x43, // ditto
+    0x44, // this will match
+    0x45, // this will match
+    0x46, // this will match too
+    0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match!
+  };
+
+  
+  // +----------------------------------+
+  // | a real-world japanese sample     |
+  // +----------------------------------+
+
+  static const uint8_t iso2022_cstr_real_ir13[] = {
+    0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3,
+    0x3d, 0x1b, 0x24, 0x42, 0x3b, 0x33, 0x45, 0x44,
+    0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x42,
+    0x40, 0x4f, 0x3a, 0x1b, 0x28, 0x4a, 0x3d, 0x1b,
+    0x24, 0x42, 0x24, 0x64, 0x24, 0x5e, 0x24, 0x40,
+    0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x24,
+    0x3f, 0x24, 0x6d, 0x24, 0x26, 0x1b, 0x28, 0x4a, 0x00
+  };
+
+  static const uint8_t iso2022_cstr_real_ir13_ref[] = {
+    0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3,
+    0x3d,
+    0x3b, 0x33, 0x45, 0x44,
+    0x5e,
+    0x42,
+    0x40, 0x4f, 0x3a,
+    0x3d,
+    0x24, 0x64, 0x24, 0x5e, 0x24, 0x40,
+    0x5e,
+    0x24,
+    0x3f, 0x24, 0x6d, 0x24, 0x26, 0x00
+  };
+
+
+
+  // +----------------------------------+
+  // | the actual test                  |
+  // +----------------------------------+
+
+  std::string iso2022_str_oneByteControl(
+    reinterpret_cast<const char*>(iso2022_cstr_oneByteControl));
+  std::string iso2022_str_oneByteControl_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_oneByteControl_ref));
+  std::string iso2022_str_twoByteControl(
+    reinterpret_cast<const char*>(iso2022_cstr_twoByteControl));
+  std::string iso2022_str_twoByteControl_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_twoByteControl_ref));
+  std::string iso2022_str_escapeSequence(
+    reinterpret_cast<const char*>(iso2022_cstr_escapeSequence));
+  std::string iso2022_str_escapeSequence_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_escapeSequence_ref));
+  std::string iso2022_str_real_ir13(
+    reinterpret_cast<const char*>(iso2022_cstr_real_ir13));
+  std::string iso2022_str_real_ir13_ref(
+    reinterpret_cast<const char*>(iso2022_cstr_real_ir13_ref));
+
+  std::string dest;
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_oneByteControl);
+  ASSERT_EQ(dest, iso2022_str_oneByteControl_ref);
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_twoByteControl);
+  ASSERT_EQ(dest, iso2022_str_twoByteControl_ref);
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_escapeSequence);
+  ASSERT_EQ(dest, iso2022_str_escapeSequence_ref);
+
+  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_real_ir13);
+  ASSERT_EQ(dest, iso2022_str_real_ir13_ref);
+}
+
+
+
+static std::string DecodeFromSpecification(const std::string& s)
+{
+  std::vector<std::string> tokens;
+  Toolbox::TokenizeString(tokens, s, ' ');
+
+  std::string result;
+  result.resize(tokens.size());
+  
+  for (size_t i = 0; i < tokens.size(); i++)
+  {
+    std::vector<std::string> components;
+    Toolbox::TokenizeString(components, tokens[i], '/');
+
+    if (components.size() != 2)
+    {
+      throw;
+    }
+
+    int a = boost::lexical_cast<int>(components[0]);
+    int b = boost::lexical_cast<int>(components[1]);
+    if (a < 0 || a > 15 ||
+        b < 0 || b > 15 ||
+        (a == 0 && b == 0))
+    {
+      throw;
+    }
+
+    result[i] = static_cast<uint8_t>(a * 16 + b);
+  }
+
+  return result;
+}
+
+
+
+// Compatibility wrapper
+static pugi::xpath_node SelectNode(const pugi::xml_document& doc,
+                                   const char* xpath)
+{
+#if PUGIXML_VERSION <= 140
+  return doc.select_single_node(xpath);  // Deprecated in pugixml 1.5
+#else
+  return doc.select_node(xpath);
+#endif
+}
+
+
+TEST(Toolbox, EncodingsKorean)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_I.2.html
+
+  std::string korean = DecodeFromSpecification(
+    "04/08 06/15 06/14 06/07 05/14 04/07 06/09 06/12 06/04 06/15 06/14 06/07 03/13 "
+    "01/11 02/04 02/09 04/03 15/11 15/03 05/14 01/11 02/04 02/09 04/03 13/01 12/14 "
+    "13/04 13/07 03/13 01/11 02/04 02/09 04/03 12/08 10/11 05/14 01/11 02/04 02/09 "
+    "04/03 11/01 14/06 11/05 11/15");
+
+  // This array can be re-generated using command-line:
+  // echo -n "Hong^Gildong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
+  static const uint8_t utf8raw[] = {
+    0x48, 0x6f, 0x6e, 0x67, 0x5e, 0x47, 0x69, 0x6c, 0x64, 0x6f, 0x6e, 0x67, 0x3d, 0xe6,
+    0xb4, 0xaa, 0x5e, 0xe5, 0x90, 0x89, 0xe6, 0xb4, 0x9e, 0x3d, 0xed, 0x99, 0x8d, 0x5e,
+    0xea, 0xb8, 0xb8, 0xeb, 0x8f, 0x99
+  };
+
+  std::string utf8(reinterpret_cast<const char*>(utf8raw), sizeof(utf8raw));
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 149");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, korean.c_str(), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_Korean, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+  
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(utf8, value);
+  
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+  ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString());
+  ASSERT_EQ(utf8.substr(13, 10), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString());
+  ASSERT_EQ(utf8.substr(24), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString());
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
+  std::string xml;
+  visitor.FormatXml(xml);
+
+  pugi::xml_document doc;
+  doc.load_buffer(xml.c_str(), xml.size());
+
+  pugi::xpath_node node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
+  ASSERT_STREQ("ISO 2022 IR 149", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
+  ASSERT_STREQ("CS", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
+  ASSERT_STREQ("PN", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
+  ASSERT_STREQ("Hong", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
+  ASSERT_STREQ("Gildong", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
+  ASSERT_EQ(utf8.substr(13, 3), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
+  ASSERT_EQ(utf8.substr(17, 6), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
+  ASSERT_EQ(utf8.substr(24, 3), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
+  ASSERT_EQ(utf8.substr(28), node.node().text().as_string());
+#endif
+  
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(2u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
+    ASSERT_EQ("ISO 2022 IR 149", s);
+
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+    std::vector<std::string> v;
+    Toolbox::TokenizeString(v, s, '=');
+    ASSERT_EQ(3u, v.size());
+    ASSERT_EQ("Hong^Gildong", v[0]);
+    ASSERT_EQ(utf8, s);
+  }
+}
+
+
+TEST(Toolbox, EncodingsJapaneseKanji)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_H.3.html
+
+  std::string japanese = DecodeFromSpecification(
+    "05/09 06/01 06/13 06/01 06/04 06/01 05/14 05/04 06/01 07/02 06/15 07/05 03/13 "
+    "01/11 02/04 04/02 03/11 03/03 04/05 04/04 01/11 02/08 04/02 05/14 01/11 02/04 "
+    "04/02 04/02 04/00 04/15 03/10 01/11 02/08 04/02 03/13 01/11 02/04 04/02 02/04 "
+    "06/04 02/04 05/14 02/04 04/00 01/11 02/08 04/02 05/14 01/11 02/04 04/02 02/04 "
+    "03/15 02/04 06/13 02/04 02/06 01/11 02/08 04/02");
+
+  // This array can be re-generated using command-line:
+  // echo -n "Yamada^Tarou=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
+  static const uint8_t utf8raw[] = {
+    0x59, 0x61, 0x6d, 0x61, 0x64, 0x61, 0x5e, 0x54, 0x61, 0x72, 0x6f, 0x75, 0x3d, 0xe5,
+    0xb1, 0xb1, 0xe7, 0x94, 0xb0, 0x5e, 0xe5, 0xa4, 0xaa, 0xe9, 0x83, 0x8e, 0x3d, 0xe3,
+    0x82, 0x84, 0xe3, 0x81, 0xbe, 0xe3, 0x81, 0xa0, 0x5e, 0xe3, 0x81, 0x9f, 0xe3, 0x82,
+    0x8d, 0xe3, 0x81, 0x86
+  };
+
+  std::string utf8(reinterpret_cast<const char*>(utf8raw), sizeof(utf8raw));
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 87");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, japanese.c_str(), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_JapaneseKanji, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(utf8, value);
+  
+  DicomWebJsonVisitor visitor;
+  dicom.Apply(visitor);
+  ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString());
+  ASSERT_EQ(utf8.substr(13, 13), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString());
+  ASSERT_EQ(utf8.substr(27), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString());
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
+  std::string xml;
+  visitor.FormatXml(xml);
+
+  pugi::xml_document doc;
+  doc.load_buffer(xml.c_str(), xml.size());
+
+  pugi::xpath_node node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
+  ASSERT_STREQ("ISO 2022 IR 87", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
+  ASSERT_STREQ("CS", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
+  ASSERT_STREQ("PN", node.node().attribute("vr").value());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
+  ASSERT_STREQ("Yamada", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
+  ASSERT_STREQ("Tarou", node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
+  ASSERT_EQ(utf8.substr(13, 6), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
+  ASSERT_EQ(utf8.substr(20, 6), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
+  ASSERT_EQ(utf8.substr(27, 9), node.node().text().as_string());
+
+  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
+  ASSERT_EQ(utf8.substr(37), node.node().text().as_string());
+#endif  
+  
+  {
+    DicomMap m;
+    m.FromDicomWeb(visitor.GetResult());
+    ASSERT_EQ(2u, m.GetSize());
+
+    std::string s;
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
+    ASSERT_EQ("ISO 2022 IR 87", s);
+
+    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
+    std::vector<std::string> v;
+    Toolbox::TokenizeString(v, s, '=');
+    ASSERT_EQ(3u, v.size());
+    ASSERT_EQ("Yamada^Tarou", v[0]);
+    ASSERT_EQ(utf8, s);
+  }
+}
+
+
+
+TEST(Toolbox, EncodingsChinese3)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.3.html
+
+  static const uint8_t chinese[] = {
+    0x57, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f,
+    0x6e, 0x67, 0x3d, 0xcd, 0xf5, 0x5e, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x00
+  };
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_Chinese, encoding);
+  ASSERT_FALSE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+
+  std::vector<std::string> tokens;
+  Orthanc::Toolbox::TokenizeString(tokens, value, '=');
+  ASSERT_EQ(3u, tokens.size());
+  ASSERT_EQ("Wang^XiaoDong", tokens[0]);
+  ASSERT_TRUE(tokens[2].empty());
+
+  std::vector<std::string> middle;
+  Orthanc::Toolbox::TokenizeString(middle, tokens[1], '^');
+  ASSERT_EQ(2u, middle.size());
+  ASSERT_EQ(3u, middle[0].size());
+  ASSERT_EQ(6u, middle[1].size());
+
+  // CDF5 in GB18030
+  ASSERT_EQ(static_cast<char>(0xe7), middle[0][0]);
+  ASSERT_EQ(static_cast<char>(0x8e), middle[0][1]);
+  ASSERT_EQ(static_cast<char>(0x8b), middle[0][2]);
+
+  // D0A1 in GB18030
+  ASSERT_EQ(static_cast<char>(0xe5), middle[1][0]);
+  ASSERT_EQ(static_cast<char>(0xb0), middle[1][1]);
+  ASSERT_EQ(static_cast<char>(0x8f), middle[1][2]);
+
+  // B6AB in GB18030
+  ASSERT_EQ(static_cast<char>(0xe4), middle[1][3]);
+  ASSERT_EQ(static_cast<char>(0xb8), middle[1][4]);
+  ASSERT_EQ(static_cast<char>(0x9c), middle[1][5]);
+}
+
+
+TEST(Toolbox, EncodingsChinese4)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.4.html
+
+  static const uint8_t chinese[] = {
+    0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e,
+    0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0xd6, 0xd0, 0xce,
+    0xc4, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e,
+    0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
+    0x65, 0x73, 0xd6, 0xd0, 0xce, 0xc4, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0x0d,
+    0x0a, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69,
+    0x6e, 0x65, 0x2e, 0x0d, 0x0a, 0x00
+  };
+
+  static const uint8_t patternRaw[] = {
+    0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87
+  };
+
+  const std::string pattern(reinterpret_cast<const char*>(patternRaw), sizeof(patternRaw));
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientComments, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_Chinese, encoding);
+  ASSERT_FALSE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_COMMENTS));
+
+  std::vector<std::string> lines;
+  Orthanc::Toolbox::TokenizeString(lines, value, '\n');
+  ASSERT_EQ(4u, lines.size());
+  ASSERT_TRUE(boost::starts_with(lines[0], "The first line includes"));
+  ASSERT_TRUE(boost::ends_with(lines[0], ".\r"));
+  ASSERT_TRUE(lines[0].find(pattern) != std::string::npos);
+  ASSERT_TRUE(boost::starts_with(lines[1], "The second line includes"));
+  ASSERT_TRUE(boost::ends_with(lines[1], ", too.\r"));
+  ASSERT_TRUE(lines[1].find(pattern) != std::string::npos);
+  ASSERT_EQ("The third line.\r", lines[2]);
+  ASSERT_FALSE(lines[1].find(pattern) == std::string::npos);
+  ASSERT_TRUE(lines[3].empty());
+}
+
+
+TEST(Toolbox, EncodingsSimplifiedChinese2)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html
+
+  static const uint8_t chinese[] = {
+    0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f,
+    0x6e, 0x67, 0x3d, 0x1b, 0x24, 0x29, 0x41, 0xd5, 0xc5, 0x5e, 0x1b, 0x24,
+    0x29, 0x41, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x20, 0x00
+  };
+
+  // echo -n "Zhang^XiaoDong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
+  static const uint8_t utf8[] = {
+    0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, 0x6e, 0x67,
+    0x3d, 0xe5, 0xbc, 0xa0, 0x5e, 0xe5, 0xb0, 0x8f, 0xe4, 0xb8, 0x9c, 0x3d
+  };
+  
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_SimplifiedChinese, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+  ASSERT_EQ(value, std::string(reinterpret_cast<const char*>(utf8), sizeof(utf8)));
+}
+
+
+TEST(Toolbox, EncodingsSimplifiedChinese3)
+{
+  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html
+
+  static const uint8_t chinese[] = {
+    0x31, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xd2, 0xbb, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a,
+    0x32, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xb6, 0xfe, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a,
+    0x33, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xc8, 0xfd, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a, 0x00
+  };
+
+  static const uint8_t line1[] = {
+    0x31, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x80, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
+    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
+  };
+
+  static const uint8_t line2[] = {
+    0x32, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xba, 0x8c, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
+    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
+  };
+
+  static const uint8_t line3[] = {
+    0x33, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x89, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
+    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
+  };
+
+  ParsedDicomFile dicom(false);
+  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58");
+  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
+              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
+
+  bool hasCodeExtensions;
+  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
+  ASSERT_EQ(Encoding_SimplifiedChinese, encoding);
+  ASSERT_TRUE(hasCodeExtensions);
+
+  std::string value;
+  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
+
+  std::vector<std::string> lines;
+  Toolbox::TokenizeString(lines, value, '\n');
+  ASSERT_EQ(4u, lines.size());
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line1), sizeof(line1)), lines[0]);
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line2), sizeof(line2)), lines[1]);
+  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line3), sizeof(line3)), lines[2]);
+  ASSERT_TRUE(lines[3].empty());
+}
+
+
+
+
+#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
+
+#include "../Core/DicomNetworking/DicomStoreUserConnection.h"
+#include "../Core/DicomParsing/DcmtkTranscoder.h"
+
+TEST(Toto, DISABLED_Transcode3)
+{
+  DicomAssociationParameters p;
+  p.SetRemotePort(2000);
+
+  DicomStoreUserConnection scu(p);
+  scu.SetCommonClassesProposed(false);
+  scu.SetRetiredBigEndianProposed(true);
+
+  DcmtkTranscoder transcoder;
+
+  for (int j = 0; j < 2; j++)
+    for (int i = 0; i <= DicomTransferSyntax_XML; i++)
+    {
+      DicomTransferSyntax a = (DicomTransferSyntax) i;
+
+      std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
+                          std::string(GetTransferSyntaxUid(a)) + ".dcm");
+      if (Orthanc::SystemToolbox::IsRegularFile(path))
+      {
+        printf("\n======= %s\n", GetTransferSyntaxUid(a));
+
+        std::string source;
+        Orthanc::SystemToolbox::ReadFile(source, path);
+
+        std::string c, i;
+        try
+        {
+          scu.Transcode(c, i, transcoder, source.c_str(), source.size(), false, "", 0);
+        }
+        catch (OrthancException& e)
+        {
+          if (e.GetErrorCode() == ErrorCode_NotImplemented)
+          {
+            LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a);
+          }
+          else
+          {
+            throw e;
+          }
+        }
+      }
+    }
+}
+
+
+TEST(Toto, DISABLED_Transcode4)
+{
+  std::string source;
+  Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
+
+  std::unique_ptr<DcmFileFormat> toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size()));
+  const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto);
+  
+  DicomTransferSyntax sourceSyntax;
+  ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto));
+
+  DcmtkTranscoder transcoder;
+
+  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
+  {
+    DicomTransferSyntax a = (DicomTransferSyntax) i;
+    
+    std::set<DicomTransferSyntax> s;
+    s.insert(a);
+
+    std::string t;
+
+    IDicomTranscoder::DicomImage source, target;
+    source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone()));
+
+    if (!transcoder.Transcode(target, source, s, true))
+    {
+      printf("**************** CANNOT: [%s] => [%s]\n",
+             GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a));
+    }
+    else
+    {
+      DicomTransferSyntax targetSyntax;
+      ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed()));
+      
+      ASSERT_EQ(targetSyntax, a);
+      bool lossy = (a == DicomTransferSyntax_JPEGProcess1 ||
+                    a == DicomTransferSyntax_JPEGProcess2_4 ||
+                    a == DicomTransferSyntax_JPEGLSLossy);
+      
+      printf("SIZE: %lu\n", t.size());
+      if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed()))
+      {
+        ASSERT_FALSE(lossy);
+      }
+      else
+      {
+        ASSERT_TRUE(lossy);
+      }
+    }
+  }
+}
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ImageTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,483 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Images/Font.h"
+#include "../Core/Images/Image.h"
+#include "../Core/Images/ImageProcessing.h"
+#include "../Core/Images/JpegReader.h"
+#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/SystemToolbox.h"
+#include "../Core/Toolbox.h"
+#include "../Core/TemporaryFile.h"
+#include "../OrthancServer/OrthancConfiguration.h"  // For the FontRegistry
+
+#include <stdint.h>
+
+
+TEST(PngWriter, ColorPattern)
+{
+  Orthanc::PngWriter 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.png", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5);
+}
+
+TEST(PngWriter, Gray8Pattern)
+{
+  Orthanc::PngWriter 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.png", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5);
+}
+
+TEST(PngWriter, Gray16Pattern)
+{
+  Orthanc::PngWriter 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.png", accessor);
+
+  std::string f, md5;
+  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png");
+  Orthanc::Toolbox::ComputeMD5(md5, f);
+  ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5);
+}
+
+TEST(PngWriter, EndToEnd)
+{
+  Orthanc::PngWriter 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::PngReader 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(*p, v);
+      }
+    }
+  }
+
+  {
+    Orthanc::TemporaryFile tmp;
+    tmp.Write(s);
+
+    Orthanc::PngReader 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);
+      }
+    }
+  }
+}
+
+
+
+
+TEST(JpegWriter, Basic)
+{
+  std::string s;
+
+  {
+    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));
+      for (unsigned int x = 0; x < img.GetWidth(); x++, p++)
+      {
+        *p = value++;
+      }
+    }
+
+    Orthanc::JpegWriter w;
+    w.WriteToFile("UnitTestsResults/hello.jpg", img);
+
+    w.WriteToMemory(s, img);
+    Orthanc::SystemToolbox::WriteFile(s, "UnitTestsResults/hello2.jpg");
+
+    std::string t;
+    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()));
+  }
+
+  {
+    Orthanc::JpegReader r1, r2;
+    r1.ReadFromFile("UnitTestsResults/hello.jpg");
+    ASSERT_EQ(16u, r1.GetWidth());
+    ASSERT_EQ(16u, r1.GetHeight());
+
+    r2.ReadFromMemory(s);
+    ASSERT_EQ(16u, r2.GetWidth());
+    ASSERT_EQ(16u, r2.GetHeight());
+
+    for (unsigned int y = 0; y < r1.GetHeight(); y++)
+    {
+      const uint8_t* p1 = reinterpret_cast<const uint8_t*>(r1.GetConstRow(y));
+      const uint8_t* p2 = reinterpret_cast<const uint8_t*>(r2.GetConstRow(y));
+      for (unsigned int x = 0; x < r1.GetWidth(); x++)
+      {
+        ASSERT_EQ(*p1, *p2);
+      }
+    }
+  }
+}
+
+
+TEST(Font, Basic)
+{
+  Orthanc::Image s(Orthanc::PixelFormat_RGB24, 640, 480, false);
+  memset(s.GetBuffer(), 0, s.GetPitch() * s.GetHeight());
+
+  {
+    Orthanc::OrthancConfiguration::ReaderLock lock;
+    ASSERT_GE(1u, lock.GetConfiguration().GetFontRegistry().GetSize());
+    lock.GetConfiguration().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);
+      }
+    }
+  }
+
+  {
+    // true means "enforce alignment by using a temporary buffer"
+    Orthanc::PamReader r(true);
+    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;
+    tmp.Write(s);
+
+    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);
+      }
+    }
+  }
+
+  {
+    Orthanc::TemporaryFile tmp;
+    tmp.Write(s);
+
+    // true means "enforce alignment by using a temporary buffer"
+    Orthanc::PamReader r2(true);
+    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);
+      }
+    }
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/LuaTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,372 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
+#  include <OrthancFramework.h>
+#endif
+
+#include "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/Toolbox.h"
+#include "../Core/Lua/LuaFunctionCall.h"
+
+#include <OrthancServerResources.h>
+
+#include <boost/lexical_cast.hpp>
+
+#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
+#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS to 0 or 1"
+#endif
+
+
+TEST(Lua, Json)
+{
+  Orthanc::LuaContext lua;
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
+  {
+    std::string command;
+    Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
+    lua.Execute(command);
+  }
+#endif
+
+  lua.Execute("a={}");
+  lua.Execute("a['x'] = 10");
+  lua.Execute("a['y'] = {}");
+  lua.Execute("a['y'][1] = 20");
+  lua.Execute("a['y'][2] = 20");
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
+  lua.Execute("PrintRecursive(a)");
+#endif
+
+  lua.Execute("function f(a) print(a.bool) return a.bool,20,30,40,50,60 end");
+
+  Json::Value v, vv, o;
+  //v["a"] = "b";
+  v.append("hello");
+  v.append("world");
+  v.append("42");
+  vv.append("sub");
+  vv.append("set");
+  v.append(vv);
+  o = Json::objectValue;
+  o["x"] = 10;
+  o["y"] = 20;
+  o["z"] = 20.5f;
+  v.append(o);
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushJson(v);
+    f.Execute();
+  }
+#endif
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "f");
+    f.PushJson(o);
+    ASSERT_THROW(f.ExecutePredicate(), Orthanc::OrthancException);
+  }
+
+  o["bool"] = false;
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "f");
+    f.PushJson(o);
+    ASSERT_FALSE(f.ExecutePredicate());
+  }
+
+  o["bool"] = true;
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "f");
+    f.PushJson(o);
+    ASSERT_TRUE(f.ExecutePredicate());
+  }
+}
+
+
+TEST(Lua, Existing)
+{
+  Orthanc::LuaContext lua;
+  lua.Execute("a={}");
+  lua.Execute("function f() end");
+
+  ASSERT_TRUE(lua.IsExistingFunction("f"));
+  ASSERT_FALSE(lua.IsExistingFunction("a"));
+  ASSERT_FALSE(lua.IsExistingFunction("Dummy"));
+}
+
+
+#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
+TEST(Lua, Simple)
+{
+  Orthanc::LuaContext lua;
+
+  {
+    std::string command;
+    Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
+    lua.Execute(command);
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushString("hello");
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushBoolean(true);
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushInteger(42);
+    f.Execute();
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
+    f.PushDouble(3.1415);
+    f.Execute();
+  }
+}
+#endif
+
+
+TEST(Lua, ReturnJson)
+{
+  Json::Value b = Json::objectValue;
+  b["a"] = 42;
+  b["b"] = 44.37;
+  b["c"] = -43;
+
+  Json::Value c = Json::arrayValue;
+  c.append("test3");
+  c.append("test1");
+  c.append("test2");
+
+  Json::Value a = Json::objectValue;
+  a["Hello"] = "World";
+  a["List"] = Json::arrayValue;
+  a["List"].append(b);
+  a["List"].append(c);
+
+  Orthanc::LuaContext lua;
+
+  // This is the identity function (it simply returns its input)
+  lua.Execute("function identity(a) return a end");
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson("hello");
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_EQ("hello", v.asString());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(42.25);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_FLOAT_EQ(42.25f, v.asFloat());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(-42);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_EQ(-42, v.asInt());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    Json::Value vv = Json::arrayValue;
+    f.PushJson(vv);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_EQ(Json::arrayValue, v.type());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    Json::Value vv = Json::objectValue;
+    f.PushJson(vv);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    // Lua does not make the distinction between empty lists and empty objects
+    ASSERT_EQ(Json::arrayValue, v.type());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(b);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_EQ(Json::objectValue, v.type());
+    ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat());
+    ASSERT_FLOAT_EQ(44.37f, v["b"].asFloat());
+    ASSERT_FLOAT_EQ(-43.0f, v["c"].asFloat());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(c);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_EQ(Json::arrayValue, v.type());
+    ASSERT_EQ("test3", v[0].asString());
+    ASSERT_EQ("test1", v[1].asString());
+    ASSERT_EQ("test2", v[2].asString());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(a);
+    Json::Value v;
+    f.ExecuteToJson(v, false);
+    ASSERT_EQ("World", v["Hello"].asString());
+    ASSERT_EQ(Json::intValue, v["List"][0]["a"].type());
+    ASSERT_EQ(Json::realValue, v["List"][0]["b"].type());
+    ASSERT_EQ(Json::intValue, v["List"][0]["c"].type());
+    ASSERT_EQ(42, v["List"][0]["a"].asInt());
+    ASSERT_FLOAT_EQ(44.37f, v["List"][0]["b"].asFloat());
+    ASSERT_EQ(44, v["List"][0]["b"].asInt());
+    ASSERT_EQ(-43, v["List"][0]["c"].asInt());
+    ASSERT_EQ("test3", v["List"][1][0].asString());
+    ASSERT_EQ("test1", v["List"][1][1].asString());
+    ASSERT_EQ("test2", v["List"][1][2].asString());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "identity");
+    f.PushJson(a);
+    Json::Value v;
+    f.ExecuteToJson(v, true);
+    ASSERT_EQ("World", v["Hello"].asString());
+    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_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());
+  }
+
+  {
+    Orthanc::LuaFunctionCall f(lua, "DumpJson");
+    f.PushJson(a);
+    std::string s;
+    f.ExecuteToString(s);
+
+    Json::FastWriter writer;
+    std::string t = writer.write(a);
+
+    ASSERT_EQ(s, t);
+  }
+}
+
+
+
+TEST(Lua, Http)
+{
+  Orthanc::LuaContext lua;
+
+#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/";
+
+#if LUA_VERSION_NUM >= 502
+  // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load"
+  lua.Execute("JSON = load(HttpGet('" + BASE + "JSON.lua')) ()");
+#else
+  lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()");
+#endif
+
+  const std::string url(BASE + "Product.json");
+#endif
+
+  std::string s;
+  lua.Execute(s, "print(HttpGet({}))");
+  ASSERT_EQ("nil", Orthanc::Toolbox::StripSpaces(s));
+
+#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1  
+  lua.Execute(s, "print(string.len(HttpGet(\"" + url + "\")))");
+  ASSERT_LE(100, boost::lexical_cast<int>(Orthanc::Toolbox::StripSpaces(s)));
+
+  // Parse a JSON file
+  lua.Execute(s, "print(JSON:decode(HttpGet(\"" + url + "\")) ['Product'])");
+  ASSERT_EQ("OrthancClient", Orthanc::Toolbox::StripSpaces(s));
+
+#if 0
+  // This part of the test can only be executed if one instance of
+  // Orthanc is running on the localhost
+
+  lua.Execute("modality = {}");
+  lua.Execute("table.insert(modality, 'ORTHANC')");
+  lua.Execute("table.insert(modality, 'localhost')");
+  lua.Execute("table.insert(modality, 4242)");
+  
+  lua.Execute(s, "print(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('hello world')\"))");
+  ASSERT_EQ("hello world", Orthanc::Toolbox::StripSpaces(s));
+
+  lua.Execute(s, "print(JSON:decode(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('[10,42,1000]')\")) [2])");
+  ASSERT_EQ("42", Orthanc::Toolbox::StripSpaces(s));
+
+  // Add/remove a modality with Lua
+  Json::Value v;
+  lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))");
+  ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size());
+  lua.Execute(s, "print(HttpPut('http://localhost:8042/modalities/lua', JSON:encode(modality)))");
+  lua.Execute(v, "print(HttpGet('http://localhost:8042/modalities/lua'))");
+  ASSERT_TRUE(v.type() == Json::arrayValue);
+  lua.Execute(s, "print(HttpDelete('http://localhost:8042/modalities/lua'))");
+  lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))");
+  ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size());
+#endif
+
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/MemoryCacheTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,460 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <algorithm>
+#include <boost/thread.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include "../Core/Cache/MemoryCache.h"
+#include "../Core/Cache/MemoryStringCache.h"
+#include "../Core/Cache/SharedArchive.h"
+#include "../Core/IDynamicObject.h"
+#include "../Core/Logging.h"
+#include "../OrthancServer/StorageCommitmentReports.h"
+
+
+TEST(LRU, Basic)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string> r;
+  
+  r.Add("d");
+  r.Add("a");
+  r.Add("c");
+  r.Add("b");
+
+  r.MakeMostRecent("a");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("b");
+  r.MakeMostRecent("c");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("c");
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ("a", r.RemoveOldest());
+  ASSERT_EQ("b", r.GetOldest());
+  ASSERT_EQ("b", r.RemoveOldest());
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ("d", r.RemoveOldest());
+  ASSERT_EQ("c", r.GetOldest());
+  ASSERT_EQ("c", r.RemoveOldest());
+
+  ASSERT_TRUE(r.IsEmpty());
+
+  ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException);
+  ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException);
+}
+
+
+TEST(LRU, Payload)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  
+  r.Add("a", 420);
+  r.Add("b", 421);
+  r.Add("c", 422);
+  r.Add("d", 423);
+
+  r.MakeMostRecent("a");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("b");
+  r.MakeMostRecent("c");
+  r.MakeMostRecent("d");
+  r.MakeMostRecent("c");
+
+  ASSERT_TRUE(r.Contains("b"));
+  ASSERT_EQ(421, r.Invalidate("b"));
+  ASSERT_FALSE(r.Contains("b"));
+
+  int p;
+  ASSERT_TRUE(r.Contains("a", p)); ASSERT_EQ(420, p);
+  ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p);
+  ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p);
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ(420, r.GetOldestPayload());
+  ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p);
+
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ(423, r.GetOldestPayload());
+  ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p);
+
+  ASSERT_EQ("c", r.GetOldest());
+  ASSERT_EQ(422, r.GetOldestPayload());
+  ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p);
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+
+TEST(LRU, PayloadUpdate)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  
+  r.Add("a", 420);
+  r.Add("b", 421);
+  r.Add("d", 423);
+
+  r.MakeMostRecent("a", 424);
+  r.MakeMostRecent("d", 421);
+
+  ASSERT_EQ("b", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ(424, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+
+
+TEST(LRU, PayloadUpdateBis)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  
+  r.AddOrMakeMostRecent("a", 420);
+  r.AddOrMakeMostRecent("b", 421);
+  r.AddOrMakeMostRecent("d", 423);
+  r.AddOrMakeMostRecent("a", 424);
+  r.AddOrMakeMostRecent("d", 421);
+
+  ASSERT_EQ("b", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("a", r.GetOldest());
+  ASSERT_EQ(424, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_EQ("d", r.GetOldest());
+  ASSERT_EQ(421, r.GetOldestPayload());
+  r.RemoveOldest();
+
+  ASSERT_TRUE(r.IsEmpty());
+}
+
+TEST(LRU, GetAllKeys)
+{
+  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
+  std::vector<std::string> keys;
+
+  r.AddOrMakeMostRecent("a", 420);
+  r.GetAllKeys(keys);
+
+  ASSERT_EQ(1u, keys.size());
+  ASSERT_EQ("a", keys[0]);
+
+  r.AddOrMakeMostRecent("b", 421);
+  r.GetAllKeys(keys);
+
+  ASSERT_EQ(2u, keys.size());
+  ASSERT_TRUE(std::find(keys.begin(), keys.end(),"a") != keys.end());
+  ASSERT_TRUE(std::find(keys.begin(), keys.end(),"b") != keys.end());
+}
+
+
+
+namespace
+{
+  class Integer : public Orthanc::IDynamicObject
+  {
+  private:
+    std::string& log_;
+    int value_;
+
+  public:
+    Integer(std::string& log, int v) : log_(log), value_(v)
+    {
+    }
+
+    virtual ~Integer() ORTHANC_OVERRIDE
+    {
+      LOG(INFO) << "Removing cache entry for " << value_;
+      log_ += boost::lexical_cast<std::string>(value_) + " ";
+    }
+  };
+
+  class IntegerProvider : public Orthanc::Deprecated::ICachePageProvider
+  {
+  public:
+    std::string log_;
+
+    virtual Orthanc::IDynamicObject* Provide(const std::string& s) ORTHANC_OVERRIDE
+    {
+      LOG(INFO) << "Providing " << s;
+      return new Integer(log_, boost::lexical_cast<int>(s));
+    }
+  };
+}
+
+
+TEST(MemoryCache, Basic)
+{
+  IntegerProvider provider;
+
+  {
+    Orthanc::Deprecated::MemoryCache cache(provider, 3);
+    cache.Access("42");  // 42 -> exit
+    cache.Access("43");  // 43, 42 -> exit
+    cache.Access("45");  // 45, 43, 42 -> exit
+    cache.Access("42");  // 42, 45, 43 -> exit
+    cache.Access("43");  // 43, 42, 45 -> exit
+    cache.Access("47");  // 45 is removed; 47, 43, 42 -> exit 
+    cache.Access("44");  // 42 is removed; 44, 47, 43 -> exit
+    cache.Access("42");  // 43 is removed; 42, 44, 47 -> exit
+    // Closing the cache: 47, 44, 42 are successively removed
+  }
+
+  ASSERT_EQ("45 42 43 47 44 42 ", provider.log_);
+}
+
+
+
+
+
+namespace
+{
+  class S : public Orthanc::IDynamicObject
+  {
+  private:
+    std::string value_;
+
+  public:
+    S(const std::string& value) : value_(value)
+    {
+    }
+
+    const std::string& GetValue() const
+    {
+      return value_;
+    }
+  };
+}
+
+
+TEST(LRU, SharedArchive)
+{
+  std::string first, second;
+  Orthanc::SharedArchive a(3);
+  first = a.Add(new S("First item"));
+  second = a.Add(new S("Second item"));
+
+  for (int i = 1; i < 100; i++)
+  {
+    a.Add(new S("Item " + boost::lexical_cast<std::string>(i)));
+    
+    // Continuously protect the two first items
+    {
+      Orthanc::SharedArchive::Accessor accessor(a, first);
+      ASSERT_TRUE(accessor.IsValid());
+      ASSERT_EQ("First item", dynamic_cast<S&>(accessor.GetItem()).GetValue());
+    }
+
+    {
+      Orthanc::SharedArchive::Accessor accessor(a, second);
+      ASSERT_TRUE(accessor.IsValid());
+      ASSERT_EQ("Second item", dynamic_cast<S&>(accessor.GetItem()).GetValue());
+    }
+
+    {
+      Orthanc::SharedArchive::Accessor accessor(a, "nope");
+      ASSERT_FALSE(accessor.IsValid());
+      ASSERT_THROW(accessor.GetItem(), Orthanc::OrthancException);
+    }
+  }
+
+  std::list<std::string> i;
+  a.List(i);
+
+  size_t count = 0;
+  for (std::list<std::string>::const_iterator
+         it = i.begin(); it != i.end(); it++)
+  {
+    if (*it == first ||
+        *it == second)
+    {
+      count++;
+    }
+  }
+
+  ASSERT_EQ(2u, count);
+}
+
+
+TEST(MemoryStringCache, Basic)
+{
+  Orthanc::MemoryStringCache c;
+  ASSERT_THROW(c.SetMaximumSize(0), Orthanc::OrthancException);
+  
+  c.SetMaximumSize(2);
+
+  std::string v;
+  ASSERT_FALSE(c.Fetch(v, "hello"));
+
+  c.Add("hello", "a");
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_FALSE(c.Fetch(v, "hello2"));
+  ASSERT_FALSE(c.Fetch(v, "hello3"));
+
+  c.Add("hello2", "b");
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_FALSE(c.Fetch(v, "hello3"));
+
+  c.Add("hello3", "too large value");
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_FALSE(c.Fetch(v, "hello3"));
+  
+  c.Add("hello3", "c");
+  ASSERT_FALSE(c.Fetch(v, "hello"));  // Recycled
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+  ASSERT_TRUE(c.Fetch(v, "hello3"));  ASSERT_EQ("c", v);
+}
+
+
+TEST(MemoryStringCache, Invalidate)
+{
+  Orthanc::MemoryStringCache c;
+  c.Add("hello", "a");
+  c.Add("hello2", "b");
+
+  std::string v;
+  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+
+  c.Invalidate("hello");
+  ASSERT_FALSE(c.Fetch(v, "hello"));
+  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
+}
+
+
+TEST(StorageCommitmentReports, Basic)
+{
+  Orthanc::StorageCommitmentReports reports(2);
+  ASSERT_EQ(2u, reports.GetMaxSize());
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "nope");
+    ASSERT_EQ("nope", accessor.GetTransactionUid());
+    ASSERT_FALSE(accessor.IsValid());
+    ASSERT_THROW(accessor.GetReport(), Orthanc::OrthancException);
+  }
+
+  reports.Store("a", new Orthanc::StorageCommitmentReports::Report("aet_a"));
+  reports.Store("b", new Orthanc::StorageCommitmentReports::Report("aet_b"));
+  reports.Store("c", new Orthanc::StorageCommitmentReports::Report("aet_c"));
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet_b", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Pending,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_EQ("aet_c", accessor.GetReport().GetRemoteAet());
+    ASSERT_TRUE(accessor.IsValid());
+  }
+
+  {
+    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
+      (new Orthanc::StorageCommitmentReports::Report("aet"));
+    report->AddSuccess("class1", "instance1");
+    report->AddFailure("class2", "instance2",
+                       Orthanc::StorageCommitmentFailureReason_ReferencedSOPClassNotSupported);
+    report->MarkAsComplete();
+    reports.Store("a", report.release());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Failure,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_TRUE(accessor.IsValid());
+  }
+
+  {
+    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
+      (new Orthanc::StorageCommitmentReports::Report("aet"));
+    report->AddSuccess("class1", "instance1");
+    report->MarkAsComplete();
+    reports.Store("a", report.release());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
+    ASSERT_TRUE(accessor.IsValid());
+    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
+    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Success,
+              accessor.GetReport().GetStatus());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
+    ASSERT_FALSE(accessor.IsValid());
+  }
+
+  {
+    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
+    ASSERT_TRUE(accessor.IsValid());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/MultiThreadingTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2228 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Compatibility.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 "../OrthancServer/Database/SQLiteDatabaseWrapper.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/DicomMoveScuJob.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() ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void Reset() ORTHANC_OVERRIDE
+    {
+    }
+    
+    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
+    {
+      if (fails_)
+      {
+        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
+      }
+      else if (count_ == steps_ - 1)
+      {
+        return JobStepResult::Success();
+      }
+      else
+      {
+        count_++;
+        return JobStepResult::Continue();
+      }
+    }
+
+    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual float GetProgress() ORTHANC_OVERRIDE
+    {
+      return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
+    }
+
+    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
+    {
+      type = "DummyJob";
+    }
+
+    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      value = Json::objectValue;
+      value["Type"] = "DummyJob";
+      return true;
+    }
+
+    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
+    {
+      value["hello"] = "world";
+    }
+
+    virtual bool GetOutput(std::string& output,
+                           MimeType& mime,
+                           const std::string& key) ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+  };
+
+
+  class DummyInstancesJob : public SetOfInstancesJob
+  {
+  private:
+    bool   trailingStepDone_;
+    
+  protected:
+    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
+    {
+      return (instance != "nope");
+    }
+
+    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
+    {
+      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) ORTHANC_OVERRIDE
+    {
+    }
+
+    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
+    {
+      s = "DummyInstancesJob";
+    }
+  };
+
+
+  class DummyUnserializer : public GenericJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
+    {
+      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:
+    int value_;
+    std::set<int>& target_;
+
+  public:
+    DynamicInteger(int value, std::set<int>& target) : 
+      value_(value), target_(target)
+    {
+    }
+
+    int GetValue() const
+    {
+      return value_;
+    }
+  };
+}
+
+
+TEST(MultiThreading, SharedMessageQueueBasic)
+{
+  std::set<int> s;
+
+  SharedMessageQueue q;
+  ASSERT_TRUE(q.WaitEmpty(0));
+  q.Enqueue(new DynamicInteger(10, s));
+  ASSERT_FALSE(q.WaitEmpty(1));
+  q.Enqueue(new DynamicInteger(20, s));
+  q.Enqueue(new DynamicInteger(30, s));
+  q.Enqueue(new DynamicInteger(40, s));
+
+  std::unique_ptr<DynamicInteger> i;
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue());
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue());
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue());
+  ASSERT_FALSE(q.WaitEmpty(1));
+  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue());
+  ASSERT_TRUE(q.WaitEmpty(0));
+  ASSERT_EQ(NULL, q.Dequeue(1));
+}
+
+
+TEST(MultiThreading, SharedMessageQueueClean)
+{
+  std::set<int> s;
+
+  try
+  {
+    SharedMessageQueue q;
+    q.Enqueue(new DynamicInteger(10, s));
+    q.Enqueue(new DynamicInteger(20, s));  
+    throw OrthancException(ErrorCode_InternalError);
+  }
+  catch (OrthancException&)
+  {
+  }
+}
+
+
+
+
+static bool CheckState(JobsRegistry& registry,
+                       const std::string& id,
+                       JobState state)
+{
+  JobState s;
+  if (registry.GetState(s, id))
+  {
+    return state == s;
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+static bool CheckErrorCode(JobsRegistry& registry,
+                           const std::string& id,
+                           ErrorCode code)
+{
+  JobInfo s;
+  if (registry.GetJobInfo(s, id))
+  {
+    return code == s.GetStatus().GetErrorCode();
+  }
+  else
+  {
+    return false;
+  }
+}
+
+
+TEST(JobsRegistry, Priority)
+{
+  JobsRegistry registry(10);
+
+  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(10);
+
+  std::string i1, i2;
+  registry.Submit(i1, new DummyJob(), 20);
+  registry.Submit(i2, new DummyJob(), 10);
+
+  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)
+{
+  JobsRegistry registry(10);
+
+  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(10);
+
+  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));
+
+  {
+    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(10);
+
+  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(10);
+
+  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(10);
+
+  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.Pause(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, Cancel)
+{
+  JobsRegistry registry(10);
+
+  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));
+  }
+
+  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));
+}
+
+
+
+TEST(JobsEngine, SubmitAndWait)
+{
+  JobsEngine engine(10);
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  Json::Value content = Json::nullValue;
+  engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10);
+  ASSERT_EQ(Json::objectValue, content.type());
+  ASSERT_EQ("world", content["hello"].asString());
+
+  content = Json::nullValue;
+  ASSERT_THROW(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10), OrthancException);
+  ASSERT_EQ(Json::nullValue, content.type());
+
+  engine.Stop();
+}
+
+
+TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
+{
+  JobsEngine engine(10);
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  std::string id;
+  SequenceOfOperationsJob* job = NULL;
+
+  {
+    std::unique_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(10);
+  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)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::unique_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::unique_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::unique_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         JobOperationValue& value)
+{
+  Json::Value a = 42;
+  value.Serialize(a);
+  
+  std::unique_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::unique_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);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  std::unique_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;
+
+  {
+    LogJobOperation operation;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+
+  {
+    std::unique_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("jobId"), OrthancException);  // Not started yet
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    job.Start();
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+
+    {
+      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::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    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
+
+  {
+    SequenceOfOperationsJob job;
+    job.SetDescription("hello");
+
+    {
+      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.SetTrailingOperationTimeout(300);
+    }
+
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").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::unique_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;
+
+  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::unique_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::unique_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));
+  }
+}
+
+
+TEST(JobsSerialization, DicomInstanceOrigin)
+{   
+  Json::Value s;
+  std::string t;
+
+  {
+    DicomInstanceOrigin origin;
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    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));
+  }
+
+  {
+    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));
+  }
+}
+
+
+namespace
+{
+  class OrthancJobsSerialization : public testing::Test
+  {
+  private:
+    MemoryStorageArea              storage_;
+    SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
+    std::unique_ptr<ServerContext>   context_;
+
+  public:
+    OrthancJobsSerialization()
+    {
+      db_.Open();
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
+      context_->SetupJobsEngine(true, false);
+    }
+
+    virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE
+    {
+      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, StoreInstanceMode_Default) == StoreStatus_Success);
+    }
+  };
+}
+
+
+TEST_F(OrthancJobsSerialization, Values)
+{
+  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::unique_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::unique_ptr<IJobOperation> operation;
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    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
+
+  {
+    TimeoutDicomConnectionManager luaManager;
+    
+    {
+      RemoteModalityParameters modality;
+      modality.SetApplicationEntityTitle("REMOTE");
+      modality.SetHost("192.168.1.1");
+      modality.SetPortNumber(1000);
+      modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+      StoreScuOperation operation(GetContext(), luaManager, "TEST", modality);
+
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+      operation.Serialize(s);
+    }
+
+    {
+      operation.reset(unserializer.UnserializeOperation(s));
+
+      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));
+
+    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
+
+  {
+    std::unique_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));
+  }
+}
+
+
+TEST_F(OrthancJobsSerialization, Jobs)
+{
+  Json::Value s;
+
+  // ArchiveJob
+
+  {
+    ArchiveJob job(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::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
+    ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().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);
+
+    OrthancPeerStoreJob job(GetContext());
+    job.SetPeer(peer);
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_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());
+    ASSERT_FALSE(tmp.IsTranscode());
+    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
+  }
+
+  {
+    OrthancPeerStoreJob job(GetContext());
+    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
+    job.SetTranscode("1.2.840.10008.1.2.4.50");
+    
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
+    ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("", tmp.GetPeer().GetPassword());
+    ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled());
+    ASSERT_TRUE(tmp.IsTranscode());
+    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
+  }
+
+  // ResourceModificationJob
+
+  {
+    std::unique_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);    
+
+    ResourceModificationJob job(GetContext());
+    job.SetModification(modification.release(), ResourceType_Patient, true);
+    job.SetOrigin(DicomInstanceOrigin::FromLua());
+
+    job.AddTrailingStep();  // Necessary since 1.7.0
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_TRUE(tmp.IsAnonymization());
+    ASSERT_FALSE(tmp.IsTranscode());
+    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
+  }
+
+  {
+    ResourceModificationJob job(GetContext());
+    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
+    job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
+
+    job.AddTrailingStep();  // Necessary since 1.7.0
+    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::unique_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_FALSE(tmp.IsAnonymization());
+    ASSERT_TRUE(tmp.IsTranscode());
+    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
+    ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin());
+  }
+
+  // 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());
+  }
+
+  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("jobId").GetCode());
+      ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+
+      study2 = job.GetTargetStudy();
+      ASSERT_FALSE(study2.empty());
+
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+      ASSERT_TRUE(job.Serialize(s));
+    }
+
+    {
+      std::unique_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));
+    }
+  }
+
+  {
+    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
+
+  {
+    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("jobId").GetCode());
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").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::unique_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(10);
+    registry.Submit(i1, new DummyJob(), 10);
+    registry.Submit(i2, new SequenceOfOperationsJob(), 30);
+    registry.Serialize(s);
+  }
+
+  {
+    DummyUnserializer unserializer;
+    JobsRegistry registry(unserializer, s, 10);
+
+    Json::Value t;
+    registry.Serialize(t);
+    ASSERT_TRUE(CheckSameJson(s, t));
+  }
+}
+
+
+TEST(JobsSerialization, TrailingStep)
+{
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    ASSERT_EQ(0u, job.GetCommandsCount());
+    ASSERT_EQ(0u, job.GetInstancesCount());
+
+    job.Start();
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    job.AddInstance("hello");
+    job.AddInstance("world");
+    ASSERT_EQ(2u, job.GetCommandsCount());
+    ASSERT_EQ(2u, job.GetInstancesCount());
+
+    job.Start();
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_FALSE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(2u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    ASSERT_EQ(0u, job.GetInstancesCount());
+    ASSERT_EQ(0u, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(0u, job.GetInstancesCount());
+    ASSERT_EQ(1u, job.GetCommandsCount());
+
+    job.Start(); // This adds the trailing step
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_TRUE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_TRUE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), OrthancException);
+  }
+
+  {
+    Json::Value s;
+    
+    DummyInstancesJob job;
+    job.AddInstance("hello");
+    ASSERT_EQ(1u, job.GetInstancesCount());
+    ASSERT_EQ(1u, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(1u, job.GetInstancesCount());
+    ASSERT_EQ(2u, job.GetCommandsCount());
+    
+    job.Start();
+    ASSERT_EQ(2u, job.GetCommandsCount());
+    ASSERT_EQ(0u, job.GetPosition());
+    ASSERT_TRUE(job.HasTrailingStep());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+    
+    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
+    ASSERT_EQ(1u, job.GetPosition());
+    ASSERT_FALSE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
+    ASSERT_EQ(2u, job.GetPosition());
+    ASSERT_TRUE(job.IsTrailingStepDone());
+    
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
+    }
+
+    ASSERT_THROW(job.Step("jobId"), 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));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+
+  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_GenericNoWildcardInDates);
+    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_GenericNoWildcardInDates, 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));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+
+  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);
+  operations.insert(DicomRequestType_NAction);
+  operations.insert(DicomRequestType_NEventReport);
+
+  ASSERT_EQ(7u, 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));
+        }
+      }
+    }
+  }
+
+  {
+    Json::Value s;
+    s["AllowStorageCommitment"] = false;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+
+  {
+    Json::Value s;
+    s["AllowNAction"] = false;
+    s["AllowNEventReport"] = true;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    s["AllowTranscoding"] = false;
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_FALSE(modality.IsTranscodingAllowed());
+  }
+
+  {
+    Json::Value s;
+    s["AllowNAction"] = true;
+    s["AllowNEventReport"] = true;
+    s["AET"] = "AET";
+    s["Host"] = "host";
+    s["Port"] = "104";
+    
+    RemoteModalityParameters modality(s);
+    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
+    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
+    ASSERT_EQ("host", modality.GetHost());
+    ASSERT_EQ(104u, modality.GetPortNumber());
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
+    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
+    ASSERT_TRUE(modality.IsTranscodingAllowed());
+  }
+}
+
+
+TEST_F(OrthancJobsSerialization, DicomAssociationParameters)
+{
+  Json::Value v;
+
+  {
+    v = Json::objectValue;
+    DicomAssociationParameters p;
+    p.SerializeJob(v);
+  }
+
+  {
+    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
+    ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle());
+    ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost());
+    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout());
+  }
+
+  {
+    v = Json::objectValue;
+    DicomAssociationParameters p;
+    p.SetLocalApplicationEntityTitle("HELLO");
+    p.SetRemoteApplicationEntityTitle("WORLD");
+    p.SetRemotePort(42);
+    p.SetRemoteHost("MY_HOST");
+    p.SetTimeout(43);
+    p.SerializeJob(v);
+  }
+
+  {
+    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
+    ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle());
+    ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost());
+    ASSERT_EQ(43u, p.GetTimeout());
+  }
+  
+  {
+    DicomModalityStoreJob job(GetContext());
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomModalityStoreJob> job(
+      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
+    ASSERT_FALSE(job->HasMoveOriginator());
+    ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException);
+    ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException);
+    ASSERT_FALSE(job->HasStorageCommitment());
+  }
+  
+  {
+    RemoteModalityParameters r;
+    r.SetApplicationEntityTitle("HELLO");
+    r.SetPortNumber(42);
+    r.SetHost("MY_HOST");
+
+    DicomModalityStoreJob job(GetContext());
+    job.SetLocalAet("WORLD");
+    job.SetRemoteModality(r);
+    job.SetTimeout(43);
+    job.SetMoveOriginator("ORIGINATOR", 100);
+    job.EnableStorageCommitment(true);
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomModalityStoreJob> job(
+      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
+    ASSERT_TRUE(job->HasMoveOriginator());
+    ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet());
+    ASSERT_EQ(100, job->GetMoveOriginatorId());
+    ASSERT_TRUE(job->HasStorageCommitment());
+  }
+    
+  {
+    DicomMoveScuJob job(GetContext());
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomMoveScuJob> job(
+      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
+  }
+  
+  {
+    RemoteModalityParameters r;
+    r.SetApplicationEntityTitle("HELLO");
+    r.SetPortNumber(42);
+    r.SetHost("MY_HOST");
+
+    DicomMoveScuJob job(GetContext());
+    job.SetLocalAet("WORLD");
+    job.SetRemoteModality(r);
+    job.SetTimeout(43);
+    job.Serialize(v);
+  }
+  
+  {
+    OrthancJobUnserializer unserializer(GetContext());
+    std::unique_ptr<DicomMoveScuJob> job(
+      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
+    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
+    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
+    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
+    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
+    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,34 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/PrecompiledHeadersUnitTests.h	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,41 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "../OrthancServer/PrecompiledHeadersServer.h"
+
+#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
+#include "../Core/EnumerationDictionary.h"
+#include <gtest/gtest.h>
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/ServerIndexTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,915 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include "../Core/Compatibility.h"
+#include "../Core/FileStorage/FilesystemStorage.h"
+#include "../Core/FileStorage/MemoryStorageArea.h"
+#include "../Core/Logging.h"
+#include "../OrthancServer/Database/SQLiteDatabaseWrapper.h"
+#include "../OrthancServer/Search/DatabaseLookup.h"
+#include "../OrthancServer/ServerContext.h"
+#include "../OrthancServer/ServerToolbox.h"
+
+#include <ctype.h>
+#include <algorithm>
+
+using namespace Orthanc;
+
+namespace
+{
+  class TestDatabaseListener : public IDatabaseListener
+  {
+  public:
+    std::vector<std::string> deletedFiles_;
+    std::vector<std::string> deletedResources_;
+    std::string ancestorId_;
+    ResourceType ancestorType_;
+
+    void Reset()
+    {
+      ancestorId_ = "";
+      deletedFiles_.clear();
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType type,
+                                         const std::string& publicId)
+      ORTHANC_OVERRIDE
+    {
+      ancestorId_ = publicId;
+      ancestorType_ = type;
+    }
+
+    virtual void SignalFileDeleted(const FileInfo& info) ORTHANC_OVERRIDE
+    {
+      const std::string fileUuid = info.GetUuid();
+      deletedFiles_.push_back(fileUuid);
+      LOG(INFO) << "A file must be removed: " << fileUuid;
+    }       
+
+    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE
+    {
+      if (change.GetChangeType() == ChangeType_Deleted)
+      {
+        deletedResources_.push_back(change.GetPublicId());        
+      }
+
+      LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " 
+                << EnumerationToString(change.GetResourceType()) << ": " 
+                << EnumerationToString(change.GetChangeType());
+    }
+  };
+
+
+  class DatabaseWrapperTest : public ::testing::Test
+  {
+  protected:
+    std::unique_ptr<TestDatabaseListener>  listener_;
+    std::unique_ptr<SQLiteDatabaseWrapper> index_;
+
+  public:
+    DatabaseWrapperTest()
+    {
+    }
+
+    virtual void SetUp() ORTHANC_OVERRIDE
+    {
+      listener_.reset(new TestDatabaseListener);
+      index_.reset(new SQLiteDatabaseWrapper);
+      index_->SetListener(*listener_);
+      index_->Open();
+    }
+
+    virtual void TearDown() ORTHANC_OVERRIDE
+    {
+      index_->Close();
+      index_.reset(NULL);
+      listener_.reset(NULL);
+    }
+
+    void CheckTableRecordCount(uint32_t expected, const char* table)
+    {
+      ASSERT_EQ(expected, index_->GetTableRecordCount(table));
+    }
+
+    void CheckNoParent(int64_t id)
+    {
+      std::string s;
+      ASSERT_FALSE(index_->GetParentPublicId(s, id));
+    }
+
+    void CheckParentPublicId(const char* expected, int64_t id)
+    {
+      std::string s;
+      ASSERT_TRUE(index_->GetParentPublicId(s, id));
+      ASSERT_EQ(expected, s);
+    }
+
+    void CheckNoChild(int64_t id)
+    {
+      std::list<std::string> j;
+      index_->GetChildren(j, id);
+      ASSERT_EQ(0u, j.size());
+    }
+
+    void CheckOneChild(const char* expected, int64_t id)
+    {
+      std::list<std::string> j;
+      index_->GetChildren(j, id);
+      ASSERT_EQ(1u, j.size());
+      ASSERT_EQ(expected, j.front());
+    }
+
+    void CheckTwoChildren(const char* expected1,
+                          const char* expected2,
+                          int64_t id)
+    {
+      std::list<std::string> j;
+      index_->GetChildren(j, id);
+      ASSERT_EQ(2u, j.size());
+      ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
+                  (expected1 == j.back() && expected2 == j.front()));                    
+    }
+
+    void DoLookupIdentifier(std::list<std::string>& result,
+                            ResourceType level,
+                            const DicomTag& tag,
+                            ConstraintType type,
+                            const std::string& value)
+    {
+      assert(ServerToolbox::IsIdentifier(tag, level));
+      
+      DicomTagConstraint c(tag, type, value, true, true);
+      
+      std::vector<DatabaseConstraint> lookup;
+      lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+    }    
+
+    void DoLookupIdentifier2(std::list<std::string>& result,
+                             ResourceType level,
+                             const DicomTag& tag,
+                             ConstraintType type1,
+                             const std::string& value1,
+                             ConstraintType type2,
+                             const std::string& value2)
+    {
+      assert(ServerToolbox::IsIdentifier(tag, level));
+      
+      DicomTagConstraint c1(tag, type1, value1, true, true);
+      DicomTagConstraint c2(tag, type2, value2, true, true);
+      
+      std::vector<DatabaseConstraint> lookup;
+      lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
+      
+      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
+    }
+  };
+}
+
+
+TEST_F(DatabaseWrapperTest, Simple)
+{
+  int64_t a[] = {
+    index_->CreateResource("a", ResourceType_Patient),   // 0
+    index_->CreateResource("b", ResourceType_Study),     // 1
+    index_->CreateResource("c", ResourceType_Series),    // 2
+    index_->CreateResource("d", ResourceType_Instance),  // 3
+    index_->CreateResource("e", ResourceType_Instance),  // 4
+    index_->CreateResource("f", ResourceType_Instance),  // 5
+    index_->CreateResource("g", ResourceType_Study)      // 6
+  };
+
+  ASSERT_EQ("a", index_->GetPublicId(a[0]));
+  ASSERT_EQ("b", index_->GetPublicId(a[1]));
+  ASSERT_EQ("c", index_->GetPublicId(a[2]));
+  ASSERT_EQ("d", index_->GetPublicId(a[3]));
+  ASSERT_EQ("e", index_->GetPublicId(a[4]));
+  ASSERT_EQ("f", index_->GetPublicId(a[5]));
+  ASSERT_EQ("g", index_->GetPublicId(a[6]));
+
+  ASSERT_EQ(ResourceType_Patient, index_->GetResourceType(a[0]));
+  ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[1]));
+  ASSERT_EQ(ResourceType_Series, index_->GetResourceType(a[2]));
+  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[3]));
+  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[4]));
+  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[5]));
+  ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[6]));
+
+  {
+    std::list<std::string> t;
+    index_->GetAllPublicIds(t, ResourceType_Patient);
+
+    ASSERT_EQ(1u, t.size());
+    ASSERT_EQ("a", t.front());
+
+    index_->GetAllPublicIds(t, ResourceType_Series);
+    ASSERT_EQ(1u, t.size());
+    ASSERT_EQ("c", t.front());
+
+    index_->GetAllPublicIds(t, ResourceType_Study);
+    ASSERT_EQ(2u, t.size());
+
+    index_->GetAllPublicIds(t, ResourceType_Instance);
+    ASSERT_EQ(3u, t.size());
+  }
+
+  index_->SetGlobalProperty(GlobalProperty_FlushSleep, "World");
+
+  index_->AttachChild(a[0], a[1]);
+  index_->AttachChild(a[1], a[2]);
+  index_->AttachChild(a[2], a[3]);
+  index_->AttachChild(a[2], a[4]);
+  index_->AttachChild(a[6], a[5]);
+
+  int64_t parent;
+  ASSERT_FALSE(index_->LookupParent(parent, a[0]));
+  ASSERT_TRUE(index_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent);
+  ASSERT_TRUE(index_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent);
+  ASSERT_TRUE(index_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent);
+  ASSERT_TRUE(index_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent);
+  ASSERT_TRUE(index_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent);
+  ASSERT_FALSE(index_->LookupParent(parent, a[6]));
+
+  std::string s;
+
+  CheckNoParent(a[0]);
+  CheckNoParent(a[6]);
+  CheckParentPublicId("a", a[1]);
+  CheckParentPublicId("b", a[2]);
+  CheckParentPublicId("c", a[3]);
+  CheckParentPublicId("c", a[4]);
+  CheckParentPublicId("g", a[5]);
+
+  std::list<std::string> l;
+  index_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front());
+  index_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front());
+  index_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); 
+  index_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); 
+  index_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); 
+  index_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front());
+
+  index_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); 
+  if (l.front() == "d")
+  {
+    ASSERT_EQ("e", l.back());
+  }
+  else
+  {
+    ASSERT_EQ("d", l.back());
+    ASSERT_EQ("e", l.front());
+  }
+
+  std::map<MetadataType, std::string> md;
+  index_->GetAllMetadata(md, a[4]);
+  ASSERT_EQ(0u, md.size());
+
+  index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
+                                       CompressionType_ZlibWithSize, 21, "compressedMD5"));
+  index_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"));
+  index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"));
+  index_->SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE");
+  
+  index_->GetAllMetadata(md, a[4]);
+  ASSERT_EQ(1u, md.size());
+  ASSERT_EQ("PINNACLE", md[MetadataType_Instance_RemoteAet]);
+  index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU");
+  index_->GetAllMetadata(md, a[4]);
+  ASSERT_EQ(2u, md.size());
+
+  std::map<MetadataType, std::string> md2;
+  index_->GetAllMetadata(md2, a[4]);
+  ASSERT_EQ(2u, md2.size());
+  ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]);
+  ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
+
+  index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
+  index_->GetAllMetadata(md, a[4]);
+  ASSERT_EQ(1u, md.size());
+  ASSERT_EQ("PINNACLE", md[MetadataType_Instance_RemoteAet]);
+
+  index_->GetAllMetadata(md2, a[4]);
+  ASSERT_EQ(1u, md2.size());
+  ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
+
+
+  ASSERT_EQ(21u + 42u + 44u, index_->GetTotalCompressedSize());
+  ASSERT_EQ(42u + 42u + 44u, index_->GetTotalUncompressedSize());
+
+  index_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");
+
+  int64_t b;
+  ResourceType t;
+  ASSERT_TRUE(index_->LookupResource(b, t, "g"));
+  ASSERT_EQ(7, b);
+  ASSERT_EQ(ResourceType_Study, t);
+
+  ASSERT_TRUE(index_->LookupMetadata(s, a[4], MetadataType_Instance_RemoteAet));
+  ASSERT_FALSE(index_->LookupMetadata(s, a[4], MetadataType_Instance_IndexInSeries));
+  ASSERT_EQ("PINNACLE", s);
+
+  std::string u;
+  ASSERT_TRUE(index_->LookupMetadata(u, a[4], MetadataType_Instance_RemoteAet));
+  ASSERT_EQ("PINNACLE", u);
+  ASSERT_FALSE(index_->LookupMetadata(u, a[4], MetadataType_Instance_IndexInSeries));
+
+  ASSERT_TRUE(index_->LookupGlobalProperty(s, GlobalProperty_FlushSleep));
+  ASSERT_FALSE(index_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42)));
+  ASSERT_EQ("World", s);
+
+  FileInfo att;
+  ASSERT_TRUE(index_->LookupAttachment(att, a[4], FileContentType_DicomAsJson));
+  ASSERT_EQ("my json file", att.GetUuid());
+  ASSERT_EQ(21u, att.GetCompressedSize());
+  ASSERT_EQ("md5", att.GetUncompressedMD5());
+  ASSERT_EQ("compressedMD5", att.GetCompressedMD5());
+  ASSERT_EQ(42u, att.GetUncompressedSize());
+  ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
+
+  ASSERT_TRUE(index_->LookupAttachment(att, a[6], FileContentType_Dicom));
+  ASSERT_EQ("world", att.GetUuid());
+  ASSERT_EQ(44u, att.GetCompressedSize());
+  ASSERT_EQ("md5", att.GetUncompressedMD5());
+  ASSERT_EQ("md5", att.GetCompressedMD5());
+  ASSERT_EQ(44u, att.GetUncompressedSize());
+  ASSERT_EQ(CompressionType_None, att.GetCompressionType());
+
+  ASSERT_EQ(0u, listener_->deletedFiles_.size());
+  ASSERT_EQ(0u, listener_->deletedResources_.size());
+
+  CheckTableRecordCount(7, "Resources");
+  CheckTableRecordCount(3, "AttachedFiles");
+  CheckTableRecordCount(1, "Metadata");
+  CheckTableRecordCount(1, "MainDicomTags");
+
+  index_->DeleteResource(a[0]);
+  ASSERT_EQ(5u, listener_->deletedResources_.size());
+  ASSERT_EQ(2u, listener_->deletedFiles_.size());
+  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
+                         listener_->deletedFiles_.end(),
+                         "my json file") == listener_->deletedFiles_.end());
+  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
+                         listener_->deletedFiles_.end(),
+                         "my dicom file") == listener_->deletedFiles_.end());
+
+  CheckTableRecordCount(2, "Resources");
+  CheckTableRecordCount(0, "Metadata");
+  CheckTableRecordCount(1, "AttachedFiles");
+  CheckTableRecordCount(0, "MainDicomTags");
+
+  index_->DeleteResource(a[5]);
+  ASSERT_EQ(7u, listener_->deletedResources_.size());
+
+  CheckTableRecordCount(0, "Resources");
+  CheckTableRecordCount(0, "AttachedFiles");
+  CheckTableRecordCount(3, "GlobalProperties");
+
+  std::string tmp;
+  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion));
+  ASSERT_EQ("6", tmp);
+  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep));
+  ASSERT_EQ("World", tmp);
+  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast));
+  ASSERT_EQ("1", tmp);
+
+  ASSERT_EQ(3u, listener_->deletedFiles_.size());
+  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
+                         listener_->deletedFiles_.end(),
+                         "world") == listener_->deletedFiles_.end());
+}
+
+
+TEST_F(DatabaseWrapperTest, Upward)
+{
+  int64_t a[] = {
+    index_->CreateResource("a", ResourceType_Patient),   // 0
+    index_->CreateResource("b", ResourceType_Study),     // 1
+    index_->CreateResource("c", ResourceType_Series),    // 2
+    index_->CreateResource("d", ResourceType_Instance),  // 3
+    index_->CreateResource("e", ResourceType_Instance),  // 4
+    index_->CreateResource("f", ResourceType_Study),     // 5
+    index_->CreateResource("g", ResourceType_Series),    // 6
+    index_->CreateResource("h", ResourceType_Series)     // 7
+  };
+
+  index_->AttachChild(a[0], a[1]);
+  index_->AttachChild(a[1], a[2]);
+  index_->AttachChild(a[2], a[3]);
+  index_->AttachChild(a[2], a[4]);
+  index_->AttachChild(a[1], a[6]);
+  index_->AttachChild(a[0], a[5]);
+  index_->AttachChild(a[5], a[7]);
+
+  CheckTwoChildren("b", "f", a[0]);
+  CheckTwoChildren("c", "g", a[1]);
+  CheckTwoChildren("d", "e", a[2]);
+  CheckNoChild(a[3]);
+  CheckNoChild(a[4]);
+  CheckOneChild("h", a[5]);
+  CheckNoChild(a[6]);
+  CheckNoChild(a[7]);
+
+  listener_->Reset();
+  index_->DeleteResource(a[3]);
+  ASSERT_EQ("c", listener_->ancestorId_);
+  ASSERT_EQ(ResourceType_Series, listener_->ancestorType_);
+
+  listener_->Reset();
+  index_->DeleteResource(a[4]);
+  ASSERT_EQ("b", listener_->ancestorId_);
+  ASSERT_EQ(ResourceType_Study, listener_->ancestorType_);
+
+  listener_->Reset();
+  index_->DeleteResource(a[7]);
+  ASSERT_EQ("a", listener_->ancestorId_);
+  ASSERT_EQ(ResourceType_Patient, listener_->ancestorType_);
+
+  listener_->Reset();
+  index_->DeleteResource(a[6]);
+  ASSERT_EQ("", listener_->ancestorId_);  // No more ancestor
+}
+
+
+TEST_F(DatabaseWrapperTest, PatientRecycling)
+{
+  std::vector<int64_t> patients;
+  for (int i = 0; i < 10; i++)
+  {
+    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
+    patients.push_back(index_->CreateResource(p, ResourceType_Patient));
+    index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
+                                                "md5-" + boost::lexical_cast<std::string>(i)));
+    ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
+  }
+
+  CheckTableRecordCount(10u, "Resources");
+  CheckTableRecordCount(10u, "PatientRecyclingOrder");
+
+  listener_->Reset();
+  ASSERT_EQ(0u, listener_->deletedResources_.size());
+
+  index_->DeleteResource(patients[5]);
+  index_->DeleteResource(patients[0]);
+  ASSERT_EQ(2u, listener_->deletedResources_.size());
+
+  CheckTableRecordCount(8u, "Resources");
+  CheckTableRecordCount(8u, "PatientRecyclingOrder");
+
+  ASSERT_EQ(2u, listener_->deletedFiles_.size());
+  ASSERT_EQ("Patient 5", listener_->deletedFiles_[0]);
+  ASSERT_EQ("Patient 0", listener_->deletedFiles_[1]);
+
+  int64_t p;
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(3u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(4u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(5u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(6u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
+  index_->DeleteResource(p);
+  index_->DeleteResource(patients[8]);
+  ASSERT_EQ(8u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(9u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
+  index_->DeleteResource(p);
+  ASSERT_FALSE(index_->SelectPatientToRecycle(p));
+  ASSERT_EQ(10u, listener_->deletedResources_.size());
+
+  ASSERT_EQ(10u, listener_->deletedFiles_.size());
+
+  CheckTableRecordCount(0, "Resources");
+  CheckTableRecordCount(0, "PatientRecyclingOrder");
+}
+
+
+TEST_F(DatabaseWrapperTest, PatientProtection)
+{
+  std::vector<int64_t> patients;
+  for (int i = 0; i < 5; i++)
+  {
+    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
+    patients.push_back(index_->CreateResource(p, ResourceType_Patient));
+    index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
+                                                "md5-" + boost::lexical_cast<std::string>(i)));
+    ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
+  }
+
+  CheckTableRecordCount(5, "Resources");
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
+
+  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  index_->SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
+  CheckTableRecordCount(5, "Resources");
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
+
+  index_->SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
+  index_->SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
+  index_->SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
+  CheckTableRecordCount(5, "Resources");
+  index_->SetProtectedPatient(patients[2], true);
+  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
+  index_->SetProtectedPatient(patients[2], false);
+  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
+  CheckTableRecordCount(5, "PatientRecyclingOrder");
+  index_->SetProtectedPatient(patients[3], true);
+  ASSERT_TRUE(index_->IsProtectedPatient(patients[3]));
+  CheckTableRecordCount(4, "PatientRecyclingOrder");
+
+  CheckTableRecordCount(5, "Resources");
+  ASSERT_EQ(0u, listener_->deletedFiles_.size());
+
+  // Unprotecting a patient puts it at the last position in the recycling queue
+  int64_t p;
+  ASSERT_EQ(0u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(1u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(2u, listener_->deletedResources_.size());
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(3u, listener_->deletedResources_.size());
+  ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2]));
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(4u, listener_->deletedResources_.size());
+  // "patients[3]" is still protected
+  ASSERT_FALSE(index_->SelectPatientToRecycle(p));
+
+  ASSERT_EQ(4u, listener_->deletedFiles_.size());
+  CheckTableRecordCount(1, "Resources");
+  CheckTableRecordCount(0, "PatientRecyclingOrder");
+
+  index_->SetProtectedPatient(patients[3], false);
+  CheckTableRecordCount(1, "PatientRecyclingOrder");
+  ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[3]));
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[2]));
+  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
+  index_->DeleteResource(p);
+  ASSERT_EQ(5u, listener_->deletedResources_.size());
+
+  ASSERT_EQ(5u, listener_->deletedFiles_.size());
+  CheckTableRecordCount(0, "Resources");
+  CheckTableRecordCount(0, "PatientRecyclingOrder");
+}
+
+
+TEST(ServerIndex, Sequence)
+{
+  const std::string path = "UnitTestsStorage";
+
+  SystemToolbox::RemoveFile(path + "/index");
+  FilesystemStorage storage(path);
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+  ServerContext context(db, storage, true /* running unit tests */, 10);
+  context.SetupJobsEngine(true, false);
+
+  ServerIndex& index = context.GetIndex();
+
+  ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+  ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
+
+  context.Stop();
+  db.Close();
+}
+
+
+TEST_F(DatabaseWrapperTest, LookupIdentifier)
+{
+  int64_t a[] = {
+    index_->CreateResource("a", ResourceType_Study),   // 0
+    index_->CreateResource("b", ResourceType_Study),   // 1
+    index_->CreateResource("c", ResourceType_Study),   // 2
+    index_->CreateResource("d", ResourceType_Series)   // 3
+  };
+
+  index_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  index_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1");
+  index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
+  index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
+
+  std::list<std::string> s;
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "0");
+  ASSERT_EQ(2u, s.size());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end());
+
+  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "0");
+  ASSERT_EQ(1u, s.size());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
+  ASSERT_EQ(1u, s.size());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
+  ASSERT_EQ(1u, s.size());
+  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
+
+  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "1");
+  ASSERT_EQ(0u, s.size());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "0");
+  ASSERT_EQ(3u, s.size());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "1");
+  ASSERT_EQ(1u, s.size());
+
+  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "2");
+  ASSERT_EQ(0u, s.size());
+
+  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
+                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "0");
+  ASSERT_EQ(2u, s.size());
+
+  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
+                      ConstraintType_GreaterOrEqual, "1", ConstraintType_SmallerOrEqual, "1");
+  ASSERT_EQ(1u, s.size());
+
+  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
+                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "1");
+  ASSERT_EQ(3u, s.size());
+}
+
+
+TEST(ServerIndex, AttachmentRecycling)
+{
+  const std::string path = "UnitTestsStorage";
+
+  SystemToolbox::RemoveFile(path + "/index");
+  FilesystemStorage storage(path);
+  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+  db.Open();
+  ServerContext context(db, storage, true /* running unit tests */, 10);
+  context.SetupJobsEngine(true, false);
+  ServerIndex& index = context.GetIndex();
+
+  index.SetMaximumStorageSize(10);
+
+  uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                            countStudies, countSeries, countInstances);
+
+  ASSERT_EQ(0u, countPatients);
+  ASSERT_EQ(0u, diskSize);
+
+  ServerIndex::Attachments attachments;
+
+  std::vector<std::string> ids;
+  for (int i = 0; i < 10; i++)
+  {
+    std::string id = boost::lexical_cast<std::string>(i);
+    DicomMap instance;
+    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;
+    DicomInstanceToStore toStore;
+    toStore.SetSummary(instance);
+    ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments,
+                                               false /* don't overwrite */));
+    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());
+
+    // The default transfer syntax depends on the OS endianness
+    std::string s = instanceMetadata[MetadataType_Instance_TransferSyntax];
+    ASSERT_TRUE(s == "1.2.840.10008.1.2.1" ||
+                s == "1.2.840.10008.1.2.2");
+
+    ASSERT_EQ("1.2.840.10008.5.1.4.1.1.1", instanceMetadata[MetadataType_Instance_SopClassUid]);
+
+    DicomInstanceHasher hasher(instance);
+    ids.push_back(hasher.HashPatient());
+    ids.push_back(hasher.HashStudy());
+    ids.push_back(hasher.HashSeries());
+    ids.push_back(hasher.HashInstance());
+
+    ASSERT_EQ(hasher.HashPatient(), toStore.GetHasher().HashPatient());
+    ASSERT_EQ(hasher.HashStudy(), toStore.GetHasher().HashStudy());
+    ASSERT_EQ(hasher.HashSeries(), toStore.GetHasher().HashSeries());
+    ASSERT_EQ(hasher.HashInstance(), toStore.GetHasher().HashInstance());
+  }
+
+  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                            countStudies, countSeries, countInstances);
+  ASSERT_EQ(10u, countPatients);
+  ASSERT_EQ(0u, diskSize);
+
+  for (size_t i = 0; i < ids.size(); i++)
+  {
+    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
+    index.AddAttachment(info, ids[i]);
+
+    index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                              countStudies, countSeries, countInstances);
+    ASSERT_GE(10u, diskSize);
+  }
+
+  // Because the DB is in memory, the SQLite index must not have been created
+  ASSERT_FALSE(SystemToolbox::IsRegularFile(path + "/index"));
+
+  context.Stop();
+  db.Close();
+}
+
+
+TEST(ServerIndex, 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;
+    SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
+    db.Open();
+    ServerContext context(db, storage, true /* running unit tests */, 10);
+    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.SetOverwriteInstances(overwrite);
+
+    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
+    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+
+    ASSERT_EQ(0u, countInstances);
+    ASSERT_EQ(0u, diskSize);
+
+    {
+      DicomInstanceToStore toStore;
+      toStore.SetSummary(instance);
+      toStore.SetOrigin(DicomInstanceOrigin::FromPlugins());
+      ASSERT_EQ(id, toStore.GetHasher().HashInstance());
+
+      std::string id2;
+      ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore, StoreInstanceMode_Default));
+      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().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+    ASSERT_EQ(1u, countInstances);
+    ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), diskSize);
+    ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), uncompressedSize);
+
+    Json::Value tmp;
+    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, StoreInstanceMode_Default));
+      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().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
+                                           countStudies, countSeries, countInstances);
+    ASSERT_EQ(1u, countInstances);
+    ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), diskSize);
+    ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), uncompressedSize);
+
+    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();
+  }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/UnitTestsMain.cpp	Wed Jun 10 20:30:34 2020 +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-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "../Core/EnumerationDictionary.h"
+
+#include "gtest/gtest.h"
+
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
+#include "../Core/OrthancException.h"
+
+#include "../OrthancServer/OrthancInitialization.h"
+#include "../OrthancServer/ServerEnumerations.h"
+
+
+using namespace Orthanc;
+
+
+TEST(EnumerationDictionary, Simple)
+{
+  EnumerationDictionary<MetadataType>  d;
+
+  ASSERT_THROW(d.Translate("ReceptionDate"), OrthancException);
+  ASSERT_EQ(MetadataType_ModifiedFrom, d.Translate("5"));
+  ASSERT_EQ(256, d.Translate("256"));
+
+  d.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate");
+
+  ASSERT_EQ(MetadataType_Instance_ReceptionDate, d.Translate("ReceptionDate"));
+  ASSERT_EQ(MetadataType_Instance_ReceptionDate, d.Translate("2"));
+  ASSERT_EQ("ReceptionDate", d.Translate(MetadataType_Instance_ReceptionDate));
+
+  ASSERT_THROW(d.Add(MetadataType_Instance_ReceptionDate, "Hello"), OrthancException);
+  ASSERT_THROW(d.Add(MetadataType_ModifiedFrom, "ReceptionDate"), OrthancException); // already used
+  ASSERT_THROW(d.Add(MetadataType_ModifiedFrom, "1024"), OrthancException); // cannot register numbers
+  d.Add(MetadataType_ModifiedFrom, "ModifiedFrom");  // ok
+}
+
+
+TEST(EnumerationDictionary, ServerEnumerations)
+{
+  ASSERT_STREQ("Patient", EnumerationToString(ResourceType_Patient));
+  ASSERT_STREQ("Study", EnumerationToString(ResourceType_Study));
+  ASSERT_STREQ("Series", EnumerationToString(ResourceType_Series));
+  ASSERT_STREQ("Instance", EnumerationToString(ResourceType_Instance));
+
+  ASSERT_STREQ("ModifiedSeries", EnumerationToString(ChangeType_ModifiedSeries));
+
+  ASSERT_STREQ("Failure", EnumerationToString(StoreStatus_Failure));
+  ASSERT_STREQ("Success", EnumerationToString(StoreStatus_Success));
+
+  ASSERT_STREQ("CompletedSeries", EnumerationToString(ChangeType_CompletedSeries));
+
+  ASSERT_EQ("IndexInSeries", EnumerationToString(MetadataType_Instance_IndexInSeries));
+  ASSERT_EQ("LastUpdate", EnumerationToString(MetadataType_LastUpdate));
+
+  ASSERT_EQ(ResourceType_Patient, StringToResourceType("PATienT"));
+  ASSERT_EQ(ResourceType_Study, StringToResourceType("STudy"));
+  ASSERT_EQ(ResourceType_Series, StringToResourceType("SeRiEs"));
+  ASSERT_EQ(ResourceType_Instance, StringToResourceType("INStance"));
+  ASSERT_EQ(ResourceType_Instance, StringToResourceType("IMagE"));
+  ASSERT_THROW(StringToResourceType("heLLo"), OrthancException);
+
+  ASSERT_EQ(2047, StringToMetadata("2047"));
+  ASSERT_THROW(StringToMetadata("Ceci est un test"), OrthancException);
+  ASSERT_THROW(RegisterUserMetadata(128, ""), OrthancException); // too low (< 1024)
+  ASSERT_THROW(RegisterUserMetadata(128000, ""), OrthancException); // too high (> 65535)
+  RegisterUserMetadata(2047, "Ceci est un test");
+  ASSERT_EQ(2047, StringToMetadata("2047"));
+  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("Vitrea", EnumerationToString(StringToModalityManufacturer("Vitrea")));
+  ASSERT_STREQ("GE", EnumerationToString(StringToModalityManufacturer("GE")));
+  // 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("Generic", EnumerationToString(StringToModalityManufacturer("ClearCanvas")));
+  ASSERT_STREQ("Generic", EnumerationToString(StringToModalityManufacturer("Dcm4Chee")));
+  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("SyngoVia")));
+  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("AgfaImpax")));
+}
+
+
+
+int main(int argc, char **argv)
+{
+  Logging::Initialize();
+  Toolbox::InitializeGlobalLocale(NULL);
+  Logging::EnableInfoLevel(true);
+  Toolbox::DetectEndianness();
+  SystemToolbox::MakeDirectory("UnitTestsResults");
+  OrthancInitialize();
+
+  ::testing::InitGoogleTest(&argc, argv);
+  int result = RUN_ALL_TESTS();
+
+  OrthancFinalize();
+  Logging::Finalize();
+
+  return result;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/UnitTestsSources/VersionsTests.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,227 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * 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 "PrecompiledHeadersUnitTests.h"
+#include "gtest/gtest.h"
+
+#include <stdint.h>
+#include <math.h>
+#include <png.h>
+#include <ctype.h>
+#include <zlib.h>
+#include <curl/curl.h>
+#include <boost/version.hpp>
+#include <sqlite3.h>
+#include <lua.h>
+#include <jpeglib.h>
+
+#if BUILDING_LIBICONV == 1
+#  include <iconv.h>
+#endif
+
+#if ORTHANC_ENABLE_SSL == 1
+#  include <openssl/opensslv.h>
+#endif
+
+#if (ORTHANC_ENABLE_CIVETWEB == 1 &&            \
+     ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1)
+#  include <civetweb.h>
+#endif
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+#  include <pugixml.hpp>
+#endif
+
+
+TEST(Versions, Zlib)
+{
+  ASSERT_STREQ(zlibVersion(), ZLIB_VERSION);
+}
+
+TEST(Versions, Curl)
+{
+  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
+  ASSERT_STREQ(LIBCURL_VERSION, v->version);
+}
+
+TEST(Versions, Png)
+{
+  ASSERT_EQ(PNG_LIBPNG_VER_MAJOR * 10000 + PNG_LIBPNG_VER_MINOR * 100 + PNG_LIBPNG_VER_RELEASE,
+            static_cast<int>(png_access_version_number()));
+}
+
+TEST(Versions, SQLite)
+{
+#if defined(__APPLE__)
+  // Under OS X, there might exist minor differences between the
+  // version of the headers and the version of the library, for the
+  // system-wide SQLite. Ignore these differences.
+#else
+  // http://www.sqlite.org/capi3ref.html#sqlite3_libversion
+  EXPECT_EQ(sqlite3_libversion_number(), SQLITE_VERSION_NUMBER);
+  EXPECT_STREQ(sqlite3_libversion(), SQLITE_VERSION);
+  
+  /**
+   * On Orthanc > 1.5.8, we comment out the following test, that is
+   * too strict for some GNU/Linux distributions to apply their own
+   * security fixes. Checking the main version macros is sufficient.
+   * https://bugzilla.suse.com/show_bug.cgi?id=1154550#c2
+   **/
+  // EXPECT_STREQ(sqlite3_sourceid(), SQLITE_SOURCE_ID);
+#endif
+
+  // Ensure that the SQLite version is above 3.7.0.
+  // "sqlite3_create_function_v2" is not defined in previous versions.
+  ASSERT_GE(SQLITE_VERSION_NUMBER, 3007000);
+}
+
+
+TEST(Versions, Lua)
+{
+  // Ensure that the Lua version is above 5.1.0. This version has
+  // introduced some API changes.
+  ASSERT_GE(LUA_VERSION_NUM, 501);
+}
+
+
+#if (ORTHANC_ENABLE_CIVETWEB == 1 &&            \
+     ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1)
+TEST(Version, CivetwebCompression)
+{
+  ASSERT_TRUE(mg_check_feature(MG_FEATURES_COMPRESSION));
+}
+#endif
+
+
+#if ORTHANC_STATIC == 1
+
+TEST(Versions, ZlibStatic)
+{
+  ASSERT_STREQ("1.2.11", zlibVersion());
+}
+
+TEST(Versions, BoostStatic)
+{
+  ASSERT_STREQ("1_69", BOOST_LIB_VERSION);
+}
+
+TEST(Versions, CurlStatic)
+{
+  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
+  ASSERT_STREQ("7.64.0", v->version);
+}
+
+TEST(Versions, PngStatic)
+{
+  ASSERT_EQ(10636u, png_access_version_number());
+  ASSERT_STREQ("1.6.36", PNG_LIBPNG_VER_STRING);
+}
+
+TEST(Versions, JpegStatic)
+{
+  ASSERT_EQ(9, JPEG_LIB_VERSION_MAJOR);
+  ASSERT_EQ(3, JPEG_LIB_VERSION_MINOR);
+}
+
+TEST(Versions, CurlSslStatic)
+{
+  curl_version_info_data * vinfo = curl_version_info(CURLVERSION_NOW);
+
+  // Check that SSL support is enabled when required
+  bool curlSupportsSsl = (vinfo->features & CURL_VERSION_SSL) != 0;
+
+#if ORTHANC_ENABLE_SSL == 0
+  ASSERT_FALSE(curlSupportsSsl);
+#else
+  ASSERT_TRUE(curlSupportsSsl);
+#endif
+}
+
+TEST(Version, LuaStatic)
+{
+  ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE);
+}
+
+
+#if BUILDING_LIBICONV == 1
+TEST(Version, LibIconvStatic)
+{
+  static const int major = 1;
+  static const int minor = 15;  
+  ASSERT_EQ((major << 8) + minor, _LIBICONV_VERSION);
+}
+#endif
+
+
+#if ORTHANC_ENABLE_SSL == 1
+TEST(Version, OpenSslStatic)
+{
+  ASSERT_TRUE(OPENSSL_VERSION_NUMBER == 0x1000210fL /* openssl-1.0.2p */ ||
+              OPENSSL_VERSION_NUMBER == 0x1010107fL /* openssl-1.1.1g */);
+}
+#endif
+
+
+#include <json/version.h>
+
+TEST(Version, JsonCpp)
+{
+#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
+}
+
+
+#if ORTHANC_ENABLE_CIVETWEB == 1
+TEST(Version, Civetweb)
+{
+  ASSERT_EQ(1, CIVETWEB_VERSION_MAJOR);
+  ASSERT_EQ(12, CIVETWEB_VERSION_MINOR);
+  ASSERT_EQ(0, CIVETWEB_VERSION_PATCH);
+}
+#endif
+
+
+#if ORTHANC_ENABLE_PUGIXML == 1
+TEST(Version, Pugixml)
+{
+  ASSERT_EQ(190, PUGIXML_VERSION);
+}
+#endif
+
+
+#endif
--- a/OrthancServer/main.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1718 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancRestApi/OrthancRestApi.h"
-
-#include <boost/algorithm/string/predicate.hpp>
-
-#include "../Core/Compatibility.h"
-#include "../Core/DicomFormat/DicomArray.h"
-#include "../Core/DicomNetworking/DicomAssociationParameters.h"
-#include "../Core/DicomNetworking/DicomServer.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/HttpServer/FilesystemHttpHandler.h"
-#include "../Core/HttpServer/HttpServer.h"
-#include "../Core/Logging.h"
-#include "../Core/Lua/LuaFunctionCall.h"
-#include "../Plugins/Engine/OrthancPlugins.h"
-#include "EmbeddedResourceHttpHandler.h"
-#include "OrthancConfiguration.h"
-#include "OrthancFindRequestHandler.h"
-#include "OrthancGetRequestHandler.h"
-#include "OrthancInitialization.h"
-#include "OrthancMoveRequestHandler.h"
-#include "ServerContext.h"
-#include "ServerJobs/StorageCommitmentScpJob.h"
-#include "ServerToolbox.h"
-#include "StorageCommitmentReports.h"
-
-using namespace Orthanc;
-
-
-class OrthancStoreRequestHandler : public IStoreRequestHandler
-{
-private:
-  ServerContext& context_;
-
-public:
-  OrthancStoreRequestHandler(ServerContext& context) :
-    context_(context)
-  {
-  }
-
-
-  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) 
-  {
-    if (dicomFile.size() > 0)
-    {
-      DicomInstanceToStore toStore;
-      toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol
-                        (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
-      toStore.SetBuffer(dicomFile.c_str(), dicomFile.size());
-      toStore.SetSummary(dicomSummary);
-      toStore.SetJson(dicomJson);
-
-      std::string id;
-      context_.Store(id, toStore, StoreInstanceMode_Default);
-    }
-  }
-};
-
-
-
-class OrthancStorageCommitmentRequestHandler : public IStorageCommitmentRequestHandler
-{
-private:
-  ServerContext& context_;
-  
-public:
-  OrthancStorageCommitmentRequestHandler(ServerContext& context) :
-    context_(context)
-  {
-  }
-
-  virtual void HandleRequest(const std::string& transactionUid,
-                             const std::vector<std::string>& referencedSopClassUids,
-                             const std::vector<std::string>& referencedSopInstanceUids,
-                             const std::string& remoteIp,
-                             const std::string& remoteAet,
-                             const std::string& calledAet)
-  {
-    if (referencedSopClassUids.size() != referencedSopInstanceUids.size())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    std::unique_ptr<StorageCommitmentScpJob> job(
-      new StorageCommitmentScpJob(context_, transactionUid, remoteAet, calledAet));
-
-    for (size_t i = 0; i < referencedSopClassUids.size(); i++)
-    {
-      job->AddInstance(referencedSopClassUids[i], referencedSopInstanceUids[i]);
-    }
-
-    job->MarkAsReady();
-
-    context_.GetJobsEngine().GetRegistry().Submit(job.release(), 0 /* default priority */);
-  }
-
-  virtual void HandleReport(const std::string& transactionUid,
-                            const std::vector<std::string>& successSopClassUids,
-                            const std::vector<std::string>& successSopInstanceUids,
-                            const std::vector<std::string>& failedSopClassUids,
-                            const std::vector<std::string>& failedSopInstanceUids,
-                            const std::vector<StorageCommitmentFailureReason>& failureReasons,
-                            const std::string& remoteIp,
-                            const std::string& remoteAet,
-                            const std::string& calledAet)
-  {
-    if (successSopClassUids.size() != successSopInstanceUids.size() ||
-        failedSopClassUids.size() != failedSopInstanceUids.size() ||
-        failedSopClassUids.size() != failureReasons.size())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    std::unique_ptr<StorageCommitmentReports::Report> report(
-      new StorageCommitmentReports::Report(remoteAet));
-
-    for (size_t i = 0; i < successSopClassUids.size(); i++)
-    {
-      report->AddSuccess(successSopClassUids[i], successSopInstanceUids[i]);
-    }
-
-    for (size_t i = 0; i < failedSopClassUids.size(); i++)
-    {
-      report->AddFailure(failedSopClassUids[i], failedSopInstanceUids[i], failureReasons[i]);
-    }
-
-    report->MarkAsComplete();
-
-    context_.GetStorageCommitmentReports().Store(transactionUid, report.release());
-  }
-};
-
-
-
-class ModalitiesFromConfiguration : public DicomServer::IRemoteModalities
-{
-public:
-  virtual bool IsSameAETitle(const std::string& aet1,
-                             const std::string& aet2) 
-  {
-    OrthancConfiguration::ReaderLock lock;
-    return lock.GetConfiguration().IsSameAETitle(aet1, aet2);
-  }
-
-  virtual bool LookupAETitle(RemoteModalityParameters& modality,
-                             const std::string& aet) 
-  {
-    OrthancConfiguration::ReaderLock lock;
-    return lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, aet);
-  }
-};
-
-
-class MyDicomServerFactory : 
-  public IStoreRequestHandlerFactory,
-  public IFindRequestHandlerFactory, 
-  public IMoveRequestHandlerFactory,
-  public IGetRequestHandlerFactory,
-  public IStorageCommitmentRequestHandlerFactory
-{
-private:
-  ServerContext& context_;
-
-public:
-  MyDicomServerFactory(ServerContext& context) : context_(context)
-  {
-  }
-
-  virtual IStoreRequestHandler* ConstructStoreRequestHandler()
-  {
-    return new OrthancStoreRequestHandler(context_);
-  }
-
-  virtual IFindRequestHandler* ConstructFindRequestHandler()
-  {
-    std::unique_ptr<OrthancFindRequestHandler> result(new OrthancFindRequestHandler(context_));
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      result->SetMaxResults(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindResults", 0));
-      result->SetMaxInstances(lock.GetConfiguration().GetUnsignedIntegerParameter("LimitFindInstances", 0));
-    }
-
-    if (result->GetMaxResults() == 0)
-    {
-      LOG(INFO) << "No limit on the number of C-FIND results at the Patient, Study and Series levels";
-    }
-    else
-    {
-      LOG(INFO) << "Maximum " << result->GetMaxResults() 
-                << " results for C-FIND queries at the Patient, Study and Series levels";
-    }
-
-    if (result->GetMaxInstances() == 0)
-    {
-      LOG(INFO) << "No limit on the number of C-FIND results at the Instance level";
-    }
-    else
-    {
-      LOG(INFO) << "Maximum " << result->GetMaxInstances() 
-                << " instances will be returned for C-FIND queries at the Instance level";
-    }
-
-    return result.release();
-  }
-
-  virtual IMoveRequestHandler* ConstructMoveRequestHandler()
-  {
-    return new OrthancMoveRequestHandler(context_);
-  }
-  
-  virtual IGetRequestHandler* ConstructGetRequestHandler()
-  {
-    return new OrthancGetRequestHandler(context_);
-  }
-  
-  virtual IStorageCommitmentRequestHandler* ConstructStorageCommitmentRequestHandler()
-  {
-    return new OrthancStorageCommitmentRequestHandler(context_);
-  }
-  
-
-  void Done()
-  {
-  }
-};
-
-
-class OrthancApplicationEntityFilter : public IApplicationEntityFilter
-{
-private:
-  ServerContext&  context_;
-  bool            alwaysAllowEcho_;
-  bool            alwaysAllowStore_;
-
-public:
-  OrthancApplicationEntityFilter(ServerContext& context) :
-    context_(context)
-  {
-    OrthancConfiguration::ReaderLock lock;
-    alwaysAllowEcho_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowEcho", true);
-    alwaysAllowStore_ = lock.GetConfiguration().GetBooleanParameter("DicomAlwaysAllowStore", true);
-  }
-
-  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;
-
-    if (alwaysAllowEcho_ ||
-        alwaysAllowStore_)
-    {
-      return true;
-    }
-    else
-    {
-      OrthancConfiguration::ReaderLock lock;
-      return lock.GetConfiguration().IsKnownAETitle(remoteAet, remoteIp);
-    }
-  }
-
-  virtual bool IsAllowedRequest(const std::string& remoteIp,
-                                const std::string& remoteAet,
-                                const std::string& calledAet,
-                                DicomRequestType type)
-  {
-    LOG(INFO) << "Incoming " << EnumerationToString(type) << " request from AET "
-              << remoteAet << " on IP " << remoteIp << ", calling AET " << calledAet;
-    
-    if (type == DicomRequestType_Echo &&
-        alwaysAllowEcho_)
-    {
-      // Incoming C-Echo requests are always accepted, even from unknown AET
-      return true;
-    }
-    else if (type == DicomRequestType_Store &&
-             alwaysAllowStore_)
-    {
-      // Incoming C-Store requests are always accepted, even from unknown AET
-      return true;
-    }
-    else
-    {
-      OrthancConfiguration::ReaderLock lock;
-
-      RemoteModalityParameters modality;
-      if (lock.GetConfiguration().LookupDicomModalityUsingAETitle(modality, remoteAet))
-      {
-        return modality.IsRequestAllowed(type);
-      }
-      else
-      {
-        return false;
-      }
-    }
-  }
-
-  virtual bool IsAllowedTransferSyntax(const std::string& remoteIp,
-                                       const std::string& remoteAet,
-                                       const std::string& calledAet,
-                                       TransferSyntax syntax)
-  {
-    std::string configuration;
-
-    switch (syntax)
-    {
-      case TransferSyntax_Deflated:
-        configuration = "DeflatedTransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_Jpeg:
-        configuration = "JpegTransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_Jpeg2000:
-        configuration = "Jpeg2000TransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_JpegLossless:
-        configuration = "JpegLosslessTransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_Jpip:
-        configuration = "JpipTransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_Mpeg2:
-        configuration = "Mpeg2TransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_Mpeg4:
-        configuration = "Mpeg4TransferSyntaxAccepted";
-        break;
-
-      case TransferSyntax_Rle:
-        configuration = "RleTransferSyntaxAccepted";
-        break;
-
-      default: 
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    {
-      std::string name = "Is" + configuration;
-
-      LuaScripting::Lock lock(context_.GetLuaScripting());
-      
-      if (lock.GetLua().IsExistingFunction(name.c_str()))
-      {
-        LuaFunctionCall call(lock.GetLua(), name.c_str());
-        call.PushString(remoteAet);
-        call.PushString(remoteIp);
-        call.PushString(calledAet);
-        return call.ExecutePredicate();
-      }
-    }
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      return lock.GetConfiguration().GetBooleanParameter(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();
-      }
-    }
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      return lock.GetConfiguration().GetBooleanParameter(configuration, false);
-    }
-  }
-};
-
-
-class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
-{
-private:
-  ServerContext&   context_;
-  OrthancPlugins*  plugins_;
-
-public:
-  MyIncomingHttpRequestFilter(ServerContext& context,
-                              OrthancPlugins* plugins) : 
-    context_(context),
-    plugins_(plugins)
-  {
-  }
-
-  virtual bool IsAllowed(HttpMethod method,
-                         const char* uri,
-                         const char* ip,
-                         const char* username,
-                         const IHttpHandler::Arguments& httpHeaders,
-                         const IHttpHandler::GetArguments& getArguments)
-  {
-#if ORTHANC_ENABLE_PLUGINS == 1
-    if (plugins_ != NULL &&
-        !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments))
-    {
-      return false;
-    }
-#endif
-
-    static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
-
-    LuaScripting::Lock lock(context_.GetLuaScripting());
-
-    // Test if the instance must be filtered out
-    if (lock.GetLua().IsExistingFunction(HTTP_FILTER))
-    {
-      LuaFunctionCall call(lock.GetLua(), HTTP_FILTER);
-
-      switch (method)
-      {
-        case HttpMethod_Get:
-          call.PushString("GET");
-          break;
-
-        case HttpMethod_Put:
-          call.PushString("PUT");
-          break;
-
-        case HttpMethod_Post:
-          call.PushString("POST");
-          break;
-
-        case HttpMethod_Delete:
-          call.PushString("DELETE");
-          break;
-
-        default:
-          return true;
-      }
-
-      call.PushString(uri);
-      call.PushString(ip);
-      call.PushString(username);
-      call.PushStringMap(httpHeaders);
-
-      if (!call.ExecutePredicate())
-      {
-        LOG(INFO) << "An incoming HTTP request has been discarded by the filter";
-        return false;
-      }
-    }
-
-    return true;
-  }
-};
-
-
-
-class MyHttpExceptionFormatter : public IHttpExceptionFormatter
-{
-private:
-  bool             describeErrors_;
-  OrthancPlugins*  plugins_;
-
-public:
-  MyHttpExceptionFormatter(bool describeErrors,
-                           OrthancPlugins* plugins) :
-    describeErrors_(describeErrors),
-    plugins_(plugins)
-  {
-  }
-
-  virtual void Format(HttpOutput& output,
-                      const OrthancException& exception,
-                      HttpMethod method,
-                      const char* uri)
-  {
-    {
-      bool isPlugin = false;
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-      if (plugins_ != NULL)
-      {
-        plugins_->GetErrorDictionary().LogError(exception.GetErrorCode(), true);
-        isPlugin = true;
-      }
-#endif
-
-      if (!isPlugin)
-      {
-        LOG(ERROR) << "Exception in the HTTP handler: " << exception.What();
-      }
-    }      
-
-    Json::Value message = Json::objectValue;
-    ErrorCode errorCode = exception.GetErrorCode();
-    HttpStatus httpStatus = exception.GetHttpStatus();
-
-    {
-      bool isPlugin = false;
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-      if (plugins_ != NULL &&
-          plugins_->GetErrorDictionary().Format(message, httpStatus, exception))
-      {
-        errorCode = ErrorCode_Plugin;
-        isPlugin = true;
-      }
-#endif
-
-      if (!isPlugin)
-      {
-        message["Message"] = exception.What();
-      }
-    }
-
-    if (!describeErrors_)
-    {
-      output.SendStatus(httpStatus);
-    }
-    else
-    {
-      message["Method"] = EnumerationToString(method);
-      message["Uri"] = uri;
-      message["HttpError"] = EnumerationToString(httpStatus);
-      message["HttpStatus"] = httpStatus;
-      message["OrthancError"] = EnumerationToString(errorCode);
-      message["OrthancStatus"] = errorCode;
-
-      if (exception.HasDetails())
-      {
-        message["Details"] = exception.GetDetails();
-      }
-
-      std::string info = message.toStyledString();
-      output.SendStatus(httpStatus, info);
-    }
-  }
-};
-
-
-
-static void PrintHelp(const char* path)
-{
-  std::cout 
-    << "Usage: " << path << " [OPTION]... [CONFIGURATION]" << std::endl
-    << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." << std::endl
-    << std::endl
-    << "The \"CONFIGURATION\" argument can be a single file or a directory. In the " << std::endl
-    << "case of a directory, all the JSON files it contains will be merged. " << std::endl
-    << "If no configuration path is given on the command line, a set of default " << std::endl
-    << "parameters is used. Please refer to the Orthanc homepage for the full " << std::endl
-    << "instructions about how to use Orthanc <http://www.orthanc-server.com/>." << std::endl
-    << std::endl
-    << "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(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
-    << "\t\t\t(if file is \"-\", dumps to stdout)" << std::endl
-    << "  --errors\t\tprint the supported error codes and exit" << std::endl
-    << "  --verbose\t\tbe verbose in logs" << std::endl
-    << "  --trace\t\thighest verbosity in logs (for debug)" << std::endl
-    << "  --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
-    << "   0 if success," << std::endl
-#if defined(_WIN32)
-    << "!= 0 if error (use the --errors option to get the list of possible errors)." << std::endl
-#else
-    << "  -1 if error (have a look at the logs)." << std::endl
-#endif
-    << std::endl;
-}
-
-
-static void PrintVersion(const char* path)
-{
-  std::cout
-    << path << " " << ORTHANC_VERSION << std::endl
-    << "Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics Department, University Hospital of Liege (Belgium)" << std::endl
-    << "Copyright (C) 2017-2020 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
-    << std::endl
-    << "Written by Sebastien Jodogne <s.jodogne@orthanc-labs.com>" << std::endl;
-}
-
-
-static void PrintErrorCode(ErrorCode code, const char* description)
-{
-  std::cout 
-    << std::right << std::setw(16) 
-    << static_cast<int>(code)
-    << "   " << description << std::endl;
-}
-
-
-static void PrintErrors(const char* path)
-{
-  std::cout
-    << path << " " << ORTHANC_VERSION << std::endl
-    << "Orthanc, lightweight, RESTful DICOM server for healthcare and medical research." 
-    << std::endl << std::endl
-    << "List of error codes that could be returned by Orthanc:" 
-    << std::endl << std::endl;
-
-  // The content of the following brackets is automatically generated
-  // by the "GenerateErrorCodes.py" script
-  {
-    PrintErrorCode(ErrorCode_InternalError, "Internal error");
-    PrintErrorCode(ErrorCode_Success, "Success");
-    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, "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");
-    PrintErrorCode(ErrorCode_BadRequest, "Bad request");
-    PrintErrorCode(ErrorCode_NetworkProtocol, "Error in the network protocol");
-    PrintErrorCode(ErrorCode_SystemCommand, "Error while calling a system command");
-    PrintErrorCode(ErrorCode_Database, "Error with the database engine");
-    PrintErrorCode(ErrorCode_UriSyntax, "Badly formatted URI");
-    PrintErrorCode(ErrorCode_InexistentFile, "Inexistent file");
-    PrintErrorCode(ErrorCode_CannotWriteFile, "Cannot write to file");
-    PrintErrorCode(ErrorCode_BadFileFormat, "Bad file format");
-    PrintErrorCode(ErrorCode_Timeout, "Timeout");
-    PrintErrorCode(ErrorCode_UnknownResource, "Unknown resource");
-    PrintErrorCode(ErrorCode_IncompatibleDatabaseVersion, "Incompatible version of the database");
-    PrintErrorCode(ErrorCode_FullStorage, "The file storage is full");
-    PrintErrorCode(ErrorCode_CorruptedFile, "Corrupted file (e.g. inconsistent MD5 hash)");
-    PrintErrorCode(ErrorCode_InexistentTag, "Inexistent tag");
-    PrintErrorCode(ErrorCode_ReadOnly, "Cannot modify a read-only data structure");
-    PrintErrorCode(ErrorCode_IncompatibleImageFormat, "Incompatible format of the images");
-    PrintErrorCode(ErrorCode_IncompatibleImageSize, "Incompatible size of the images");
-    PrintErrorCode(ErrorCode_SharedLibrary, "Error while using a shared library (plugin)");
-    PrintErrorCode(ErrorCode_UnknownPluginService, "Plugin invoking an unknown service");
-    PrintErrorCode(ErrorCode_UnknownDicomTag, "Unknown DICOM tag");
-    PrintErrorCode(ErrorCode_BadJson, "Cannot parse a JSON document");
-    PrintErrorCode(ErrorCode_Unauthorized, "Bad credentials were provided to an HTTP request");
-    PrintErrorCode(ErrorCode_BadFont, "Badly formatted font file");
-    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_BadGeometry, "Geometry error encountered in Stone");
-    PrintErrorCode(ErrorCode_SslInitialization, "Cannot initialize SSL encryption, check out your certificates");
-    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");
-    PrintErrorCode(ErrorCode_SQLiteStatementAlreadyUsed, "SQLite: This cached statement is already being referred to");
-    PrintErrorCode(ErrorCode_SQLiteExecute, "SQLite: Cannot execute a command");
-    PrintErrorCode(ErrorCode_SQLiteRollbackWithoutTransaction, "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)");
-    PrintErrorCode(ErrorCode_SQLiteCommitWithoutTransaction, "SQLite: Committing a nonexistent transaction");
-    PrintErrorCode(ErrorCode_SQLiteRegisterFunction, "SQLite: Unable to register a function");
-    PrintErrorCode(ErrorCode_SQLiteFlush, "SQLite: Unable to flush the database");
-    PrintErrorCode(ErrorCode_SQLiteCannotRun, "SQLite: Cannot run a cached statement");
-    PrintErrorCode(ErrorCode_SQLiteCannotStep, "SQLite: Cannot step over a cached statement");
-    PrintErrorCode(ErrorCode_SQLiteBindOutOfRange, "SQLite: Bing a value while out of range (serious error)");
-    PrintErrorCode(ErrorCode_SQLitePrepareStatement, "SQLite: Cannot prepare a cached statement");
-    PrintErrorCode(ErrorCode_SQLiteTransactionAlreadyStarted, "SQLite: Beginning the same transaction twice");
-    PrintErrorCode(ErrorCode_SQLiteTransactionCommit, "SQLite: Failure when committing the transaction");
-    PrintErrorCode(ErrorCode_SQLiteTransactionBegin, "SQLite: Cannot start a transaction");
-    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 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");
-    PrintErrorCode(ErrorCode_MakeDirectory, "Cannot create a directory");
-    PrintErrorCode(ErrorCode_BadApplicationEntityTitle, "An application entity title (AET) cannot be empty or be longer than 16 characters");
-    PrintErrorCode(ErrorCode_NoCFindHandler, "No request handler factory for DICOM C-FIND SCP");
-    PrintErrorCode(ErrorCode_NoCMoveHandler, "No request handler factory for DICOM C-MOVE SCP");
-    PrintErrorCode(ErrorCode_NoCStoreHandler, "No request handler factory for DICOM C-STORE SCP");
-    PrintErrorCode(ErrorCode_NoApplicationEntityFilter, "No application entity filter");
-    PrintErrorCode(ErrorCode_NoSopClassOrInstance, "DicomUserConnection: Unable to find the SOP class and instance");
-    PrintErrorCode(ErrorCode_NoPresentationContext, "DicomUserConnection: No acceptable presentation context for modality");
-    PrintErrorCode(ErrorCode_DicomFindUnavailable, "DicomUserConnection: The C-FIND command is not supported by the remote SCP");
-    PrintErrorCode(ErrorCode_DicomMoveUnavailable, "DicomUserConnection: The C-MOVE command is not supported by the remote SCP");
-    PrintErrorCode(ErrorCode_CannotStoreInstance, "Cannot store an instance");
-    PrintErrorCode(ErrorCode_CreateDicomNotString, "Only string values are supported when creating DICOM instances");
-    PrintErrorCode(ErrorCode_CreateDicomOverrideTag, "Trying to override a value inherited from a parent module");
-    PrintErrorCode(ErrorCode_CreateDicomUseContent, "Use \"Content\" to inject an image into a new DICOM instance");
-    PrintErrorCode(ErrorCode_CreateDicomNoPayload, "No payload is present for one instance in the series");
-    PrintErrorCode(ErrorCode_CreateDicomUseDataUriScheme, "The payload of the DICOM instance must be specified according to Data URI scheme");
-    PrintErrorCode(ErrorCode_CreateDicomBadParent, "Trying to attach a new DICOM instance to an inexistent resource");
-    PrintErrorCode(ErrorCode_CreateDicomParentIsInstance, "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)");
-    PrintErrorCode(ErrorCode_CreateDicomParentEncoding, "Unable to get the encoding of the parent resource");
-    PrintErrorCode(ErrorCode_UnknownModality, "Unknown modality");
-    PrintErrorCode(ErrorCode_BadJobOrdering, "Bad ordering of filters in a job");
-    PrintErrorCode(ErrorCode_JsonToLuaTable, "Cannot convert the given JSON object to a Lua table");
-    PrintErrorCode(ErrorCode_CannotCreateLua, "Cannot create the Lua context");
-    PrintErrorCode(ErrorCode_CannotExecuteLua, "Cannot execute a Lua command");
-    PrintErrorCode(ErrorCode_LuaAlreadyExecuted, "Arguments cannot be pushed after the Lua function is executed");
-    PrintErrorCode(ErrorCode_LuaBadOutput, "The Lua function does not give the expected number of outputs");
-    PrintErrorCode(ErrorCode_NotLuaPredicate, "The Lua function is not a predicate (only true/false outputs allowed)");
-    PrintErrorCode(ErrorCode_LuaReturnsNoString, "The Lua function does not return a string");
-    PrintErrorCode(ErrorCode_StorageAreaAlreadyRegistered, "Another plugin has already registered a custom storage area");
-    PrintErrorCode(ErrorCode_DatabaseBackendAlreadyRegistered, "Another plugin has already registered a custom database back-end");
-    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");
-    PrintErrorCode(ErrorCode_NoCGetHandler, "No request handler factory for DICOM C-GET SCP");
-    PrintErrorCode(ErrorCode_NoStorageCommitmentHandler, "No request handler factory for DICOM N-ACTION SCP (storage commitment)");
-    PrintErrorCode(ErrorCode_UnsupportedMediaType, "Unsupported media type");
-  }
-
-  std::cout << std::endl;
-}
-
-
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-static void LoadPlugins(OrthancPlugins& plugins)
-{
-  std::list<std::string> path;
-
-  {
-    OrthancConfiguration::ReaderLock lock;
-    lock.GetConfiguration().GetListOfStringsParameter(path, "Plugins");
-  }
-
-  for (std::list<std::string>::const_iterator
-         it = path.begin(); it != path.end(); ++it)
-  {
-    std::string path;
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      path = lock.GetConfiguration().InterpretStringParameterAsPath(*it);
-    }
-
-    LOG(WARNING) << "Loading plugin(s) from: " << path;
-    plugins.GetManager().RegisterPlugin(path);
-  }  
-}
-#endif
-
-
-
-// Returns "true" if restart is required
-static bool WaitForExit(ServerContext& context,
-                        OrthancRestApi& restApi)
-{
-  LOG(WARNING) << "Orthanc has started";
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-  if (context.HasPlugins())
-  {
-    context.GetPlugins().SignalOrthancStarted();
-  }
-#endif
-
-  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
-
-      OrthancConfiguration::ReaderLock lock;
-      if (lock.GetConfiguration().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 on the disk";
-        Logging::Flush();
-        continue;
-      }
-    }
-    else
-    {
-      break;
-    }
-  }
-
-  context.GetLuaScripting().Execute("Finalize");
-  context.GetLuaScripting().Stop();
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-  if (context.HasPlugins())
-  {
-    context.GetPlugins().SignalOrthancStopped();
-  }
-#endif
-
-  if (restart)
-  {
-    LOG(WARNING) << "Reset request received, restarting Orthanc";
-  }
-
-  // We're done
-  LOG(WARNING) << "Orthanc is stopping";
-
-  return restart;
-}
-
-
-
-static bool StartHttpServer(ServerContext& context,
-                            OrthancRestApi& restApi,
-                            OrthancPlugins* plugins)
-{
-  bool httpServerEnabled;
-
-  {
-    OrthancConfiguration::ReaderLock lock;
-    httpServerEnabled = lock.GetConfiguration().GetBooleanParameter("HttpServerEnabled", true);
-  }
-
-  if (!httpServerEnabled)
-  {
-    LOG(WARNING) << "The HTTP server is disabled";
-    return WaitForExit(context, restApi);
-  }
-  else
-  {
-    MyIncomingHttpRequestFilter httpFilter(context, plugins);
-    HttpServer httpServer;
-    bool httpDescribeErrors;
-
-#if ORTHANC_ENABLE_MONGOOSE == 1
-    const bool defaultKeepAlive = false;
-#elif ORTHANC_ENABLE_CIVETWEB == 1
-    const bool defaultKeepAlive = true;
-#else
-#  error "Either Mongoose or Civetweb must be enabled to compile this file"
-#endif
-  
-    {
-      OrthancConfiguration::ReaderLock lock;
-      
-      httpDescribeErrors = lock.GetConfiguration().GetBooleanParameter("HttpDescribeErrors", true);
-  
-      // HTTP server
-      httpServer.SetThreadsCount(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpThreadsCount", 50));
-      httpServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpPort", 8042));
-      httpServer.SetRemoteAccessAllowed(lock.GetConfiguration().GetBooleanParameter("RemoteAccessAllowed", false));
-      httpServer.SetKeepAliveEnabled(lock.GetConfiguration().GetBooleanParameter("KeepAlive", defaultKeepAlive));
-      httpServer.SetHttpCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("HttpCompressionEnabled", true));
-      httpServer.SetTcpNoDelay(lock.GetConfiguration().GetBooleanParameter("TcpNoDelay", true));
-      httpServer.SetRequestTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpRequestTimeout", 30));
-
-      // Let's assume that the HTTP server is secure
-      context.SetHttpServerSecure(true);
-
-      bool authenticationEnabled;
-      if (lock.GetConfiguration().LookupBooleanParameter(authenticationEnabled, "AuthenticationEnabled"))
-      {
-        httpServer.SetAuthenticationEnabled(authenticationEnabled);
-
-        if (httpServer.IsRemoteAccessAllowed() &&
-            !authenticationEnabled)
-        {
-          LOG(WARNING) << "====> Remote access is enabled while user authentication is explicitly disabled, "
-                       << "your setup is POSSIBLY INSECURE <====";
-          context.SetHttpServerSecure(false);
-        }
-      }
-      else if (httpServer.IsRemoteAccessAllowed())
-      {
-        // Starting with Orthanc 1.5.8, it is impossible to enable
-        // remote access without having explicitly disabled user
-        // authentication.
-        LOG(WARNING) << "Remote access is allowed but \"AuthenticationEnabled\" is not in the configuration, "
-                     << "automatically enabling HTTP authentication for security";          
-        httpServer.SetAuthenticationEnabled(true);
-      }
-      else
-      {
-        // If Orthanc only listens on the localhost, it is OK to have
-        // "AuthenticationEnabled" disabled
-        httpServer.SetAuthenticationEnabled(false);
-      }
-
-      bool hasUsers = lock.GetConfiguration().SetupRegisteredUsers(httpServer);
-
-      if (httpServer.IsAuthenticationEnabled() &&
-          !hasUsers)
-      {
-        if (httpServer.IsRemoteAccessAllowed())
-        {
-          /**
-           * Starting with Orthanc 1.5.8, if no user is explicitly
-           * defined while remote access is allowed, we create a
-           * default user, and Orthanc Explorer shows a warning
-           * message about an "Insecure setup". This convention is
-           * used in Docker images "jodogne/orthanc",
-           * "jodogne/orthanc-plugins" and "osimis/orthanc".
-           **/
-          LOG(WARNING) << "====> HTTP authentication is enabled, but no user is declared. "
-                       << "Creating a default user: Review your configuration option \"RegisteredUsers\". "
-                       << "Your setup is INSECURE <====";
-
-          context.SetHttpServerSecure(false);
-
-          // This is the username/password of the default user in Orthanc.
-          httpServer.RegisterUser("orthanc", "orthanc");
-        }
-        else
-        {
-          LOG(WARNING) << "HTTP authentication is enabled, but no user is declared, "
-                       << "check the value of configuration option \"RegisteredUsers\"";
-        }
-      }
-      
-      if (lock.GetConfiguration().GetBooleanParameter("SslEnabled", false))
-      {
-        std::string certificate = lock.GetConfiguration().InterpretStringParameterAsPath(
-          lock.GetConfiguration().GetStringParameter("SslCertificate", "certificate.pem"));
-        httpServer.SetSslEnabled(true);
-        httpServer.SetSslCertificate(certificate.c_str());
-      }
-      else
-      {
-        httpServer.SetSslEnabled(false);
-      }
-
-      if (lock.GetConfiguration().GetBooleanParameter("ExecuteLuaEnabled", false))
-      {
-        context.SetExecuteLuaEnabled(true);
-        LOG(WARNING) << "====> Remote LUA script execution is enabled.  Review your configuration option \"ExecuteLuaEnabled\". "
-                     << "Your setup is POSSIBLY INSECURE <====";
-      }
-      else
-      {
-        context.SetExecuteLuaEnabled(false);
-        LOG(WARNING) << "Remote LUA script execution is disabled";
-      }
-    }
-
-    MyHttpExceptionFormatter exceptionFormatter(httpDescribeErrors, plugins);
-        
-    httpServer.SetIncomingHttpRequestFilter(httpFilter);
-    httpServer.SetHttpExceptionFormatter(exceptionFormatter);
-    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();
-  
-    bool restart = WaitForExit(context, restApi);
-
-    httpServer.Stop();
-    LOG(WARNING) << "    HTTP server has stopped";
-
-    return restart;
-  }
-}
-
-
-static bool StartDicomServer(ServerContext& context,
-                             OrthancRestApi& restApi,
-                             OrthancPlugins* plugins)
-{
-  bool dicomServerEnabled;
-
-  {
-    OrthancConfiguration::ReaderLock lock;
-    dicomServerEnabled = lock.GetConfiguration().GetBooleanParameter("DicomServerEnabled", true);
-  }
-
-  if (!dicomServerEnabled)
-  {
-    LOG(WARNING) << "The DICOM server is disabled";
-    return StartHttpServer(context, restApi, plugins);
-  }
-  else
-  {
-    MyDicomServerFactory serverFactory(context);
-    OrthancApplicationEntityFilter dicomFilter(context);
-    ModalitiesFromConfiguration modalities;
-  
-    // Setup the DICOM server  
-    DicomServer dicomServer;
-    dicomServer.SetRemoteModalities(modalities);
-    dicomServer.SetStoreRequestHandlerFactory(serverFactory);
-    dicomServer.SetMoveRequestHandlerFactory(serverFactory);
-    dicomServer.SetGetRequestHandlerFactory(serverFactory);
-    dicomServer.SetFindRequestHandlerFactory(serverFactory);
-    dicomServer.SetStorageCommitmentRequestHandlerFactory(serverFactory);
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      dicomServer.SetCalledApplicationEntityTitleCheck(lock.GetConfiguration().GetBooleanParameter("DicomCheckCalledAet", false));
-      dicomServer.SetAssociationTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScpTimeout", 30));
-      dicomServer.SetPortNumber(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomPort", 4242));
-      dicomServer.SetApplicationEntityTitle(lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"));
-    }
-
-#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.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 with AET " << dicomServer.GetApplicationEntityTitle() 
-                 << " on port: " << dicomServer.GetPortNumber();
-
-    bool restart = false;
-    ErrorCode error = ErrorCode_Success;
-
-    try
-    {
-      restart = StartHttpServer(context, restApi, plugins);
-    }
-    catch (OrthancException& e)
-    {
-      error = e.GetErrorCode();
-    }
-
-    dicomServer.Stop();
-    LOG(WARNING) << "    DICOM server has stopped";
-
-    serverFactory.Done();
-
-    if (error != ErrorCode_Success)
-    {
-      throw OrthancException(error);
-    }
-
-    return restart;
-  }
-}
-
-
-static bool ConfigureHttpHandler(ServerContext& context,
-                                 OrthancPlugins *plugins,
-                                 bool loadJobsFromDatabase)
-{
-#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)
-  {
-    assert(context.HasPlugins());
-    context.GetHttpHandler().Register(*plugins, false);
-  }
-#endif
-  
-  // Secondly, apply the "static resources" layer
-#if ORTHANC_STANDALONE == 1
-  EmbeddedResourceHttpHandler staticResources("/app", ServerResources::ORTHANC_EXPLORER);
-#else
-  FilesystemHttpHandler staticResources("/app", ORTHANC_PATH "/OrthancExplorer");
-#endif
-
-  context.GetHttpHandler().Register(staticResources, false);
-
-  // Thirdly, consider the built-in REST API of Orthanc
-  OrthancRestApi restApi(context);
-  context.GetHttpHandler().Register(restApi, true);
-
-  context.SetupJobsEngine(false /* not running unit tests */, loadJobsFromDatabase);
-
-  bool restart = StartDicomServer(context, restApi, plugins);
-
-  context.Stop();
-
-  return restart;
-}
-
-
-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)
-  {
-    LOG(WARNING) << "No upgrade is needed, start Orthanc without the \"--upgrade\" argument";
-    return;
-  }
-
-  if (currentVersion > ORTHANC_DATABASE_VERSION)
-  {
-    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
-                           "The version of the database schema (" +
-                           boost::lexical_cast<std::string>(currentVersion) +
-                           ") is too recent for this version of Orthanc. Please upgrade Orthanc.");
-  }
-
-  LOG(WARNING) << "Upgrading the database from schema version "
-               << currentVersion << " to " << ORTHANC_DATABASE_VERSION;
-
-  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)
-  {
-    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
-                           "The database schema was not properly upgraded, it is still at version " +
-                           boost::lexical_cast<std::string>(currentVersion));
-  }
-  else
-  {
-    LOG(WARNING) << "The database schema was successfully upgraded, "
-                 << "you can now start Orthanc without the \"--upgrade\" argument";
-  }
-}
-
-
-
-namespace
-{
-  class ServerContextConfigurator : public boost::noncopyable
-  {
-  private:
-    ServerContext&   context_;
-    OrthancPlugins*  plugins_;
-
-  public:
-    ServerContextConfigurator(ServerContext& context,
-                              OrthancPlugins* plugins) :
-      context_(context),
-      plugins_(plugins)
-    {
-      {
-        OrthancConfiguration::WriterLock lock;
-        lock.GetConfiguration().SetServerIndex(context.GetIndex());
-      }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-      if (plugins_ != NULL)
-      {
-        plugins_->SetServerContext(context_);
-        context_.SetPlugins(*plugins_);
-      }
-#endif
-    }
-
-    ~ServerContextConfigurator()
-    {
-      {
-        OrthancConfiguration::WriterLock lock;
-        lock.GetConfiguration().ResetServerIndex();
-      }
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-      if (plugins_ != NULL)
-      {
-        plugins_->ResetServerContext();
-        context_.ResetPlugins();
-      }
-#endif
-    }
-  };
-}
-
-
-static bool ConfigureServerContext(IDatabaseWrapper& database,
-                                   IStorageArea& storageArea,
-                                   OrthancPlugins *plugins,
-                                   bool loadJobsFromDatabase)
-{
-  size_t maxCompletedJobs;
-  
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    // 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(lock.GetConfiguration().GetBooleanParameter("HttpsVerifyPeers", true),
-                             lock.GetConfiguration().InterpretStringParameterAsPath
-                             (lock.GetConfiguration().GetStringParameter("HttpsCACertificates", "")));
-    HttpClient::SetDefaultVerbose(lock.GetConfiguration().GetBooleanParameter("HttpVerbose", false));
-
-    // The value "0" below makes the class HttpClient use its default
-    // value (DEFAULT_HTTP_TIMEOUT = 60 seconds in Orthanc 1.5.7)
-    HttpClient::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("HttpTimeout", 0));
-    
-    HttpClient::SetDefaultProxy(lock.GetConfiguration().GetStringParameter("HttpProxy", ""));
-    
-    DicomAssociationParameters::SetDefaultTimeout(lock.GetConfiguration().GetUnsignedIntegerParameter("DicomScuTimeout", 10));
-
-    maxCompletedJobs = lock.GetConfiguration().GetUnsignedIntegerParameter("JobsHistorySize", 10);
-
-    if (maxCompletedJobs == 0)
-    {
-      LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended";
-    }
-  }
-  
-  ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);
-
-  {
-    OrthancConfiguration::ReaderLock lock;
-
-    context.SetCompressionEnabled(lock.GetConfiguration().GetBooleanParameter("StorageCompression", false));
-    context.SetStoreMD5ForAttachments(lock.GetConfiguration().GetBooleanParameter("StoreMD5ForAttachments", true));
-
-    // New option in Orthanc 1.4.2
-    context.SetOverwriteInstances(lock.GetConfiguration().GetBooleanParameter("OverwriteInstances", false));
-
-    try
-    {
-      context.GetIndex().SetMaximumPatientCount(lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumPatientCount", 0));
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumPatientCount(0);
-    }
-
-    try
-    {
-      uint64_t size = lock.GetConfiguration().GetUnsignedIntegerParameter("MaximumStorageSize", 0);
-      context.GetIndex().SetMaximumStorageSize(size * 1024 * 1024);
-    }
-    catch (...)
-    {
-      context.GetIndex().SetMaximumStorageSize(0);
-    }
-  }
-
-  {
-    ServerContextConfigurator configurator(context, plugins);
-
-    {
-      OrthancConfiguration::WriterLock lock;
-      lock.GetConfiguration().LoadModalitiesAndPeers();
-    }
-
-    return ConfigureHttpHandler(context, plugins, loadJobsFromDatabase);
-  }
-}
-
-
-static bool ConfigureDatabase(IDatabaseWrapper& database,
-                              IStorageArea& storageArea,
-                              OrthancPlugins *plugins,
-                              bool upgradeDatabase,
-                              bool loadJobsFromDatabase)
-{
-  database.Open();
-
-  unsigned int currentVersion = database.GetDatabaseVersion();
-
-  if (upgradeDatabase)
-  {
-    UpgradeDatabase(database, storageArea);
-    return false;  // Stop and don't restart Orthanc (cf. issue 29)
-  }
-  else if (currentVersion != ORTHANC_DATABASE_VERSION)
-  {
-    throw OrthancException(ErrorCode_IncompatibleDatabaseVersion,
-                           "The database schema must be upgraded from version " +
-                           boost::lexical_cast<std::string>(currentVersion) + " to " +
-                           boost::lexical_cast<std::string>(ORTHANC_DATABASE_VERSION) +
-                           ": Please run Orthanc with the \"--upgrade\" argument");
-  }
-
-  bool success = ConfigureServerContext
-    (database, storageArea, plugins, loadJobsFromDatabase);
-
-  database.Close();
-
-  return success;
-}
-
-
-static bool ConfigurePlugins(int argc, 
-                             char* argv[],
-                             bool upgradeDatabase,
-                             bool loadJobsFromDatabase)
-{
-  std::unique_ptr<IDatabaseWrapper>  databasePtr;
-  std::unique_ptr<IStorageArea>  storage;
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-  OrthancPlugins plugins;
-  plugins.SetCommandLineArguments(argc, argv);
-  LoadPlugins(plugins);
-
-  IDatabaseWrapper* database = NULL;
-  if (plugins.HasDatabaseBackend())
-  {
-    LOG(WARNING) << "Using a custom database from plugins";
-    database = &plugins.GetDatabaseBackend();
-  }
-  else
-  {
-    databasePtr.reset(CreateDatabaseWrapper());
-    database = databasePtr.get();
-  }
-
-  if (plugins.HasStorageArea())
-  {
-    LOG(WARNING) << "Using a custom storage area from plugins";
-    storage.reset(plugins.CreateStorageArea());
-  }
-  else
-  {
-    storage.reset(CreateStorageArea());
-  }
-
-  assert(database != NULL);
-  assert(storage.get() != NULL);
-
-  return ConfigureDatabase(*database, *storage, &plugins,
-                           upgradeDatabase, loadJobsFromDatabase);
-
-#elif ORTHANC_ENABLE_PLUGINS == 0
-  // The plugins are disabled
-
-  databasePtr.reset(CreateDatabaseWrapper());
-  storage.reset(CreateStorageArea());
-
-  assert(databasePtr.get() != NULL);
-  assert(storage.get() != NULL);
-
-  return ConfigureDatabase(*databasePtr, *storage, NULL,
-                           upgradeDatabase, loadJobsFromDatabase);
-
-#else
-#  error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
-#endif
-}
-
-
-static bool StartOrthanc(int argc, 
-                         char* argv[],
-                         bool upgradeDatabase,
-                         bool loadJobsFromDatabase)
-{
-  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;
-}
-
-
-int main(int argc, char* argv[]) 
-{
-  Logging::Initialize();
-
-  bool upgradeDatabase = false;
-  bool loadJobsFromDatabase = true;
-  const char* configurationFile = NULL;
-
-
-  /**
-   * Parse the command-line options.
-   **/ 
-
-  for (int i = 1; i < argc; i++)
-  {
-    std::string argument(argv[i]); 
-
-    if (argument.empty())
-    {
-      // Ignore empty arguments
-    }
-    else if (argument[0] != '-')
-    {
-      if (configurationFile != NULL)
-      {
-        LOG(ERROR) << "More than one configuration path were provided on the command line, aborting";
-        return -1;
-      }
-      else
-      {
-        // Use the first argument that does not start with a "-" as
-        // the configuration file
-
-        // TODO WHAT IS THE ENCODING?
-        configurationFile = argv[i];
-      }
-    }
-    else if (argument == "--errors")
-    {
-      PrintErrors(argv[0]);
-      return 0;
-    }
-    else if (argument == "--help")
-    {
-      PrintHelp(argv[0]);
-      return 0;
-    }
-    else if (argument == "--version")
-    {
-      PrintVersion(argv[0]);
-      return 0;
-    }
-    else if (argument == "--verbose")
-    {
-      Logging::EnableInfoLevel(true);
-    }
-    else if (argument == "--trace")
-    {
-      Logging::EnableTraceLevel(true);
-    }
-    else if (boost::starts_with(argument, "--logdir="))
-    {
-      // TODO WHAT IS THE ENCODING?
-      std::string directory = argument.substr(9);
-
-      try
-      {
-        Logging::SetTargetFolder(directory);
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "The directory where to store the log files (" 
-                   << directory << ") is inexistent, aborting.";
-        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")
-    {
-      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, ServerResources::CONFIGURATION_SAMPLE);
-
-#if defined(_WIN32)
-      // Replace UNIX newlines with DOS newlines 
-      boost::replace_all(configurationSample, "\n", "\r\n");
-#endif
-
-      std::string target = argument.substr(9);
-
-      try
-      {
-        if (target == "-")
-        {
-          // New in 1.5.8: Print to stdout
-          std::cout << configurationSample;
-        }
-        else
-        {
-          SystemToolbox::WriteFile(configurationSample, target);
-        }
-        return 0;
-      }
-      catch (OrthancException&)
-      {
-        LOG(ERROR) << "Cannot write sample configuration as file \"" << target << "\"";
-        return -1;
-      }
-    }
-    else
-    {
-      LOG(WARNING) << "Option unsupported by the core of Orthanc: " << argument;
-    }
-  }
-
-
-  /**
-   * Launch Orthanc.
-   **/
-
-  {
-    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());
-
-    std::string s = "Architecture: ";
-    if (sizeof(void*) == 4)
-    {
-      s += "32-bit, ";
-    }
-    else if (sizeof(void*) == 8)
-    {
-      s += "64-bit, ";
-    }
-    else
-    {
-      s += "unsupported pointer size, ";
-    }
-
-    switch (Toolbox::DetectEndianness())
-    {
-      case Endianness_Little:
-        s += "little endian";
-        break;
-      
-      case Endianness_Big:
-        s += "big endian";
-        break;
-      
-      default:
-        s += "unsupported endianness";
-        break;
-    }
-    
-    LOG(INFO) << s;
-  }
-
-  int status = 0;
-  try
-  {
-    for (;;)
-    {
-      OrthancInitialize(configurationFile);
-
-      bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase);
-      if (restart)
-      {
-        OrthancFinalize();
-        LOG(WARNING) << "Logging system is resetting";
-        Logging::Reset();
-      }
-      else
-      {
-        break;
-      }
-    }
-  }
-  catch (const OrthancException& e)
-  {
-    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.What() << "] (code " << e.GetErrorCode() << ")";
-#if defined(_WIN32)
-    if (e.GetErrorCode() >= ErrorCode_START_PLUGINS)
-    {
-      status = static_cast<int>(ErrorCode_Plugin);
-    }
-    else
-    {
-      status = static_cast<int>(e.GetErrorCode());
-    }
-
-#else
-    status = -1;
-#endif
-  }
-  catch (const std::exception& e) 
-  {
-    LOG(ERROR) << "Uncaught exception, stopping now: [" << e.what() << "]";
-    status = -1;
-  }
-  catch (const std::string& s) 
-  {
-    LOG(ERROR) << "Uncaught exception, stopping now: [" << s << "]";
-    status = -1;
-  }
-  catch (...)
-  {
-    LOG(ERROR) << "Native exception, stopping now. Check your plugins, if any.";
-    status = -1;
-  }
-
-  LOG(WARNING) << "Orthanc has stopped";
-
-  OrthancFinalize();
-
-  return status;
-}
--- a/Plugins/Engine/IPluginServiceProvider.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Include/orthanc/OrthancCPlugin.h"
-
-#include "../../Core/SharedLibrary.h"
-
-namespace Orthanc
-{
-  class IPluginServiceProvider : boost::noncopyable
-  {
-  public:
-    virtual ~IPluginServiceProvider()
-    {
-    }
-
-    virtual bool InvokeService(SharedLibrary& plugin,
-                               _OrthancPluginService service,
-                               const void* parameters) = 0;
-  };
-}
-
-#endif
--- a/Plugins/Engine/OrthancPluginDatabase.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1534 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancPluginDatabase.h"
-
-#if ORTHANC_ENABLE_PLUGINS != 1
-#error The plugin support is disabled
-#endif
-
-
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "PluginsEnumerations.h"
-
-#include <cassert>
-
-namespace Orthanc
-{
-  class OrthancPluginDatabase::Transaction : public IDatabaseWrapper::ITransaction
-  {
-  private:
-    OrthancPluginDatabase&  that_;
-
-    void CheckSuccess(OrthancPluginErrorCode code) const
-    {
-      if (code != OrthancPluginErrorCode_Success)
-      {
-        that_.errorDictionary_.LogError(code, true);
-        throw OrthancException(static_cast<ErrorCode>(code));
-      }
-    }
-
-  public:
-    Transaction(OrthancPluginDatabase& that) :
-    that_(that)
-    {
-    }
-
-    virtual void Begin()
-    {
-      CheckSuccess(that_.backend_.startTransaction(that_.payload_));
-    }
-
-    virtual void Rollback()
-    {
-      CheckSuccess(that_.backend_.rollbackTransaction(that_.payload_));
-    }
-
-    virtual void Commit(int64_t diskSizeDelta)
-    {
-      if (that_.fastGetTotalSize_)
-      {
-        CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
-      }
-      else
-      {
-        if (static_cast<int64_t>(that_.currentDiskSize_) + diskSizeDelta < 0)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        uint64_t newDiskSize = (that_.currentDiskSize_ + diskSizeDelta);
-
-        assert(newDiskSize == that_.GetTotalCompressedSize());
-
-        CheckSuccess(that_.backend_.commitTransaction(that_.payload_));
-
-        // The transaction has succeeded, we can commit the new disk size
-        that_.currentDiskSize_ = newDiskSize;
-      }
-    }
-  };
-
-
-  static FileInfo Convert(const OrthancPluginAttachment& attachment)
-  {
-    return FileInfo(attachment.uuid,
-                    static_cast<FileContentType>(attachment.contentType),
-                    attachment.uncompressedSize,
-                    attachment.uncompressedHash,
-                    static_cast<CompressionType>(attachment.compressionType),
-                    attachment.compressedSize,
-                    attachment.compressedHash);
-  }
-
-
-  void OrthancPluginDatabase::CheckSuccess(OrthancPluginErrorCode code)
-  {
-    if (code != OrthancPluginErrorCode_Success)
-    {
-      errorDictionary_.LogError(code, true);
-      throw OrthancException(static_cast<ErrorCode>(code));
-    }
-  }
-
-
-  void OrthancPluginDatabase::ResetAnswers()
-  {
-    type_ = _OrthancPluginDatabaseAnswerType_None;
-
-    answerDicomMap_ = NULL;
-    answerChanges_ = NULL;
-    answerExportedResources_ = NULL;
-    answerDone_ = NULL;
-    answerMatchingResources_ = NULL;
-    answerMatchingInstances_ = NULL;
-    answerMetadata_ = NULL;
-  }
-
-
-  void OrthancPluginDatabase::ForwardAnswers(std::list<int64_t>& target)
-  {
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_Int64)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_Int64)
-    {
-      for (std::list<int64_t>::const_iterator 
-             it = answerInt64_.begin(); it != answerInt64_.end(); ++it)
-      {
-        target.push_back(*it);
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::ForwardAnswers(std::list<std::string>& target)
-  {
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_String)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_String)
-    {
-      for (std::list<std::string>::const_iterator 
-             it = answerStrings_.begin(); it != answerStrings_.end(); ++it)
-      {
-        target.push_back(*it);
-      }
-    }
-  }
-
-
-  bool OrthancPluginDatabase::ForwardSingleAnswer(std::string& target)
-  {
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_String &&
-             answerStrings_.size() == 1)
-    {
-      target = answerStrings_.front();
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  bool OrthancPluginDatabase::ForwardSingleAnswer(int64_t& target)
-  {
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_Int64 &&
-             answerInt64_.size() == 1)
-    {
-      target = answerInt64_.front();
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  OrthancPluginDatabase::OrthancPluginDatabase(SharedLibrary& library,
-                                               PluginsErrorDictionary&  errorDictionary,
-                                               const OrthancPluginDatabaseBackend& backend,
-                                               const OrthancPluginDatabaseExtensions* extensions,
-                                               size_t extensionsSize,
-                                               void *payload) : 
-    library_(library),
-    errorDictionary_(errorDictionary),
-    backend_(backend),
-    payload_(payload),
-    listener_(NULL)
-  {
-    static const char* const MISSING = "  Missing extension in database index plugin: ";
-    
-    ResetAnswers();
-
-    memset(&extensions_, 0, sizeof(extensions_));
-
-    size_t size = sizeof(extensions_);
-    if (extensionsSize < size)
-    {
-      size = extensionsSize;  // Not all the extensions are available
-    }
-
-    memcpy(&extensions_, extensions, size);
-
-    bool isOptimal = true;
-
-    if (extensions_.lookupResources == NULL)
-    {
-      LOG(INFO) << MISSING << "LookupIdentifierRange()";
-      isOptimal = false;
-    }
-
-    if (extensions_.createInstance == NULL)
-    {
-      LOG(INFO) << MISSING << "CreateInstance()";
-      isOptimal = false;
-    }
-
-    if (extensions_.setResourcesContent == NULL)
-    {
-      LOG(INFO) << MISSING << "SetResourcesContent()";
-      isOptimal = false;
-    }
-
-    if (extensions_.getChildrenMetadata == NULL)
-    {
-      LOG(INFO) << MISSING << "GetChildrenMetadata()";
-      isOptimal = false;
-    }
-
-    if (extensions_.getAllMetadata == NULL)
-    {
-      LOG(INFO) << MISSING << "GetAllMetadata()";
-      isOptimal = false;
-    }
-
-    if (extensions_.lookupResourceAndParent == NULL)
-    {
-      LOG(INFO) << MISSING << "LookupResourceAndParent()";
-      isOptimal = false;
-    }
-
-    if (isOptimal)
-    {
-      LOG(INFO) << "The performance of the database index plugin "
-                << "is optimal for this version of Orthanc";
-    }
-    else
-    {
-      LOG(WARNING) << "Performance warning in the database index: "
-                   << "Some extensions are missing in the plugin";
-    }
-
-    if (extensions_.getLastChangeIndex == NULL)
-    {
-      LOG(WARNING) << "The database extension GetLastChangeIndex() is missing";
-    }
-
-    if (extensions_.tagMostRecentPatient == NULL)
-    {
-      LOG(WARNING) << "The database extension TagMostRecentPatient() is missing "
-                   << "(affected by issue 58)";
-    }
-  }
-
-
-  void OrthancPluginDatabase::Open()
-  {
-    CheckSuccess(backend_.open(payload_));
-
-    {
-      Transaction transaction(*this);
-      transaction.Begin();
-
-      std::string tmp;
-      fastGetTotalSize_ =
-        (LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast) &&
-         tmp == "1");
-      
-      if (fastGetTotalSize_)
-      {
-        currentDiskSize_ = 0;   // Unused
-      }
-      else
-      {
-        // This is the case of database plugins using Orthanc SDK <= 1.5.2
-        LOG(WARNING) << "Your database index plugin is not compatible with multiple Orthanc writers";
-        currentDiskSize_ = GetTotalCompressedSize();
-      }
-
-      transaction.Commit(0);
-    }
-  }
-
-
-  void OrthancPluginDatabase::AddAttachment(int64_t id,
-                                            const FileInfo& attachment)
-  {
-    OrthancPluginAttachment tmp;
-    tmp.uuid = attachment.GetUuid().c_str();
-    tmp.contentType = static_cast<int32_t>(attachment.GetContentType());
-    tmp.uncompressedSize = attachment.GetUncompressedSize();
-    tmp.uncompressedHash = attachment.GetUncompressedMD5().c_str();
-    tmp.compressionType = static_cast<int32_t>(attachment.GetCompressionType());
-    tmp.compressedSize = attachment.GetCompressedSize();
-    tmp.compressedHash = attachment.GetCompressedMD5().c_str();
-
-    CheckSuccess(backend_.addAttachment(payload_, id, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::AttachChild(int64_t parent,
-                                          int64_t child)
-  {
-    CheckSuccess(backend_.attachChild(payload_, parent, child));
-  }
-
-
-  void OrthancPluginDatabase::ClearChanges()
-  {
-    CheckSuccess(backend_.clearChanges(payload_));
-  }
-
-
-  void OrthancPluginDatabase::ClearExportedResources()
-  {
-    CheckSuccess(backend_.clearExportedResources(payload_));
-  }
-
-
-  int64_t OrthancPluginDatabase::CreateResource(const std::string& publicId,
-                                                ResourceType type)
-  {
-    int64_t id;
-    CheckSuccess(backend_.createResource(&id, payload_, publicId.c_str(), Plugins::Convert(type)));
-    return id;
-  }
-
-
-  void OrthancPluginDatabase::DeleteAttachment(int64_t id,
-                                               FileContentType attachment)
-  {
-    CheckSuccess(backend_.deleteAttachment(payload_, id, static_cast<int32_t>(attachment)));
-  }
-
-
-  void OrthancPluginDatabase::DeleteMetadata(int64_t id,
-                                             MetadataType type)
-  {
-    CheckSuccess(backend_.deleteMetadata(payload_, id, static_cast<int32_t>(type)));
-  }
-
-
-  void OrthancPluginDatabase::DeleteResource(int64_t id)
-  {
-    CheckSuccess(backend_.deleteResource(payload_, id));
-  }
-
-
-  void OrthancPluginDatabase::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                             int64_t id)
-  {
-    if (extensions_.getAllMetadata == NULL)
-    {
-      // Fallback implementation if extension is missing
-      target.clear();
-
-      ResetAnswers();
-      CheckSuccess(backend_.listAvailableMetadata(GetContext(), payload_, id));
-
-      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-          type_ != _OrthancPluginDatabaseAnswerType_Int32)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-
-      target.clear();
-
-      if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
-      {
-        for (std::list<int32_t>::const_iterator 
-               it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
-        {
-          MetadataType type = static_cast<MetadataType>(*it);
-
-          std::string value;
-          if (LookupMetadata(value, id, type))
-          {
-            target[type] = value;
-          }
-        }
-      }
-    }
-    else
-    {
-      ResetAnswers();
-
-      answerMetadata_ = &target;
-      target.clear();
-      
-      CheckSuccess(extensions_.getAllMetadata(GetContext(), payload_, id));
-
-      if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-          type_ != _OrthancPluginDatabaseAnswerType_Metadata)
-      {
-        throw OrthancException(ErrorCode_DatabasePlugin);
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::GetAllInternalIds(std::list<int64_t>& target,
-                                                ResourceType resourceType)
-  {
-    if (extensions_.getAllInternalIds == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "The database plugin does not implement the mandatory GetAllInternalIds() extension");
-    }
-
-    ResetAnswers();
-    CheckSuccess(extensions_.getAllInternalIds(GetContext(), payload_, Plugins::Convert(resourceType)));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
-                                              ResourceType resourceType)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.getAllPublicIds(GetContext(), payload_, Plugins::Convert(resourceType)));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetAllPublicIds(std::list<std::string>& target,
-                                              ResourceType resourceType,
-                                              size_t since,
-                                              size_t limit)
-  {
-    if (extensions_.getAllPublicIdsWithLimit != NULL)
-    {
-      // This extension is available since Orthanc 0.9.4
-      ResetAnswers();
-      CheckSuccess(extensions_.getAllPublicIdsWithLimit
-                   (GetContext(), payload_, Plugins::Convert(resourceType), since, limit));
-      ForwardAnswers(target);
-    }
-    else
-    {
-      // The extension is not available in the database plugin, use a
-      // fallback implementation
-      target.clear();
-
-      if (limit == 0)
-      {
-        return;
-      }
-
-      std::list<std::string> tmp;
-      GetAllPublicIds(tmp, resourceType);
-    
-      if (tmp.size() <= since)
-      {
-        // Not enough results => empty answer
-        return;
-      }
-
-      std::list<std::string>::iterator current = tmp.begin();
-      std::advance(current, since);
-
-      while (limit > 0 && current != tmp.end())
-      {
-        target.push_back(*current);
-        --limit;
-        ++current;
-      }
-    }
-  }
-
-
-
-  void OrthancPluginDatabase::GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                                         bool& done /*out*/,
-                                         int64_t since,
-                                         uint32_t maxResults)
-  {
-    ResetAnswers();
-    answerChanges_ = &target;
-    answerDone_ = &done;
-    done = false;
-
-    CheckSuccess(backend_.getChanges(GetContext(), payload_, since, maxResults));
-  }
-
-
-  void OrthancPluginDatabase::GetChildrenInternalId(std::list<int64_t>& target,
-                                                    int64_t id)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.getChildrenInternalId(GetContext(), payload_, id));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetChildrenPublicId(std::list<std::string>& target,
-                                                  int64_t id)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.getChildrenPublicId(GetContext(), payload_, id));
-    ForwardAnswers(target);
-  }
-
-
-  void OrthancPluginDatabase::GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                                   bool& done /*out*/,
-                                                   int64_t since,
-                                                   uint32_t maxResults)
-  {
-    ResetAnswers();
-    answerExportedResources_ = &target;
-    answerDone_ = &done;
-    done = false;
-
-    CheckSuccess(backend_.getExportedResources(GetContext(), payload_, since, maxResults));
-  }
-
-
-  void OrthancPluginDatabase::GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-  {
-    bool ignored = false;
-
-    ResetAnswers();
-    answerChanges_ = &target;
-    answerDone_ = &ignored;
-
-    CheckSuccess(backend_.getLastChange(GetContext(), payload_));
-  }
-
-
-  void OrthancPluginDatabase::GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
-  {
-    bool ignored = false;
-
-    ResetAnswers();
-    answerExportedResources_ = &target;
-    answerDone_ = &ignored;
-
-    CheckSuccess(backend_.getLastExportedResource(GetContext(), payload_));
-  }
-
-
-  void OrthancPluginDatabase::GetMainDicomTags(DicomMap& map,
-                                               int64_t id)
-  {
-    ResetAnswers();
-    answerDicomMap_ = &map;
-
-    CheckSuccess(backend_.getMainDicomTags(GetContext(), payload_, id));
-  }
-
-
-  std::string OrthancPluginDatabase::GetPublicId(int64_t resourceId)
-  {
-    ResetAnswers();
-    std::string s;
-
-    CheckSuccess(backend_.getPublicId(GetContext(), payload_, resourceId));
-
-    if (!ForwardSingleAnswer(s))
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    return s;
-  }
-
-
-  uint64_t OrthancPluginDatabase::GetResourceCount(ResourceType resourceType)
-  {
-    uint64_t count;
-    CheckSuccess(backend_.getResourceCount(&count, payload_, Plugins::Convert(resourceType)));
-    return count;
-  }
-
-
-  ResourceType OrthancPluginDatabase::GetResourceType(int64_t resourceId)
-  {
-    OrthancPluginResourceType type;
-    CheckSuccess(backend_.getResourceType(&type, payload_, resourceId));
-    return Plugins::Convert(type);
-  }
-
-
-  uint64_t OrthancPluginDatabase::GetTotalCompressedSize()
-  {
-    uint64_t size;
-    CheckSuccess(backend_.getTotalCompressedSize(&size, payload_));
-    return size;
-  }
-
-    
-  uint64_t OrthancPluginDatabase::GetTotalUncompressedSize()
-  {
-    uint64_t size;
-    CheckSuccess(backend_.getTotalUncompressedSize(&size, payload_));
-    return size;
-  }
-
-
-  bool OrthancPluginDatabase::IsExistingResource(int64_t internalId)
-  {
-    int32_t existing;
-    CheckSuccess(backend_.isExistingResource(&existing, payload_, internalId));
-    return (existing != 0);
-  }
-
-
-  bool OrthancPluginDatabase::IsProtectedPatient(int64_t internalId)
-  {
-    int32_t isProtected;
-    CheckSuccess(backend_.isProtectedPatient(&isProtected, payload_, internalId));
-    return (isProtected != 0);
-  }
-
-
-  void OrthancPluginDatabase::ListAvailableAttachments(std::list<FileContentType>& target,
-                                                       int64_t id)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.listAvailableAttachments(GetContext(), payload_, id));
-
-    if (type_ != _OrthancPluginDatabaseAnswerType_None &&
-        type_ != _OrthancPluginDatabaseAnswerType_Int32)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    target.clear();
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_Int32)
-    {
-      for (std::list<int32_t>::const_iterator 
-             it = answerInt32_.begin(); it != answerInt32_.end(); ++it)
-      {
-        target.push_back(static_cast<FileContentType>(*it));
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::LogChange(int64_t internalId,
-                                        const ServerIndexChange& change)
-  {
-    OrthancPluginChange tmp;
-    tmp.seq = change.GetSeq();
-    tmp.changeType = static_cast<int32_t>(change.GetChangeType());
-    tmp.resourceType = Plugins::Convert(change.GetResourceType());
-    tmp.publicId = change.GetPublicId().c_str();
-    tmp.date = change.GetDate().c_str();
-
-    CheckSuccess(backend_.logChange(payload_, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::LogExportedResource(const ExportedResource& resource)
-  {
-    OrthancPluginExportedResource tmp;
-    tmp.seq = resource.GetSeq();
-    tmp.resourceType = Plugins::Convert(resource.GetResourceType());
-    tmp.publicId = resource.GetPublicId().c_str();
-    tmp.modality = resource.GetModality().c_str();
-    tmp.date = resource.GetDate().c_str();
-    tmp.patientId = resource.GetPatientId().c_str();
-    tmp.studyInstanceUid = resource.GetStudyInstanceUid().c_str();
-    tmp.seriesInstanceUid = resource.GetSeriesInstanceUid().c_str();
-    tmp.sopInstanceUid = resource.GetSopInstanceUid().c_str();
-
-    CheckSuccess(backend_.logExportedResource(payload_, &tmp));
-  }
-
-    
-  bool OrthancPluginDatabase::LookupAttachment(FileInfo& attachment,
-                                               int64_t id,
-                                               FileContentType contentType)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.lookupAttachment
-                 (GetContext(), payload_, id, static_cast<int32_t>(contentType)));
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_Attachment &&
-             answerAttachments_.size() == 1)
-    {
-      attachment = answerAttachments_.front();
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  bool OrthancPluginDatabase::LookupGlobalProperty(std::string& target,
-                                                   GlobalProperty property)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.lookupGlobalProperty
-                 (GetContext(), payload_, static_cast<int32_t>(property)));
-
-    return ForwardSingleAnswer(target);
-  }
-
-
-  bool OrthancPluginDatabase::LookupMetadata(std::string& target,
-                                             int64_t id,
-                                             MetadataType type)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.lookupMetadata(GetContext(), payload_, id, static_cast<int32_t>(type)));
-    return ForwardSingleAnswer(target);
-  }
-
-
-  bool OrthancPluginDatabase::LookupParent(int64_t& parentId,
-                                           int64_t resourceId)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.lookupParent(GetContext(), payload_, resourceId));
-    return ForwardSingleAnswer(parentId);
-  }
-
-
-  bool OrthancPluginDatabase::LookupResource(int64_t& id,
-                                             ResourceType& type,
-                                             const std::string& publicId)
-  {
-    ResetAnswers();
-
-    CheckSuccess(backend_.lookupResource(GetContext(), payload_, publicId.c_str()));
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      return false;
-    }
-    else if (type_ == _OrthancPluginDatabaseAnswerType_Resource &&
-             answerResources_.size() == 1)
-    {
-      id = answerResources_.front().first;
-      type = answerResources_.front().second;
-      return true; 
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.selectPatientToRecycle(GetContext(), payload_));
-    return ForwardSingleAnswer(internalId);
-  }
-
-
-  bool OrthancPluginDatabase::SelectPatientToRecycle(int64_t& internalId,
-                                                     int64_t patientIdToAvoid)
-  {
-    ResetAnswers();
-    CheckSuccess(backend_.selectPatientToRecycle2(GetContext(), payload_, patientIdToAvoid));
-    return ForwardSingleAnswer(internalId);
-  }
-
-
-  void OrthancPluginDatabase::SetGlobalProperty(GlobalProperty property,
-                                                const std::string& value)
-  {
-    CheckSuccess(backend_.setGlobalProperty
-                 (payload_, static_cast<int32_t>(property), value.c_str()));
-  }
-
-
-  void OrthancPluginDatabase::ClearMainDicomTags(int64_t id)
-  {
-    if (extensions_.clearMainDicomTags == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "Your custom index plugin does not implement the mandatory ClearMainDicomTags() extension");
-    }
-
-    CheckSuccess(extensions_.clearMainDicomTags(payload_, id));
-  }
-
-
-  void OrthancPluginDatabase::SetMainDicomTag(int64_t id,
-                                              const DicomTag& tag,
-                                              const std::string& value)
-  {
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    CheckSuccess(backend_.setMainDicomTag(payload_, id, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::SetIdentifierTag(int64_t id,
-                                               const DicomTag& tag,
-                                               const std::string& value)
-  {
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    CheckSuccess(backend_.setIdentifierTag(payload_, id, &tmp));
-  }
-
-
-  void OrthancPluginDatabase::SetMetadata(int64_t id,
-                                          MetadataType type,
-                                          const std::string& value)
-  {
-    CheckSuccess(backend_.setMetadata
-                 (payload_, id, static_cast<int32_t>(type), value.c_str()));
-  }
-
-
-  void OrthancPluginDatabase::SetProtectedPatient(int64_t internalId, 
-                                                  bool isProtected)
-  {
-    CheckSuccess(backend_.setProtectedPatient(payload_, internalId, isProtected));
-  }
-
-
-  IDatabaseWrapper::ITransaction* OrthancPluginDatabase::StartTransaction()
-  {
-    return new Transaction(*this);
-  }
-
-
-  static void ProcessEvent(IDatabaseListener& listener,
-                           const _OrthancPluginDatabaseAnswer& answer)
-  {
-    switch (answer.type)
-    {
-      case _OrthancPluginDatabaseAnswerType_DeletedAttachment:
-      {
-        const OrthancPluginAttachment& attachment = 
-          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
-        listener.SignalFileDeleted(Convert(attachment));
-        break;
-      }
-        
-      case _OrthancPluginDatabaseAnswerType_RemainingAncestor:
-      {
-        ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
-        listener.SignalRemainingAncestor(type, answer.valueString);
-        break;
-      }
-      
-      case _OrthancPluginDatabaseAnswerType_DeletedResource:
-      {
-        ResourceType type = Plugins::Convert(static_cast<OrthancPluginResourceType>(answer.valueInt32));
-        ServerIndexChange change(ChangeType_Deleted, type, answer.valueString);
-        listener.SignalChange(change);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-  }
-
-
-  unsigned int OrthancPluginDatabase::GetDatabaseVersion()
-  {
-    if (extensions_.getDatabaseVersion != NULL)
-    {
-      uint32_t version;
-      CheckSuccess(extensions_.getDatabaseVersion(&version, payload_));
-      return version;
-    }
-    else
-    {
-      // Before adding the "GetDatabaseVersion()" extension in plugins
-      // (OrthancPostgreSQL <= 1.2), the only supported DB schema was
-      // version 5.
-      return 5;
-    }
-  }
-
-
-  void OrthancPluginDatabase::Upgrade(unsigned int targetVersion,
-                                      IStorageArea& storageArea)
-  {
-    if (extensions_.upgradeDatabase != NULL)
-    {
-      Transaction transaction(*this);
-      transaction.Begin();
-
-      OrthancPluginErrorCode code = extensions_.upgradeDatabase(
-        payload_, targetVersion, 
-        reinterpret_cast<OrthancPluginStorageArea*>(&storageArea));
-
-      if (code == OrthancPluginErrorCode_Success)
-      {
-        transaction.Commit(0);
-      }
-      else
-      {
-        transaction.Rollback();
-        errorDictionary_.LogError(code, true);
-        throw OrthancException(static_cast<ErrorCode>(code));
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::AnswerReceived(const _OrthancPluginDatabaseAnswer& answer)
-  {
-    if (answer.type == _OrthancPluginDatabaseAnswerType_None)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin);
-    }
-
-    if (answer.type == _OrthancPluginDatabaseAnswerType_DeletedAttachment ||
-        answer.type == _OrthancPluginDatabaseAnswerType_DeletedResource ||
-        answer.type == _OrthancPluginDatabaseAnswerType_RemainingAncestor)
-    {
-      assert(listener_ != NULL);
-      ProcessEvent(*listener_, answer);
-      return;
-    }
-
-    if (type_ == _OrthancPluginDatabaseAnswerType_None)
-    {
-      type_ = answer.type;
-
-      switch (type_)
-      {
-        case _OrthancPluginDatabaseAnswerType_Int32:
-          answerInt32_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Int64:
-          answerInt64_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Resource:
-          answerResources_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Attachment:
-          answerAttachments_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_String:
-          answerStrings_.clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_DicomTag:
-          assert(answerDicomMap_ != NULL);
-          answerDicomMap_->Clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Change:
-          assert(answerChanges_ != NULL);
-          answerChanges_->clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_ExportedResource:
-          assert(answerExportedResources_ != NULL);
-          answerExportedResources_->clear();
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_MatchingResource:
-          assert(answerMatchingResources_ != NULL);
-          answerMatchingResources_->clear();
-
-          if (answerMatchingInstances_ != NULL)
-          {
-            answerMatchingInstances_->clear();
-          }
-          
-          break;
-
-        case _OrthancPluginDatabaseAnswerType_Metadata:
-          assert(answerMetadata_ != NULL);
-          answerMetadata_->clear();
-          break;
-
-        default:
-          throw OrthancException(ErrorCode_DatabasePlugin,
-                                 "Unhandled type of answer for custom index plugin: " +
-                                 boost::lexical_cast<std::string>(answer.type));
-      }
-    }
-    else if (type_ != answer.type)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "Error in the plugin protocol: Cannot change the answer type");
-    }
-
-    switch (answer.type)
-    {
-      case _OrthancPluginDatabaseAnswerType_Int32:
-      {
-        answerInt32_.push_back(answer.valueInt32);
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Int64:
-      {
-        answerInt64_.push_back(answer.valueInt64);
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Resource:
-      {
-        OrthancPluginResourceType type = static_cast<OrthancPluginResourceType>(answer.valueInt32);
-        answerResources_.push_back(std::make_pair(answer.valueInt64, Plugins::Convert(type)));
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Attachment:
-      {
-        const OrthancPluginAttachment& attachment = 
-          *reinterpret_cast<const OrthancPluginAttachment*>(answer.valueGeneric);
-
-        answerAttachments_.push_back(Convert(attachment));
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_DicomTag:
-      {
-        const OrthancPluginDicomTag& tag = *reinterpret_cast<const OrthancPluginDicomTag*>(answer.valueGeneric);
-        assert(answerDicomMap_ != NULL);
-        answerDicomMap_->SetValue(tag.group, tag.element, std::string(tag.value), false);
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_String:
-      {
-        if (answer.valueString == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        if (type_ == _OrthancPluginDatabaseAnswerType_None)
-        {
-          type_ = _OrthancPluginDatabaseAnswerType_String;
-          answerStrings_.clear();
-        }
-        else if (type_ != _OrthancPluginDatabaseAnswerType_String)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        answerStrings_.push_back(std::string(answer.valueString));
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Change:
-      {
-        assert(answerDone_ != NULL);
-        if (answer.valueUint32 == 1)
-        {
-          *answerDone_ = true;
-        }
-        else if (*answerDone_)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          const OrthancPluginChange& change =
-            *reinterpret_cast<const OrthancPluginChange*>(answer.valueGeneric);
-          assert(answerChanges_ != NULL);
-          answerChanges_->push_back
-            (ServerIndexChange(change.seq,
-                               static_cast<ChangeType>(change.changeType),
-                               Plugins::Convert(change.resourceType),
-                               change.publicId,
-                               change.date));                                   
-        }
-
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_ExportedResource:
-      {
-        assert(answerDone_ != NULL);
-        if (answer.valueUint32 == 1)
-        {
-          *answerDone_ = true;
-        }
-        else if (*answerDone_)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-        else
-        {
-          const OrthancPluginExportedResource& exported = 
-            *reinterpret_cast<const OrthancPluginExportedResource*>(answer.valueGeneric);
-          assert(answerExportedResources_ != NULL);
-          answerExportedResources_->push_back
-            (ExportedResource(exported.seq,
-                              Plugins::Convert(exported.resourceType),
-                              exported.publicId,
-                              exported.modality,
-                              exported.date,
-                              exported.patientId,
-                              exported.studyInstanceUid,
-                              exported.seriesInstanceUid,
-                              exported.sopInstanceUid));
-        }
-
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_MatchingResource:
-      {
-        const OrthancPluginMatchingResource& match = 
-          *reinterpret_cast<const OrthancPluginMatchingResource*>(answer.valueGeneric);
-
-        if (match.resourceId == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        assert(answerMatchingResources_ != NULL);
-        answerMatchingResources_->push_back(match.resourceId);
-
-        if (answerMatchingInstances_ != NULL)
-        {
-          if (match.someInstanceId == NULL)
-          {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-
-          answerMatchingInstances_->push_back(match.someInstanceId);
-        }
- 
-        break;
-      }
-
-      case _OrthancPluginDatabaseAnswerType_Metadata:
-      {
-        const OrthancPluginResourcesContentMetadata& metadata =
-          *reinterpret_cast<const OrthancPluginResourcesContentMetadata*>(answer.valueGeneric);
-
-        MetadataType type = static_cast<MetadataType>(metadata.metadata);
-
-        if (metadata.value == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        assert(answerMetadata_ != NULL &&
-               answerMetadata_->find(type) == answerMetadata_->end());
-        (*answerMetadata_) [type] = metadata.value;
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_DatabasePlugin,
-                               "Unhandled type of answer for custom index plugin: " +
-                               boost::lexical_cast<std::string>(answer.type));
-    }
-  }
-
-    
-  bool OrthancPluginDatabase::IsDiskSizeAbove(uint64_t threshold)
-  {
-    if (fastGetTotalSize_)
-    {
-      return GetTotalCompressedSize() > threshold;
-    }
-    else
-    {
-      assert(GetTotalCompressedSize() == currentDiskSize_);
-      return currentDiskSize_ > threshold;
-    }      
-  }
-
-
-  void OrthancPluginDatabase::ApplyLookupResources(std::list<std::string>& resourcesId,
-                                                   std::list<std::string>* instancesId,
-                                                   const std::vector<DatabaseConstraint>& lookup,
-                                                   ResourceType queryLevel,
-                                                   size_t limit)
-  {
-    if (extensions_.lookupResources == NULL)
-    {
-      // Fallback to compatibility mode
-      ILookupResources::Apply
-        (*this, *this, resourcesId, instancesId, lookup, queryLevel, limit);
-    }
-    else
-    {
-      std::vector<OrthancPluginDatabaseConstraint> constraints;
-      std::vector< std::vector<const char*> > constraintsValues;
-
-      constraints.resize(lookup.size());
-      constraintsValues.resize(lookup.size());
-
-      for (size_t i = 0; i < lookup.size(); i++)
-      {
-        lookup[i].EncodeForPlugins(constraints[i], constraintsValues[i]);
-      }
-
-      ResetAnswers();
-      answerMatchingResources_ = &resourcesId;
-      answerMatchingInstances_ = instancesId;
-      
-      CheckSuccess(extensions_.lookupResources(GetContext(), payload_, lookup.size(),
-                                               (lookup.empty() ? NULL : &constraints[0]),
-                                               Plugins::Convert(queryLevel),
-                                               limit, (instancesId == NULL ? 0 : 1)));
-    }
-  }
-
-
-  bool OrthancPluginDatabase::CreateInstance(
-    IDatabaseWrapper::CreateInstanceResult& result,
-    int64_t& instanceId,
-    const std::string& patient,
-    const std::string& study,
-    const std::string& series,
-    const std::string& instance)
-  {
-    if (extensions_.createInstance == NULL)
-    {
-      // Fallback to compatibility mode
-      return ICreateInstance::Apply
-        (*this, result, instanceId, patient, study, series, instance);
-    }
-    else
-    {
-      OrthancPluginCreateInstanceResult output;
-      memset(&output, 0, sizeof(output));
-
-      CheckSuccess(extensions_.createInstance(&output, payload_, patient.c_str(),
-                                              study.c_str(), series.c_str(), instance.c_str()));
-
-      instanceId = output.instanceId;
-      
-      if (output.isNewInstance)
-      {
-        result.isNewPatient_ = output.isNewPatient;
-        result.isNewStudy_ = output.isNewStudy;
-        result.isNewSeries_ = output.isNewSeries;
-        result.patientId_ = output.patientId;
-        result.studyId_ = output.studyId;
-        result.seriesId_ = output.seriesId;
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-  }
-
-
-  void OrthancPluginDatabase::LookupIdentifier(std::list<int64_t>& result,
-                                               ResourceType level,
-                                               const DicomTag& tag,
-                                               Compatibility::IdentifierConstraintType type,
-                                               const std::string& value)
-  {
-    if (extensions_.lookupIdentifier3 == NULL)
-    {
-      throw OrthancException(ErrorCode_DatabasePlugin,
-                             "The database plugin does not implement the mandatory LookupIdentifier3() extension");
-    }
-
-    OrthancPluginDicomTag tmp;
-    tmp.group = tag.GetGroup();
-    tmp.element = tag.GetElement();
-    tmp.value = value.c_str();
-
-    ResetAnswers();
-    CheckSuccess(extensions_.lookupIdentifier3(GetContext(), payload_, Plugins::Convert(level),
-                                               &tmp, Compatibility::Convert(type)));
-    ForwardAnswers(result);
-  }
-
-
-  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, Compatibility::IdentifierConstraintType_GreaterOrEqual, start);
-
-      std::list<int64_t> b;
-      LookupIdentifier(result, level, tag, Compatibility::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);
-    }
-  }
-
-
-  void OrthancPluginDatabase::SetResourcesContent(const Orthanc::ResourcesContent& content)
-  {
-    if (extensions_.setResourcesContent == NULL)
-    {
-      ISetResourcesContent::Apply(*this, content);
-    }
-    else
-    {
-      std::vector<OrthancPluginResourcesContentTags> identifierTags;
-      std::vector<OrthancPluginResourcesContentTags> mainDicomTags;
-      std::vector<OrthancPluginResourcesContentMetadata> metadata;
-
-      identifierTags.reserve(content.GetListTags().size());
-      mainDicomTags.reserve(content.GetListTags().size());
-      metadata.reserve(content.GetListMetadata().size());
-
-      for (ResourcesContent::ListTags::const_iterator
-             it = content.GetListTags().begin(); it != content.GetListTags().end(); ++it)
-      {
-        OrthancPluginResourcesContentTags tmp;
-        tmp.resource = it->resourceId_;
-        tmp.group = it->tag_.GetGroup();
-        tmp.element = it->tag_.GetElement();
-        tmp.value = it->value_.c_str();
-
-        if (it->isIdentifier_)
-        {
-          identifierTags.push_back(tmp);
-        }
-        else
-        {
-          mainDicomTags.push_back(tmp);
-        }
-      }
-
-      for (ResourcesContent::ListMetadata::const_iterator
-             it = content.GetListMetadata().begin(); it != content.GetListMetadata().end(); ++it)
-      {
-        OrthancPluginResourcesContentMetadata tmp;
-        tmp.resource = it->resourceId_;
-        tmp.metadata = it->metadata_;
-        tmp.value = it->value_.c_str();
-        metadata.push_back(tmp);
-      }
-
-      assert(identifierTags.size() + mainDicomTags.size() == content.GetListTags().size() &&
-             metadata.size() == content.GetListMetadata().size());
-       
-      CheckSuccess(extensions_.setResourcesContent(
-                     payload_,
-                     identifierTags.size(),
-                     (identifierTags.empty() ? NULL : &identifierTags[0]),
-                     mainDicomTags.size(),
-                     (mainDicomTags.empty() ? NULL : &mainDicomTags[0]),
-                     metadata.size(),
-                     (metadata.empty() ? NULL : &metadata[0])));
-    }
-  }
-
-
-
-  void OrthancPluginDatabase::GetChildrenMetadata(std::list<std::string>& target,
-                                                  int64_t resourceId,
-                                                  MetadataType metadata)
-  {
-    if (extensions_.getChildrenMetadata == NULL)
-    {
-      IGetChildrenMetadata::Apply(*this, target, resourceId, metadata);
-    }
-    else
-    {
-      ResetAnswers();
-      CheckSuccess(extensions_.getChildrenMetadata
-                   (GetContext(), payload_, resourceId, static_cast<int32_t>(metadata)));
-      ForwardAnswers(target);
-    }
-  }
-
-
-  int64_t OrthancPluginDatabase::GetLastChangeIndex()
-  {
-    if (extensions_.getLastChangeIndex == NULL)
-    {
-      // This was the default behavior in Orthanc <= 1.5.1
-      // https://groups.google.com/d/msg/orthanc-users/QhzB6vxYeZ0/YxabgqpfBAAJ
-      return 0;
-    }
-    else
-    {
-      int64_t result = 0;
-      CheckSuccess(extensions_.getLastChangeIndex(&result, payload_));
-      return result;
-    }
-  }
-
-  
-  void OrthancPluginDatabase::TagMostRecentPatient(int64_t patient)
-  {
-    if (extensions_.tagMostRecentPatient != NULL)
-    {
-      CheckSuccess(extensions_.tagMostRecentPatient(payload_, patient));
-    }
-  }
-
-
-  bool OrthancPluginDatabase::LookupResourceAndParent(int64_t& id,
-                                                      ResourceType& type,
-                                                      std::string& parentPublicId,
-                                                      const std::string& publicId)
-  {
-    if (extensions_.lookupResourceAndParent == NULL)
-    {
-      return ILookupResourceAndParent::Apply(*this, id, type, parentPublicId, publicId);
-    }
-    else
-    {
-      std::list<std::string> parent;
-
-      uint8_t isExisting;
-      OrthancPluginResourceType pluginType = OrthancPluginResourceType_Patient;
-      
-      ResetAnswers();
-      CheckSuccess(extensions_.lookupResourceAndParent
-                   (GetContext(), &isExisting, &id, &pluginType, payload_, publicId.c_str()));
-      ForwardAnswers(parent);
-
-      if (isExisting)
-      {
-        type = Plugins::Convert(pluginType);
-
-        if (parent.empty())
-        {
-          if (type != ResourceType_Patient)
-          {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-        }
-        else if (parent.size() == 1)
-        {
-          if ((type != ResourceType_Study &&
-               type != ResourceType_Series &&
-               type != ResourceType_Instance) ||
-              parent.front().empty())
-          {
-            throw OrthancException(ErrorCode_DatabasePlugin);
-          }
-
-          parentPublicId = parent.front();
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin);
-        }
-
-        return true;
-      }
-      else
-      {
-        return false;
-      }
-    }
-  }
-}
--- a/Plugins/Engine/OrthancPluginDatabase.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,376 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/SharedLibrary.h"
-#include "../../OrthancServer/Database/Compatibility/ICreateInstance.h"
-#include "../../OrthancServer/Database/Compatibility/IGetChildrenMetadata.h"
-#include "../../OrthancServer/Database/Compatibility/ILookupResources.h"
-#include "../../OrthancServer/Database/Compatibility/ILookupResourceAndParent.h"
-#include "../../OrthancServer/Database/Compatibility/ISetResourcesContent.h"
-#include "../Include/orthanc/OrthancCDatabasePlugin.h"
-#include "PluginsErrorDictionary.h"
-
-namespace Orthanc
-{
-  class OrthancPluginDatabase :
-    public IDatabaseWrapper,
-    public Compatibility::ICreateInstance,
-    public Compatibility::IGetChildrenMetadata,
-    public Compatibility::ILookupResources,
-    public Compatibility::ILookupResourceAndParent,
-    public Compatibility::ISetResourcesContent
-  {
-  private:
-    class Transaction;
-
-    typedef std::pair<int64_t, ResourceType>     AnswerResource;
-    typedef std::map<MetadataType, std::string>  AnswerMetadata;
-
-    SharedLibrary&  library_;
-    PluginsErrorDictionary&  errorDictionary_;
-    _OrthancPluginDatabaseAnswerType type_;
-    OrthancPluginDatabaseBackend backend_;
-    OrthancPluginDatabaseExtensions extensions_;
-    void* payload_;
-    IDatabaseListener* listener_;
-
-    bool      fastGetTotalSize_;
-    uint64_t  currentDiskSize_;
-
-    std::list<std::string>         answerStrings_;
-    std::list<int32_t>             answerInt32_;
-    std::list<int64_t>             answerInt64_;
-    std::list<AnswerResource>      answerResources_;
-    std::list<FileInfo>            answerAttachments_;
-
-    DicomMap*                      answerDicomMap_;
-    std::list<ServerIndexChange>*  answerChanges_;
-    std::list<ExportedResource>*   answerExportedResources_;
-    bool*                          answerDone_;
-    std::list<std::string>*        answerMatchingResources_;
-    std::list<std::string>*        answerMatchingInstances_;
-    AnswerMetadata*                answerMetadata_;
-
-    OrthancPluginDatabaseContext* GetContext()
-    {
-      return reinterpret_cast<OrthancPluginDatabaseContext*>(this);
-    }
-
-    void CheckSuccess(OrthancPluginErrorCode code);
-
-    void ResetAnswers();
-
-    void ForwardAnswers(std::list<int64_t>& target);
-
-    void ForwardAnswers(std::list<std::string>& target);
-
-    bool ForwardSingleAnswer(std::string& target);
-
-    bool ForwardSingleAnswer(int64_t& target);
-
-  public:
-    OrthancPluginDatabase(SharedLibrary& library,
-                          PluginsErrorDictionary&  errorDictionary,
-                          const OrthancPluginDatabaseBackend& backend,
-                          const OrthancPluginDatabaseExtensions* extensions,
-                          size_t extensionsSize,
-                          void *payload);
-
-    virtual void Open() 
-      ORTHANC_OVERRIDE;
-
-    virtual void Close() 
-      ORTHANC_OVERRIDE
-    {
-      CheckSuccess(backend_.close(payload_));
-    }
-
-    const SharedLibrary& GetSharedLibrary() const
-    {
-      return library_;
-    }
-
-    virtual void AddAttachment(int64_t id,
-                               const FileInfo& attachment) 
-      ORTHANC_OVERRIDE;
-
-    virtual void AttachChild(int64_t parent,
-                             int64_t child) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearChanges() 
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearExportedResources() 
-      ORTHANC_OVERRIDE;
-
-    virtual int64_t CreateResource(const std::string& publicId,
-                                   ResourceType type) 
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteAttachment(int64_t id,
-                                  FileContentType attachment) 
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteMetadata(int64_t id,
-                                MetadataType type) 
-      ORTHANC_OVERRIDE;
-
-    virtual void DeleteResource(int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void FlushToDisk() 
-      ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual bool HasFlushToDisk() const 
-      ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
-    virtual void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetAllPublicIds(std::list<std::string>& target,
-                                 ResourceType resourceType,
-                                 size_t since,
-                                 size_t limit) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                            bool& done /*out*/,
-                            int64_t since,
-                            uint32_t maxResults) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenInternalId(std::list<int64_t>& target,
-                                       int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenPublicId(std::list<std::string>& target,
-                                     int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                      bool& done /*out*/,
-                                      int64_t since,
-                                      uint32_t maxResults) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastChange(std::list<ServerIndexChange>& target /*out*/) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetLastExportedResource(std::list<ExportedResource>& target /*out*/) 
-      ORTHANC_OVERRIDE;
-
-    virtual void GetMainDicomTags(DicomMap& map,
-                                  int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual std::string GetPublicId(int64_t resourceId) 
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetResourceCount(ResourceType resourceType) 
-      ORTHANC_OVERRIDE;
-
-    virtual ResourceType GetResourceType(int64_t resourceId) 
-      ORTHANC_OVERRIDE;
-
-    virtual uint64_t GetTotalCompressedSize() 
-      ORTHANC_OVERRIDE;
-    
-    virtual uint64_t GetTotalUncompressedSize() 
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsExistingResource(int64_t internalId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool IsProtectedPatient(int64_t internalId) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ListAvailableAttachments(std::list<FileContentType>& target,
-                                          int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void LogChange(int64_t internalId,
-                           const ServerIndexChange& change) 
-      ORTHANC_OVERRIDE;
-
-    virtual void LogExportedResource(const ExportedResource& resource) 
-      ORTHANC_OVERRIDE;
-    
-    virtual bool LookupAttachment(FileInfo& attachment,
-                                  int64_t id,
-                                  FileContentType contentType) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupGlobalProperty(std::string& target,
-                                      GlobalProperty property) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupMetadata(std::string& target,
-                                int64_t id,
-                                MetadataType type) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupParent(int64_t& parentId,
-                              int64_t resourceId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool LookupResource(int64_t& id,
-                                ResourceType& type,
-                                const std::string& publicId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId) 
-      ORTHANC_OVERRIDE;
-
-    virtual bool SelectPatientToRecycle(int64_t& internalId,
-                                        int64_t patientIdToAvoid) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetGlobalProperty(GlobalProperty property,
-                                   const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ClearMainDicomTags(int64_t id) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMainDicomTag(int64_t id,
-                                 const DicomTag& tag,
-                                 const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetIdentifierTag(int64_t id,
-                                  const DicomTag& tag,
-                                  const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetMetadata(int64_t id,
-                             MetadataType type,
-                             const std::string& value) 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetProtectedPatient(int64_t internalId, 
-                                     bool isProtected) 
-      ORTHANC_OVERRIDE;
-
-    virtual IDatabaseWrapper::ITransaction* StartTransaction() 
-      ORTHANC_OVERRIDE;
-
-    virtual void SetListener(IDatabaseListener& listener) 
-      ORTHANC_OVERRIDE
-    {
-      listener_ = &listener;
-    }
-
-    virtual unsigned int GetDatabaseVersion() 
-      ORTHANC_OVERRIDE;
-
-    virtual void Upgrade(unsigned int targetVersion,
-                         IStorageArea& storageArea) 
-      ORTHANC_OVERRIDE;
-
-    void AnswerReceived(const _OrthancPluginDatabaseAnswer& answer);
-
-    virtual bool IsDiskSizeAbove(uint64_t threshold) 
-      ORTHANC_OVERRIDE;
-
-    virtual void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                      std::list<std::string>* instancesId,
-                                      const std::vector<DatabaseConstraint>& lookup,
-                                      ResourceType queryLevel,
-                                      size_t limit)
-      ORTHANC_OVERRIDE;
-
-    virtual bool CreateInstance(CreateInstanceResult& result,
-                                int64_t& instanceId,
-                                const std::string& patient,
-                                const std::string& study,
-                                const std::string& series,
-                                const std::string& instance)
-      ORTHANC_OVERRIDE;
-
-    // From the "ILookupResources" interface
-    virtual void GetAllInternalIds(std::list<int64_t>& target,
-                                   ResourceType resourceType) 
-      ORTHANC_OVERRIDE;
-
-    // From the "ILookupResources" interface
-    virtual void LookupIdentifier(std::list<int64_t>& result,
-                                  ResourceType level,
-                                  const DicomTag& tag,
-                                  Compatibility::IdentifierConstraintType type,
-                                  const std::string& value)
-      ORTHANC_OVERRIDE;
-    
-    // From the "ILookupResources" interface
-    virtual void LookupIdentifierRange(std::list<int64_t>& result,
-                                       ResourceType level,
-                                       const DicomTag& tag,
-                                       const std::string& start,
-                                       const std::string& end)
-      ORTHANC_OVERRIDE;
-
-    virtual void SetResourcesContent(const Orthanc::ResourcesContent& content)
-      ORTHANC_OVERRIDE;
-
-    virtual void GetChildrenMetadata(std::list<std::string>& target,
-                                     int64_t resourceId,
-                                     MetadataType metadata)
-      ORTHANC_OVERRIDE;
-
-    virtual int64_t GetLastChangeIndex() ORTHANC_OVERRIDE;
-  
-    virtual void TagMostRecentPatient(int64_t patient) ORTHANC_OVERRIDE;
-
-    virtual bool LookupResourceAndParent(int64_t& id,
-                                         ResourceType& type,
-                                         std::string& parentPublicId,
-                                         const std::string& publicId)
-      ORTHANC_OVERRIDE;
-  };
-}
-
-#endif
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5256 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancPlugins.h"
-
-#if ORTHANC_ENABLE_PLUGINS != 1
-#error The plugin support is disabled
-#endif
-
-#if !defined(DCMTK_VERSION_NUMBER)
-#  error The macro DCMTK_VERSION_NUMBER must be defined
-#endif
-
-
-#include "../../Core/Compression/GzipCompressor.h"
-#include "../../Core/Compression/ZlibCompressor.h"
-#include "../../Core/DicomFormat/DicomArray.h"
-#include "../../Core/DicomParsing/DicomWebJsonVisitor.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../../Core/DicomParsing/ToDcmtkBridge.h"
-#include "../../Core/HttpServer/HttpToolbox.h"
-#include "../../Core/Images/Image.h"
-#include "../../Core/Images/ImageProcessing.h"
-#include "../../Core/Images/JpegReader.h"
-#include "../../Core/Images/JpegWriter.h"
-#include "../../Core/Images/PngReader.h"
-#include "../../Core/Images/PngWriter.h"
-#include "../../Core/Logging.h"
-#include "../../Core/MetricsRegistry.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/SerializationToolbox.h"
-#include "../../Core/Toolbox.h"
-#include "../../OrthancServer/OrthancConfiguration.h"
-#include "../../OrthancServer/OrthancFindRequestHandler.h"
-#include "../../OrthancServer/Search/HierarchicalMatcher.h"
-#include "../../OrthancServer/ServerContext.h"
-#include "../../OrthancServer/ServerToolbox.h"
-#include "PluginsEnumerations.h"
-#include "PluginsJob.h"
-
-#include <boost/regex.hpp>
-#include <dcmtk/dcmdata/dcdict.h>
-#include <dcmtk/dcmdata/dcdicent.h>
-
-#define ERROR_MESSAGE_64BIT "A 64bit version of the Orthanc API is necessary"
-
-
-namespace Orthanc
-{
-  static void CopyToMemoryBuffer(OrthancPluginMemoryBuffer& target,
-                                 const void* data,
-                                 size_t size)
-  {
-    if (static_cast<uint32_t>(size) != size)
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
-    }
-
-    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)
-  {
-    if (static_cast<uint32_t>(str.size()) != str.size())
-    {
-      throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
-    }
-
-    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
-    {
-    private:
-      _OrthancPluginRegisterStorageArea callbacks_;
-      PluginsErrorDictionary&  errorDictionary_;
-
-      void Free(void* buffer) const
-      {
-        if (buffer != NULL)
-        {
-          callbacks_.free(buffer);
-        }
-      }
-
-    public:
-      PluginStorageArea(const _OrthancPluginRegisterStorageArea& callbacks,
-                        PluginsErrorDictionary&  errorDictionary) : 
-        callbacks_(callbacks),
-        errorDictionary_(errorDictionary)
-      {
-      }
-
-
-      virtual void Create(const std::string& uuid,
-                          const void* content, 
-                          size_t size,
-                          FileContentType type)
-      {
-        OrthancPluginErrorCode error = callbacks_.create
-          (uuid.c_str(), content, size, Plugins::Convert(type));
-
-        if (error != OrthancPluginErrorCode_Success)
-        {
-          errorDictionary_.LogError(error, true);
-          throw OrthancException(static_cast<ErrorCode>(error));
-        }
-      }
-
-
-      virtual void Read(std::string& content,
-                        const std::string& uuid,
-                        FileContentType type)
-      {
-        void* buffer = NULL;
-        int64_t size = 0;
-
-        OrthancPluginErrorCode error = callbacks_.read
-          (&buffer, &size, uuid.c_str(), Plugins::Convert(type));
-
-        if (error != OrthancPluginErrorCode_Success)
-        {
-          errorDictionary_.LogError(error, true);
-          throw OrthancException(static_cast<ErrorCode>(error));
-        }
-
-        try
-        {
-          content.resize(static_cast<size_t>(size));
-        }
-        catch (...)
-        {
-          Free(buffer);
-          throw OrthancException(ErrorCode_NotEnoughMemory);
-        }
-
-        if (size > 0)
-        {
-          memcpy(&content[0], buffer, static_cast<size_t>(size));
-        }
-
-        Free(buffer);
-      }
-
-
-      virtual void Remove(const std::string& uuid,
-                          FileContentType type) 
-      {
-        OrthancPluginErrorCode error = callbacks_.remove
-          (uuid.c_str(), Plugins::Convert(type));
-
-        if (error != OrthancPluginErrorCode_Success)
-        {
-          errorDictionary_.LogError(error, true);
-          throw OrthancException(static_cast<ErrorCode>(error));
-        }
-      }
-    };
-
-
-    class StorageAreaFactory : public boost::noncopyable
-    {
-    private:
-      SharedLibrary&   sharedLibrary_;
-      _OrthancPluginRegisterStorageArea  callbacks_;
-      PluginsErrorDictionary&  errorDictionary_;
-
-    public:
-      StorageAreaFactory(SharedLibrary& sharedLibrary,
-                         const _OrthancPluginRegisterStorageArea& callbacks,
-                         PluginsErrorDictionary&  errorDictionary) :
-        sharedLibrary_(sharedLibrary),
-        callbacks_(callbacks),
-        errorDictionary_(errorDictionary)
-      {
-      }
-
-      SharedLibrary&  GetSharedLibrary()
-      {
-        return sharedLibrary_;
-      }
-
-      IStorageArea* Create() const
-      {
-        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()
-      {
-        OrthancConfiguration::ReaderLock lock;
-
-        std::set<std::string> peers;
-        lock.GetConfiguration().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 (lock.GetConfiguration().LookupOrthancPeer(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];
-      }
-    };
-
-
-    class DicomWebBinaryFormatter : public DicomWebJsonVisitor::IBinaryFormatter
-    {
-    private:
-      OrthancPluginDicomWebBinaryCallback   oldCallback_;
-      OrthancPluginDicomWebBinaryCallback2  newCallback_;  // New in Orthanc 1.7.0
-      void*                                 newPayload_;   // New in Orthanc 1.7.0
-      DicomWebJsonVisitor::BinaryMode       currentMode_;
-      std::string                           currentBulkDataUri_;
-
-      static void Setter(OrthancPluginDicomWebNode*       node,
-                         OrthancPluginDicomWebBinaryMode  mode,
-                         const char*                      bulkDataUri)
-      {
-        DicomWebBinaryFormatter& that = *reinterpret_cast<DicomWebBinaryFormatter*>(node);
-
-        switch (mode)
-        {
-          case OrthancPluginDicomWebBinaryMode_Ignore:
-            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
-            break;
-              
-          case OrthancPluginDicomWebBinaryMode_InlineBinary:
-            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_InlineBinary;
-            break;
-              
-          case OrthancPluginDicomWebBinaryMode_BulkDataUri:
-            if (bulkDataUri == NULL)
-            {
-              throw OrthancException(ErrorCode_NullPointer);
-            }              
-            
-            that.currentBulkDataUri_ = bulkDataUri;
-            that.currentMode_ = DicomWebJsonVisitor::BinaryMode_BulkDataUri;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-      }
-      
-    public:
-      DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback callback) :
-        oldCallback_(callback),
-        newCallback_(NULL),
-        newPayload_(NULL)
-      {
-      }
-      
-      DicomWebBinaryFormatter(OrthancPluginDicomWebBinaryCallback2 callback,
-                              void* payload) :
-        oldCallback_(NULL),
-        newCallback_(callback),
-        newPayload_(payload)
-      {
-      }
-      
-      virtual DicomWebJsonVisitor::BinaryMode Format(std::string& bulkDataUri,
-                                                     const std::vector<DicomTag>& parentTags,
-                                                     const std::vector<size_t>& parentIndexes,
-                                                     const DicomTag& tag,
-                                                     ValueRepresentation vr)
-      {
-        if (oldCallback_ == NULL &&
-            newCallback_ == NULL)
-        {
-          return DicomWebJsonVisitor::BinaryMode_InlineBinary;
-        }
-        else
-        {
-          assert(parentTags.size() == parentIndexes.size());
-          std::vector<uint16_t> groups(parentTags.size());
-          std::vector<uint16_t> elements(parentTags.size());
-          std::vector<uint32_t> indexes(parentTags.size());
-
-          for (size_t i = 0; i < parentTags.size(); i++)
-          {
-            groups[i] = parentTags[i].GetGroup();
-            elements[i] = parentTags[i].GetElement();
-            indexes[i] = static_cast<uint32_t>(parentIndexes[i]);
-          }
-          bool empty = parentTags.empty();
-
-          currentMode_ = DicomWebJsonVisitor::BinaryMode_Ignore;
-
-          if (oldCallback_ != NULL)
-          {
-            oldCallback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
-                         DicomWebBinaryFormatter::Setter,
-                         static_cast<uint32_t>(parentTags.size()),
-                         (empty ? NULL : &groups[0]),
-                         (empty ? NULL : &elements[0]),
-                         (empty ? NULL : &indexes[0]),
-                         tag.GetGroup(),
-                         tag.GetElement(),
-                         Plugins::Convert(vr));
-          }
-          else
-          {
-            assert(newCallback_ != NULL);
-            newCallback_(reinterpret_cast<OrthancPluginDicomWebNode*>(this),
-                         DicomWebBinaryFormatter::Setter,
-                         static_cast<uint32_t>(parentTags.size()),
-                         (empty ? NULL : &groups[0]),
-                         (empty ? NULL : &elements[0]),
-                         (empty ? NULL : &indexes[0]),
-                         tag.GetGroup(),
-                         tag.GetElement(),
-                         Plugins::Convert(vr),
-                         newPayload_);
-          }          
-
-          bulkDataUri = currentBulkDataUri_;          
-          return currentMode_;
-        }
-      }
-
-      void Apply(char** target,
-                 bool isJson,
-                 ParsedDicomFile& dicom)
-      {
-        DicomWebJsonVisitor visitor;
-        visitor.SetFormatter(*this);
-
-        dicom.Apply(visitor);
-
-        std::string s;
-
-        if (isJson)
-        {
-          s = visitor.GetResult().toStyledString();
-        }
-        else
-        {
-          visitor.FormatXml(s);
-        }
-
-        *target = CopyString(s);
-      }
-
-  
-      void Apply(char** target,
-                 bool isJson,
-                 const void* dicom,
-                 size_t dicomSize) 
-      {
-        ParsedDicomFile parsed(dicom, dicomSize);
-        Apply(target, isJson, parsed);
-      }
-    };
-  }
-
-
-  class OrthancPlugins::PImpl
-  {
-  private:
-    boost::mutex   contextMutex_;
-    ServerContext* context_;
-    
-  public:
-    class PluginHttpOutput : public boost::noncopyable
-    {
-    private:
-      enum MultipartState
-      {
-        MultipartState_None,
-        MultipartState_FirstPart,
-        MultipartState_SecondPart,
-        MultipartState_NextParts
-      };
-
-      HttpOutput&                 output_;
-      std::unique_ptr<std::string>  errorDetails_;
-      bool                        logDetails_;
-      MultipartState              multipartState_;
-      std::string                 multipartSubType_;
-      std::string                 multipartContentType_;
-      std::string                 multipartFirstPart_;
-      std::map<std::string, std::string>  multipartFirstHeaders_;
-
-    public:
-      PluginHttpOutput(HttpOutput& output) :
-        output_(output),
-        logDetails_(false),
-        multipartState_(MultipartState_None)
-      {
-      }
-
-      HttpOutput& GetOutput()
-      {
-        if (multipartState_ == MultipartState_None)
-        {
-          return output_;
-        }
-        else
-        {
-          // Must use "SendMultipartItem()" on multipart streams
-          throw OrthancException(ErrorCode_BadSequenceOfCalls);
-        }
-      }
-
-      void SetErrorDetails(const std::string& details,
-                           bool logDetails)
-      {
-        errorDetails_.reset(new std::string(details));
-        logDetails_ = logDetails;
-      }
-
-      bool HasErrorDetails() const
-      {
-        return errorDetails_.get() != NULL;
-      }
-
-      bool IsLogDetails() const
-      {
-        return logDetails_;
-      }
-
-      const std::string& GetErrorDetails() const
-      {
-        if (errorDetails_.get() == NULL)
-        {
-          throw OrthancException(ErrorCode_BadSequenceOfCalls);
-        }
-        else
-        {
-          return *errorDetails_;
-        }
-      }
-
-      void StartMultipart(const char* subType,
-                          const char* contentType)
-      {
-        if (multipartState_ != MultipartState_None)
-        {
-          throw OrthancException(ErrorCode_BadSequenceOfCalls);
-        }
-        else
-        {
-          multipartState_ = MultipartState_FirstPart;
-          multipartSubType_ = subType;
-          multipartContentType_ = contentType;
-        }
-      }
-
-      void SendMultipartItem(const void* data,
-                             size_t size,
-                             const std::map<std::string, std::string>& headers)
-      {
-        if (size != 0 && data == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        switch (multipartState_)
-        {
-          case MultipartState_None:
-            // Must call "StartMultipart()" before
-            throw OrthancException(ErrorCode_BadSequenceOfCalls);
-
-          case MultipartState_FirstPart:
-            multipartFirstPart_.assign(reinterpret_cast<const char*>(data), size);
-            multipartFirstHeaders_ = headers;
-            multipartState_ = MultipartState_SecondPart;
-            break;
-
-          case MultipartState_SecondPart:
-            // Start an actual stream for chunked transfer as soon as
-            // there are more than 2 elements in the multipart stream
-            output_.StartMultipart(multipartSubType_, multipartContentType_);
-            output_.SendMultipartItem(multipartFirstPart_.c_str(), multipartFirstPart_.size(), 
-                                      multipartFirstHeaders_);
-            multipartFirstPart_.clear();  // Release memory
-
-            output_.SendMultipartItem(data, size, headers);
-            multipartState_ = MultipartState_NextParts;
-            break;
-
-          case MultipartState_NextParts:
-            output_.SendMultipartItem(data, size, headers);
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-      }
-
-      void Close(OrthancPluginErrorCode error,
-                 PluginsErrorDictionary& dictionary)
-      {
-        if (error == OrthancPluginErrorCode_Success)
-        {
-          switch (multipartState_)
-          {
-            case MultipartState_None:
-              assert(!output_.IsWritingMultipart());
-              break;
-
-            case MultipartState_FirstPart:   // Multipart started, but no part was sent
-            case MultipartState_SecondPart:  // Multipart started, first part is pending
-            {
-              assert(!output_.IsWritingMultipart());
-              std::vector<const void*> parts;
-              std::vector<size_t> sizes;
-              std::vector<const std::map<std::string, std::string>*> headers;
-
-              if (multipartState_ == MultipartState_SecondPart)
-              {
-                parts.push_back(multipartFirstPart_.c_str());
-                sizes.push_back(multipartFirstPart_.size());
-                headers.push_back(&multipartFirstHeaders_);
-              }
-
-              output_.AnswerMultipartWithoutChunkedTransfer(multipartSubType_, multipartContentType_,
-                                                            parts, sizes, headers);
-              break;
-            }
-
-            case MultipartState_NextParts:
-              assert(output_.IsWritingMultipart());
-              output_.CloseMultipart();
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        else
-        {
-          dictionary.LogError(error, false);
-
-          if (HasErrorDetails())
-          {
-            throw OrthancException(static_cast<ErrorCode>(error),
-                                   GetErrorDetails(),
-                                   IsLogDetails());
-          }
-          else
-          {
-            throw OrthancException(static_cast<ErrorCode>(error));
-          }
-        }
-      }
-    };
-
-    
-    class RestCallback : public boost::noncopyable
-    {
-    private:
-      boost::regex              regex_;
-      OrthancPluginRestCallback callback_;
-      bool                      lock_;
-
-      OrthancPluginErrorCode InvokeInternal(PluginHttpOutput& output,
-                                            const std::string& flatUri,
-                                            const OrthancPluginHttpRequest& request)
-      {
-        return callback_(reinterpret_cast<OrthancPluginRestOutput*>(&output), 
-                         flatUri.c_str(), 
-                         &request);
-      }
-
-    public:
-      RestCallback(const char* regex,
-                   OrthancPluginRestCallback callback,
-                   bool lockRestCallbacks) :
-        regex_(regex),
-        callback_(callback),
-        lock_(lockRestCallbacks)
-      {
-      }
-
-      const boost::regex& GetRegularExpression() const
-      {
-        return regex_;
-      }
-
-      OrthancPluginErrorCode Invoke(boost::recursive_mutex& restCallbackMutex,
-                                    PluginHttpOutput& output,
-                                    const std::string& flatUri,
-                                    const OrthancPluginHttpRequest& request)
-      {
-        if (lock_)
-        {
-          boost::recursive_mutex::scoped_lock lock(restCallbackMutex);
-          return InvokeInternal(output, flatUri, request);
-        }
-        else
-        {
-          return InvokeInternal(output, flatUri, request);
-        }
-      }
-    };
-
-
-    class ChunkedRestCallback : public boost::noncopyable
-    {
-    private:
-      _OrthancPluginChunkedRestCallback parameters_;
-      boost::regex                      regex_;
-
-    public:
-      ChunkedRestCallback(_OrthancPluginChunkedRestCallback parameters) :
-        parameters_(parameters),
-        regex_(parameters.pathRegularExpression)
-      {
-      }
-
-      const boost::regex& GetRegularExpression() const
-      {
-        return regex_;
-      }
-
-      const _OrthancPluginChunkedRestCallback& GetParameters() const
-      {
-        return parameters_;
-      }
-    };
-
-
-
-    class StorageCommitmentScp : public IStorageCommitmentFactory
-    {
-    private:
-      class Handler : public IStorageCommitmentFactory::ILookupHandler
-      {
-      private:
-        _OrthancPluginRegisterStorageCommitmentScpCallback  parameters_;
-        void*    handler_;
-
-      public:
-        Handler(_OrthancPluginRegisterStorageCommitmentScpCallback  parameters,
-                void* handler) :
-          parameters_(parameters),
-          handler_(handler)
-        {
-          if (handler == NULL)
-          {
-            throw OrthancException(ErrorCode_NullPointer);
-          }
-        }
-
-        virtual ~Handler()
-        {
-          assert(handler_ != NULL);
-          parameters_.destructor(handler_);
-          handler_ = NULL;
-        }
-
-        virtual StorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
-                                                      const std::string& sopInstanceUid)
-        {
-          assert(handler_ != NULL);
-          OrthancPluginStorageCommitmentFailureReason reason =
-            OrthancPluginStorageCommitmentFailureReason_Success;
-          OrthancPluginErrorCode error = parameters_.lookup(
-            &reason, handler_, sopClassUid.c_str(), sopInstanceUid.c_str());
-          if (error == OrthancPluginErrorCode_Success)
-          {
-            return Plugins::Convert(reason);
-          }
-          else
-          {
-            throw OrthancException(static_cast<ErrorCode>(error));
-          }
-        }
-      };
-      
-      _OrthancPluginRegisterStorageCommitmentScpCallback  parameters_;
-
-    public:
-      StorageCommitmentScp(_OrthancPluginRegisterStorageCommitmentScpCallback parameters) :
-        parameters_(parameters)
-      {
-      }
-
-      virtual ILookupHandler* CreateStorageCommitment(
-        const std::string& jobId,
-        const std::string& transactionUid,
-        const std::vector<std::string>& sopClassUids,
-        const std::vector<std::string>& sopInstanceUids,
-        const std::string& remoteAet,
-        const std::string& calledAet) ORTHANC_OVERRIDE
-      {
-        const size_t n = sopClassUids.size();
-        
-        if (sopInstanceUids.size() != n)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        
-        std::vector<const char*> a, b;
-        a.resize(n);
-        b.resize(n);
-
-        for (size_t i = 0; i < n; i++)
-        {
-          a[i] = sopClassUids[i].c_str();
-          b[i] = sopInstanceUids[i].c_str();
-        }
-
-        void* handler = NULL;
-        OrthancPluginErrorCode error = parameters_.factory(
-          &handler, jobId.c_str(), transactionUid.c_str(),
-          a.empty() ? NULL : &a[0], b.empty() ? NULL : &b[0], static_cast<uint32_t>(n),
-          remoteAet.c_str(), calledAet.c_str());
-
-        if (error != OrthancPluginErrorCode_Success)
-        {
-          throw OrthancException(static_cast<ErrorCode>(error));          
-        }
-        else if (handler == NULL)
-        {
-          // This plugin won't handle this storage commitment request
-          return NULL;
-        }
-        else
-        {
-          return new Handler(parameters_, handler);
-        }
-      }
-    };
-
-
-    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<ChunkedRestCallback*>  ChunkedRestCallbacks;
-    typedef std::list<OrthancPluginOnStoredInstanceCallback>  OnStoredCallbacks;
-    typedef std::list<OrthancPluginOnChangeCallback>  OnChangeCallbacks;
-    typedef std::list<OrthancPluginIncomingHttpRequestFilter>  IncomingHttpRequestFilters;
-    typedef std::list<OrthancPluginIncomingHttpRequestFilter2>  IncomingHttpRequestFilters2;
-    typedef std::list<OrthancPluginIncomingDicomInstanceFilter>  IncomingDicomInstanceFilters;
-    typedef std::list<OrthancPluginDecodeImageCallback>  DecodeImageCallbacks;
-    typedef std::list<OrthancPluginTranscoderCallback>  TranscoderCallbacks;
-    typedef std::list<OrthancPluginJobsUnserializer>  JobsUnserializers;
-    typedef std::list<OrthancPluginRefreshMetricsCallback>  RefreshMetricsCallbacks;
-    typedef std::list<StorageCommitmentScp*>  StorageCommitmentScpCallbacks;
-    typedef std::map<Property, std::string>  Properties;
-
-    PluginsManager manager_;
-
-    RestCallbacks restCallbacks_;
-    ChunkedRestCallbacks chunkedRestCallbacks_;
-    OnStoredCallbacks  onStoredCallbacks_;
-    OnChangeCallbacks  onChangeCallbacks_;
-    OrthancPluginFindCallback  findCallback_;
-    OrthancPluginWorklistCallback  worklistCallback_;
-    DecodeImageCallbacks  decodeImageCallbacks_;
-    TranscoderCallbacks  transcoderCallbacks_;
-    JobsUnserializers  jobsUnserializers_;
-    _OrthancPluginMoveCallback moveCallbacks_;
-    IncomingHttpRequestFilters  incomingHttpRequestFilters_;
-    IncomingHttpRequestFilters2 incomingHttpRequestFilters2_;
-    IncomingDicomInstanceFilters  incomingDicomInstanceFilters_;
-    RefreshMetricsCallbacks refreshMetricsCallbacks_;
-    StorageCommitmentScpCallbacks storageCommitmentScpCallbacks_;
-    std::unique_ptr<StorageAreaFactory>  storageArea_;
-
-    boost::recursive_mutex restCallbackMutex_;
-    boost::recursive_mutex storedCallbackMutex_;
-    boost::recursive_mutex changeCallbackMutex_;
-    boost::mutex findCallbackMutex_;
-    boost::mutex worklistCallbackMutex_;
-    boost::shared_mutex decoderTranscoderMutex_;  // Changed from "boost::mutex" in Orthanc 1.7.0
-    boost::mutex jobsUnserializersMutex_;
-    boost::mutex refreshMetricsMutex_;
-    boost::mutex storageCommitmentScpMutex_;
-    boost::recursive_mutex invokeServiceMutex_;
-
-    Properties properties_;
-    int argc_;
-    char** argv_;
-    std::unique_ptr<OrthancPluginDatabase>  database_;
-    PluginsErrorDictionary  dictionary_;
-
-    PImpl() : 
-      context_(NULL), 
-      findCallback_(NULL),
-      worklistCallback_(NULL),
-      argc_(1),
-      argv_(NULL)
-    {
-      memset(&moveCallbacks_, 0, sizeof(moveCallbacks_));
-    }
-  };
-
-
-  
-  class OrthancPlugins::WorklistHandler : public IWorklistRequestHandler
-  {
-  private:
-    OrthancPlugins&  that_;
-    std::unique_ptr<HierarchicalMatcher> matcher_;
-    std::unique_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,
-                                                          "" /* no private creator */));
-          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
-    {
-      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::unique_ptr<ParsedDicomFile> summary(matcher_->Extract(f));
-      reinterpret_cast<DicomFindAnswers*>(answers)->Add(*summary);
-    }
-  };
-
-  
-  class OrthancPlugins::FindHandler : public IFindRequestHandler
-  {
-  private:
-    OrthancPlugins&            that_;
-    std::unique_ptr<DicomArray>  currentQuery_;
-
-    void Reset()
-    {
-      currentQuery_.reset(NULL);
-    }
-
-  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
-    {
-      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);
-      }
-    }
-  };
-  
-
-
-  class OrthancPlugins::MoveHandler : public IMoveRequestHandler
-  {
-  private:
-    class Driver : public IMoveRequestIterator
-    {
-    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();
-      }
-    }
-                        
-
-
-  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)
-      {
-        throw OrthancException(ErrorCode_Plugin,
-                               "Plugin cannot create a driver for an incoming C-MOVE request");
-      }
-
-      unsigned int size = params_.getMoveSize(driver);
-
-      return new Driver(driver, size, params_.applyMove, params_.freeMove);
-    }
-  };
-
-
-
-  class OrthancPlugins::HttpClientChunkedRequest : public HttpClient::IRequestBody
-  {
-  private:
-    const _OrthancPluginChunkedHttpClient&  params_;
-    PluginsErrorDictionary&                 errorDictionary_;
-
-  public:
-    HttpClientChunkedRequest(const _OrthancPluginChunkedHttpClient& params,
-                             PluginsErrorDictionary&  errorDictionary) :
-      params_(params),
-      errorDictionary_(errorDictionary)
-    {
-    }
-
-    virtual bool ReadNextChunk(std::string& chunk)
-    {
-      if (params_.requestIsDone(params_.request))
-      {
-        return false;
-      }
-      else
-      {
-        size_t size = params_.requestChunkSize(params_.request);
-
-        chunk.resize(size);
-        
-        if (size != 0)
-        {
-          const void* data = params_.requestChunkData(params_.request);
-          memcpy(&chunk[0], data, size);
-        }
-
-        OrthancPluginErrorCode error = params_.requestNext(params_.request);
-        
-        if (error != OrthancPluginErrorCode_Success)
-        {
-          errorDictionary_.LogError(error, true);
-          throw OrthancException(static_cast<ErrorCode>(error));
-        }
-        else
-        {
-          return true;
-        }
-      }
-    }
-  };
-
-
-  class OrthancPlugins::HttpClientChunkedAnswer : public HttpClient::IAnswer
-  {
-  private:
-    const _OrthancPluginChunkedHttpClient&  params_;
-    PluginsErrorDictionary&                 errorDictionary_;
-
-  public:
-    HttpClientChunkedAnswer(const _OrthancPluginChunkedHttpClient& params,
-                            PluginsErrorDictionary&  errorDictionary) :
-      params_(params),
-      errorDictionary_(errorDictionary)
-    {
-    }
-
-    virtual void AddHeader(const std::string& key,
-                           const std::string& value)
-    {
-      OrthancPluginErrorCode error = params_.answerAddHeader(params_.answer, key.c_str(), value.c_str());
-        
-      if (error != OrthancPluginErrorCode_Success)
-      {
-        errorDictionary_.LogError(error, true);
-        throw OrthancException(static_cast<ErrorCode>(error));
-      }
-    }
-      
-    virtual void AddChunk(const void* data,
-                          size_t size)
-    {
-      OrthancPluginErrorCode error = params_.answerAddChunk(params_.answer, data, size);
-        
-      if (error != OrthancPluginErrorCode_Success)
-      {
-        errorDictionary_.LogError(error, true);
-        throw OrthancException(static_cast<ErrorCode>(error));
-      }
-    }
-  };
-
-
-  OrthancPlugins::OrthancPlugins()
-  {
-    /* Sanity check of the compiler */
-    if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
-        sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
-        sizeof(int32_t) != sizeof(_OrthancPluginService) ||
-        sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
-        sizeof(int32_t) != sizeof(OrthancPluginPixelFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginContentType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginResourceType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginChangeType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
-        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) ||
-        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
-        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason) ||
-        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>(OrthancPluginCreateDicomFlags_DecodeDataUriScheme) != static_cast<int>(DicomFromJsonFlags_DecodeDataUriScheme) ||
-        static_cast<int>(OrthancPluginCreateDicomFlags_GenerateIdentifiers) != static_cast<int>(DicomFromJsonFlags_GenerateIdentifiers))
-
-    {
-      throw OrthancException(ErrorCode_Plugin);
-    }
-
-    pimpl_.reset(new PImpl());
-    pimpl_->manager_.RegisterServiceProvider(*this);
-  }
-
-  
-  void OrthancPlugins::SetServerContext(ServerContext& context)
-  {
-    pimpl_->SetServerContext(&context);
-  }
-
-
-  void OrthancPlugins::ResetServerContext()
-  {
-    pimpl_->SetServerContext(NULL);
-  }
-
-  
-  OrthancPlugins::~OrthancPlugins()
-  {
-    for (PImpl::RestCallbacks::iterator it = pimpl_->restCallbacks_.begin(); 
-         it != pimpl_->restCallbacks_.end(); ++it)
-    {
-      delete *it;
-    }
-
-    for (PImpl::ChunkedRestCallbacks::iterator it = pimpl_->chunkedRestCallbacks_.begin(); 
-         it != pimpl_->chunkedRestCallbacks_.end(); ++it)
-    {
-      delete *it;
-    }
-
-    for (PImpl::StorageCommitmentScpCallbacks::iterator
-           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
-         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
-    {
-      delete *it;
-    } 
-  }
-
-
-  static void ArgumentsToPlugin(std::vector<const char*>& keys,
-                                std::vector<const char*>& values,
-                                const IHttpHandler::Arguments& arguments)
-  {
-    keys.resize(arguments.size());
-    values.resize(arguments.size());
-
-    size_t pos = 0;
-    for (IHttpHandler::Arguments::const_iterator 
-           it = arguments.begin(); it != arguments.end(); ++it)
-    {
-      keys[pos] = it->first.c_str();
-      values[pos] = it->second.c_str();
-      pos++;
-    }
-  }
-
-
-  static void ArgumentsToPlugin(std::vector<const char*>& keys,
-                                std::vector<const char*>& values,
-                                const IHttpHandler::GetArguments& arguments)
-  {
-    keys.resize(arguments.size());
-    values.resize(arguments.size());
-
-    for (size_t i = 0; i < arguments.size(); i++)
-    {
-      keys[i] = arguments[i].first.c_str();
-      values[i] = arguments[i].second.c_str();
-    }
-  }
-
-
-  namespace
-  {
-    class RestCallbackMatcher : public boost::noncopyable
-    {
-    private:
-      std::string               flatUri_;
-      std::vector<std::string>  groups_;
-      std::vector<const char*>  cgroups_;
-      
-    public:
-      RestCallbackMatcher(const UriComponents& uri) :
-        flatUri_(Toolbox::FlattenUri(uri))
-      {
-      }
-
-      bool IsMatch(const boost::regex& re)
-      {
-        // Check whether the regular expression associated to this
-        // callback matches the URI
-        boost::cmatch what;
-
-        if (boost::regex_match(flatUri_.c_str(), what, re))
-        {
-          // Extract the value of the free parameters of the regular expression
-          if (what.size() > 1)
-          {
-            groups_.resize(what.size() - 1);
-            cgroups_.resize(what.size() - 1);
-            for (size_t i = 1; i < what.size(); i++)
-            {
-              groups_[i - 1] = what[i];
-              cgroups_[i - 1] = groups_[i - 1].c_str();
-            }
-          }
-
-          return true;
-        }
-        else
-        {
-          // Not a match
-          return false;
-        }
-      }
-
-      uint32_t GetGroupsCount() const
-      {
-        return cgroups_.size();
-      }
-
-      const char* const* GetGroups() const
-      {
-        return cgroups_.empty() ? NULL : &cgroups_[0];
-      }
-
-      const std::string& GetFlatUri() const
-      {
-        return flatUri_;
-      }
-    };
-
-
-    // WARNING - The lifetime of this internal object must be smaller
-    // than "matcher", "headers" and "getArguments" objects
-    class HttpRequestConverter
-    {
-    private:
-      std::vector<const char*>    getKeys_;
-      std::vector<const char*>    getValues_;
-      std::vector<const char*>    headersKeys_;
-      std::vector<const char*>    headersValues_;
-      OrthancPluginHttpRequest    converted_;
-
-    public:
-      HttpRequestConverter(const RestCallbackMatcher& matcher,
-                           HttpMethod method,
-                           const IHttpHandler::Arguments& headers)
-      {
-        memset(&converted_, 0, sizeof(OrthancPluginHttpRequest));
-
-        ArgumentsToPlugin(headersKeys_, headersValues_, headers);
-        assert(headersKeys_.size() == headersValues_.size());
-
-        switch (method)
-        {
-          case HttpMethod_Get:
-            converted_.method = OrthancPluginHttpMethod_Get;
-            break;
-
-          case HttpMethod_Post:
-            converted_.method = OrthancPluginHttpMethod_Post;
-            break;
-
-          case HttpMethod_Delete:
-            converted_.method = OrthancPluginHttpMethod_Delete;
-            break;
-
-          case HttpMethod_Put:
-            converted_.method = OrthancPluginHttpMethod_Put;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-
-        converted_.groups = matcher.GetGroups();
-        converted_.groupsCount = matcher.GetGroupsCount();
-        converted_.getCount = 0;
-        converted_.getKeys = NULL;
-        converted_.getValues = NULL;
-        converted_.body = NULL;
-        converted_.bodySize = 0;
-        converted_.headersCount = headers.size();
-       
-        if (headers.size() > 0)
-        {
-          converted_.headersKeys = &headersKeys_[0];
-          converted_.headersValues = &headersValues_[0];
-        }
-      }
-
-      void SetGetArguments(const IHttpHandler::GetArguments& getArguments)
-      {
-        ArgumentsToPlugin(getKeys_, getValues_, getArguments);
-        assert(getKeys_.size() == getValues_.size());
-
-        converted_.getCount = getArguments.size();
-
-        if (getArguments.size() > 0)
-        {
-          converted_.getKeys = &getKeys_[0];
-          converted_.getValues = &getValues_[0];
-        }
-      }
-
-      OrthancPluginHttpRequest& GetRequest()
-      {
-        return converted_;
-      }
-    };
-  }
-
-
-  static std::string GetAllowedMethods(_OrthancPluginChunkedRestCallback parameters)
-  {
-    std::string s;
-
-    if (parameters.getHandler != NULL)
-    {
-      s += "GET";
-    }
-
-    if (parameters.postHandler != NULL)
-    {
-      if (!s.empty())
-      {
-        s+= ",";
-      }
-      
-      s += "POST";
-    }
-
-    if (parameters.deleteHandler != NULL)
-    {
-      if (!s.empty())
-      {
-        s+= ",";
-      }
-      
-      s += "DELETE";
-    }
-
-    if (parameters.putHandler != NULL)
-    {
-      if (!s.empty())
-      {
-        s+= ",";
-      }
-      
-      s += "PUT";
-    }
-
-    return s;
-  }
-
-
-  bool OrthancPlugins::HandleChunkedGetDelete(HttpOutput& output,
-                                              HttpMethod method,
-                                              const UriComponents& uri,
-                                              const Arguments& headers,
-                                              const GetArguments& getArguments)
-  {
-    RestCallbackMatcher matcher(uri);
-
-    PImpl::ChunkedRestCallback* callback = NULL;
-
-    // Loop over the callbacks registered by the plugins
-    for (PImpl::ChunkedRestCallbacks::const_iterator it = pimpl_->chunkedRestCallbacks_.begin(); 
-         it != pimpl_->chunkedRestCallbacks_.end(); ++it)
-    {
-      if (matcher.IsMatch((*it)->GetRegularExpression()))
-      {
-        callback = *it;
-        break;
-      }
-    }
-
-    if (callback == NULL)
-    {
-      return false;
-    }
-    else
-    {
-      LOG(INFO) << "Delegating HTTP request to plugin for URI: " << matcher.GetFlatUri();
-
-      OrthancPluginRestCallback handler;
-
-      switch (method)
-      {
-        case HttpMethod_Get:
-          handler = callback->GetParameters().getHandler;
-          break;
-
-        case HttpMethod_Delete:
-          handler = callback->GetParameters().deleteHandler;
-          break;
-
-        default:
-          handler = NULL;
-          break;
-      }
-
-      if (handler == NULL)
-      {
-        output.SendMethodNotAllowed(GetAllowedMethods(callback->GetParameters()));
-      }
-      else
-      {
-        HttpRequestConverter converter(matcher, method, headers);
-        converter.SetGetArguments(getArguments);
-      
-        PImpl::PluginHttpOutput pluginOutput(output);
-        
-        OrthancPluginErrorCode error = handler(
-          reinterpret_cast<OrthancPluginRestOutput*>(&pluginOutput), 
-          matcher.GetFlatUri().c_str(), &converter.GetRequest());
-        
-        pluginOutput.Close(error, GetErrorDictionary());
-      }
-      
-      return true;
-    }
-  }
-
-
-  bool OrthancPlugins::Handle(HttpOutput& output,
-                              RequestOrigin /*origin*/,
-                              const char* /*remoteIp*/,
-                              const char* /*username*/,
-                              HttpMethod method,
-                              const UriComponents& uri,
-                              const Arguments& headers,
-                              const GetArguments& getArguments,
-                              const void* bodyData,
-                              size_t bodySize)
-  {
-    RestCallbackMatcher matcher(uri);
-
-    PImpl::RestCallback* callback = NULL;
-
-    // Loop over the callbacks registered by the plugins
-    for (PImpl::RestCallbacks::const_iterator it = pimpl_->restCallbacks_.begin(); 
-         it != pimpl_->restCallbacks_.end(); ++it)
-    {
-      if (matcher.IsMatch((*it)->GetRegularExpression()))
-      {
-        callback = *it;
-        break;
-      }
-    }
-
-    if (callback == NULL)
-    {
-      // Callback not found, try to find a chunked callback
-      return HandleChunkedGetDelete(output, method, uri, headers, getArguments);
-    }
-
-    LOG(INFO) << "Delegating HTTP request to plugin for URI: " << matcher.GetFlatUri();
-
-    HttpRequestConverter converter(matcher, method, headers);
-    converter.SetGetArguments(getArguments);
-    converter.GetRequest().body = bodyData;
-    converter.GetRequest().bodySize = bodySize;
-
-    PImpl::PluginHttpOutput pluginOutput(output);
-
-    assert(callback != NULL);
-    OrthancPluginErrorCode error = callback->Invoke
-      (pimpl_->restCallbackMutex_, pluginOutput, matcher.GetFlatUri(), converter.GetRequest());
-
-    pluginOutput.Close(error, GetErrorDictionary());
-    return true;
-  }
-
-
-  class OrthancPlugins::IDicomInstance : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomInstance()
-    {
-    }
-
-    virtual bool CanBeFreed() const = 0;
-
-    virtual const DicomInstanceToStore& GetInstance() const = 0;
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromCallback : public IDicomInstance
-  {
-  private:
-    const DicomInstanceToStore&  instance_;
-
-  public:
-    DicomInstanceFromCallback(const DicomInstanceToStore& instance) :
-      instance_(instance)
-    {
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromBuffer : public IDicomInstance
-  {
-  private:
-    std::string           buffer_;
-    DicomInstanceToStore  instance_;
-
-  public:
-    DicomInstanceFromBuffer(const void* buffer,
-                            size_t size)
-    {
-      buffer_.assign(reinterpret_cast<const char*>(buffer), size);
-      instance_.SetBuffer(buffer_.empty() ? NULL : buffer_.c_str(), buffer_.size());
-      instance_.SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return instance_;
-    };
-  };
-
-
-  class OrthancPlugins::DicomInstanceFromTranscoded : public IDicomInstance
-  {
-  private:
-    std::unique_ptr<ParsedDicomFile>  parsed_;
-    DicomInstanceToStore              instance_;
-
-  public:
-    DicomInstanceFromTranscoded(IDicomTranscoder::DicomImage& transcoded) :
-      parsed_(transcoded.ReleaseAsParsedDicomFile())
-    {
-      instance_.SetParsedDicomFile(*parsed_);
-      instance_.SetOrigin(DicomInstanceOrigin::FromPlugins());
-    }
-
-    virtual bool CanBeFreed() const ORTHANC_OVERRIDE
-    {
-      return true;
-    }
-
-    virtual const DicomInstanceToStore& GetInstance() const ORTHANC_OVERRIDE
-    {
-      return instance_;
-    };
-  };
-
-
-  void OrthancPlugins::SignalStoredInstance(const std::string& instanceId,
-                                            const DicomInstanceToStore& instance,
-                                            const Json::Value& simplifiedTags)
-  {
-    DicomInstanceFromCallback wrapped(instance);
-    
-    boost::recursive_mutex::scoped_lock lock(pimpl_->storedCallbackMutex_);
-
-    for (PImpl::OnStoredCallbacks::const_iterator
-           callback = pimpl_->onStoredCallbacks_.begin(); 
-         callback != pimpl_->onStoredCallbacks_.end(); ++callback)
-    {
-      OrthancPluginErrorCode error = (*callback) (
-        reinterpret_cast<OrthancPluginDicomInstance*>(&wrapped),
-        instanceId.c_str());
-
-      if (error != OrthancPluginErrorCode_Success)
-      {
-        GetErrorDictionary().LogError(error, true);
-        throw OrthancException(static_cast<ErrorCode>(error));
-      }
-    }
-  }
-
-
-  bool OrthancPlugins::FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                              const Json::Value& simplified)
-  {
-    DicomInstanceFromCallback wrapped(instance);
-    
-    boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
-    
-    for (PImpl::IncomingDicomInstanceFilters::const_iterator
-           filter = pimpl_->incomingDicomInstanceFilters_.begin();
-         filter != pimpl_->incomingDicomInstanceFilters_.end(); ++filter)
-    {
-      int32_t allowed = (*filter) (reinterpret_cast<const OrthancPluginDicomInstance*>(&wrapped));
-
-      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;
-  }
-
-  
-  void OrthancPlugins::SignalChangeInternal(OrthancPluginChangeType changeType,
-                                            OrthancPluginResourceType resourceType,
-                                            const char* resource)
-  {
-    boost::recursive_mutex::scoped_lock lock(pimpl_->changeCallbackMutex_);
-
-    for (std::list<OrthancPluginOnChangeCallback>::const_iterator 
-           callback = pimpl_->onChangeCallbacks_.begin(); 
-         callback != pimpl_->onChangeCallbacks_.end(); ++callback)
-    {
-      OrthancPluginErrorCode error = (*callback) (changeType, resourceType, resource);
-
-      if (error != OrthancPluginErrorCode_Success)
-      {
-        GetErrorDictionary().LogError(error, true);
-        throw OrthancException(static_cast<ErrorCode>(error));
-      }
-    }
-  }
-
-
-
-  void OrthancPlugins::SignalChange(const ServerIndexChange& change)
-  {
-    SignalChangeInternal(Plugins::Convert(change.GetChangeType()),
-                         Plugins::Convert(change.GetResourceType()),
-                         change.GetPublicId().c_str());
-  }
-
-
-
-  void OrthancPlugins::RegisterRestCallback(const void* parameters,
-                                            bool lock)
-  {
-    const _OrthancPluginRestCallback& p = 
-      *reinterpret_cast<const _OrthancPluginRestCallback*>(parameters);
-
-    LOG(INFO) << "Plugin has registered a REST callback "
-              << (lock ? "with" : "without")
-              << " mutual exclusion on: " 
-              << p.pathRegularExpression;
-
-    pimpl_->restCallbacks_.push_back(new PImpl::RestCallback(p.pathRegularExpression, p.callback, lock));
-  }
-
-
-  void OrthancPlugins::RegisterChunkedRestCallback(const void* parameters)
-  {
-    const _OrthancPluginChunkedRestCallback& p = 
-      *reinterpret_cast<const _OrthancPluginChunkedRestCallback*>(parameters);
-
-    LOG(INFO) << "Plugin has registered a REST callback for chunked streams on: " 
-              << p.pathRegularExpression;
-
-    pimpl_->chunkedRestCallbacks_.push_back(new PImpl::ChunkedRestCallback(p));
-  }
-
-
-  void OrthancPlugins::RegisterOnStoredInstanceCallback(const void* parameters)
-  {
-    const _OrthancPluginOnStoredInstanceCallback& p = 
-      *reinterpret_cast<const _OrthancPluginOnStoredInstanceCallback*>(parameters);
-
-    LOG(INFO) << "Plugin has registered an OnStoredInstance callback";
-    pimpl_->onStoredCallbacks_.push_back(p.callback);
-  }
-
-
-  void OrthancPlugins::RegisterOnChangeCallback(const void* parameters)
-  {
-    const _OrthancPluginOnChangeCallback& p = 
-      *reinterpret_cast<const _OrthancPluginOnChangeCallback*>(parameters);
-
-    LOG(INFO) << "Plugin has registered an OnChange callback";
-    pimpl_->onChangeCallbacks_.push_back(p.callback);
-  }
-
-
-  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)
-    {
-      throw OrthancException(ErrorCode_Plugin,
-                             "Can only register one plugin to handle modality worklists");
-    }
-    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)
-    {
-      throw OrthancException(ErrorCode_Plugin,
-                             "Can only register one plugin to handle C-FIND requests");
-    }
-    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)
-    {
-      throw OrthancException(ErrorCode_Plugin,
-                             "Can only register one plugin to handle C-MOVE requests");
-    }
-    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::unique_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
-
-    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::RegisterTranscoderCallback(const void* parameters)
-  {
-    const _OrthancPluginTranscoderCallback& p = 
-      *reinterpret_cast<const _OrthancPluginTranscoderCallback*>(parameters);
-
-    boost::unique_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
-
-    pimpl_->transcoderCallbacks_.push_back(p.callback);
-    LOG(INFO) << "Plugin has registered a callback to transcode DICOM images (" 
-              << pimpl_->transcoderCallbacks_.size() << " transcoder(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::RegisterIncomingDicomInstanceFilter(const void* parameters)
-  {
-    const _OrthancPluginIncomingDicomInstanceFilter& p = 
-      *reinterpret_cast<const _OrthancPluginIncomingDicomInstanceFilter*>(parameters);
-
-    LOG(INFO) << "Plugin has registered a callback to filter incoming DICOM instances";
-    pimpl_->incomingDicomInstanceFilters_.push_back(p.callback);
-  }
-
-
-  void OrthancPlugins::RegisterRefreshMetricsCallback(const void* parameters)
-  {
-    const _OrthancPluginRegisterRefreshMetricsCallback& p = 
-      *reinterpret_cast<const _OrthancPluginRegisterRefreshMetricsCallback*>(parameters);
-
-    boost::mutex::scoped_lock lock(pimpl_->refreshMetricsMutex_);
-
-    LOG(INFO) << "Plugin has registered a callback to refresh its metrics";
-    pimpl_->refreshMetricsCallbacks_.push_back(p.callback);
-  }
-
-
-  void OrthancPlugins::RegisterStorageCommitmentScpCallback(const void* parameters)
-  {
-    const _OrthancPluginRegisterStorageCommitmentScpCallback& p = 
-      *reinterpret_cast<const _OrthancPluginRegisterStorageCommitmentScpCallback*>(parameters);
-
-    boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_);
-    LOG(INFO) << "Plugin has registered a storage commitment callback";
-
-    pimpl_->storageCommitmentScpCallbacks_.push_back(new PImpl::StorageCommitmentScp(p));
-  }
-
-
-  void OrthancPlugins::AnswerBuffer(const void* parameters)
-  {
-    const _OrthancPluginAnswerBuffer& p = 
-      *reinterpret_cast<const _OrthancPluginAnswerBuffer*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.SetContentType(p.mimeType);
-    translatedOutput.Answer(p.answer, p.answerSize);
-  }
-
-
-  void OrthancPlugins::Redirect(const void* parameters)
-  {
-    const _OrthancPluginOutputPlusArgument& p = 
-      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.Redirect(p.argument);
-  }
-
-
-  void OrthancPlugins::SendHttpStatusCode(const void* parameters)
-  {
-    const _OrthancPluginSendHttpStatusCode& p = 
-      *reinterpret_cast<const _OrthancPluginSendHttpStatusCode*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.SendStatus(static_cast<HttpStatus>(p.status));
-  }
-
-
-  void OrthancPlugins::SendHttpStatus(const void* parameters)
-  {
-    const _OrthancPluginSendHttpStatus& p = 
-      *reinterpret_cast<const _OrthancPluginSendHttpStatus*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    HttpStatus status = static_cast<HttpStatus>(p.status);
-
-    if (p.bodySize > 0 && p.body != NULL)
-    {
-      translatedOutput.SendStatus(status, p.body, p.bodySize);
-    }
-    else
-    {
-      translatedOutput.SendStatus(status);
-    }
-  }
-
-
-  void OrthancPlugins::SendUnauthorized(const void* parameters)
-  {
-    const _OrthancPluginOutputPlusArgument& p = 
-      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.SendUnauthorized(p.argument);
-  }
-
-
-  void OrthancPlugins::SendMethodNotAllowed(const void* parameters)
-  {
-    const _OrthancPluginOutputPlusArgument& p = 
-      *reinterpret_cast<const _OrthancPluginOutputPlusArgument*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.SendMethodNotAllowed(p.argument);
-  }
-
-
-  void OrthancPlugins::SetCookie(const void* parameters)
-  {
-    const _OrthancPluginSetHttpHeader& p = 
-      *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.SetCookie(p.key, p.value);
-  }
-
-
-  void OrthancPlugins::SetHttpHeader(const void* parameters)
-  {
-    const _OrthancPluginSetHttpHeader& p = 
-      *reinterpret_cast<const _OrthancPluginSetHttpHeader*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-    translatedOutput.AddHeader(p.key, p.value);
-  }
-
-
-  void OrthancPlugins::SetHttpErrorDetails(const void* parameters)
-  {
-    const _OrthancPluginSetHttpErrorDetails& p = 
-      *reinterpret_cast<const _OrthancPluginSetHttpErrorDetails*>(parameters);
-
-    PImpl::PluginHttpOutput* output =
-      reinterpret_cast<PImpl::PluginHttpOutput*>(p.output);
-    output->SetErrorDetails(p.details, p.log);
-  }
-
-
-  void OrthancPlugins::CompressAndAnswerPngImage(const void* parameters)
-  {
-    // Bridge for backward compatibility with Orthanc <= 0.9.3
-    const _OrthancPluginCompressAndAnswerPngImage& p = 
-      *reinterpret_cast<const _OrthancPluginCompressAndAnswerPngImage*>(parameters);
-
-    _OrthancPluginCompressAndAnswerImage p2;
-    p2.output = p.output;
-    p2.imageFormat = OrthancPluginImageFormat_Png;
-    p2.pixelFormat = p.format;
-    p2.width = p.width;
-    p2.height = p.height;
-    p2.pitch = p.height;
-    p2.buffer = p.buffer;
-    p2.quality = 0;
-
-    CompressAndAnswerImage(&p2);
-  }
-
-
-  void OrthancPlugins::CompressAndAnswerImage(const void* parameters)
-  {
-    const _OrthancPluginCompressAndAnswerImage& p = 
-      *reinterpret_cast<const _OrthancPluginCompressAndAnswerImage*>(parameters);
-
-    HttpOutput& translatedOutput = reinterpret_cast<PImpl::PluginHttpOutput*>(p.output)->GetOutput();
-
-    ImageAccessor accessor;
-    accessor.AssignReadOnly(Plugins::Convert(p.pixelFormat), p.width, p.height, p.pitch, p.buffer);
-
-    std::string compressed;
-
-    switch (p.imageFormat)
-    {
-      case OrthancPluginImageFormat_Png:
-      {
-        PngWriter writer;
-        writer.WriteToMemory(compressed, accessor);
-        translatedOutput.SetContentType(MimeType_Png);
-        break;
-      }
-
-      case OrthancPluginImageFormat_Jpeg:
-      {
-        JpegWriter writer;
-        writer.SetQuality(p.quality);
-        writer.WriteToMemory(compressed, accessor);
-        translatedOutput.SetContentType(MimeType_Jpeg);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    translatedOutput.Answer(compressed);
-  }
-
-
-  void OrthancPlugins::GetDicomForInstance(const void* parameters)
-  {
-    const _OrthancPluginGetDicomForInstance& p = 
-      *reinterpret_cast<const _OrthancPluginGetDicomForInstance*>(parameters);
-
-    std::string dicom;
-
-    {
-      PImpl::ServerContextLock lock(*pimpl_);
-      lock.GetContext().ReadDicom(dicom, p.instanceId);
-    }
-
-    CopyToMemoryBuffer(*p.target, dicom);
-  }
-
-
-  void OrthancPlugins::RestApiGet(const void* parameters,
-                                  bool afterPlugins)
-  {
-    const _OrthancPluginRestApiGet& p = 
-      *reinterpret_cast<const _OrthancPluginRestApiGet*>(parameters);
-        
-    LOG(INFO) << "Plugin making REST GET call on URI " << p.uri
-              << (afterPlugins ? " (after plugins)" : " (built-in API)");
-
-    IHttpHandler* handler;
-
-    {
-      PImpl::ServerContextLock lock(*pimpl_);
-      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
-    }
-
-    std::map<std::string, std::string> httpHeaders;
-
-    std::string result;
-    if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri, httpHeaders))
-    {
-      CopyToMemoryBuffer(*p.target, result);
-    }
-    else
-    {
-      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);
-    }
-  }
-
-
-  void OrthancPlugins::RestApiPostPut(bool isPost, 
-                                      const void* parameters,
-                                      bool afterPlugins)
-  {
-    const _OrthancPluginRestApiPostPut& p = 
-      *reinterpret_cast<const _OrthancPluginRestApiPostPut*>(parameters);
-
-    LOG(INFO) << "Plugin making REST " << EnumerationToString(isPost ? HttpMethod_Post : HttpMethod_Put)
-              << " call on URI " << p.uri << (afterPlugins ? " (after plugins)" : " (built-in API)");
-
-    IHttpHandler* handler;
-
-    {
-      PImpl::ServerContextLock lock(*pimpl_);
-      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
-    }
-      
-    std::map<std::string, std::string> httpHeaders;
-
-    std::string result;
-    if (isPost ? 
-        HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders) :
-        HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders))
-    {
-      CopyToMemoryBuffer(*p.target, result);
-    }
-    else
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  void OrthancPlugins::RestApiDelete(const void* parameters,
-                                     bool afterPlugins)
-  {
-    const char* uri = reinterpret_cast<const char*>(parameters);
-    LOG(INFO) << "Plugin making REST DELETE call on URI " << uri
-              << (afterPlugins ? " (after plugins)" : " (built-in API)");
-
-    IHttpHandler* handler;
-
-    {
-      PImpl::ServerContextLock lock(*pimpl_);
-      handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
-    }
-      
-    std::map<std::string, std::string> httpHeaders;
-
-    if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri, httpHeaders))
-    {
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  void OrthancPlugins::LookupResource(_OrthancPluginService service,
-                                      const void* parameters)
-  {
-    const _OrthancPluginRetrieveDynamicString& p = 
-      *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters);
-
-    /**
-     * The enumeration below only uses the tags that are indexed in
-     * the Orthanc database. It reflects the
-     * "CandidateResources::ApplyFilter()" method of the
-     * "OrthancFindRequestHandler" class.
-     **/
-
-    DicomTag tag(0, 0);
-    ResourceType level;
-    switch (service)
-    {
-      case _OrthancPluginService_LookupPatient:
-        tag = DICOM_TAG_PATIENT_ID;
-        level = ResourceType_Patient;
-        break;
-
-      case _OrthancPluginService_LookupStudy:
-        tag = DICOM_TAG_STUDY_INSTANCE_UID;
-        level = ResourceType_Study;
-        break;
-
-      case _OrthancPluginService_LookupStudyWithAccessionNumber:
-        tag = DICOM_TAG_ACCESSION_NUMBER;
-        level = ResourceType_Study;
-        break;
-
-      case _OrthancPluginService_LookupSeries:
-        tag = DICOM_TAG_SERIES_INSTANCE_UID;
-        level = ResourceType_Series;
-        break;
-
-      case _OrthancPluginService_LookupInstance:
-        tag = DICOM_TAG_SOP_INSTANCE_UID;
-        level = ResourceType_Instance;
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    std::vector<std::string> result;
-
-    {
-      PImpl::ServerContextLock lock(*pimpl_);
-      lock.GetContext().GetIndex().LookupIdentifierExact(result, level, tag, p.argument);
-    }
-
-    if (result.size() == 1)
-    {
-      *p.result = CopyString(result[0]);
-    }
-    else
-    {
-      if (result.size() > 1)
-      {
-        LOG(WARNING) << "LookupResource(): Multiple resources match the query (instead of 0 or 1), which indicates "
-                     << "your DICOM database breaks the DICOM model of the real world";
-      }
-      
-      throw OrthancException(ErrorCode_UnknownResource);
-    }
-  }
-
-
-  static void AccessInstanceMetadataInternal(bool checkExistence,
-                                             const _OrthancPluginAccessDicomInstance& params,
-                                             const DicomInstanceToStore& instance)
-  {
-    MetadataType metadata;
-
-    try
-    {
-      metadata = StringToMetadata(params.key);
-    }
-    catch (OrthancException&)
-    {
-      // Unknown metadata
-      if (checkExistence)
-      {
-        *params.resultInt64 = -1;
-      }
-      else
-      {
-        *params.resultString = NULL;
-      }
-
-      return;
-    }
-
-    ServerIndex::MetadataMap::const_iterator it = 
-      instance.GetMetadata().find(std::make_pair(ResourceType_Instance, metadata));
-
-    if (checkExistence)
-    {
-      if (it != instance.GetMetadata().end())
-      {
-        *params.resultInt64 = 1;
-      }
-      else
-      {
-        *params.resultInt64 = 0;
-      }
-    }
-    else
-    {
-      if (it != instance.GetMetadata().end())
-      {      
-        *params.resultString = it->second.c_str();
-      }
-      else
-      {
-        // Error: Missing metadata
-        *params.resultString = NULL;
-      }
-    }
-  }
-
-
-  void OrthancPlugins::AccessDicomInstance(_OrthancPluginService service,
-                                           const void* parameters)
-  {
-    const _OrthancPluginAccessDicomInstance& p = 
-      *reinterpret_cast<const _OrthancPluginAccessDicomInstance*>(parameters);
-
-    if (p.instance == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    const DicomInstanceToStore& instance =
-      reinterpret_cast<const IDicomInstance*>(p.instance)->GetInstance();
-
-    switch (service)
-    {
-      case _OrthancPluginService_GetInstanceRemoteAet:
-        *p.resultString = instance.GetOrigin().GetRemoteAetC();
-        return;
-
-      case _OrthancPluginService_GetInstanceSize:
-        *p.resultInt64 = instance.GetBufferSize();
-        return;
-
-      case _OrthancPluginService_GetInstanceData:
-        *p.resultString = reinterpret_cast<const char*>(instance.GetBufferData());
-        return;
-
-      case _OrthancPluginService_HasInstanceMetadata:
-        AccessInstanceMetadataInternal(true, p, instance);
-        return;
-
-      case _OrthancPluginService_GetInstanceMetadata:
-        AccessInstanceMetadataInternal(false, p, instance);
-        return;
-
-      case _OrthancPluginService_GetInstanceJson:
-      case _OrthancPluginService_GetInstanceSimplifiedJson:
-      {
-        Json::StyledWriter writer;
-        std::string s;
-
-        if (service == _OrthancPluginService_GetInstanceJson)
-        {
-          s = writer.write(instance.GetJson());
-        }
-        else
-        {
-          Json::Value simplified;
-          ServerToolbox::SimplifyTags(simplified, instance.GetJson(), DicomToJsonFormat_Human);
-          s = writer.write(simplified);
-        }
-
-        *p.resultStringToFree = CopyString(s);
-        return;
-      }
-
-      case _OrthancPluginService_GetInstanceOrigin:   // New in Orthanc 0.9.5
-        *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin());
-        return;
-
-      case _OrthancPluginService_GetInstanceTransferSyntaxUid:   // New in Orthanc 1.6.1
-      {
-        std::string s;
-        if (!instance.LookupTransferSyntax(s))
-        {
-          s.clear();
-        }
-        
-        *p.resultStringToFree = CopyString(s);
-        return;
-      }
-
-      case _OrthancPluginService_HasInstancePixelData:   // New in Orthanc 1.6.1
-        *p.resultInt64 = instance.HasPixelData();
-        return;
-
-      case _OrthancPluginService_GetInstanceFramesCount:  // New in Orthanc 1.7.0
-        *p.resultInt64 = instance.GetParsedDicomFile().GetFramesCount();
-        return;
-        
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  void OrthancPlugins::BufferCompression(const void* parameters)
-  {
-    const _OrthancPluginBufferCompression& p = 
-      *reinterpret_cast<const _OrthancPluginBufferCompression*>(parameters);
-
-    std::string result;
-
-    {
-      std::unique_ptr<DeflateBaseCompressor> compressor;
-
-      switch (p.compression)
-      {
-        case OrthancPluginCompressionType_Zlib:
-        {
-          compressor.reset(new ZlibCompressor);
-          compressor->SetPrefixWithUncompressedSize(false);
-          break;
-        }
-
-        case OrthancPluginCompressionType_ZlibWithSize:
-        {
-          compressor.reset(new ZlibCompressor);
-          compressor->SetPrefixWithUncompressedSize(true);
-          break;
-        }
-
-        case OrthancPluginCompressionType_Gzip:
-        {
-          compressor.reset(new GzipCompressor);
-          compressor->SetPrefixWithUncompressedSize(false);
-          break;
-        }
-
-        case OrthancPluginCompressionType_GzipWithSize:
-        {
-          compressor.reset(new GzipCompressor);
-          compressor->SetPrefixWithUncompressedSize(true);
-          break;
-        }
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-
-      if (p.uncompress)
-      {
-        compressor->Uncompress(result, p.source, p.size);
-      }
-      else
-      {
-        compressor->Compress(result, p.source, p.size);
-      }
-    }
-
-    CopyToMemoryBuffer(*p.target, result);
-  }
-
-
-  static OrthancPluginImage* ReturnImage(std::unique_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.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    
-    if (image->IsReadOnly())
-    {
-      std::unique_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::AccessDicomInstance2(_OrthancPluginService service,
-                                            const void* parameters)
-  {
-    const _OrthancPluginAccessDicomInstance2& p = 
-      *reinterpret_cast<const _OrthancPluginAccessDicomInstance2*>(parameters);
-
-    if (p.instance == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    const DicomInstanceToStore& instance =
-      reinterpret_cast<const IDicomInstance*>(p.instance)->GetInstance();
-
-    switch (service)
-    {
-      case _OrthancPluginService_GetInstanceFramesCount:
-        *p.targetUint32 = instance.GetParsedDicomFile().GetFramesCount();
-        return;
-        
-      case _OrthancPluginService_GetInstanceRawFrame:
-      {
-        if (p.targetBuffer == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        p.targetBuffer->data = NULL;
-        p.targetBuffer->size = 0;
-        
-        MimeType mime;
-        std::string frame;
-        instance.GetParsedDicomFile().GetRawFrame(frame, mime, p.frameIndex);
-        CopyToMemoryBuffer(*p.targetBuffer, frame);
-        return;
-      }
-        
-      case _OrthancPluginService_GetInstanceDecodedFrame:
-      {
-        if (p.targetImage == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        std::unique_ptr<ImageAccessor> decoded;
-        {
-          PImpl::ServerContextLock lock(*pimpl_);
-          decoded.reset(lock.GetContext().DecodeDicomFrame(instance, p.frameIndex));
-        }
-        
-        *(p.targetImage) = ReturnImage(decoded);
-        return;
-      }
-        
-      case _OrthancPluginService_SerializeDicomInstance:
-      {
-        if (p.targetBuffer == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        p.targetBuffer->data = NULL;
-        p.targetBuffer->size = 0;
-        
-        std::string serialized;
-        instance.GetParsedDicomFile().SaveToMemoryBuffer(serialized);
-        CopyToMemoryBuffer(*p.targetBuffer, serialized);
-        return;
-      }
-
-      case _OrthancPluginService_GetInstanceAdvancedJson:
-      {
-        if (p.targetStringToFree == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-        
-        Json::Value json;
-        instance.GetParsedDicomFile().DatasetToJson(
-          json, Plugins::Convert(p.format), 
-          static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
-
-        Json::FastWriter writer;
-        *p.targetStringToFree = CopyString(writer.write(json));        
-        return;
-      }
-      
-      case _OrthancPluginService_GetInstanceDicomWebJson:
-      case _OrthancPluginService_GetInstanceDicomWebXml:
-      {
-        if (p.targetStringToFree == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        DicomWebBinaryFormatter formatter(p.dicomWebCallback, p.dicomWebPayload);
-        formatter.Apply(p.targetStringToFree,
-                        (service == _OrthancPluginService_GetInstanceDicomWebJson),
-                        instance.GetParsedDicomFile());
-        return;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  void OrthancPlugins::UncompressImage(const void* parameters)
-  {
-    const _OrthancPluginUncompressImage& p = *reinterpret_cast<const _OrthancPluginUncompressImage*>(parameters);
-
-    std::unique_ptr<ImageAccessor> image;
-
-    switch (p.format)
-    {
-      case OrthancPluginImageFormat_Png:
-      {
-        image.reset(new PngReader);
-        reinterpret_cast<PngReader&>(*image).ReadFromMemory(p.data, p.size);
-        break;
-      }
-
-      case OrthancPluginImageFormat_Jpeg:
-      {
-        image.reset(new JpegReader);
-        reinterpret_cast<JpegReader&>(*image).ReadFromMemory(p.data, p.size);
-        break;
-      }
-
-      case OrthancPluginImageFormat_Dicom:
-      {
-        PImpl::ServerContextLock lock(*pimpl_);
-        image.reset(lock.GetContext().DecodeDicomFrame(p.data, p.size, 0));
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    *(p.target) = ReturnImage(image);
-  }
-
-
-  void OrthancPlugins::CompressImage(const void* parameters)
-  {
-    const _OrthancPluginCompressImage& p = *reinterpret_cast<const _OrthancPluginCompressImage*>(parameters);
-
-    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, accessor);
-        break;
-      }
-
-      case OrthancPluginImageFormat_Jpeg:
-      {
-        JpegWriter writer;
-        writer.SetQuality(p.quality);
-        writer.WriteToMemory(compressed, accessor);
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    CopyToMemoryBuffer(*p.target, compressed.size() > 0 ? compressed.c_str() : NULL, compressed.size());
-  }
-
-
-  static void SetupHttpClient(HttpClient& client,
-                              const _OrthancPluginCallHttpClient2& parameters)
-  {
-    client.SetUrl(parameters.url);
-    client.SetConvertHeadersToLowerCase(false);
-
-    if (parameters.timeout != 0)
-    {
-      client.SetTimeout(parameters.timeout);
-    }
-
-    if (parameters.username != NULL && 
-        parameters.password != NULL)
-    {
-      client.SetCredentials(parameters.username, parameters.password);
-    }
-
-    if (parameters.certificateFile != NULL)
-    {
-      std::string certificate(parameters.certificateFile);
-      std::string key, password;
-
-      if (parameters.certificateKeyFile)
-      {
-        key.assign(parameters.certificateKeyFile);
-      }
-
-      if (parameters.certificateKeyPassword)
-      {
-        password.assign(parameters.certificateKeyPassword);
-      }
-
-      client.SetClientCertificate(certificate, key, password);
-    }
-
-    client.SetPkcs11Enabled(parameters.pkcs11 ? true : false);
-
-    for (uint32_t i = 0; i < parameters.headersCount; i++)
-    {
-      if (parameters.headersKeys[i] == NULL ||
-          parameters.headersValues[i] == NULL)
-      {
-        throw OrthancException(ErrorCode_NullPointer);
-      }
-
-      client.AddHeader(parameters.headersKeys[i], parameters.headersValues[i]);
-    }
-
-    switch (parameters.method)
-    {
-      case OrthancPluginHttpMethod_Get:
-        client.SetMethod(HttpMethod_Get);
-        break;
-
-      case OrthancPluginHttpMethod_Post:
-        client.SetMethod(HttpMethod_Post);
-        client.GetBody().assign(reinterpret_cast<const char*>(parameters.body), parameters.bodySize);
-        break;
-
-      case OrthancPluginHttpMethod_Put:
-        client.SetMethod(HttpMethod_Put);
-        client.GetBody().assign(reinterpret_cast<const char*>(parameters.body), parameters.bodySize);
-        break;
-
-      case OrthancPluginHttpMethod_Delete:
-        client.SetMethod(HttpMethod_Delete);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  static void ExecuteHttpClientWithoutChunkedBody(uint16_t& httpStatus,
-                                                  OrthancPluginMemoryBuffer* answerBody,
-                                                  OrthancPluginMemoryBuffer* answerHeaders,
-                                                  HttpClient& client)
-  {
-    if (answerBody == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    std::string body;
-    HttpClient::HttpHeaders headers;
-
-    bool success = client.Apply(body, headers);
-
-    // The HTTP request has succeeded
-    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 (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(*answerHeaders, s);
-    }
-
-    // Copy the body of the answer if it makes sense
-    if (client.GetMethod() != HttpMethod_Delete)
-    {
-      CopyToMemoryBuffer(*answerBody, body);
-    }
-  }
-
-
-  void OrthancPlugins::CallHttpClient(const void* parameters)
-  {
-    const _OrthancPluginCallHttpClient& p = *reinterpret_cast<const _OrthancPluginCallHttpClient*>(parameters);
-
-    HttpClient client;
-
-    {    
-      _OrthancPluginCallHttpClient2 converted;
-      memset(&converted, 0, sizeof(converted));
-
-      converted.answerBody = NULL;
-      converted.answerHeaders = NULL;
-      converted.httpStatus = NULL;
-      converted.method = p.method;
-      converted.url = p.url;
-      converted.headersCount = 0;
-      converted.headersKeys = NULL;
-      converted.headersValues = NULL;
-      converted.body = p.body;
-      converted.bodySize = p.bodySize;
-      converted.username = p.username;
-      converted.password = p.password;
-      converted.timeout = 0;  // Use default timeout
-      converted.certificateFile = NULL;
-      converted.certificateKeyFile = NULL;
-      converted.certificateKeyPassword = NULL;
-      converted.pkcs11 = false;
-
-      SetupHttpClient(client, converted);
-    }
-
-    uint16_t status;
-    ExecuteHttpClientWithoutChunkedBody(status, p.target, NULL, client);
-  }
-
-
-  void OrthancPlugins::CallHttpClient2(const void* parameters)
-  {
-    const _OrthancPluginCallHttpClient2& p = *reinterpret_cast<const _OrthancPluginCallHttpClient2*>(parameters);
-    
-    if (p.httpStatus == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    HttpClient client;
-
-    if (p.method == OrthancPluginHttpMethod_Post ||
-        p.method == OrthancPluginHttpMethod_Put)
-    {
-      client.GetBody().assign(reinterpret_cast<const char*>(p.body), p.bodySize);
-    }
-    
-    SetupHttpClient(client, p);
-    ExecuteHttpClientWithoutChunkedBody(*p.httpStatus, p.answerBody, p.answerHeaders, client);
-  }
-
-
-  void OrthancPlugins::ChunkedHttpClient(const void* parameters)
-  {
-    const _OrthancPluginChunkedHttpClient& p =
-      *reinterpret_cast<const _OrthancPluginChunkedHttpClient*>(parameters);
-        
-    if (p.httpStatus == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-
-    HttpClient client;
-
-    {
-      _OrthancPluginCallHttpClient2 converted;
-      memset(&converted, 0, sizeof(converted));
-
-      converted.answerBody = NULL;
-      converted.answerHeaders = NULL;
-      converted.httpStatus = NULL;
-      converted.method = p.method;
-      converted.url = p.url;
-      converted.headersCount = p.headersCount;
-      converted.headersKeys = p.headersKeys;
-      converted.headersValues = p.headersValues;
-      converted.body = NULL;
-      converted.bodySize = 0;
-      converted.username = p.username;
-      converted.password = p.password;
-      converted.timeout = p.timeout;
-      converted.certificateFile = p.certificateFile;
-      converted.certificateKeyFile = p.certificateKeyFile;
-      converted.certificateKeyPassword = p.certificateKeyPassword;
-      converted.pkcs11 = p.pkcs11;
-
-      SetupHttpClient(client, converted);
-    }
-    
-    HttpClientChunkedRequest body(p, pimpl_->dictionary_);
-    client.SetBody(body);
-
-    HttpClientChunkedAnswer answer(p, pimpl_->dictionary_);
-
-    bool success = client.Apply(answer);
-
-    *p.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
-
-    if (!success)
-    {
-      HttpClient::ThrowException(client.GetLastStatus());
-    }
-  }
-
-
-  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(reinterpret_cast<const char*>(p.body), p.bodySize);
-        break;
-
-      case OrthancPluginHttpMethod_Put:
-        client.SetMethod(HttpMethod_Put);
-        client.GetBody().assign(reinterpret_cast<const char*>(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::unique_ptr<ImageAccessor> target(new Image(Plugins::Convert(p.targetFormat), source.GetWidth(), source.GetHeight(), false));
-    ImageProcessing::Convert(*target, source);
-
-    *(p.target) = ReturnImage(target);
-  }
-
-
-
-  void OrthancPlugins::GetFontInfo(const void* parameters)
-  {
-    const _OrthancPluginGetFontInfo& p = *reinterpret_cast<const _OrthancPluginGetFontInfo*>(parameters);
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-
-      const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex);
-
-      if (p.name != NULL)
-      {
-        *(p.name) = font.GetName().c_str();
-      }
-      else if (p.size != NULL)
-      {
-        *(p.size) = font.GetSize();
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-    }
-  }
-
-
-  void OrthancPlugins::DrawText(const void* parameters)
-  {
-    const _OrthancPluginDrawText& p = *reinterpret_cast<const _OrthancPluginDrawText*>(parameters);
-
-    ImageAccessor& target = *reinterpret_cast<ImageAccessor*>(p.image);
-
-    {
-      OrthancConfiguration::ReaderLock lock;
-      const Font& font = lock.GetConfiguration().GetFontRegistry().GetFont(p.fontIndex);
-      font.Draw(target, p.utf8Text, p.x, p.y, p.r, p.g, p.b);
-    }
-  }
-
-
-  void OrthancPlugins::ApplyDicomToJson(_OrthancPluginService service,
-                                        const void* parameters)
-  {
-    const _OrthancPluginDicomToJson& p =
-      *reinterpret_cast<const _OrthancPluginDicomToJson*>(parameters);
-
-    std::unique_ptr<ParsedDicomFile> dicom;
-
-    if (service == _OrthancPluginService_DicomBufferToJson)
-    {
-      dicom.reset(new ParsedDicomFile(p.buffer, p.size));
-    }
-    else
-    {
-      if (p.instanceId == NULL)
-      {
-        throw OrthancException(ErrorCode_NullPointer);
-      }
-
-      std::string content;
-
-      {
-        PImpl::ServerContextLock lock(*pimpl_);
-        lock.GetContext().ReadDicom(content, p.instanceId);
-      }
-
-      dicom.reset(new ParsedDicomFile(content));
-    }
-
-    Json::Value json;
-    dicom->DatasetToJson(json, Plugins::Convert(p.format), 
-                         static_cast<DicomToJsonFlags>(p.flags), p.maxStringLength);
-
-    Json::FastWriter writer;
-    *p.result = CopyString(writer.write(json));
-  }
-        
-
-  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;
-
-    {
-      // Fix issue 168 (Plugins can't read private tags from the
-      // configuration file)
-      // https://bitbucket.org/sjodogne/orthanc/issues/168/
-      std::string privateCreator;
-      {
-        OrthancConfiguration::ReaderLock lock;
-        privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
-      }
-      
-      std::unique_ptr<ParsedDicomFile> file
-        (ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(p.flags),
-                                         privateCreator));
-
-      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::GetTagName(const void* parameters)
-  {
-    const _OrthancPluginGetTagName& p =
-      *reinterpret_cast<const _OrthancPluginGetTagName*>(parameters);
-
-    std::string privateCreator;
-    
-    if (p.privateCreator != NULL)
-    {
-      privateCreator = p.privateCreator;
-    }
-   
-    DicomTag tag(p.group, p.element);
-    *p.result = CopyString(FromDcmtkBridge::GetTagName(tag, privateCreator));
-  }
-
-
-  void OrthancPlugins::ApplyCreateImage(_OrthancPluginService service,
-                                        const void* parameters)
-  {
-    const _OrthancPluginCreateImage& p =
-      *reinterpret_cast<const _OrthancPluginCreateImage*>(parameters);
-
-    std::unique_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:
-      {
-        PImpl::ServerContextLock lock(*pimpl_);
-        result.reset(lock.GetContext().DecodeDicomFrame(p.constBuffer, p.bufferSize, p.frameIndex));
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    *(p.target) = ReturnImage(result);
-  }
-
-
-  void OrthancPlugins::ApplySendMultipartItem(const void* parameters)
-  {
-    // 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);
-
-    std::map<std::string, std::string> headers;  // No custom headers
-    reinterpret_cast<PImpl::PluginHttpOutput*>(p.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);
-    
-    std::map<std::string, std::string> headers;
-    for (uint32_t i = 0; i < p.headersCount; i++)
-    {
-      headers[p.headersKeys[i]] = p.headersValues[i];
-    }
-    
-    reinterpret_cast<PImpl::PluginHttpOutput*>(p.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
-    {
-      throw OrthancException(ErrorCode_BadRequest,
-                             "Cannot invoke this service without a custom database back-end");
-    }
-  }
-
-
-  namespace
-  {
-    class DictionaryReadLocker
-    {
-    private:
-      const DcmDataDictionary& dictionary_;
-
-    public:
-      DictionaryReadLocker() : dictionary_(dcmDataDict.rdlock())
-      {
-      }
-
-      ~DictionaryReadLocker()
-      {
-#if DCMTK_VERSION_NUMBER >= 364
-        dcmDataDict.rdunlock();
-#else
-        dcmDataDict.unlock();
-#endif
-      }
-
-      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 = NULL;
-
-    if (tag.IsPrivate())
-    {
-      // Fix issue 168 (Plugins can't read private tags from the
-      // configuration file)
-      // https://bitbucket.org/sjodogne/orthanc/issues/168/
-      std::string privateCreator;
-      {
-        OrthancConfiguration::ReaderLock lock;
-        privateCreator = lock.GetConfiguration().GetDefaultPrivateCreator();
-      }
-
-      entry = locker->findEntry(tag2, privateCreator.c_str());
-    }
-    else
-    {
-      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 = SystemToolbox::GetPathToExecutable();
-        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
-        return true;
-      }
-
-      case _OrthancPluginService_GetOrthancDirectory:
-      {
-        std::string s = SystemToolbox::GetDirectoryOfExecutable();
-        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
-        return true;
-      }
-
-      case _OrthancPluginService_GetConfigurationPath:
-      {
-        std::string s;
-
-        {
-          OrthancConfiguration::ReaderLock lock;
-          s = lock.GetConfiguration().GetConfigurationAbsolutePath();
-        }
-
-        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
-        return true;
-      }
-
-      case _OrthancPluginService_GetConfiguration:
-      {
-        std::string s;
-
-        {
-          OrthancConfiguration::ReaderLock lock;
-          lock.GetConfiguration().Format(s);
-        }
-
-        *reinterpret_cast<const _OrthancPluginRetrieveDynamicString*>(parameters)->result = CopyString(s);
-        return true;
-      }
-
-      case _OrthancPluginService_BufferCompression:
-        BufferCompression(parameters);
-        return true;
-
-      case _OrthancPluginService_AnswerBuffer:
-        AnswerBuffer(parameters);
-        return true;
-
-      case _OrthancPluginService_CompressAndAnswerPngImage:
-        CompressAndAnswerPngImage(parameters);
-        return true;
-
-      case _OrthancPluginService_CompressAndAnswerImage:
-        CompressAndAnswerImage(parameters);
-        return true;
-
-      case _OrthancPluginService_GetDicomForInstance:
-        GetDicomForInstance(parameters);
-        return true;
-
-      case _OrthancPluginService_RestApiGet:
-        RestApiGet(parameters, false);
-        return true;
-
-      case _OrthancPluginService_RestApiGetAfterPlugins:
-        RestApiGet(parameters, true);
-        return true;
-
-      case _OrthancPluginService_RestApiGet2:
-        RestApiGet2(parameters);
-        return true;
-
-      case _OrthancPluginService_RestApiPost:
-        RestApiPostPut(true, parameters, false);
-        return true;
-
-      case _OrthancPluginService_RestApiPostAfterPlugins:
-        RestApiPostPut(true, parameters, true);
-        return true;
-
-      case _OrthancPluginService_RestApiDelete:
-        RestApiDelete(parameters, false);
-        return true;
-
-      case _OrthancPluginService_RestApiDeleteAfterPlugins:
-        RestApiDelete(parameters, true);
-        return true;
-
-      case _OrthancPluginService_RestApiPut:
-        RestApiPostPut(false, parameters, false);
-        return true;
-
-      case _OrthancPluginService_RestApiPutAfterPlugins:
-        RestApiPostPut(false, parameters, true);
-        return true;
-
-      case _OrthancPluginService_Redirect:
-        Redirect(parameters);
-        return true;
-
-      case _OrthancPluginService_SendUnauthorized:
-        SendUnauthorized(parameters);
-        return true;
-
-      case _OrthancPluginService_SendMethodNotAllowed:
-        SendMethodNotAllowed(parameters);
-        return true;
-
-      case _OrthancPluginService_SendHttpStatus:
-        SendHttpStatus(parameters);
-        return true;
-
-      case _OrthancPluginService_SendHttpStatusCode:
-        SendHttpStatusCode(parameters);
-        return true;
-
-      case _OrthancPluginService_SetCookie:
-        SetCookie(parameters);
-        return true;
-
-      case _OrthancPluginService_SetHttpHeader:
-        SetHttpHeader(parameters);
-        return true;
-
-      case _OrthancPluginService_SetHttpErrorDetails:
-        SetHttpErrorDetails(parameters);
-        return true;
-
-      case _OrthancPluginService_LookupPatient:
-      case _OrthancPluginService_LookupStudy:
-      case _OrthancPluginService_LookupStudyWithAccessionNumber:
-      case _OrthancPluginService_LookupSeries:
-      case _OrthancPluginService_LookupInstance:
-        LookupResource(service, parameters);
-        return true;
-
-      case _OrthancPluginService_GetInstanceRemoteAet:
-      case _OrthancPluginService_GetInstanceSize:
-      case _OrthancPluginService_GetInstanceData:
-      case _OrthancPluginService_GetInstanceJson:
-      case _OrthancPluginService_GetInstanceSimplifiedJson:
-      case _OrthancPluginService_HasInstanceMetadata:
-      case _OrthancPluginService_GetInstanceMetadata:
-      case _OrthancPluginService_GetInstanceOrigin:
-      case _OrthancPluginService_GetInstanceTransferSyntaxUid:
-      case _OrthancPluginService_HasInstancePixelData:
-        AccessDicomInstance(service, parameters);
-        return true;
-
-      case _OrthancPluginService_GetInstanceFramesCount:
-      case _OrthancPluginService_GetInstanceRawFrame:
-      case _OrthancPluginService_GetInstanceDecodedFrame:
-      case _OrthancPluginService_SerializeDicomInstance:
-      case _OrthancPluginService_GetInstanceAdvancedJson:
-      case _OrthancPluginService_GetInstanceDicomWebJson:
-      case _OrthancPluginService_GetInstanceDicomWebXml:
-        AccessDicomInstance2(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);
-        reinterpret_cast<PImpl::PluginHttpOutput*>(p.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_ChunkedHttpClient:
-        ChunkedHttpClient(parameters);
-        return true;
-
-      case _OrthancPluginService_ConvertPixelFormat:
-        ConvertPixelFormat(parameters);
-        return true;
-
-      case _OrthancPluginService_GetFontsCount:
-      {
-        const _OrthancPluginReturnSingleValue& p =
-          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
-
-        {
-          OrthancConfiguration::ReaderLock lock;
-          *(p.resultUint32) = lock.GetConfiguration().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;
-      }
-
-      case _OrthancPluginService_AutodetectMimeType:
-      {
-        const _OrthancPluginRetrieveStaticString& p =
-          *reinterpret_cast<const _OrthancPluginRetrieveStaticString*>(parameters);
-        *p.result = EnumerationToString(SystemToolbox::AutodetectMimeType(p.argument));
-        return true;
-      }
-
-      case _OrthancPluginService_SetMetricsValue:
-      {
-        const _OrthancPluginSetMetricsValue& p =
-          *reinterpret_cast<const _OrthancPluginSetMetricsValue*>(parameters);
-
-        MetricsType type;
-        switch (p.type)
-        {
-          case OrthancPluginMetricsType_Default:
-            type = MetricsType_Default;
-            break;
-
-          case OrthancPluginMetricsType_Timer:
-            type = MetricsType_MaxOver10Seconds;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        
-        {
-          PImpl::ServerContextLock lock(*pimpl_);
-          lock.GetContext().GetMetricsRegistry().SetValue(p.name, p.value, type);
-        }
-
-        return true;
-      }
-
-      case _OrthancPluginService_EncodeDicomWebJson:
-      case _OrthancPluginService_EncodeDicomWebXml:
-      {
-        const _OrthancPluginEncodeDicomWeb& p =
-          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb*>(parameters);
-
-        DicomWebBinaryFormatter formatter(p.callback);
-        formatter.Apply(p.target,
-                        (service == _OrthancPluginService_EncodeDicomWebJson),
-                        p.dicom, p.dicomSize);
-        return true;
-      }
-
-      case _OrthancPluginService_EncodeDicomWebJson2:
-      case _OrthancPluginService_EncodeDicomWebXml2:
-      {
-        const _OrthancPluginEncodeDicomWeb2& p =
-          *reinterpret_cast<const _OrthancPluginEncodeDicomWeb2*>(parameters);
-
-        DicomWebBinaryFormatter formatter(p.callback, p.payload);
-        formatter.Apply(p.target,
-                        (service == _OrthancPluginService_EncodeDicomWebJson2),
-                        p.dicom, p.dicomSize);
-        return true;
-      }
-
-      case _OrthancPluginService_GetTagName:
-        GetTagName(parameters);
-        return true;
-
-      case _OrthancPluginService_CreateDicomInstance:
-      {
-        const _OrthancPluginCreateDicomInstance& p =
-          *reinterpret_cast<const _OrthancPluginCreateDicomInstance*>(parameters);
-        *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
-          new DicomInstanceFromBuffer(p.buffer, p.size));
-        return true;
-      }
-        
-      case _OrthancPluginService_FreeDicomInstance:
-      {
-        const _OrthancPluginFreeDicomInstance& p =
-          *reinterpret_cast<const _OrthancPluginFreeDicomInstance*>(parameters);
-
-        if (p.dicom != NULL)
-        {
-          IDicomInstance* obj = reinterpret_cast<IDicomInstance*>(p.dicom);
-          
-          if (obj->CanBeFreed())
-          {
-            delete obj;
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_Plugin, "Cannot free a DICOM instance provided to a callback");
-          }
-        }
-
-        return true;
-      }
-
-      case _OrthancPluginService_TranscodeDicomInstance:
-      {
-        const _OrthancPluginCreateDicomInstance& p =
-          *reinterpret_cast<const _OrthancPluginCreateDicomInstance*>(parameters);
-
-        DicomTransferSyntax transferSyntax;
-        if (p.transferSyntax == NULL ||
-            !LookupTransferSyntax(transferSyntax, p.transferSyntax))
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange, "Unsupported transfer syntax: " +
-                                 std::string(p.transferSyntax == NULL ? "(null)" : p.transferSyntax));
-        }
-        else
-        {
-          std::set<DicomTransferSyntax> syntaxes;
-          syntaxes.insert(transferSyntax);
-
-          IDicomTranscoder::DicomImage source;
-          source.SetExternalBuffer(p.buffer, p.size);
-
-          IDicomTranscoder::DicomImage transcoded;
-          bool success;
-          
-          {
-            PImpl::ServerContextLock lock(*pimpl_);
-            success = lock.GetContext().Transcode(
-              transcoded, source, syntaxes, true /* allow new sop */);
-          }
-
-          if (success)
-          {
-            *(p.target) = reinterpret_cast<OrthancPluginDicomInstance*>(
-              new DicomInstanceFromTranscoded(transcoded));
-            return true;
-          }
-          else
-          {
-            throw OrthancException(ErrorCode_NotImplemented, "Cannot transcode image");
-          }
-        }
-      }
-
-      case _OrthancPluginService_CreateMemoryBuffer:
-      {
-        const _OrthancPluginCreateMemoryBuffer& p =
-          *reinterpret_cast<const _OrthancPluginCreateMemoryBuffer*>(parameters);
-
-        p.target->size = p.size;
-        
-        if (p.size == 0)
-        {
-          p.target->data = NULL;
-        }
-        else
-        {
-          p.target->data = malloc(p.size);
-        }          
-        
-        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_RegisterChunkedRestCallback:
-        RegisterChunkedRestCallback(parameters);
-        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_RegisterTranscoderCallback:
-        RegisterTranscoderCallback(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_RegisterIncomingDicomInstanceFilter:
-        RegisterIncomingDicomInstanceFilter(parameters);
-        return true;
-
-      case _OrthancPluginService_RegisterRefreshMetricsCallback:
-        RegisterRefreshMetricsCallback(parameters);
-        return true;
-
-      case _OrthancPluginService_RegisterStorageCommitmentScpCallback:
-        RegisterStorageCommitmentScpCallback(parameters);
-        return true;
-
-      case _OrthancPluginService_RegisterStorageArea:
-      {
-        LOG(INFO) << "Plugin has registered a custom storage area";
-        const _OrthancPluginRegisterStorageArea& p = 
-          *reinterpret_cast<const _OrthancPluginRegisterStorageArea*>(parameters);
-        
-        if (pimpl_->storageArea_.get() == NULL)
-        {
-          pimpl_->storageArea_.reset(new StorageAreaFactory(plugin, p, GetErrorDictionary()));
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_StorageAreaAlreadyRegistered);
-        }
-
-        return true;
-      }
-
-      case _OrthancPluginService_SetPluginProperty:
-      {
-        const _OrthancPluginSetPluginProperty& p = 
-          *reinterpret_cast<const _OrthancPluginSetPluginProperty*>(parameters);
-        pimpl_->properties_[std::make_pair(p.plugin, p.property)] = p.value;
-        return true;
-      }
-
-      case _OrthancPluginService_GetCommandLineArgumentsCount:
-      {
-        const _OrthancPluginReturnSingleValue& p =
-          *reinterpret_cast<const _OrthancPluginReturnSingleValue*>(parameters);
-        *(p.resultUint32) = pimpl_->argc_ - 1;
-        return true;
-      }
-
-      case _OrthancPluginService_GetCommandLineArgument:
-      {
-        const _OrthancPluginGlobalProperty& p =
-          *reinterpret_cast<const _OrthancPluginGlobalProperty*>(parameters);
-        
-        if (p.property + 1 > pimpl_->argc_)
-        {
-          return false;
-        }
-        else
-        {
-          std::string arg = std::string(pimpl_->argv_[p.property + 1]);
-          *(p.result) = CopyString(arg);
-          return true;
-        }
-      }
-
-      case _OrthancPluginService_RegisterDatabaseBackend:
-      {
-        LOG(INFO) << "Plugin has registered a custom database back-end";
-
-        const _OrthancPluginRegisterDatabaseBackend& p =
-          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackend*>(parameters);
-
-        if (pimpl_->database_.get() == NULL)
-        {
-          pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(), 
-                                                            *p.backend, NULL, 0, p.payload));
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered);
-        }
-
-        *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get());
-
-        return true;
-      }
-
-      case _OrthancPluginService_RegisterDatabaseBackendV2:
-      {
-        LOG(INFO) << "Plugin has registered a custom database back-end";
-
-        const _OrthancPluginRegisterDatabaseBackendV2& p =
-          *reinterpret_cast<const _OrthancPluginRegisterDatabaseBackendV2*>(parameters);
-
-        if (pimpl_->database_.get() == NULL)
-        {
-          pimpl_->database_.reset(new OrthancPluginDatabase(plugin, GetErrorDictionary(),
-                                                            *p.backend, p.extensions,
-                                                            p.extensionsSize, p.payload));
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_DatabaseBackendAlreadyRegistered);
-        }
-
-        *(p.result) = reinterpret_cast<OrthancPluginDatabaseContext*>(pimpl_->database_.get());
-
-        return true;
-      }
-
-      case _OrthancPluginService_DatabaseAnswer:
-        throw OrthancException(ErrorCode_InternalError);   // Implemented before locking (*)
-
-      case _OrthancPluginService_RegisterErrorCode:
-      {
-        const _OrthancPluginRegisterErrorCode& p =
-          *reinterpret_cast<const _OrthancPluginRegisterErrorCode*>(parameters);
-        *(p.target) = pimpl_->dictionary_.Register(plugin, p.code, p.httpStatus, p.message);
-        return true;
-      }
-
-      case _OrthancPluginService_RegisterDictionaryTag:
-      {
-        const _OrthancPluginRegisterDictionaryTag& p =
-          *reinterpret_cast<const _OrthancPluginRegisterDictionaryTag*>(parameters);
-        FromDcmtkBridge::RegisterDictionaryTag(DicomTag(p.group, p.element),
-                                               Plugins::Convert(p.vr), p.name,
-                                               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;
-      }
-
-      case _OrthancPluginService_ReconstructMainDicomTags:
-      {
-        const _OrthancPluginReconstructMainDicomTags& p =
-          *reinterpret_cast<const _OrthancPluginReconstructMainDicomTags*>(parameters);
-
-        if (pimpl_->database_.get() == NULL)
-        {
-          throw OrthancException(ErrorCode_DatabasePlugin,
-                                 "The service ReconstructMainDicomTags can only be invoked by custom database plugins");
-        }
-
-        IStorageArea& storage = *reinterpret_cast<IStorageArea*>(p.storageArea);
-        ServerToolbox::ReconstructMainDicomTags(*pimpl_->database_, storage, Plugins::Convert(p.level));
-
-        return true;
-      }
-
-      default:
-      {
-        // This service is unknown to the Orthanc plugin engine
-        return false;
-      }
-    }
-  }
-
-
-
-  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;
-  }
-
-
-  IStorageArea* OrthancPlugins::CreateStorageArea()
-  {
-    if (!HasStorageArea())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return pimpl_->storageArea_->Create();
-    }
-  }
-
-
-  const SharedLibrary& OrthancPlugins::GetStorageAreaLibrary() const
-  {
-    if (!HasStorageArea())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return pimpl_->storageArea_->GetSharedLibrary();
-    }
-  }
-
-
-  IDatabaseWrapper& OrthancPlugins::GetDatabaseBackend()
-  {
-    if (!HasDatabaseBackend())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return *pimpl_->database_;
-    }
-  }
-
-
-  const SharedLibrary& OrthancPlugins::GetDatabaseBackendLibrary() const
-  {
-    if (!HasDatabaseBackend())
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      return pimpl_->database_->GetSharedLibrary();
-    }
-  }
-
-
-  const char* OrthancPlugins::GetProperty(const char* plugin,
-                                          _OrthancPluginProperty property) const
-  {
-    PImpl::Property p = std::make_pair(plugin, property);
-    PImpl::Properties::const_iterator it = pimpl_->properties_.find(p);
-
-    if (it == pimpl_->properties_.end())
-    {
-      return NULL;
-    }
-    else
-    {
-      return it->second.c_str();
-    }
-  }
-
-
-  void OrthancPlugins::SetCommandLineArguments(int argc, char* argv[])
-  {
-    if (argc < 1 || argv == NULL)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    pimpl_->argc_ = argc;
-    pimpl_->argv_ = argv;
-  }
-
-
-  PluginsManager& OrthancPlugins::GetManager()
-  {
-    return pimpl_->manager_;
-  }
-
-
-  const PluginsManager& OrthancPlugins::GetManager() const
-  {
-    return pimpl_->manager_;
-  }
-
-
-  PluginsErrorDictionary&  OrthancPlugins::GetErrorDictionary()
-  {
-    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::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
-    return !pimpl_->decodeImageCallbacks_.empty();
-  }
-
-
-  bool OrthancPlugins::HasCustomTranscoder()
-  {
-    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
-    return !pimpl_->transcoderCallbacks_.empty();
-  }
-
-
-  ImageAccessor* OrthancPlugins::Decode(const void* dicom,
-                                        size_t size,
-                                        unsigned int frame)
-  {
-    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
-
-    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;
-  }
-
-  
-  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();
-    }
-
-    {
-      boost::recursive_mutex::scoped_lock lock(pimpl_->invokeServiceMutex_);
-    
-      // 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;
-  }
-
-
-  void OrthancPlugins::RefreshMetrics()
-  {
-    boost::mutex::scoped_lock lock(pimpl_->refreshMetricsMutex_);
-
-    for (PImpl::RefreshMetricsCallbacks::iterator 
-           it = pimpl_->refreshMetricsCallbacks_.begin();
-         it != pimpl_->refreshMetricsCallbacks_.end(); ++it)
-    {
-      if (*it != NULL)
-      {
-        (*it) ();
-      }
-    }
-  }
-
-
-  class OrthancPlugins::HttpServerChunkedReader : public IHttpHandler::IChunkedRequestReader
-  {
-  private:
-    OrthancPluginServerChunkedRequestReader*  reader_;
-    _OrthancPluginChunkedRestCallback         parameters_;
-    PluginsErrorDictionary&                   errorDictionary_;
-
-  public:
-    HttpServerChunkedReader(OrthancPluginServerChunkedRequestReader* reader,
-                            const _OrthancPluginChunkedRestCallback& parameters,
-                            PluginsErrorDictionary& errorDictionary) :
-      reader_(reader),
-      parameters_(parameters),
-      errorDictionary_(errorDictionary)
-    {
-      assert(reader_ != NULL);
-    }
-
-    virtual ~HttpServerChunkedReader()
-    {
-      assert(reader_ != NULL);
-      parameters_.finalize(reader_);
-    }
-
-    virtual void AddBodyChunk(const void* data,
-                              size_t size)
-    {
-      if (static_cast<uint32_t>(size) != size)
-      {
-        throw OrthancException(ErrorCode_NotEnoughMemory, ERROR_MESSAGE_64BIT);
-      }
-
-      assert(reader_ != NULL);
-      parameters_.addChunk(reader_, data, size);
-    }    
-
-    virtual void Execute(HttpOutput& output)
-    {
-      assert(reader_ != NULL);
-
-      PImpl::PluginHttpOutput pluginOutput(output);
-
-      OrthancPluginErrorCode error = parameters_.execute(
-        reader_, reinterpret_cast<OrthancPluginRestOutput*>(&pluginOutput));
-
-      pluginOutput.Close(error, errorDictionary_);
-    }
-  };
-
-
-  bool OrthancPlugins::CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                                  RequestOrigin origin,
-                                                  const char* remoteIp,
-                                                  const char* username,
-                                                  HttpMethod method,
-                                                  const UriComponents& uri,
-                                                  const Arguments& headers)
-  {
-    if (method != HttpMethod_Post &&
-        method != HttpMethod_Put)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    RestCallbackMatcher matcher(uri);
-
-    PImpl::ChunkedRestCallback* callback = NULL;
-
-    // Loop over the callbacks registered by the plugins
-    for (PImpl::ChunkedRestCallbacks::const_iterator it = pimpl_->chunkedRestCallbacks_.begin(); 
-         it != pimpl_->chunkedRestCallbacks_.end(); ++it)
-    {
-      if (matcher.IsMatch((*it)->GetRegularExpression()))
-      {
-        callback = *it;
-        break;
-      }
-    }
-
-    if (callback == NULL)
-    {
-      // Callback not found
-      return false;
-    }
-    else
-    {
-      OrthancPluginServerChunkedRequestReaderFactory handler;
-
-      switch (method)
-      {
-        case HttpMethod_Post:
-          handler = callback->GetParameters().postHandler;
-          break;
-
-        case HttpMethod_Put:
-          handler = callback->GetParameters().putHandler;
-          break;
-
-        default:
-          handler = NULL;
-          break;
-      }
-
-      if (handler == NULL)
-      {
-        return false;
-      }
-      else
-      {
-        LOG(INFO) << "Delegating chunked HTTP request to plugin for URI: " << matcher.GetFlatUri();
-
-        HttpRequestConverter converter(matcher, method, headers);
-        converter.GetRequest().body = NULL;
-        converter.GetRequest().bodySize = 0;
-
-        OrthancPluginServerChunkedRequestReader* reader = NULL;
-    
-        OrthancPluginErrorCode errorCode = handler(
-          &reader, matcher.GetFlatUri().c_str(), &converter.GetRequest());
-    
-        if (reader == NULL)
-        {
-          // The plugin has not created a reader for chunked body
-          return false;
-        }
-        else if (errorCode != OrthancPluginErrorCode_Success)
-        {
-          throw OrthancException(static_cast<ErrorCode>(errorCode));
-        }
-        else
-        {
-          target.reset(new HttpServerChunkedReader(reader, callback->GetParameters(), GetErrorDictionary()));
-          return true;
-        }
-      }
-    }
-  }
-
-
-  IStorageCommitmentFactory::ILookupHandler* OrthancPlugins::CreateStorageCommitment(
-    const std::string& jobId,
-    const std::string& transactionUid,
-    const std::vector<std::string>& sopClassUids,
-    const std::vector<std::string>& sopInstanceUids,
-    const std::string& remoteAet,
-    const std::string& calledAet)
-  {
-    boost::mutex::scoped_lock lock(pimpl_->storageCommitmentScpMutex_);
-
-    for (PImpl::StorageCommitmentScpCallbacks::iterator
-           it = pimpl_->storageCommitmentScpCallbacks_.begin(); 
-         it != pimpl_->storageCommitmentScpCallbacks_.end(); ++it)
-    {
-      assert(*it != NULL);
-      IStorageCommitmentFactory::ILookupHandler* handler = (*it)->CreateStorageCommitment
-        (jobId, transactionUid, sopClassUids, sopInstanceUids, remoteAet, calledAet);
-
-      if (handler != NULL)
-      {
-        return handler;
-      }
-    } 
-    
-    return NULL;
-  }
-
-
-  class MemoryBufferRaii : public boost::noncopyable
-  {
-  private:
-    OrthancPluginMemoryBuffer  buffer_;
-
-  public:
-    MemoryBufferRaii()
-    {
-      buffer_.size = 0;
-      buffer_.data = NULL;
-    }
-
-    ~MemoryBufferRaii()
-    {
-      if (buffer_.size != 0)
-      {
-        free(buffer_.data);
-      }
-    }
-
-    OrthancPluginMemoryBuffer* GetObject()
-    {
-      return &buffer_;
-    }
-
-    void ToString(std::string& target) const
-    {
-      target.resize(buffer_.size);
-
-      if (buffer_.size != 0)
-      {
-        memcpy(&target[0], buffer_.data, buffer_.size);
-      }
-    }
-  };
-  
-
-  bool OrthancPlugins::TranscodeBuffer(std::string& target,
-                                       const void* buffer,
-                                       size_t size,
-                                       const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                       bool allowNewSopInstanceUid)
-  {
-    boost::shared_lock<boost::shared_mutex> lock(pimpl_->decoderTranscoderMutex_);
-
-    if (pimpl_->transcoderCallbacks_.empty())
-    {
-      return false;
-    }
-
-    std::vector<const char*> uids;
-    uids.reserve(allowedSyntaxes.size());
-    for (std::set<DicomTransferSyntax>::const_iterator
-           it = allowedSyntaxes.begin(); it != allowedSyntaxes.end(); ++it)
-    {
-      uids.push_back(GetTransferSyntaxUid(*it));
-    }
-    
-    for (PImpl::TranscoderCallbacks::const_iterator
-           transcoder = pimpl_->transcoderCallbacks_.begin();
-         transcoder != pimpl_->transcoderCallbacks_.end(); ++transcoder)
-    {
-      MemoryBufferRaii a;
-
-      if ((*transcoder) (a.GetObject(), buffer, size, uids.empty() ? NULL : &uids[0],
-                         static_cast<uint32_t>(uids.size()), allowNewSopInstanceUid) ==
-          OrthancPluginErrorCode_Success)
-      {
-        a.ToString(target);
-        return true;
-      }
-    }
-
-    return false;
-  }
-}
--- a/Plugins/Engine/OrthancPlugins.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,391 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PluginsErrorDictionary.h"
-
-#if !defined(ORTHANC_ENABLE_PLUGINS)
-#  error The macro ORTHANC_ENABLE_PLUGINS must be defined
-#endif
-
-
-#if ORTHANC_ENABLE_PLUGINS != 1
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class OrthancPlugins : public boost::noncopyable
-  {
-  };
-}
-
-#else
-
-#include "../../Core/DicomNetworking/IFindRequestHandlerFactory.h"
-#include "../../Core/DicomNetworking/IMoveRequestHandlerFactory.h"
-#include "../../Core/DicomNetworking/IWorklistRequestHandlerFactory.h"
-#include "../../Core/DicomParsing/MemoryBufferTranscoder.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 "../../OrthancServer/ServerJobs/IStorageCommitmentFactory.h"
-#include "OrthancPluginDatabase.h"
-#include "PluginsManager.h"
-
-#include <list>
-#include <boost/shared_ptr.hpp>
-
-namespace Orthanc
-{
-  class ServerContext;
-
-  class OrthancPlugins : 
-    public IHttpHandler, 
-    public IPluginServiceProvider, 
-    public IServerListener,
-    public IWorklistRequestHandlerFactory,
-    public IDicomImageDecoder,
-    public IIncomingHttpRequestFilter,
-    public IFindRequestHandlerFactory,
-    public IMoveRequestHandlerFactory,
-    public IStorageCommitmentFactory,
-    public MemoryBufferTranscoder
-  {
-  private:
-    class PImpl;
-    boost::shared_ptr<PImpl> pimpl_;
-
-    class WorklistHandler;
-    class FindHandler;
-    class MoveHandler;
-    class HttpClientChunkedRequest;
-    class HttpClientChunkedAnswer;
-    class HttpServerChunkedReader;
-    class IDicomInstance;
-    class DicomInstanceFromCallback;
-    class DicomInstanceFromBuffer;
-    class DicomInstanceFromTranscoded;
-    
-    void RegisterRestCallback(const void* parameters,
-                              bool lock);
-
-    void RegisterChunkedRestCallback(const void* parameters);
-
-    bool HandleChunkedGetDelete(HttpOutput& output,
-                                HttpMethod method,
-                                const UriComponents& uri,
-                                const Arguments& headers,
-                                const GetArguments& getArguments);
-
-    void RegisterOnStoredInstanceCallback(const void* parameters);
-
-    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 RegisterTranscoderCallback(const void* parameters);
-
-    void RegisterJobsUnserializer(const void* parameters);
-
-    void RegisterIncomingHttpRequestFilter(const void* parameters);
-
-    void RegisterIncomingHttpRequestFilter2(const void* parameters);
-
-    void RegisterIncomingDicomInstanceFilter(const void* parameters);
-
-    void RegisterRefreshMetricsCallback(const void* parameters);
-
-    void RegisterStorageCommitmentScpCallback(const void* parameters);
-
-    void AnswerBuffer(const void* parameters);
-
-    void Redirect(const void* parameters);
-
-    void CompressAndAnswerPngImage(const void* parameters);
-
-    void CompressAndAnswerImage(const void* parameters);
-
-    void GetDicomForInstance(const void* parameters);
-
-    void RestApiGet(const void* parameters,
-                    bool afterPlugins);
-
-    void RestApiGet2(const void* parameters);
-
-    void RestApiPostPut(bool isPost, 
-                        const void* parameters,
-                        bool afterPlugins);
-
-    void RestApiDelete(const void* parameters,
-                       bool afterPlugins);
-
-    void LookupResource(_OrthancPluginService service,
-                        const void* parameters);
-
-    void AccessDicomInstance(_OrthancPluginService service,
-                             const void* parameters);
-    
-    void AccessDicomInstance2(_OrthancPluginService service,
-                              const void* parameters);
-    
-    void SendHttpStatusCode(const void* parameters);
-
-    void SendHttpStatus(const void* parameters);
-
-    void SendUnauthorized(const void* parameters);
-
-    void SendMethodNotAllowed(const void* parameters);
-
-    void SetCookie(const void* parameters);
-
-    void SetHttpHeader(const void* parameters);
-
-    void SetHttpErrorDetails(const void* parameters);
-
-    void BufferCompression(const void* parameters);
-
-    void UncompressImage(const void* parameters);
-
-    void CompressImage(const void* parameters);
-
-    void ConvertPixelFormat(const void* parameters);
-
-    void CallHttpClient(const void* parameters);
-
-    void CallHttpClient2(const void* parameters);
-
-    void ChunkedHttpClient(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 GetTagName(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);
-
-  protected:
-    // From "MemoryBufferTranscoder"
-    virtual bool TranscodeBuffer(std::string& target,
-                                 const void* buffer,
-                                 size_t size,
-                                 const std::set<DicomTransferSyntax>& allowedSyntaxes,
-                                 bool allowNewSopInstanceUid) ORTHANC_OVERRIDE;
-    
-  public:
-    OrthancPlugins();
-
-    virtual ~OrthancPlugins();
-
-    void SetServerContext(ServerContext& context);
-
-    void ResetServerContext();
-
-    virtual bool Handle(HttpOutput& output,
-                        RequestOrigin origin,
-                        const char* remoteIp,
-                        const char* username,
-                        HttpMethod method,
-                        const UriComponents& uri,
-                        const Arguments& headers,
-                        const GetArguments& getArguments,
-                        const void* bodyData,
-                        size_t bodySize) ORTHANC_OVERRIDE;
-
-    virtual bool InvokeService(SharedLibrary& plugin,
-                               _OrthancPluginService service,
-                               const void* parameters) ORTHANC_OVERRIDE;
-
-    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE;
-    
-    virtual void SignalStoredInstance(const std::string& instanceId,
-                                      const DicomInstanceToStore& instance,
-                                      const Json::Value& simplifiedTags) ORTHANC_OVERRIDE;
-
-    virtual bool FilterIncomingInstance(const DicomInstanceToStore& instance,
-                                        const Json::Value& simplified) ORTHANC_OVERRIDE;
-
-    bool HasStorageArea() const;
-
-    IStorageArea* CreateStorageArea();  // To be freed after use
-
-    const SharedLibrary& GetStorageAreaLibrary() const;
-
-    bool HasDatabaseBackend() const;
-
-    IDatabaseWrapper& GetDatabaseBackend();
-
-    const SharedLibrary& GetDatabaseBackendLibrary() const;
-
-    const char* GetProperty(const char* plugin,
-                            _OrthancPluginProperty property) const;
-
-    void SetCommandLineArguments(int argc, char* argv[]);
-
-    PluginsManager& GetManager();
-
-    const PluginsManager& GetManager() const;
-
-    PluginsErrorDictionary& GetErrorDictionary();
-
-    void SignalOrthancStarted()
-    {
-      SignalChangeInternal(OrthancPluginChangeType_OrthancStarted, OrthancPluginResourceType_None, NULL);
-    }
-
-    void SignalOrthancStopped()
-    {
-      SignalChangeInternal(OrthancPluginChangeType_OrthancStopped, OrthancPluginResourceType_None, NULL);
-    }
-
-    void SignalJobSubmitted(const std::string& jobId)
-    {
-      SignalChangeInternal(OrthancPluginChangeType_JobSubmitted, OrthancPluginResourceType_None, jobId.c_str());
-    }
-
-    void SignalJobSuccess(const std::string& jobId)
-    {
-      SignalChangeInternal(OrthancPluginChangeType_JobSuccess, OrthancPluginResourceType_None, jobId.c_str());
-    }
-
-    void SignalJobFailure(const std::string& jobId)
-    {
-      SignalChangeInternal(OrthancPluginChangeType_JobFailure, OrthancPluginResourceType_None, jobId.c_str());
-    }
-
-    void SignalUpdatedPeers()
-    {
-      SignalChangeInternal(OrthancPluginChangeType_UpdatedPeers, OrthancPluginResourceType_None, NULL);
-    }
-
-    void SignalUpdatedModalities()
-    {
-      SignalChangeInternal(OrthancPluginChangeType_UpdatedModalities, OrthancPluginResourceType_None, NULL);
-    }
-
-    bool HasWorklistHandler();
-
-    virtual IWorklistRequestHandler* ConstructWorklistRequestHandler() ORTHANC_OVERRIDE;
-
-    bool HasCustomImageDecoder();
-
-    bool HasCustomTranscoder();
-
-    virtual ImageAccessor* Decode(const void* dicom,
-                                  size_t size,
-                                  unsigned int frame) ORTHANC_OVERRIDE;
-
-    virtual bool IsAllowed(HttpMethod method,
-                           const char* uri,
-                           const char* ip,
-                           const char* username,
-                           const IHttpHandler::Arguments& httpHeaders,
-                           const IHttpHandler::GetArguments& getArguments) ORTHANC_OVERRIDE;
-
-    bool HasFindHandler();
-
-    virtual IFindRequestHandler* ConstructFindRequestHandler() ORTHANC_OVERRIDE;
-
-    bool HasMoveHandler();
-
-    virtual IMoveRequestHandler* ConstructMoveRequestHandler() ORTHANC_OVERRIDE;
-
-    IJob* UnserializeJob(const std::string& type,
-                         const Json::Value& value);
-
-    void RefreshMetrics();
-
-    // New in Orthanc 1.5.7
-    virtual bool CreateChunkedRequestReader(std::unique_ptr<IChunkedRequestReader>& target,
-                                            RequestOrigin origin,
-                                            const char* remoteIp,
-                                            const char* username,
-                                            HttpMethod method,
-                                            const UriComponents& uri,
-                                            const Arguments& headers) ORTHANC_OVERRIDE;
-
-    // New in Orthanc 1.6.0
-    IStorageCommitmentFactory::ILookupHandler* CreateStorageCommitment(
-      const std::string& jobId,
-      const std::string& transactionUid,
-      const std::vector<std::string>& sopClassUids,
-      const std::vector<std::string>& sopInstanceUids,
-      const std::string& remoteAet,
-      const std::string& calledAet) ORTHANC_OVERRIDE;
-  };
-}
-
-#endif
--- a/Plugins/Engine/PluginsEnumerations.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,584 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PluginsEnumerations.h"
-
-#if ORTHANC_ENABLE_PLUGINS != 1
-#error The plugin support is disabled
-#endif
-
-
-#include "../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint)
-    {
-      switch (constraint)
-      {
-        case Compatibility::IdentifierConstraintType_Equal:
-          return OrthancPluginIdentifierConstraint_Equal;
-
-        case Compatibility::IdentifierConstraintType_GreaterOrEqual:
-          return OrthancPluginIdentifierConstraint_GreaterOrEqual;
-
-        case Compatibility::IdentifierConstraintType_SmallerOrEqual:
-          return OrthancPluginIdentifierConstraint_SmallerOrEqual;
-
-        case Compatibility::IdentifierConstraintType_Wildcard:
-          return OrthancPluginIdentifierConstraint_Wildcard;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint)
-    {
-      switch (constraint)
-      {
-        case OrthancPluginIdentifierConstraint_Equal:
-          return Compatibility::IdentifierConstraintType_Equal;
-
-        case OrthancPluginIdentifierConstraint_GreaterOrEqual:
-          return Compatibility::IdentifierConstraintType_GreaterOrEqual;
-
-        case OrthancPluginIdentifierConstraint_SmallerOrEqual:
-          return Compatibility::IdentifierConstraintType_SmallerOrEqual;
-
-        case OrthancPluginIdentifierConstraint_Wildcard:
-          return Compatibility::IdentifierConstraintType_Wildcard;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-  }
-
-
-  namespace Plugins
-  {
-    OrthancPluginChangeType Convert(ChangeType type)
-    {
-      switch (type)
-      {
-        case ChangeType_CompletedSeries:
-          return OrthancPluginChangeType_CompletedSeries;
-
-        case ChangeType_Deleted:
-          return OrthancPluginChangeType_Deleted;
-
-        case ChangeType_NewChildInstance:
-          return OrthancPluginChangeType_NewChildInstance;
-
-        case ChangeType_NewInstance:
-          return OrthancPluginChangeType_NewInstance;
-
-        case ChangeType_NewPatient:
-          return OrthancPluginChangeType_NewPatient;
-
-        case ChangeType_NewSeries:
-          return OrthancPluginChangeType_NewSeries;
-
-        case ChangeType_NewStudy:
-          return OrthancPluginChangeType_NewStudy;
-
-        case ChangeType_StablePatient:
-          return OrthancPluginChangeType_StablePatient;
-
-        case ChangeType_StableSeries:
-          return OrthancPluginChangeType_StableSeries;
-
-        case ChangeType_StableStudy:
-          return OrthancPluginChangeType_StableStudy;
-
-        case ChangeType_UpdatedAttachment:
-          return OrthancPluginChangeType_UpdatedAttachment;
-
-        case ChangeType_UpdatedMetadata:
-          return OrthancPluginChangeType_UpdatedMetadata;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    OrthancPluginPixelFormat Convert(PixelFormat format)
-    {
-      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;
-
-        case PixelFormat_SignedGrayscale16:
-          return OrthancPluginPixelFormat_SignedGrayscale16;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    PixelFormat Convert(OrthancPluginPixelFormat format)
-    {
-      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;
-
-        case OrthancPluginPixelFormat_SignedGrayscale16:
-          return PixelFormat_SignedGrayscale16;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    OrthancPluginContentType Convert(FileContentType type)
-    {
-      switch (type)
-      {
-        case FileContentType_Dicom:
-          return OrthancPluginContentType_Dicom;
-
-        case FileContentType_DicomAsJson:
-          return OrthancPluginContentType_DicomAsJson;
-
-        default:
-          return OrthancPluginContentType_Unknown;
-      }
-    }
-
-
-    FileContentType Convert(OrthancPluginContentType type)
-    {
-      switch (type)
-      {
-        case OrthancPluginContentType_Dicom:
-          return FileContentType_Dicom;
-
-        case OrthancPluginContentType_DicomAsJson:
-          return FileContentType_DicomAsJson;
-
-        default:
-          return FileContentType_Unknown;
-      }
-    }
-
-
-    DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format)
-    {
-      switch (format)
-      {
-        case OrthancPluginDicomToJsonFormat_Full:
-          return DicomToJsonFormat_Full;
-
-        case OrthancPluginDicomToJsonFormat_Short:
-          return DicomToJsonFormat_Short;
-
-        case OrthancPluginDicomToJsonFormat_Human:
-          return DicomToJsonFormat_Human;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    OrthancPluginInstanceOrigin Convert(RequestOrigin origin)
-    {
-      switch (origin)
-      {
-        case RequestOrigin_DicomProtocol:
-          return OrthancPluginInstanceOrigin_DicomProtocol;
-
-        case RequestOrigin_RestApi:
-          return OrthancPluginInstanceOrigin_RestApi;
-
-        case RequestOrigin_Lua:
-          return OrthancPluginInstanceOrigin_Lua;
-
-        case RequestOrigin_Plugins:
-          return OrthancPluginInstanceOrigin_Plugin;
-
-        case RequestOrigin_Unknown:
-          return OrthancPluginInstanceOrigin_Unknown;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    OrthancPluginHttpMethod Convert(HttpMethod method)
-    {
-      switch (method)
-      {
-        case HttpMethod_Get:
-          return OrthancPluginHttpMethod_Get;
-
-        case HttpMethod_Post:
-          return OrthancPluginHttpMethod_Post;
-
-        case HttpMethod_Put:
-          return OrthancPluginHttpMethod_Put;
-
-        case HttpMethod_Delete:
-          return OrthancPluginHttpMethod_Delete;
-
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-
-
-    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);
-      }
-    }
-
-
-    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason)
-    {
-      switch (reason)
-      {
-        case OrthancPluginStorageCommitmentFailureReason_Success:
-          return StorageCommitmentFailureReason_Success;
-          
-        case OrthancPluginStorageCommitmentFailureReason_ProcessingFailure:
-          return StorageCommitmentFailureReason_ProcessingFailure;
-
-        case OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance:
-          return StorageCommitmentFailureReason_NoSuchObjectInstance;
-
-        case OrthancPluginStorageCommitmentFailureReason_ResourceLimitation:
-          return StorageCommitmentFailureReason_ResourceLimitation;
-
-        case OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported:
-          return StorageCommitmentFailureReason_ReferencedSOPClassNotSupported;
-
-        case OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict:
-          return StorageCommitmentFailureReason_ClassInstanceConflict;
-
-        case OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID:
-          return StorageCommitmentFailureReason_DuplicateTransactionUID;
-             
-        default:
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-    }
-  }
-}
--- a/Plugins/Engine/PluginsEnumerations.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-
-/**
- * NB: Conversions to/from "OrthancPluginConstraintType" and
- * "OrthancPluginResourceType" are located in file
- * "../../OrthancServer/Search/DatabaseConstraint.h" to be shared with
- * the "orthanc-databases" project.
- **/
-
-#include "../Include/orthanc/OrthancCPlugin.h"
-#include "../../OrthancServer/Search/DatabaseConstraint.h"
-
-namespace Orthanc
-{
-  namespace Compatibility
-  {
-    OrthancPluginIdentifierConstraint Convert(IdentifierConstraintType constraint);
-
-    IdentifierConstraintType Convert(OrthancPluginIdentifierConstraint constraint);
-  }
-
-  namespace Plugins
-  {
-    OrthancPluginChangeType Convert(ChangeType type);
-
-    OrthancPluginPixelFormat Convert(PixelFormat format);
-
-    PixelFormat Convert(OrthancPluginPixelFormat format);
-
-    OrthancPluginContentType Convert(FileContentType type);
-
-    FileContentType Convert(OrthancPluginContentType type);
-
-    DicomToJsonFormat Convert(OrthancPluginDicomToJsonFormat format);
-
-    OrthancPluginInstanceOrigin Convert(RequestOrigin origin);
-
-    OrthancPluginHttpMethod Convert(HttpMethod method);
-
-    ValueRepresentation Convert(OrthancPluginValueRepresentation vr);
-
-    OrthancPluginValueRepresentation Convert(ValueRepresentation vr);
-
-    OrthancPluginJobStepStatus Convert(JobStepCode step);
-
-    JobStepCode Convert(OrthancPluginJobStepStatus step);
-
-    StorageCommitmentFailureReason Convert(OrthancPluginStorageCommitmentFailureReason reason);
-  }
-}
-
-#endif
--- a/Plugins/Engine/PluginsErrorDictionary.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,139 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PluginsErrorDictionary.h"
-
-#if ORTHANC_ENABLE_PLUGINS != 1
-#error The plugin support is disabled
-#endif
-
-
-
-#include "PluginsEnumerations.h"
-#include "PluginsManager.h"
-#include "../../Core/Logging.h"
-
-#include <memory>
-
-
-namespace Orthanc
-{
-  PluginsErrorDictionary::PluginsErrorDictionary() : 
-    pos_(ErrorCode_START_PLUGINS)
-  {
-  }
-
-
-  PluginsErrorDictionary::~PluginsErrorDictionary()
-  {
-    for (Errors::iterator it = errors_.begin(); it != errors_.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-
-  OrthancPluginErrorCode PluginsErrorDictionary::Register(SharedLibrary& library,
-                                                          int32_t  pluginCode,
-                                                          uint16_t httpStatus,
-                                                          const char* message)
-  {
-    std::unique_ptr<Error> error(new Error);
-
-    error->pluginName_ = PluginsManager::GetPluginName(library);
-    error->pluginCode_ = pluginCode;
-    error->message_ = message;
-    error->httpStatus_ = static_cast<HttpStatus>(httpStatus);
-
-    OrthancPluginErrorCode code;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      errors_[pos_] = error.release();
-      code = static_cast<OrthancPluginErrorCode>(pos_);
-      pos_ += 1;
-    }
-
-    return code;
-  }
-
-
-  void  PluginsErrorDictionary::LogError(ErrorCode code,
-                                         bool ignoreBuiltinErrors)
-  {
-    if (code >= ErrorCode_START_PLUGINS)
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      Errors::const_iterator error = errors_.find(static_cast<int32_t>(code));
-      
-      if (error != errors_.end())
-      {
-        LOG(ERROR) << "Error code " << error->second->pluginCode_
-                   << " inside plugin \"" << error->second->pluginName_
-                   << "\": " << error->second->message_;
-        return;
-      }
-    }
-
-    if (!ignoreBuiltinErrors)
-    {
-      LOG(ERROR) << "Exception inside the plugin engine: "
-                 << EnumerationToString(code);
-    }
-  }
-
-
-  bool  PluginsErrorDictionary::Format(Json::Value& message,    /* out */
-                                       HttpStatus& httpStatus,  /* out */
-                                       const OrthancException& exception)
-  {
-    if (exception.GetErrorCode() >= ErrorCode_START_PLUGINS)
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-      Errors::const_iterator error = errors_.find(static_cast<int32_t>(exception.GetErrorCode()));
-      
-      if (error != errors_.end())
-      {
-        httpStatus = error->second->httpStatus_;
-        message["PluginName"] = error->second->pluginName_;
-        message["PluginCode"] = error->second->pluginCode_;
-        message["Message"] = error->second->message_;
-
-        return true;
-      }
-    }
-
-    return false;
-  }
-}
--- a/Plugins/Engine/PluginsErrorDictionary.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,93 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../Include/orthanc/OrthancCPlugin.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/SharedLibrary.h"
-
-#include <map>
-#include <string>
-#include <boost/noncopyable.hpp>
-#include <boost/thread/mutex.hpp>
-#include <json/value.h>
-
-
-namespace Orthanc
-{
-  class PluginsErrorDictionary : public boost::noncopyable
-  {
-  private:
-    struct Error
-    {
-      std::string  pluginName_;
-      int32_t      pluginCode_;
-      HttpStatus   httpStatus_;
-      std::string  message_;
-    };
-    
-    typedef std::map<int32_t, Error*>  Errors;
-
-    boost::mutex  mutex_;
-    int32_t  pos_;
-    Errors   errors_;
-
-  public:
-    PluginsErrorDictionary();
-
-    ~PluginsErrorDictionary();
-
-    OrthancPluginErrorCode  Register(SharedLibrary& library,
-                                     int32_t  pluginCode,
-                                     uint16_t httpStatus,
-                                     const char* message);
-
-    void  LogError(ErrorCode code,
-                   bool ignoreBuiltinErrors);
-
-    void  LogError(OrthancPluginErrorCode code,
-                   bool ignoreBuiltinErrors)
-    {
-      LogError(static_cast<ErrorCode>(code), ignoreBuiltinErrors);
-    }
-
-    bool  Format(Json::Value& message,    /* out */
-                 HttpStatus& httpStatus,  /* out */
-                 const OrthancException& exception);
-  };
-}
-
-#endif
--- a/Plugins/Engine/PluginsJob.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,189 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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(const std::string& jobId)
-  {
-    OrthancPluginJobStepStatus status = parameters_.step(parameters_.job);
-
-    switch (status)
-    {
-      case OrthancPluginJobStepStatus_Success:
-        return JobStepResult::Success();
-
-      case OrthancPluginJobStepStatus_Failure:
-        return JobStepResult::Failure(ErrorCode_Plugin, NULL);
-
-      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)
-      {
-        throw OrthancException(ErrorCode_Plugin,
-                               "A job plugin must provide a JSON object as its public content");
-      }
-    }
-  }
-
-  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)
-      {
-        throw OrthancException(ErrorCode_Plugin,
-                               "A job plugin must provide a JSON object as its serialized content");
-      }
-
-
-      static const char* KEY_TYPE = "Type";
-      
-      if (value.isMember(KEY_TYPE))
-      {
-        throw OrthancException(ErrorCode_Plugin,
-                               "The \"Type\" field is for reserved use for serialized job");
-      }
-
-      value[KEY_TYPE] = type_;
-      return true;
-    }
-  }
-}
--- a/Plugins/Engine/PluginsJob.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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() ORTHANC_OVERRIDE
-    {
-    }
-    
-    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE;
-
-    virtual void Reset() ORTHANC_OVERRIDE;
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE;
-
-    virtual float GetProgress() ORTHANC_OVERRIDE;
-
-    virtual void GetJobType(std::string& target) ORTHANC_OVERRIDE
-    {
-      target = type_;
-    }
-    
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE;
-
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) ORTHANC_OVERRIDE
-    {
-      // TODO
-      return false;
-    }
-  };
-}
-
-#endif
--- a/Plugins/Engine/PluginsManager.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,360 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PluginsManager.h"
-
-#if ORTHANC_ENABLE_PLUGINS != 1
-#error The plugin support is disabled
-#endif
-
-#include "../../Core/HttpServer/HttpOutput.h"
-#include "../../Core/Logging.h"
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
-
-#include <cassert>
-#include <memory>
-#include <boost/filesystem.hpp>
-
-#ifdef WIN32
-#define PLUGIN_EXTENSION ".dll"
-#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__) || defined(__OpenBSD__)
-#define PLUGIN_EXTENSION ".so"
-#elif defined(__APPLE__) && defined(__MACH__)
-#define PLUGIN_EXTENSION ".dylib"
-#else
-#error Support your platform here
-#endif
-
-
-namespace Orthanc
-{
-  PluginsManager::Plugin::Plugin(PluginsManager& pluginManager,
-                                 const std::string& path) : 
-    library_(path),
-    pluginManager_(pluginManager)
-  {
-    memset(&context_, 0, sizeof(context_));
-    context_.pluginsManager = this;
-    context_.orthancVersion = ORTHANC_VERSION;
-    context_.Free = ::free;
-    context_.InvokeService = InvokeService;
-  }
-
-
-  static void CallInitialize(SharedLibrary& plugin,
-                             const OrthancPluginContext& context)
-  {
-    typedef int32_t (*Initialize) (const OrthancPluginContext*);
-
-#if defined(_WIN32)
-    Initialize initialize = (Initialize) plugin.GetFunction("OrthancPluginInitialize");
-#else
-    /**
-     * gcc would complain about "ISO C++ forbids casting between
-     * pointer-to-function and pointer-to-object" without the trick
-     * below, that is known as "the POSIX.1-2003 (Technical Corrigendum
-     * 1) workaround". See the man page of "dlsym()".
-     * http://www.trilithium.com/johan/2004/12/problem-with-dlsym/
-     * http://stackoverflow.com/a/14543811/881731
-     **/
-
-    Initialize initialize;
-    *(void **) (&initialize) = plugin.GetFunction("OrthancPluginInitialize");
-#endif
-
-    assert(initialize != NULL);
-    int32_t error = initialize(&context);
-
-    if (error != 0)
-    {
-      LOG(ERROR) << "Error while initializing plugin " << plugin.GetPath()
-                 << " (code " << error << ")";
-      throw OrthancException(ErrorCode_SharedLibrary);
-    }
-  }
-
-
-  static void CallFinalize(SharedLibrary& plugin)
-  {
-    typedef void (*Finalize) ();
-
-#if defined(_WIN32)
-    Finalize finalize = (Finalize) plugin.GetFunction("OrthancPluginFinalize");
-#else
-    Finalize finalize;
-    *(void **) (&finalize) = plugin.GetFunction("OrthancPluginFinalize");
-#endif
-
-    assert(finalize != NULL);
-    finalize();
-  }
-
-
-  static const char* CallGetName(SharedLibrary& plugin)
-  {
-    typedef const char* (*GetName) ();
-
-#if defined(_WIN32)
-    GetName getName = (GetName) plugin.GetFunction("OrthancPluginGetName");
-#else
-    GetName getName;
-    *(void **) (&getName) = plugin.GetFunction("OrthancPluginGetName");
-#endif
-
-    assert(getName != NULL);
-    return getName();
-  }
-
-
-  static const char* CallGetVersion(SharedLibrary& plugin)
-  {
-    typedef const char* (*GetVersion) ();
-
-#if defined(_WIN32)
-    GetVersion getVersion = (GetVersion) plugin.GetFunction("OrthancPluginGetVersion");
-#else
-    GetVersion getVersion;
-    *(void **) (&getVersion) = plugin.GetFunction("OrthancPluginGetVersion");
-#endif
-
-    assert(getVersion != NULL);
-    return getVersion();
-  }
-
-
-  OrthancPluginErrorCode PluginsManager::InvokeService(OrthancPluginContext* context,
-                                                       _OrthancPluginService service, 
-                                                       const void* params)
-  {
-    switch (service)
-    {
-      case _OrthancPluginService_LogError:
-        LOG(ERROR) << reinterpret_cast<const char*>(params);
-        return OrthancPluginErrorCode_Success;
-
-      case _OrthancPluginService_LogWarning:
-        LOG(WARNING) << reinterpret_cast<const char*>(params);
-        return OrthancPluginErrorCode_Success;
-
-      case _OrthancPluginService_LogInfo:
-        LOG(INFO) << reinterpret_cast<const char*>(params);
-        return OrthancPluginErrorCode_Success;
-
-      default:
-        break;
-    }
-
-    Plugin* that = reinterpret_cast<Plugin*>(context->pluginsManager);
-
-    for (std::list<IPluginServiceProvider*>::iterator
-           it = that->GetPluginManager().serviceProviders_.begin(); 
-         it != that->GetPluginManager().serviceProviders_.end(); ++it)
-    {
-      try
-      {
-        if ((*it)->InvokeService(that->GetSharedLibrary(), service, params))
-        {
-          return OrthancPluginErrorCode_Success;
-        }
-      }
-      catch (OrthancException& e)
-      {
-        // This service provider has failed
-        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());
-      }
-    }
-
-    LOG(ERROR) << "Plugin invoking unknown service: " << service;
-    return OrthancPluginErrorCode_UnknownPluginService;
-  }
-
-
-  PluginsManager::PluginsManager()
-  {
-  }
-
-  PluginsManager::~PluginsManager()
-  {
-    for (Plugins::iterator it = plugins_.begin(); it != plugins_.end(); ++it)
-    {
-      if (it->second != NULL)
-      {
-        LOG(WARNING) << "Unregistering plugin '" << it->first
-                     << "' (version " << it->second->GetVersion() << ")";
-
-        CallFinalize(it->second->GetSharedLibrary());
-        delete it->second;
-      }
-    }
-  }
-
-
-  static bool IsOrthancPlugin(SharedLibrary& library)
-  {
-    return (library.HasFunction("OrthancPluginInitialize") &&
-            library.HasFunction("OrthancPluginFinalize") &&
-            library.HasFunction("OrthancPluginGetName") &&
-            library.HasFunction("OrthancPluginGetVersion"));
-  }
-
-  
-  void PluginsManager::RegisterPlugin(const std::string& path)
-  {
-    if (!boost::filesystem::exists(path))
-    {
-      LOG(ERROR) << "Inexistent path to plugins: " << path;
-      return;
-    }
-
-    if (boost::filesystem::is_directory(path))
-    {
-      ScanFolderForPlugins(path, false);
-      return;
-    }
-
-    std::unique_ptr<Plugin> plugin(new Plugin(*this, path));
-
-    if (!IsOrthancPlugin(plugin->GetSharedLibrary()))
-    {
-      LOG(ERROR) << "Plugin " << plugin->GetSharedLibrary().GetPath()
-                 << " does not declare the proper entry functions";
-      throw OrthancException(ErrorCode_SharedLibrary);
-    }
-
-    std::string name(CallGetName(plugin->GetSharedLibrary()));
-    if (plugins_.find(name) != plugins_.end())
-    {
-      LOG(ERROR) << "Plugin '" << name << "' already registered";
-      throw OrthancException(ErrorCode_SharedLibrary);
-    }
-
-    plugin->SetVersion(CallGetVersion(plugin->GetSharedLibrary()));
-    LOG(WARNING) << "Registering plugin '" << name
-                 << "' (version " << plugin->GetVersion() << ")";
-
-    CallInitialize(plugin->GetSharedLibrary(), plugin->GetContext());
-
-    plugins_[name] = plugin.release();
-  }
-
-
-  void PluginsManager::ScanFolderForPlugins(const std::string& folder,
-                                            bool isRecursive)
-  {
-    using namespace boost::filesystem;
-
-    if (!exists(folder))
-    {
-      return;
-    }
-
-    LOG(INFO) << "Scanning folder " << folder << " for plugins";
-
-    directory_iterator end_it; // default construction yields past-the-end
-    for (directory_iterator it(folder);
-          it != end_it;
-          ++it)
-    {
-      std::string path = it->path().string();
-
-      if (is_directory(it->status()))
-      {
-        if (isRecursive)
-        {
-          ScanFolderForPlugins(path, true);
-        }
-      }
-      else
-      {
-        std::string extension = boost::filesystem::extension(it->path());
-        Toolbox::ToLowerCase(extension);
-
-        if (extension == PLUGIN_EXTENSION)
-        {
-          LOG(INFO) << "Found a shared library: " << it->path();
-
-          SharedLibrary plugin(path);
-          if (IsOrthancPlugin(plugin))
-          {
-            RegisterPlugin(path);
-          }
-        }
-      }
-    }
-  }
-
-
-  void PluginsManager::ListPlugins(std::list<std::string>& result) const
-  {
-    result.clear();
-
-    for (Plugins::const_iterator it = plugins_.begin(); 
-         it != plugins_.end(); ++it)
-    {
-      result.push_back(it->first);
-    }
-  }
-
-
-  bool PluginsManager::HasPlugin(const std::string& name) const
-  {
-    return plugins_.find(name) != plugins_.end();
-  }
-
-
-  const std::string& PluginsManager::GetPluginVersion(const std::string& name) const
-  {
-    Plugins::const_iterator it = plugins_.find(name);
-    if (it == plugins_.end())
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    else
-    {
-      return it->second->GetVersion();
-    }
-  }
-
-  
-  std::string PluginsManager::GetPluginName(SharedLibrary& library)
-  {
-    return CallGetName(library);
-  }
-}
--- a/Plugins/Engine/PluginsManager.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "IPluginServiceProvider.h"
-
-#include <map>
-#include <list>
-
-namespace Orthanc
-{
-  class PluginsManager : public boost::noncopyable
-  {
-  private:
-    class Plugin : public boost::noncopyable
-    {
-    private:
-      OrthancPluginContext  context_;
-      SharedLibrary         library_;
-      std::string           version_;
-      PluginsManager&       pluginManager_;
-
-    public:
-      Plugin(PluginsManager& pluginManager,
-             const std::string& path);
-
-      SharedLibrary& GetSharedLibrary()
-      {
-        return library_;
-      }
-
-      void SetVersion(const std::string& version)
-      {
-        version_ = version;
-      }
-
-      const std::string& GetVersion() const
-      {
-        return version_;
-      }
-
-      PluginsManager& GetPluginManager()
-      {
-        return pluginManager_;
-      }
-
-      OrthancPluginContext& GetContext()
-      {
-        return context_;
-      }
-    };
-
-    typedef std::map<std::string, Plugin*>  Plugins;
-
-    Plugins  plugins_;
-    std::list<IPluginServiceProvider*> serviceProviders_;
-
-    static OrthancPluginErrorCode InvokeService(OrthancPluginContext* context,
-                                                _OrthancPluginService service,
-                                                const void* parameters);
-
-  public:
-    PluginsManager();
-
-    ~PluginsManager();
-
-    void RegisterPlugin(const std::string& path);
-
-    void ScanFolderForPlugins(const std::string& path,
-                              bool isRecursive);
-
-    void RegisterServiceProvider(IPluginServiceProvider& provider)
-    {
-      serviceProviders_.push_back(&provider);
-    }
-
-    void ListPlugins(std::list<std::string>& result) const;
-
-    bool HasPlugin(const std::string& name) const;
-
-    const std::string& GetPluginVersion(const std::string& name) const;
-
-    static std::string GetPluginName(SharedLibrary& library);
-  };
-}
-
-#endif
--- a/Plugins/Include/orthanc/OrthancCDatabasePlugin.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,987 +0,0 @@
-/**
- * @ingroup CInterface
- **/
-
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "OrthancCPlugin.h"
-
-
-/** @{ */
-
-#ifdef __cplusplus
-extern "C"
-{
-#endif
-
-
-  /**
-   * Opaque structure that represents the context of a custom database engine.
-   * @ingroup Callbacks
-   **/
-  typedef struct _OrthancPluginDatabaseContext_t OrthancPluginDatabaseContext;
-
-
-/*<! @cond Doxygen_Suppress */
-  typedef enum
-  {
-    _OrthancPluginDatabaseAnswerType_None = 0,
-
-    /* Events */
-    _OrthancPluginDatabaseAnswerType_DeletedAttachment = 1,
-    _OrthancPluginDatabaseAnswerType_DeletedResource = 2,
-    _OrthancPluginDatabaseAnswerType_RemainingAncestor = 3,
-
-    /* Return value */
-    _OrthancPluginDatabaseAnswerType_Attachment = 10,
-    _OrthancPluginDatabaseAnswerType_Change = 11,
-    _OrthancPluginDatabaseAnswerType_DicomTag = 12,
-    _OrthancPluginDatabaseAnswerType_ExportedResource = 13,
-    _OrthancPluginDatabaseAnswerType_Int32 = 14,
-    _OrthancPluginDatabaseAnswerType_Int64 = 15,
-    _OrthancPluginDatabaseAnswerType_Resource = 16,
-    _OrthancPluginDatabaseAnswerType_String = 17,
-    _OrthancPluginDatabaseAnswerType_MatchingResource = 18,  /* New in Orthanc 1.5.2 */
-    _OrthancPluginDatabaseAnswerType_Metadata = 19,          /* New in Orthanc 1.5.4 */
-
-    _OrthancPluginDatabaseAnswerType_INTERNAL = 0x7fffffff
-  } _OrthancPluginDatabaseAnswerType;
-
-
-  typedef struct
-  {
-    const char* uuid;
-    int32_t     contentType;
-    uint64_t    uncompressedSize;
-    const char* uncompressedHash;
-    int32_t     compressionType;
-    uint64_t    compressedSize;
-    const char* compressedHash;
-  } OrthancPluginAttachment;
-
-  typedef struct
-  {
-    uint16_t     group;
-    uint16_t     element;
-    const char*  value;
-  } OrthancPluginDicomTag;
-
-  typedef struct
-  {
-    int64_t                    seq;
-    int32_t                    changeType;
-    OrthancPluginResourceType  resourceType;
-    const char*                publicId;
-    const char*                date;
-  } OrthancPluginChange;
-
-  typedef struct
-  {
-    int64_t                    seq;
-    OrthancPluginResourceType  resourceType;
-    const char*                publicId;
-    const char*                modality;
-    const char*                date;
-    const char*                patientId;
-    const char*                studyInstanceUid;
-    const char*                seriesInstanceUid;
-    const char*                sopInstanceUid;
-  } OrthancPluginExportedResource;
-
-  typedef struct   /* New in Orthanc 1.5.2 */
-  {
-    OrthancPluginResourceType    level;
-    uint16_t                     tagGroup;
-    uint16_t                     tagElement;
-    uint8_t                      isIdentifierTag;
-    uint8_t                      isCaseSensitive;
-    uint8_t                      isMandatory;
-    OrthancPluginConstraintType  type;
-    uint32_t                     valuesCount;
-    const char* const*           values;
-  } OrthancPluginDatabaseConstraint;
-
-  typedef struct   /* New in Orthanc 1.5.2 */
-  {
-    const char*  resourceId;
-    const char*  someInstanceId;  /* Can be NULL if not requested */
-  } OrthancPluginMatchingResource;
-
-  typedef struct   /* New in Orthanc 1.5.2 */
-  {
-    /* Mandatory field */
-    uint8_t  isNewInstance;
-    int64_t  instanceId;
-
-    /* The following fields must only be set if "isNewInstance" is "true" */
-    uint8_t  isNewPatient;
-    uint8_t  isNewStudy;
-    uint8_t  isNewSeries;
-    int64_t  patientId;
-    int64_t  studyId;
-    int64_t  seriesId;
-  } OrthancPluginCreateInstanceResult;
-
-  typedef struct  /* New in Orthanc 1.5.2 */
-  {
-    int64_t      resource;
-    uint16_t     group;
-    uint16_t     element;
-    const char*  value;
-  } OrthancPluginResourcesContentTags;
-    
-  typedef struct  /* New in Orthanc 1.5.2 */
-  {
-    int64_t      resource;
-    int32_t      metadata;
-    const char*  value;
-  } OrthancPluginResourcesContentMetadata;
-
-
-  typedef struct
-  {
-    OrthancPluginDatabaseContext* database;
-    _OrthancPluginDatabaseAnswerType  type;
-    int32_t      valueInt32;
-    uint32_t     valueUint32;
-    int64_t      valueInt64;
-    const char  *valueString;
-    const void  *valueGeneric;
-  } _OrthancPluginDatabaseAnswer;
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerString(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const char*                    value)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_String;
-    params.valueString = value;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChange(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const OrthancPluginChange*     change)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Change;
-    params.valueUint32 = 0;
-    params.valueGeneric = change;
-
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerChangesDone(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Change;
-    params.valueUint32 = 1;
-    params.valueGeneric = NULL;
-
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt32(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    int32_t                        value)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Int32;
-    params.valueInt32 = value;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerInt64(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    int64_t                        value)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Int64;
-    params.valueInt64 = value;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResource(
-    OrthancPluginContext*                 context,
-    OrthancPluginDatabaseContext*         database,
-    const OrthancPluginExportedResource*  exported)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_ExportedResource;
-    params.valueUint32 = 0;
-    params.valueGeneric = exported;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerExportedResourcesDone(
-    OrthancPluginContext*                 context,
-    OrthancPluginDatabaseContext*         database)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_ExportedResource;
-    params.valueUint32 = 1;
-    params.valueGeneric = NULL;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerDicomTag(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const OrthancPluginDicomTag*   tag)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_DicomTag;
-    params.valueGeneric = tag;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerAttachment(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const OrthancPluginAttachment* attachment)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Attachment;
-    params.valueGeneric = attachment;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerResource(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    int64_t                        id,
-    OrthancPluginResourceType      resourceType)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Resource;
-    params.valueInt64 = id;
-    params.valueInt32 = (int32_t) resourceType;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMatchingResource(
-    OrthancPluginContext*                 context,
-    OrthancPluginDatabaseContext*         database,
-    const OrthancPluginMatchingResource*  match)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_MatchingResource;
-    params.valueGeneric = match;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseAnswerMetadata(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    int64_t                        resourceId,
-    int32_t                        type,
-    const char*                    value)
-  {
-    OrthancPluginResourcesContentMetadata metadata;
-    _OrthancPluginDatabaseAnswer params;
-    metadata.resource = resourceId;
-    metadata.metadata = type;
-    metadata.value = value;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_Metadata;
-    params.valueGeneric = &metadata;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedAttachment(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const OrthancPluginAttachment* attachment)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_DeletedAttachment;
-    params.valueGeneric = attachment;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalDeletedResource(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const char*                    publicId,
-    OrthancPluginResourceType      resourceType)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_DeletedResource;
-    params.valueString = publicId;
-    params.valueInt32 = (int32_t) resourceType;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-  ORTHANC_PLUGIN_INLINE void OrthancPluginDatabaseSignalRemainingAncestor(
-    OrthancPluginContext*          context,
-    OrthancPluginDatabaseContext*  database,
-    const char*                    ancestorId,
-    OrthancPluginResourceType      ancestorType)
-  {
-    _OrthancPluginDatabaseAnswer params;
-    memset(&params, 0, sizeof(params));
-    params.database = database;
-    params.type = _OrthancPluginDatabaseAnswerType_RemainingAncestor;
-    params.valueString = ancestorId;
-    params.valueInt32 = (int32_t) ancestorType;
-    context->InvokeService(context, _OrthancPluginService_DatabaseAnswer, &params);
-  }
-
-
-
-
-
-  typedef struct
-  {
-    OrthancPluginErrorCode  (*addAttachment) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      const OrthancPluginAttachment* attachment);
-                             
-    OrthancPluginErrorCode  (*attachChild) (
-      /* inputs */
-      void* payload,
-      int64_t parent,
-      int64_t child);
-                             
-    OrthancPluginErrorCode  (*clearChanges) (
-      /* inputs */
-      void* payload);
-                             
-    OrthancPluginErrorCode  (*clearExportedResources) (
-      /* inputs */
-      void* payload);
-
-    OrthancPluginErrorCode  (*createResource) (
-      /* outputs */
-      int64_t* id, 
-      /* inputs */
-      void* payload,
-      const char* publicId,
-      OrthancPluginResourceType resourceType);           
-                   
-    OrthancPluginErrorCode  (*deleteAttachment) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      int32_t contentType);
-   
-    OrthancPluginErrorCode  (*deleteMetadata) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      int32_t metadataType);
-   
-    OrthancPluginErrorCode  (*deleteResource) (
-      /* inputs */
-      void* payload,
-      int64_t id);    
-
-    /* Output: Use OrthancPluginDatabaseAnswerString() */
-    OrthancPluginErrorCode  (*getAllPublicIds) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      OrthancPluginResourceType resourceType);
-
-    /* Output: Use OrthancPluginDatabaseAnswerChange() and
-     * OrthancPluginDatabaseAnswerChangesDone() */
-    OrthancPluginErrorCode  (*getChanges) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t since,
-      uint32_t maxResult);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*getChildrenInternalId) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-                   
-    /* Output: Use OrthancPluginDatabaseAnswerString() */
-    OrthancPluginErrorCode  (*getChildrenPublicId) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    /* Output: Use OrthancPluginDatabaseAnswerExportedResource() and
-     * OrthancPluginDatabaseAnswerExportedResourcesDone() */
-    OrthancPluginErrorCode  (*getExportedResources) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t  since,
-      uint32_t  maxResult);
-                   
-    /* Output: Use OrthancPluginDatabaseAnswerChange() */
-    OrthancPluginErrorCode  (*getLastChange) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload);
-
-    /* Output: Use OrthancPluginDatabaseAnswerExportedResource() */
-    OrthancPluginErrorCode  (*getLastExportedResource) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload);
-                   
-    /* Output: Use OrthancPluginDatabaseAnswerDicomTag() */
-    OrthancPluginErrorCode  (*getMainDicomTags) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-                   
-    /* Output: Use OrthancPluginDatabaseAnswerString() */
-    OrthancPluginErrorCode  (*getPublicId) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    OrthancPluginErrorCode  (*getResourceCount) (
-      /* outputs */
-      uint64_t* target,
-      /* inputs */
-      void* payload,
-      OrthancPluginResourceType  resourceType);
-                   
-    OrthancPluginErrorCode  (*getResourceType) (
-      /* outputs */
-      OrthancPluginResourceType* resourceType,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    OrthancPluginErrorCode  (*getTotalCompressedSize) (
-      /* outputs */
-      uint64_t* target,
-      /* inputs */
-      void* payload);
-                   
-    OrthancPluginErrorCode  (*getTotalUncompressedSize) (
-      /* outputs */
-      uint64_t* target,
-      /* inputs */
-      void* payload);
-                   
-    OrthancPluginErrorCode  (*isExistingResource) (
-      /* outputs */
-      int32_t* existing,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    OrthancPluginErrorCode  (*isProtectedPatient) (
-      /* outputs */
-      int32_t* isProtected,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt32() */
-    OrthancPluginErrorCode  (*listAvailableMetadata) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-                   
-    /* Output: Use OrthancPluginDatabaseAnswerInt32() */
-    OrthancPluginErrorCode  (*listAvailableAttachments) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    OrthancPluginErrorCode  (*logChange) (
-      /* inputs */
-      void* payload,
-      const OrthancPluginChange* change);
-                   
-    OrthancPluginErrorCode  (*logExportedResource) (
-      /* inputs */
-      void* payload,
-      const OrthancPluginExportedResource* exported);
-                   
-    /* Output: Use OrthancPluginDatabaseAnswerAttachment() */
-    OrthancPluginErrorCode  (*lookupAttachment) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id,
-      int32_t contentType);
-
-    /* Output: Use OrthancPluginDatabaseAnswerString() */
-    OrthancPluginErrorCode  (*lookupGlobalProperty) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int32_t property);
-
-    /* Use "OrthancPluginDatabaseExtensions::lookupIdentifier3" 
-       instead of this function as of Orthanc 0.9.5 (db v6), can be set to NULL.
-       Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*lookupIdentifier) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      const OrthancPluginDicomTag* tag);
-
-    /* Unused starting with Orthanc 0.9.5 (db v6), can be set to NULL.
-       Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*lookupIdentifier2) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      const char* value);
-
-    /* Output: Use OrthancPluginDatabaseAnswerString() */
-    OrthancPluginErrorCode  (*lookupMetadata) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id,
-      int32_t metadata);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*lookupParent) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    /* Output: Use OrthancPluginDatabaseAnswerResource() */
-    OrthancPluginErrorCode  (*lookupResource) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      const char* publicId);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*selectPatientToRecycle) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*selectPatientToRecycle2) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t patientIdToAvoid);
-
-    OrthancPluginErrorCode  (*setGlobalProperty) (
-      /* inputs */
-      void* payload,
-      int32_t property,
-      const char* value);
-
-    OrthancPluginErrorCode  (*setMainDicomTag) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      const OrthancPluginDicomTag* tag);
-
-    OrthancPluginErrorCode  (*setIdentifierTag) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      const OrthancPluginDicomTag* tag);
-
-    OrthancPluginErrorCode  (*setMetadata) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      int32_t metadata,
-      const char* value);
-
-    OrthancPluginErrorCode  (*setProtectedPatient) (
-      /* inputs */
-      void* payload,
-      int64_t id,
-      int32_t isProtected);
-
-    OrthancPluginErrorCode  (*startTransaction) (
-      /* inputs */
-      void* payload);
-
-    OrthancPluginErrorCode  (*rollbackTransaction) (
-      /* inputs */
-      void* payload);
-
-    OrthancPluginErrorCode  (*commitTransaction) (
-      /* inputs */
-      void* payload);
-
-    OrthancPluginErrorCode  (*open) (
-      /* inputs */
-      void* payload);
-
-    OrthancPluginErrorCode  (*close) (
-      /* inputs */
-      void* payload);
-
-  } OrthancPluginDatabaseBackend;
-
-
-  typedef struct
-  {
-    /**
-     * Base extensions since Orthanc 1.0.0
-     **/
-    
-    /* Output: Use OrthancPluginDatabaseAnswerString() */
-    OrthancPluginErrorCode  (*getAllPublicIdsWithLimit) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      OrthancPluginResourceType resourceType,
-      uint64_t since,
-      uint64_t limit);
-
-    OrthancPluginErrorCode  (*getDatabaseVersion) (
-      /* outputs */
-      uint32_t* version,
-      /* inputs */
-      void* payload);
-
-    OrthancPluginErrorCode  (*upgradeDatabase) (
-      /* inputs */
-      void* payload,
-      uint32_t targetVersion,
-      OrthancPluginStorageArea* storageArea);
- 
-    OrthancPluginErrorCode  (*clearMainDicomTags) (
-      /* inputs */
-      void* payload,
-      int64_t id);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*getAllInternalIds) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      OrthancPluginResourceType resourceType);
-
-    /* Output: Use OrthancPluginDatabaseAnswerInt64() */
-    OrthancPluginErrorCode  (*lookupIdentifier3) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      OrthancPluginResourceType resourceType,
-      const OrthancPluginDicomTag* tag,
-      OrthancPluginIdentifierConstraint constraint);
-
-
-    /**
-     * Extensions since Orthanc 1.4.0
-     **/
-    
-    /* 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);
-
-    
-    /**
-     * Extensions since Orthanc 1.5.2
-     **/
-    
-    /* Ouput: Use OrthancPluginDatabaseAnswerMatchingResource */
-    OrthancPluginErrorCode  (*lookupResources) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      uint32_t constraintsCount,
-      const OrthancPluginDatabaseConstraint* constraints,
-      OrthancPluginResourceType queryLevel,
-      uint32_t limit,
-      uint8_t requestSomeInstance);
-    
-    OrthancPluginErrorCode  (*createInstance) (
-      /* output */
-      OrthancPluginCreateInstanceResult* output,
-      /* inputs */
-      void* payload,
-      const char* hashPatient,
-      const char* hashStudy,
-      const char* hashSeries,
-      const char* hashInstance);
-
-    OrthancPluginErrorCode  (*setResourcesContent) (
-      /* inputs */
-      void* payload,
-      uint32_t countIdentifierTags,
-      const OrthancPluginResourcesContentTags* identifierTags,
-      uint32_t countMainDicomTags,
-      const OrthancPluginResourcesContentTags* mainDicomTags,
-      uint32_t countMetadata,
-      const OrthancPluginResourcesContentMetadata* metadata);
-
-    /* Ouput: Use OrthancPluginDatabaseAnswerString */
-    OrthancPluginErrorCode  (*getChildrenMetadata) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t resourceId,
-      int32_t metadata);
-
-    OrthancPluginErrorCode  (*getLastChangeIndex) (
-      /* outputs */
-      int64_t* target,
-      /* inputs */
-      void* payload);
-                   
-    OrthancPluginErrorCode  (*tagMostRecentPatient) (
-      /* inputs */
-      void* payload,
-      int64_t patientId);
-                   
-    
-    /**
-     * Extensions since Orthanc 1.5.4
-     **/
-
-    /* Ouput: Use OrthancPluginDatabaseAnswerMetadata */
-    OrthancPluginErrorCode  (*getAllMetadata) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      /* inputs */
-      void* payload,
-      int64_t resourceId);
-    
-    /* Ouput: Use OrthancPluginDatabaseAnswerString to send 
-       the public ID of the parent (if the resource is not a patient) */
-    OrthancPluginErrorCode  (*lookupResourceAndParent) (
-      /* outputs */
-      OrthancPluginDatabaseContext* context,
-      uint8_t* isExisting,
-      int64_t* id,
-      OrthancPluginResourceType* type,
-      
-      /* inputs */
-      void* payload,
-      const char* publicId);
-
-  } OrthancPluginDatabaseExtensions;
-
-/*<! @endcond */
-
-
-  typedef struct
-  {
-    OrthancPluginDatabaseContext**       result;
-    const OrthancPluginDatabaseBackend*  backend;
-    void*                                payload;
-  } _OrthancPluginRegisterDatabaseBackend;
-
-  /**
-   * Register a custom database back-end (for legacy plugins).
-   *
-   * @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.
-   * @return The context of the database engine (it must not be manually freed).
-   * @ingroup Callbacks
-   * @deprecated
-   * @see OrthancPluginRegisterDatabaseBackendV2
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginDatabaseContext* OrthancPluginRegisterDatabaseBackend(
-    OrthancPluginContext*                context,
-    const OrthancPluginDatabaseBackend*  backend,
-    void*                                payload)
-  {
-    OrthancPluginDatabaseContext* result = NULL;
-    _OrthancPluginRegisterDatabaseBackend params;
-
-    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
-    {
-      return NULL;
-    }
-
-    memset(&params, 0, sizeof(params));
-    params.backend = backend;
-    params.result = &result;
-    params.payload = payload;
-
-    if (context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackend, &params) ||
-        result == NULL)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  typedef struct
-  {
-    OrthancPluginDatabaseContext**          result;
-    const OrthancPluginDatabaseBackend*     backend;
-    void*                                   payload;
-    const OrthancPluginDatabaseExtensions*  extensions;
-    uint32_t                                extensionsSize;
-  } _OrthancPluginRegisterDatabaseBackendV2;
-
-
-  /**
-   * Register a custom database back-end.
-   *
-   * @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.
-   * @param extensions Extensions to the base database SDK that was shipped until Orthanc 0.9.3.
-   * @return The context of the database engine (it must not be manually freed).
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginDatabaseContext* OrthancPluginRegisterDatabaseBackendV2(
-    OrthancPluginContext*                   context,
-    const OrthancPluginDatabaseBackend*     backend,
-    const OrthancPluginDatabaseExtensions*  extensions,
-    void*                                   payload)
-  {
-    OrthancPluginDatabaseContext* result = NULL;
-    _OrthancPluginRegisterDatabaseBackendV2 params;
-
-    if (sizeof(int32_t) != sizeof(_OrthancPluginDatabaseAnswerType))
-    {
-      return NULL;
-    }
-
-    memset(&params, 0, sizeof(params));
-    params.backend = backend;
-    params.result = &result;
-    params.payload = payload;
-    params.extensions = extensions;
-    params.extensionsSize = sizeof(OrthancPluginDatabaseExtensions);
-
-    if (context->InvokeService(context, _OrthancPluginService_RegisterDatabaseBackendV2, &params) ||
-        result == NULL)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-#ifdef  __cplusplus
-}
-#endif
-
-
-/** @} */
-
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8193 +0,0 @@
-/**
- * \mainpage
- *
- * This C/C++ SDK allows external developers to create plugins that
- * can be loaded into Orthanc to extend its functionality. Each
- * Orthanc plugin must expose 4 public functions with the following
- * signatures:
- * 
- * -# <tt>int32_t OrthancPluginInitialize(const OrthancPluginContext* context)</tt>:
- *    This function is invoked by Orthanc when it loads the plugin on startup.
- *    The plugin must:
- *    - Check its compatibility with the Orthanc version using
- *      ::OrthancPluginCheckVersion().
- *    - Store the context pointer so that it can use the plugin 
- *      services of Orthanc.
- *    - Register all its REST callbacks using ::OrthancPluginRegisterRestCallback().
- *    - Possibly register its callback for received DICOM instances using ::OrthancPluginRegisterOnStoredInstanceCallback().
- *    - 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().
- *    - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback().
- *    - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback().
- *    - Possibly register a callback for Storage Commitment SCP using ::OrthancPluginRegisterStorageCommitmentScpCallback().
- *    - Possibly register a callback to filter incoming DICOM instance using OrthancPluginRegisterIncomingDicomInstanceFilter().
- *    - Possibly register a custom transcoder for DICOM images using OrthancPluginRegisterTranscoderCallback().
- * -# <tt>void OrthancPluginFinalize()</tt>:
- *    This function is invoked by Orthanc during its shutdown. The plugin
- *    must free all its memory.
- * -# <tt>const char* OrthancPluginGetName()</tt>:
- *    The plugin must return a short string to identify itself.
- * -# <tt>const char* OrthancPluginGetVersion()</tt>:
- *    The plugin must return a string containing its version number.
- *
- * The name and the version of a plugin is only used to prevent it
- * from being loaded twice. Note that, in C++, it is mandatory to
- * declare these functions within an <tt>extern "C"</tt> section.
- * 
- * To ensure multi-threading safety, the various REST callbacks are
- * guaranteed to be executed in mutual exclusion since Orthanc
- * 0.8.5. If this feature is undesired (notably when developing
- * high-performance plugins handling simultaneous requests), use
- * ::OrthancPluginRegisterRestCallbackNoLock().
- **/
-
-
-
-/**
- * @defgroup Images Images and compression
- * @brief Functions to deal with images and compressed buffers.
- *
- * @defgroup REST REST
- * @brief Functions to answer REST requests in a callback.
- *
- * @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, storage commitment).
- *
- * @defgroup Orthanc Orthanc
- * @brief Functions to access the content of the Orthanc server.
- *
- * @defgroup DicomInstance DicomInstance
- * @brief Functions to access DICOM images that are managed by the Orthanc core.
- **/
-
-
-
-/**
- * @defgroup Toolbox Toolbox
- * @brief Generic functions to help with the creation of plugins.
- **/
-
-
-
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <stdio.h>
-#include <string.h>
-
-#ifdef WIN32
-#  define ORTHANC_PLUGINS_API __declspec(dllexport)
-#elif __GNUC__ >= 4
-#  define ORTHANC_PLUGINS_API __attribute__ ((visibility ("default")))
-#else
-#  define ORTHANC_PLUGINS_API
-#endif
-
-#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
-#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     7
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  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
-
-
-
-/********************************************************************
- ** Check that function inlining is properly supported. The use of
- ** inlining is required, to avoid the duplication of object code
- ** between two compilation modules that would use the Orthanc Plugin
- ** API.
- ********************************************************************/
-
-/* If the auto-detection of the "inline" keyword below does not work
-   automatically and that your compiler is known to properly support
-   inlining, uncomment the following #define and adapt the definition
-   of "static inline". */
-
-/* #define ORTHANC_PLUGIN_INLINE static inline */
-
-#ifndef ORTHANC_PLUGIN_INLINE
-#  if __STDC_VERSION__ >= 199901L
-/*   This is C99 or above: http://predef.sourceforge.net/prestd.html */
-#    define ORTHANC_PLUGIN_INLINE static inline
-#  elif defined(__cplusplus)
-/*   This is C++ */
-#    define ORTHANC_PLUGIN_INLINE static inline
-#  elif defined(__GNUC__)
-/*   This is GCC running in C89 mode */
-#    define ORTHANC_PLUGIN_INLINE static __inline
-#  elif defined(_MSC_VER)
-/*   This is Visual Studio running in C89 mode */
-#    define ORTHANC_PLUGIN_INLINE static __inline
-#  else
-#    error Your compiler is not known to support the "inline" keyword
-#  endif
-#endif
-
-
-
-/********************************************************************
- ** Inclusion of standard libraries.
- ********************************************************************/
-
-/**
- * For Microsoft Visual Studio, a compatibility "stdint.h" can be
- * downloaded at the following URL:
- * https://hg.orthanc-server.com/orthanc/raw-file/tip/Resources/ThirdParty/VisualStudio/stdint.h
- **/
-#include <stdint.h>
-
-#include <stdlib.h>
-
-
-
-/********************************************************************
- ** Definition of the Orthanc Plugin API.
- ********************************************************************/
-
-/** @{ */
-
-#ifdef __cplusplus
-extern "C"
-{
-#endif
-
-  /**
-   * The various error codes that can be returned by the Orthanc core.
-   **/
-  typedef enum
-  {
-    OrthancPluginErrorCode_InternalError = -1    /*!< Internal error */,
-    OrthancPluginErrorCode_Success = 0    /*!< Success */,
-    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    /*!< 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 */,
-    OrthancPluginErrorCode_BadRequest = 8    /*!< Bad request */,
-    OrthancPluginErrorCode_NetworkProtocol = 9    /*!< Error in the network protocol */,
-    OrthancPluginErrorCode_SystemCommand = 10    /*!< Error while calling a system command */,
-    OrthancPluginErrorCode_Database = 11    /*!< Error with the database engine */,
-    OrthancPluginErrorCode_UriSyntax = 12    /*!< Badly formatted URI */,
-    OrthancPluginErrorCode_InexistentFile = 13    /*!< Inexistent file */,
-    OrthancPluginErrorCode_CannotWriteFile = 14    /*!< Cannot write to file */,
-    OrthancPluginErrorCode_BadFileFormat = 15    /*!< Bad file format */,
-    OrthancPluginErrorCode_Timeout = 16    /*!< Timeout */,
-    OrthancPluginErrorCode_UnknownResource = 17    /*!< Unknown resource */,
-    OrthancPluginErrorCode_IncompatibleDatabaseVersion = 18    /*!< Incompatible version of the database */,
-    OrthancPluginErrorCode_FullStorage = 19    /*!< The file storage is full */,
-    OrthancPluginErrorCode_CorruptedFile = 20    /*!< Corrupted file (e.g. inconsistent MD5 hash) */,
-    OrthancPluginErrorCode_InexistentTag = 21    /*!< Inexistent tag */,
-    OrthancPluginErrorCode_ReadOnly = 22    /*!< Cannot modify a read-only data structure */,
-    OrthancPluginErrorCode_IncompatibleImageFormat = 23    /*!< Incompatible format of the images */,
-    OrthancPluginErrorCode_IncompatibleImageSize = 24    /*!< Incompatible size of the images */,
-    OrthancPluginErrorCode_SharedLibrary = 25    /*!< Error while using a shared library (plugin) */,
-    OrthancPluginErrorCode_UnknownPluginService = 26    /*!< Plugin invoking an unknown service */,
-    OrthancPluginErrorCode_UnknownDicomTag = 27    /*!< Unknown DICOM tag */,
-    OrthancPluginErrorCode_BadJson = 28    /*!< Cannot parse a JSON document */,
-    OrthancPluginErrorCode_Unauthorized = 29    /*!< Bad credentials were provided to an HTTP request */,
-    OrthancPluginErrorCode_BadFont = 30    /*!< Badly formatted font file */,
-    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_BadGeometry = 38    /*!< Geometry error encountered in Stone */,
-    OrthancPluginErrorCode_SslInitialization = 39    /*!< Cannot initialize SSL encryption, check out your certificates */,
-    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 */,
-    OrthancPluginErrorCode_SQLiteStatementAlreadyUsed = 1003    /*!< SQLite: This cached statement is already being referred to */,
-    OrthancPluginErrorCode_SQLiteExecute = 1004    /*!< SQLite: Cannot execute a command */,
-    OrthancPluginErrorCode_SQLiteRollbackWithoutTransaction = 1005    /*!< SQLite: Rolling back a nonexistent transaction (have you called Begin()?) */,
-    OrthancPluginErrorCode_SQLiteCommitWithoutTransaction = 1006    /*!< SQLite: Committing a nonexistent transaction */,
-    OrthancPluginErrorCode_SQLiteRegisterFunction = 1007    /*!< SQLite: Unable to register a function */,
-    OrthancPluginErrorCode_SQLiteFlush = 1008    /*!< SQLite: Unable to flush the database */,
-    OrthancPluginErrorCode_SQLiteCannotRun = 1009    /*!< SQLite: Cannot run a cached statement */,
-    OrthancPluginErrorCode_SQLiteCannotStep = 1010    /*!< SQLite: Cannot step over a cached statement */,
-    OrthancPluginErrorCode_SQLiteBindOutOfRange = 1011    /*!< SQLite: Bing a value while out of range (serious error) */,
-    OrthancPluginErrorCode_SQLitePrepareStatement = 1012    /*!< SQLite: Cannot prepare a cached statement */,
-    OrthancPluginErrorCode_SQLiteTransactionAlreadyStarted = 1013    /*!< SQLite: Beginning the same transaction twice */,
-    OrthancPluginErrorCode_SQLiteTransactionCommit = 1014    /*!< SQLite: Failure when committing the transaction */,
-    OrthancPluginErrorCode_SQLiteTransactionBegin = 1015    /*!< SQLite: Cannot start a transaction */,
-    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 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 */,
-    OrthancPluginErrorCode_MakeDirectory = 2008    /*!< Cannot create a directory */,
-    OrthancPluginErrorCode_BadApplicationEntityTitle = 2009    /*!< An application entity title (AET) cannot be empty or be longer than 16 characters */,
-    OrthancPluginErrorCode_NoCFindHandler = 2010    /*!< No request handler factory for DICOM C-FIND SCP */,
-    OrthancPluginErrorCode_NoCMoveHandler = 2011    /*!< No request handler factory for DICOM C-MOVE SCP */,
-    OrthancPluginErrorCode_NoCStoreHandler = 2012    /*!< No request handler factory for DICOM C-STORE SCP */,
-    OrthancPluginErrorCode_NoApplicationEntityFilter = 2013    /*!< No application entity filter */,
-    OrthancPluginErrorCode_NoSopClassOrInstance = 2014    /*!< DicomUserConnection: Unable to find the SOP class and instance */,
-    OrthancPluginErrorCode_NoPresentationContext = 2015    /*!< DicomUserConnection: No acceptable presentation context for modality */,
-    OrthancPluginErrorCode_DicomFindUnavailable = 2016    /*!< DicomUserConnection: The C-FIND command is not supported by the remote SCP */,
-    OrthancPluginErrorCode_DicomMoveUnavailable = 2017    /*!< DicomUserConnection: The C-MOVE command is not supported by the remote SCP */,
-    OrthancPluginErrorCode_CannotStoreInstance = 2018    /*!< Cannot store an instance */,
-    OrthancPluginErrorCode_CreateDicomNotString = 2019    /*!< Only string values are supported when creating DICOM instances */,
-    OrthancPluginErrorCode_CreateDicomOverrideTag = 2020    /*!< Trying to override a value inherited from a parent module */,
-    OrthancPluginErrorCode_CreateDicomUseContent = 2021    /*!< Use \"Content\" to inject an image into a new DICOM instance */,
-    OrthancPluginErrorCode_CreateDicomNoPayload = 2022    /*!< No payload is present for one instance in the series */,
-    OrthancPluginErrorCode_CreateDicomUseDataUriScheme = 2023    /*!< The payload of the DICOM instance must be specified according to Data URI scheme */,
-    OrthancPluginErrorCode_CreateDicomBadParent = 2024    /*!< Trying to attach a new DICOM instance to an inexistent resource */,
-    OrthancPluginErrorCode_CreateDicomParentIsInstance = 2025    /*!< Trying to attach a new DICOM instance to an instance (must be a series, study or patient) */,
-    OrthancPluginErrorCode_CreateDicomParentEncoding = 2026    /*!< Unable to get the encoding of the parent resource */,
-    OrthancPluginErrorCode_UnknownModality = 2027    /*!< Unknown modality */,
-    OrthancPluginErrorCode_BadJobOrdering = 2028    /*!< Bad ordering of filters in a job */,
-    OrthancPluginErrorCode_JsonToLuaTable = 2029    /*!< Cannot convert the given JSON object to a Lua table */,
-    OrthancPluginErrorCode_CannotCreateLua = 2030    /*!< Cannot create the Lua context */,
-    OrthancPluginErrorCode_CannotExecuteLua = 2031    /*!< Cannot execute a Lua command */,
-    OrthancPluginErrorCode_LuaAlreadyExecuted = 2032    /*!< Arguments cannot be pushed after the Lua function is executed */,
-    OrthancPluginErrorCode_LuaBadOutput = 2033    /*!< The Lua function does not give the expected number of outputs */,
-    OrthancPluginErrorCode_NotLuaPredicate = 2034    /*!< The Lua function is not a predicate (only true/false outputs allowed) */,
-    OrthancPluginErrorCode_LuaReturnsNoString = 2035    /*!< The Lua function does not return a string */,
-    OrthancPluginErrorCode_StorageAreaAlreadyRegistered = 2036    /*!< Another plugin has already registered a custom storage area */,
-    OrthancPluginErrorCode_DatabaseBackendAlreadyRegistered = 2037    /*!< Another plugin has already registered a custom database back-end */,
-    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_NoStorageCommitmentHandler = 2043    /*!< No request handler factory for DICOM N-ACTION SCP (storage commitment) */,
-    OrthancPluginErrorCode_NoCGetHandler = 2044    /*!< No request handler factory for DICOM C-GET SCP */,
-    OrthancPluginErrorCode_UnsupportedMediaType = 3000    /*!< Unsupported media type */,
-
-    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
-  } OrthancPluginErrorCode;
-
-
-  /**
-   * Forward declaration of one of the mandatory functions for Orthanc
-   * plugins.
-   **/
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName();
-
-
-  /**
-   * The various HTTP methods for a REST call.
-   **/
-  typedef enum
-  {
-    OrthancPluginHttpMethod_Get = 1,    /*!< GET request */
-    OrthancPluginHttpMethod_Post = 2,   /*!< POST request */
-    OrthancPluginHttpMethod_Put = 3,    /*!< PUT request */
-    OrthancPluginHttpMethod_Delete = 4, /*!< DELETE request */
-
-    _OrthancPluginHttpMethod_INTERNAL = 0x7fffffff
-  } OrthancPluginHttpMethod;
-
-
-  /**
-   * @brief The parameters of a REST request.
-   * @ingroup Callbacks
-   **/
-  typedef struct
-  {
-    /**
-     * @brief The HTTP method.
-     **/
-    OrthancPluginHttpMethod method;
-
-    /**
-     * @brief The number of groups of the regular expression.
-     **/
-    uint32_t                groupsCount;
-
-    /**
-     * @brief The matched values for the groups of the regular expression.
-     **/
-    const char* const*      groups;
-
-    /**
-     * @brief For a GET request, the number of GET parameters.
-     **/
-    uint32_t                getCount;
-
-    /**
-     * @brief For a GET request, the keys of the GET parameters.
-     **/
-    const char* const*      getKeys;
-
-    /**
-     * @brief For a GET request, the values of the GET parameters.
-     **/
-    const char* const*      getValues;
-
-    /**
-     * @brief For a PUT or POST request, the content of the body.
-     **/
-    const void*             body;
-
-    /**
-     * @brief For a PUT or POST request, the number of bytes of the body.
-     **/
-    uint32_t                bodySize;
-
-
-    /* --------------------------------------------------
-       New in version 0.8.1
-       -------------------------------------------------- */
-
-    /**
-     * @brief The number of HTTP headers.
-     **/
-    uint32_t                headersCount;
-
-    /**
-     * @brief The keys of the HTTP headers (always converted to low-case).
-     **/
-    const char* const*      headersKeys;
-
-    /**
-     * @brief The values of the HTTP headers.
-     **/
-    const char* const*      headersValues;
-
-  } OrthancPluginHttpRequest;
-
-
-  typedef enum 
-  {
-    /* Generic services */
-    _OrthancPluginService_LogInfo = 1,
-    _OrthancPluginService_LogWarning = 2,
-    _OrthancPluginService_LogError = 3,
-    _OrthancPluginService_GetOrthancPath = 4,
-    _OrthancPluginService_GetOrthancDirectory = 5,
-    _OrthancPluginService_GetConfigurationPath = 6,
-    _OrthancPluginService_SetPluginProperty = 7,
-    _OrthancPluginService_GetGlobalProperty = 8,
-    _OrthancPluginService_SetGlobalProperty = 9,
-    _OrthancPluginService_GetCommandLineArgumentsCount = 10,
-    _OrthancPluginService_GetCommandLineArgument = 11,
-    _OrthancPluginService_GetExpectedDatabaseVersion = 12,
-    _OrthancPluginService_GetConfiguration = 13,
-    _OrthancPluginService_BufferCompression = 14,
-    _OrthancPluginService_ReadFile = 15,
-    _OrthancPluginService_WriteFile = 16,
-    _OrthancPluginService_GetErrorDescription = 17,
-    _OrthancPluginService_CallHttpClient = 18,
-    _OrthancPluginService_RegisterErrorCode = 19,
-    _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,
-    _OrthancPluginService_AutodetectMimeType = 30,
-    _OrthancPluginService_SetMetricsValue = 31,
-    _OrthancPluginService_EncodeDicomWebJson = 32,
-    _OrthancPluginService_EncodeDicomWebXml = 33,
-    _OrthancPluginService_ChunkedHttpClient = 34,    /* New in Orthanc 1.5.7 */
-    _OrthancPluginService_GetTagName = 35,           /* New in Orthanc 1.5.7 */
-    _OrthancPluginService_EncodeDicomWebJson2 = 36,  /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_EncodeDicomWebXml2 = 37,   /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_CreateMemoryBuffer = 38,   /* New in Orthanc 1.7.0 */
-    
-    /* Registration of callbacks */
-    _OrthancPluginService_RegisterRestCallback = 1000,
-    _OrthancPluginService_RegisterOnStoredInstanceCallback = 1001,
-    _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,
-    _OrthancPluginService_RegisterRefreshMetricsCallback = 1011,
-    _OrthancPluginService_RegisterChunkedRestCallback = 1012,  /* New in Orthanc 1.5.7 */
-    _OrthancPluginService_RegisterStorageCommitmentScpCallback = 1013,
-    _OrthancPluginService_RegisterIncomingDicomInstanceFilter = 1014,
-    _OrthancPluginService_RegisterTranscoderCallback = 1015,   /* New in Orthanc 1.7.0 */
-    
-    /* Sending answers to REST calls */
-    _OrthancPluginService_AnswerBuffer = 2000,
-    _OrthancPluginService_CompressAndAnswerPngImage = 2001,  /* Unused as of Orthanc 0.9.4 */
-    _OrthancPluginService_Redirect = 2002,
-    _OrthancPluginService_SendHttpStatusCode = 2003,
-    _OrthancPluginService_SendUnauthorized = 2004,
-    _OrthancPluginService_SendMethodNotAllowed = 2005,
-    _OrthancPluginService_SetCookie = 2006,
-    _OrthancPluginService_SetHttpHeader = 2007,
-    _OrthancPluginService_StartMultipartAnswer = 2008,
-    _OrthancPluginService_SendMultipartItem = 2009,
-    _OrthancPluginService_SendHttpStatus = 2010,
-    _OrthancPluginService_CompressAndAnswerImage = 2011,
-    _OrthancPluginService_SendMultipartItem2 = 2012,
-    _OrthancPluginService_SetHttpErrorDetails = 2013,
-
-    /* Access to the Orthanc database and API */
-    _OrthancPluginService_GetDicomForInstance = 3000,
-    _OrthancPluginService_RestApiGet = 3001,
-    _OrthancPluginService_RestApiPost = 3002,
-    _OrthancPluginService_RestApiDelete = 3003,
-    _OrthancPluginService_RestApiPut = 3004,
-    _OrthancPluginService_LookupPatient = 3005,
-    _OrthancPluginService_LookupStudy = 3006,
-    _OrthancPluginService_LookupSeries = 3007,
-    _OrthancPluginService_LookupInstance = 3008,
-    _OrthancPluginService_LookupStudyWithAccessionNumber = 3009,
-    _OrthancPluginService_RestApiGetAfterPlugins = 3010,
-    _OrthancPluginService_RestApiPostAfterPlugins = 3011,
-    _OrthancPluginService_RestApiDeleteAfterPlugins = 3012,
-    _OrthancPluginService_RestApiPutAfterPlugins = 3013,
-    _OrthancPluginService_ReconstructMainDicomTags = 3014,
-    _OrthancPluginService_RestApiGet2 = 3015,
-
-    /* Access to DICOM instances */
-    _OrthancPluginService_GetInstanceRemoteAet = 4000,
-    _OrthancPluginService_GetInstanceSize = 4001,
-    _OrthancPluginService_GetInstanceData = 4002,
-    _OrthancPluginService_GetInstanceJson = 4003,
-    _OrthancPluginService_GetInstanceSimplifiedJson = 4004,
-    _OrthancPluginService_HasInstanceMetadata = 4005,
-    _OrthancPluginService_GetInstanceMetadata = 4006,
-    _OrthancPluginService_GetInstanceOrigin = 4007,
-    _OrthancPluginService_GetInstanceTransferSyntaxUid = 4008,
-    _OrthancPluginService_HasInstancePixelData = 4009,
-    _OrthancPluginService_CreateDicomInstance = 4010,      /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_FreeDicomInstance = 4011,        /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_GetInstanceFramesCount = 4012,   /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_GetInstanceRawFrame = 4013,      /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_GetInstanceDecodedFrame = 4014,  /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_TranscodeDicomInstance = 4015,   /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_SerializeDicomInstance = 4016,   /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_GetInstanceAdvancedJson = 4017,  /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_GetInstanceDicomWebJson = 4018,  /* New in Orthanc 1.7.0 */
-    _OrthancPluginService_GetInstanceDicomWebXml = 4019,   /* New in Orthanc 1.7.0 */
-    
-    /* Services for plugins implementing a database back-end */
-    _OrthancPluginService_RegisterDatabaseBackend = 5000,
-    _OrthancPluginService_DatabaseAnswer = 5001,
-    _OrthancPluginService_RegisterDatabaseBackendV2 = 5002,
-    _OrthancPluginService_StorageAreaCreate = 5003,
-    _OrthancPluginService_StorageAreaRead = 5004,
-    _OrthancPluginService_StorageAreaRemove = 5005,
-
-    /* Primitives for handling images */
-    _OrthancPluginService_GetImagePixelFormat = 6000,
-    _OrthancPluginService_GetImageWidth = 6001,
-    _OrthancPluginService_GetImageHeight = 6002,
-    _OrthancPluginService_GetImagePitch = 6003,
-    _OrthancPluginService_GetImageBuffer = 6004,
-    _OrthancPluginService_UncompressImage = 6005,
-    _OrthancPluginService_FreeImage = 6006,
-    _OrthancPluginService_CompressImage = 6007,
-    _OrthancPluginService_ConvertPixelFormat = 6008,
-    _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;
-
-
-  typedef enum
-  {
-    _OrthancPluginProperty_Description = 1,
-    _OrthancPluginProperty_RootUri = 2,
-    _OrthancPluginProperty_OrthancExplorer = 3,
-
-    _OrthancPluginProperty_INTERNAL = 0x7fffffff
-  } _OrthancPluginProperty;
-
-
-
-  /**
-   * The memory layout of the pixels of an image.
-   * @ingroup Images
-   **/
-  typedef enum
-  {
-    /**
-     * @brief Graylevel 8bpp image.
-     *
-     * The image is graylevel. Each pixel is unsigned and stored in
-     * one byte.
-     **/
-    OrthancPluginPixelFormat_Grayscale8 = 1,
-
-    /**
-     * @brief Graylevel, unsigned 16bpp image.
-     *
-     * The image is graylevel. Each pixel is unsigned and stored in
-     * two bytes.
-     **/
-    OrthancPluginPixelFormat_Grayscale16 = 2,
-
-    /**
-     * @brief Graylevel, signed 16bpp image.
-     *
-     * The image is graylevel. Each pixel is signed and stored in two
-     * bytes.
-     **/
-    OrthancPluginPixelFormat_SignedGrayscale16 = 3,
-
-    /**
-     * @brief Color image in RGB24 format.
-     *
-     * This format describes a color image. The pixels are stored in 3
-     * consecutive bytes. The memory layout is RGB.
-     **/
-    OrthancPluginPixelFormat_RGB24 = 4,
-
-    /**
-     * @brief Color image in RGBA32 format.
-     *
-     * This format describes a color image. The pixels are stored in 4
-     * consecutive bytes. The memory layout is RGBA.
-     **/
-    OrthancPluginPixelFormat_RGBA32 = 5,
-
-    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;
-
-
-
-  /**
-   * The content types that are supported by Orthanc plugins.
-   **/
-  typedef enum
-  {
-    OrthancPluginContentType_Unknown = 0,      /*!< Unknown content type */
-    OrthancPluginContentType_Dicom = 1,        /*!< DICOM */
-    OrthancPluginContentType_DicomAsJson = 2,  /*!< JSON summary of a DICOM file */
-
-    _OrthancPluginContentType_INTERNAL = 0x7fffffff
-  } OrthancPluginContentType;
-
-
-
-  /**
-   * The supported types of DICOM resources.
-   **/
-  typedef enum
-  {
-    OrthancPluginResourceType_Patient = 0,     /*!< Patient */
-    OrthancPluginResourceType_Study = 1,       /*!< Study */
-    OrthancPluginResourceType_Series = 2,      /*!< Series */
-    OrthancPluginResourceType_Instance = 3,    /*!< Instance */
-    OrthancPluginResourceType_None = 4,        /*!< Unavailable resource type */
-
-    _OrthancPluginResourceType_INTERNAL = 0x7fffffff
-  } OrthancPluginResourceType;
-
-
-
-  /**
-   * The supported types of changes that can be signaled to the change callback.
-   * @ingroup Callbacks
-   **/
-  typedef enum
-  {
-    OrthancPluginChangeType_CompletedSeries = 0,    /*!< Series is now complete */
-    OrthancPluginChangeType_Deleted = 1,            /*!< Deleted resource */
-    OrthancPluginChangeType_NewChildInstance = 2,   /*!< A new instance was added to this resource */
-    OrthancPluginChangeType_NewInstance = 3,        /*!< New instance received */
-    OrthancPluginChangeType_NewPatient = 4,         /*!< New patient created */
-    OrthancPluginChangeType_NewSeries = 5,          /*!< New series created */
-    OrthancPluginChangeType_NewStudy = 6,           /*!< New study created */
-    OrthancPluginChangeType_StablePatient = 7,      /*!< Timeout: No new instance in this patient */
-    OrthancPluginChangeType_StableSeries = 8,       /*!< Timeout: No new instance in this series */
-    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_JobSubmitted = 16,      /*!< New Job submitted */
-    OrthancPluginChangeType_JobSuccess = 17,        /*!< A Job has completed successfully */
-    OrthancPluginChangeType_JobFailure = 18,        /*!< A Job has failed */
-
-    _OrthancPluginChangeType_INTERNAL = 0x7fffffff
-  } OrthancPluginChangeType;
-
-
-  /**
-   * The compression algorithms that are supported by the Orthanc core.
-   * @ingroup Images
-   **/
-  typedef enum
-  {
-    OrthancPluginCompressionType_Zlib = 0,          /*!< Standard zlib compression */
-    OrthancPluginCompressionType_ZlibWithSize = 1,  /*!< zlib, prefixed with uncompressed size (uint64_t) */
-    OrthancPluginCompressionType_Gzip = 2,          /*!< Standard gzip compression */
-    OrthancPluginCompressionType_GzipWithSize = 3,  /*!< gzip, prefixed with uncompressed size (uint64_t) */
-
-    _OrthancPluginCompressionType_INTERNAL = 0x7fffffff
-  } OrthancPluginCompressionType;
-
-
-  /**
-   * The image formats that are supported by the Orthanc core.
-   * @ingroup Images
-   **/
-  typedef enum
-  {
-    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;
-
-
-  /**
-   * The value representations present in the DICOM standard (version 2013).
-   * @ingroup Toolbox
-   **/
-  typedef enum
-  {
-    OrthancPluginValueRepresentation_AE = 1,   /*!< Application Entity */
-    OrthancPluginValueRepresentation_AS = 2,   /*!< Age String */
-    OrthancPluginValueRepresentation_AT = 3,   /*!< Attribute Tag */
-    OrthancPluginValueRepresentation_CS = 4,   /*!< Code String */
-    OrthancPluginValueRepresentation_DA = 5,   /*!< Date */
-    OrthancPluginValueRepresentation_DS = 6,   /*!< Decimal String */
-    OrthancPluginValueRepresentation_DT = 7,   /*!< Date Time */
-    OrthancPluginValueRepresentation_FD = 8,   /*!< Floating Point Double */
-    OrthancPluginValueRepresentation_FL = 9,   /*!< Floating Point Single */
-    OrthancPluginValueRepresentation_IS = 10,  /*!< Integer String */
-    OrthancPluginValueRepresentation_LO = 11,  /*!< Long String */
-    OrthancPluginValueRepresentation_LT = 12,  /*!< Long Text */
-    OrthancPluginValueRepresentation_OB = 13,  /*!< Other Byte String */
-    OrthancPluginValueRepresentation_OF = 14,  /*!< Other Float String */
-    OrthancPluginValueRepresentation_OW = 15,  /*!< Other Word String */
-    OrthancPluginValueRepresentation_PN = 16,  /*!< Person Name */
-    OrthancPluginValueRepresentation_SH = 17,  /*!< Short String */
-    OrthancPluginValueRepresentation_SL = 18,  /*!< Signed Long */
-    OrthancPluginValueRepresentation_SQ = 19,  /*!< Sequence of Items */
-    OrthancPluginValueRepresentation_SS = 20,  /*!< Signed Short */
-    OrthancPluginValueRepresentation_ST = 21,  /*!< Short Text */
-    OrthancPluginValueRepresentation_TM = 22,  /*!< Time */
-    OrthancPluginValueRepresentation_UI = 23,  /*!< Unique Identifier (UID) */
-    OrthancPluginValueRepresentation_UL = 24,  /*!< Unsigned Long */
-    OrthancPluginValueRepresentation_UN = 25,  /*!< Unknown */
-    OrthancPluginValueRepresentation_US = 26,  /*!< Unsigned Short */
-    OrthancPluginValueRepresentation_UT = 27,  /*!< Unlimited Text */
-
-    _OrthancPluginValueRepresentation_INTERNAL = 0x7fffffff
-  } OrthancPluginValueRepresentation;
-
-
-  /**
-   * 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_Human = 3,   /*!< Human-readable JSON */
-
-    _OrthancPluginDicomToJsonFormat_INTERNAL = 0x7fffffff
-  } OrthancPluginDicomToJsonFormat;
-
-
-  /**
-   * Flags to customize a DICOM-to-JSON conversion. By default, binary
-   * tags are formatted using Data URI scheme.
-   * @ingroup Toolbox
-   **/
-  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 */
-    OrthancPluginDicomToJsonFlags_IncludePixelData      = (1 << 3),  /*!< Include the pixel data */
-    OrthancPluginDicomToJsonFlags_ConvertBinaryToAscii  = (1 << 4),  /*!< Output binary tags as-is, dropping non-ASCII */
-    OrthancPluginDicomToJsonFlags_ConvertBinaryToNull   = (1 << 5),  /*!< Signal binary tags as null values */
-
-    _OrthancPluginDicomToJsonFlags_INTERNAL = 0x7fffffff
-  } OrthancPluginDicomToJsonFlags;
-
-
-  /**
-   * 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.
-   * @deprecated Plugins using OrthancPluginConstraintType will be faster
-   **/
-  typedef enum
-  {
-    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 constraints on the tags (main DICOM tags and identifier tags)
-   * that must be supported by the database plugins.
-   **/
-  typedef enum
-  {
-    OrthancPluginConstraintType_Equal = 1,           /*!< Equal */
-    OrthancPluginConstraintType_SmallerOrEqual = 2,  /*!< Less or equal */
-    OrthancPluginConstraintType_GreaterOrEqual = 3,  /*!< More or equal */
-    OrthancPluginConstraintType_Wildcard = 4,        /*!< Wildcard matching */
-    OrthancPluginConstraintType_List = 5,            /*!< List of values */
-
-    _OrthancPluginConstraintType_INTERNAL = 0x7fffffff
-  } OrthancPluginConstraintType;
-
-
-  /**
-   * 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;
-
-
-  /**
-   * The available types of metrics.
-   **/
-  typedef enum
-  {
-    OrthancPluginMetricsType_Default = 0,   /*!< Default metrics */
-
-    /**
-     * This metrics represents a time duration. Orthanc will keep the
-     * maximum value of the metrics over a sliding window of ten
-     * seconds, which is useful if the metrics is sampled frequently.
-     **/
-    OrthancPluginMetricsType_Timer = 1
-  } OrthancPluginMetricsType;
-  
-
-  /**
-   * The available modes to export a binary DICOM tag into a DICOMweb
-   * JSON or XML document.
-   **/
-  typedef enum
-  {
-    OrthancPluginDicomWebBinaryMode_Ignore = 0,        /*!< Don't include binary tags */
-    OrthancPluginDicomWebBinaryMode_InlineBinary = 1,  /*!< Inline encoding using Base64 */
-    OrthancPluginDicomWebBinaryMode_BulkDataUri = 2    /*!< Use a bulk data URI field */
-  } OrthancPluginDicomWebBinaryMode;
-
-
-  /**
-   * The available values for the Failure Reason (0008,1197) during
-   * storage commitment.
-   * http://dicom.nema.org/medical/dicom/2019e/output/chtml/part03/sect_C.14.html#sect_C.14.1.1
-   **/
-  typedef enum
-  {
-    OrthancPluginStorageCommitmentFailureReason_Success = 0,
-    /*!< Success: The DICOM instance is properly stored in the SCP */
-
-    OrthancPluginStorageCommitmentFailureReason_ProcessingFailure = 1,
-    /*!< 0110H: A general failure in processing the operation was encountered */
-
-    OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance = 2,
-    /*!< 0112H: One or more of the elements in the Referenced SOP
-      Instance Sequence was not available */
-
-    OrthancPluginStorageCommitmentFailureReason_ResourceLimitation = 3,
-    /*!< 0213H: The SCP does not currently have enough resources to
-      store the requested SOP Instance(s) */
-
-    OrthancPluginStorageCommitmentFailureReason_ReferencedSOPClassNotSupported = 4,
-    /*!< 0122H: Storage Commitment has been requested for a SOP
-      Instance with a SOP Class that is not supported by the SCP */
-
-    OrthancPluginStorageCommitmentFailureReason_ClassInstanceConflict = 5,
-    /*!< 0119H: The SOP Class of an element in the Referenced SOP
-      Instance Sequence did not correspond to the SOP class registered
-      for this SOP Instance at the SCP */
-
-    OrthancPluginStorageCommitmentFailureReason_DuplicateTransactionUID = 6
-    /*!< 0131H: The Transaction UID of the Storage Commitment Request
-      is already in use */
-  } OrthancPluginStorageCommitmentFailureReason;
-
-  
-
-  /**
-   * @brief A memory buffer allocated by the core system of Orthanc.
-   *
-   * A memory buffer allocated by the core system of Orthanc. When the
-   * content of the buffer is not useful anymore, it must be free by a
-   * call to ::OrthancPluginFreeMemoryBuffer().
-   **/
-  typedef struct
-  {
-    /**
-     * @brief The content of the buffer.
-     **/
-    void*      data;
-
-    /**
-     * @brief The number of bytes in the buffer.
-     **/
-    uint32_t   size;
-  } OrthancPluginMemoryBuffer;
-
-
-
-
-  /**
-   * @brief Opaque structure that represents the HTTP connection to the client application.
-   * @ingroup Callback
-   **/
-  typedef struct _OrthancPluginRestOutput_t OrthancPluginRestOutput;
-
-
-
-  /**
-   * @brief Opaque structure that represents a DICOM instance that is managed by the Orthanc core.
-   * @ingroup DicomInstance
-   **/
-  typedef struct _OrthancPluginDicomInstance_t OrthancPluginDicomInstance;
-
-
-
-  /**
-   * @brief Opaque structure that represents an image that is uncompressed in memory.
-   * @ingroup Images
-   **/
-  typedef struct _OrthancPluginImage_t OrthancPluginImage;
-
-
-
-  /**
-   * @brief Opaque structure that represents the storage area that is actually used by Orthanc.
-   * @ingroup Images
-   **/
-  typedef struct _OrthancPluginStorageArea_t OrthancPluginStorageArea;
-
-
-
-  /**
-   * @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 _OrthancPluginFindMatcher_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 Opaque structure that represents a node in a JSON or XML
-   * document used in DICOMweb.
-   * @ingroup Toolbox
-   **/
-  typedef struct _OrthancPluginDicomWebNode_t OrthancPluginDicomWebNode;
-
-  
-
-  /**
-   * @brief Signature of a callback function that answers to a REST request.
-   * @ingroup Callbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginRestCallback) (
-    OrthancPluginRestOutput* output,
-    const char* url,
-    const OrthancPluginHttpRequest* request);
-
-
-
-  /**
-   * @brief Signature of a callback function that is triggered when Orthanc stores a new DICOM instance.
-   * @ingroup Callbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginOnStoredInstanceCallback) (
-    const OrthancPluginDicomInstance* instance,
-    const char* instanceId);
-
-
-
-  /**
-   * @brief Signature of a callback function that is triggered when a change happens to some DICOM resource.
-   * @ingroup Callbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginOnChangeCallback) (
-    OrthancPluginChangeType changeType,
-    OrthancPluginResourceType resourceType,
-    const char* resourceId);
-
-
-
-  /**
-   * @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.
-   * @ingroup Callbacks
-   **/
-  typedef void (*OrthancPluginFree) (void* buffer);
-
-
-
-  /**
-   * @brief Signature of a function to set the content of a node
-   * encoding a binary DICOM tag, into a JSON or XML document
-   * generated for DICOMweb.
-   * @ingroup Callbacks
-   **/
-  typedef void (*OrthancPluginDicomWebSetBinaryNode) (
-    OrthancPluginDicomWebNode*       node,
-    OrthancPluginDicomWebBinaryMode  mode,
-    const char*                      bulkDataUri);
-    
-
-
-  /**
-   * @brief Callback for writing to the storage area.
-   *
-   * Signature of a callback function that is triggered when Orthanc writes a file to the storage area.
-   *
-   * @param uuid The UUID of the file.
-   * @param content The content of the file.
-   * @param size The size of the file.
-   * @param type The content type corresponding to this file. 
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginStorageCreate) (
-    const char* uuid,
-    const void* content,
-    int64_t size,
-    OrthancPluginContentType type);
-
-
-
-  /**
-   * @brief Callback for reading from the storage area.
-   *
-   * Signature of a callback function that is triggered when Orthanc reads a file from the storage area.
-   *
-   * @param content The content of the file (output).
-   * @param size The size of the file (output).
-   * @param uuid The UUID of the file of interest.
-   * @param type The content type corresponding to this file. 
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   * 
-   * @warning The "content" buffer *must* have been allocated using
-   * the "malloc()" function of your C standard library (i.e. nor
-   * "new[]", neither a pointer to a buffer). The "free()" function of
-   * your C standard library will automatically be invoked on the
-   * "content" pointer.
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginStorageRead) (
-    void** content,
-    int64_t* size,
-    const char* uuid,
-    OrthancPluginContentType type);
-
-
-
-  /**
-   * @brief Callback for removing a file from the storage area.
-   *
-   * Signature of a callback function that is triggered when Orthanc deletes a file from the storage area.
-   *
-   * @param uuid The UUID of the file to be removed.
-   * @param type The content type corresponding to this file. 
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginStorageRemove) (
-    const char* uuid,
-    OrthancPluginContentType type);
-
-
-
-  /**
-   * @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 unserialize 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 Callback executed to update the metrics of the plugin.
-   * 
-   * Signature of a callback function that is called by Orthanc
-   * whenever a monitoring tool (such as Prometheus) asks the current
-   * values of the metrics. This callback gives the plugin a chance to
-   * update its metrics, by calling OrthancPluginSetMetricsValue().
-   * This is typically useful for metrics that are expensive to
-   * acquire.
-   * 
-   * @see OrthancPluginRegisterRefreshMetrics()
-   * @ingroup Callbacks
-   **/
-  typedef void (*OrthancPluginRefreshMetricsCallback) ();
-
-  
-
-  /**
-   * @brief Callback executed to encode a binary tag in DICOMweb.
-   * 
-   * Signature of a callback function that is called by Orthanc
-   * whenever a DICOM tag that contains a binary value must be written
-   * to a JSON or XML node, while a DICOMweb document is being
-   * generated. The value representation (VR) of the DICOM tag can be
-   * OB, OD, OF, OL, OW, or UN.
-   * 
-   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
-   * @param node The node being generated, as provided by Orthanc.
-   * @param setter The setter to be used to encode the content of the node. If
-   * the setter is not called, the binary tag is not written to the output document.
-   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
-   * This parameter gives the number of elements in the "levelTagGroup", 
-   * "levelTagElement", and "levelIndex" arrays.
-   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
-   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
-   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
-   * @param tagGroup The group of the DICOM tag of interest.
-   * @param tagElement The element of the DICOM tag of interest.
-   * @param vr The value representation of the binary DICOM node.
-   * @ingroup Callbacks
-   **/
-  typedef void (*OrthancPluginDicomWebBinaryCallback) (
-    OrthancPluginDicomWebNode*          node,
-    OrthancPluginDicomWebSetBinaryNode  setter,
-    uint32_t                            levelDepth,
-    const uint16_t*                     levelTagGroup,
-    const uint16_t*                     levelTagElement,
-    const uint32_t*                     levelIndex,
-    uint16_t                            tagGroup,
-    uint16_t                            tagElement,
-    OrthancPluginValueRepresentation    vr);
-
-
-
-  /**
-   * @brief Callback executed to encode a binary tag in DICOMweb.
-   * 
-   * Signature of a callback function that is called by Orthanc
-   * whenever a DICOM tag that contains a binary value must be written
-   * to a JSON or XML node, while a DICOMweb document is being
-   * generated. The value representation (VR) of the DICOM tag can be
-   * OB, OD, OF, OL, OW, or UN.
-   * 
-   * @see OrthancPluginEncodeDicomWebJson() and OrthancPluginEncodeDicomWebXml()
-   * @param node The node being generated, as provided by Orthanc.
-   * @param setter The setter to be used to encode the content of the node. If
-   * the setter is not called, the binary tag is not written to the output document.
-   * @param levelDepth The depth of the node in the DICOM hierarchy of sequences.
-   * This parameter gives the number of elements in the "levelTagGroup", 
-   * "levelTagElement", and "levelIndex" arrays.
-   * @param levelTagGroup The group of the parent DICOM tags in the hierarchy.
-   * @param levelTagElement The element of the parent DICOM tags in the hierarchy.
-   * @param levelIndex The index of the node in the parent sequences of the hierarchy.
-   * @param tagGroup The group of the DICOM tag of interest.
-   * @param tagElement The element of the DICOM tag of interest.
-   * @param vr The value representation of the binary DICOM node.
-   * @param payload The user payload.
-   * @ingroup Callbacks
-   **/
-  typedef void (*OrthancPluginDicomWebBinaryCallback2) (
-    OrthancPluginDicomWebNode*          node,
-    OrthancPluginDicomWebSetBinaryNode  setter,
-    uint32_t                            levelDepth,
-    const uint16_t*                     levelTagGroup,
-    const uint16_t*                     levelTagElement,
-    const uint32_t*                     levelIndex,
-    uint16_t                            tagGroup,
-    uint16_t                            tagElement,
-    OrthancPluginValueRepresentation    vr,
-    void*                               payload);
-
-
-
-  /**
-   * @brief Data structure that contains information about the Orthanc core.
-   **/
-  typedef struct _OrthancPluginContext_t
-  {
-    void*                     pluginsManager;
-    const char*               orthancVersion;
-    OrthancPluginFree         Free;
-    OrthancPluginErrorCode  (*InvokeService) (struct _OrthancPluginContext_t* context,
-                                              _OrthancPluginService service,
-                                              const void* params);
-  } 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.
-   * 
-   * Free a string that was allocated by the core system of Orthanc.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param str The string to be freed.
-   **/
-  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeString(
-    OrthancPluginContext* context, 
-    char* str)
-  {
-    if (str != NULL)
-    {
-      context->Free(str);
-    }
-  }
-
-
-  /**
-   * @brief Check that the version of the hosting Orthanc is above a given version.
-   * 
-   * 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  OrthancPluginCheckVersionAdvanced(
-    OrthancPluginContext* context,
-    int expectedMajor,
-    int expectedMinor,
-    int expectedRevision)
-  {
-    int major, minor, revision;
-
-    if (sizeof(int32_t) != sizeof(OrthancPluginErrorCode) ||
-        sizeof(int32_t) != sizeof(OrthancPluginHttpMethod) ||
-        sizeof(int32_t) != sizeof(_OrthancPluginService) ||
-        sizeof(int32_t) != sizeof(_OrthancPluginProperty) ||
-        sizeof(int32_t) != sizeof(OrthancPluginPixelFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginContentType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginResourceType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginChangeType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginCompressionType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginImageFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginValueRepresentation) ||
-        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFormat) ||
-        sizeof(int32_t) != sizeof(OrthancPluginDicomToJsonFlags) ||
-        sizeof(int32_t) != sizeof(OrthancPluginCreateDicomFlags) ||
-        sizeof(int32_t) != sizeof(OrthancPluginIdentifierConstraint) ||
-        sizeof(int32_t) != sizeof(OrthancPluginInstanceOrigin) ||
-        sizeof(int32_t) != sizeof(OrthancPluginJobStepStatus) ||
-        sizeof(int32_t) != sizeof(OrthancPluginConstraintType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginMetricsType) ||
-        sizeof(int32_t) != sizeof(OrthancPluginDicomWebBinaryMode) ||
-        sizeof(int32_t) != sizeof(OrthancPluginStorageCommitmentFailureReason))
-    {
-      /* Mismatch in the size of the enumerations */
-      return 0;
-    }
-
-    /* Assume compatibility with the mainline */
-    if (!strcmp(context->orthancVersion, "mainline"))
-    {
-      return 1;
-    }
-
-    /* Parse the version of the Orthanc core */
-    if ( 
-#ifdef _MSC_VER
-      sscanf_s
-#else
-      sscanf
-#endif
-      (context->orthancVersion, "%4d.%4d.%4d", &major, &minor, &revision) != 3)
-    {
-      return 0;
-    }
-
-    /* Check the major number of the version */
-
-    if (major > expectedMajor)
-    {
-      return 1;
-    }
-
-    if (major < expectedMajor)
-    {
-      return 0;
-    }
-
-    /* Check the minor number of the version */
-
-    if (minor > expectedMinor)
-    {
-      return 1;
-    }
-
-    if (minor < expectedMinor)
-    {
-      return 0;
-    }
-
-    /* Check the revision number of the version */
-
-    if (revision >= expectedRevision)
-    {
-      return 1;
-    }
-    else
-    {
-      return 0;
-    }
-  }
-
-
-  /**
-   * @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.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param buffer The memory buffer to release.
-   **/
-  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeMemoryBuffer(
-    OrthancPluginContext* context, 
-    OrthancPluginMemoryBuffer* buffer)
-  {
-    context->Free(buffer->data);
-  }
-
-
-  /**
-   * @brief Log an error.
-   *
-   * Log an error message using the Orthanc logging system.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param message The message to be logged.
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginLogError(
-    OrthancPluginContext* context,
-    const char* message)
-  {
-    context->InvokeService(context, _OrthancPluginService_LogError, message);
-  }
-
-
-  /**
-   * @brief Log a warning.
-   *
-   * Log a warning message using the Orthanc logging system.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param message The message to be logged.
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginLogWarning(
-    OrthancPluginContext* context,
-    const char* message)
-  {
-    context->InvokeService(context, _OrthancPluginService_LogWarning, message);
-  }
-
-
-  /**
-   * @brief Log an information.
-   *
-   * Log an information message using the Orthanc logging system.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param message The message to be logged.
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginLogInfo(
-    OrthancPluginContext* context,
-    const char* message)
-  {
-    context->InvokeService(context, _OrthancPluginService_LogInfo, message);
-  }
-
-
-
-  typedef struct
-  {
-    const char* pathRegularExpression;
-    OrthancPluginRestCallback callback;
-  } _OrthancPluginRestCallback;
-
-  /**
-   * @brief Register a REST callback.
-   *
-   * This function registers a REST callback against a regular
-   * expression for a URI. This function must be called during the
-   * initialization of the plugin, i.e. inside the
-   * OrthancPluginInitialize() public function.
-   *
-   * Each REST callback is guaranteed to run in mutual exclusion.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
-   * @param callback The callback function to handle the REST call.
-   * @see OrthancPluginRegisterRestCallbackNoLock()
-   *
-   * @note
-   * The regular expression is case sensitive and must follow the
-   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
-   *
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallback(
-    OrthancPluginContext*     context,
-    const char*               pathRegularExpression,
-    OrthancPluginRestCallback callback)
-  {
-    _OrthancPluginRestCallback params;
-    params.pathRegularExpression = pathRegularExpression;
-    params.callback = callback;
-    context->InvokeService(context, _OrthancPluginService_RegisterRestCallback, &params);
-  }
-
-
-
-  /**
-   * @brief Register a REST callback, without locking.
-   *
-   * This function registers a REST callback against a regular
-   * expression for a URI. This function must be called during the
-   * initialization of the plugin, i.e. inside the
-   * OrthancPluginInitialize() public function.
-   *
-   * Contrarily to OrthancPluginRegisterRestCallback(), the callback
-   * will NOT be invoked in mutual exclusion. This can be useful for
-   * high-performance plugins that must handle concurrent requests
-   * (Orthanc uses a pool of threads, one thread being assigned to
-   * each incoming HTTP request). Of course, if using this function,
-   * it is up to the plugin to implement the required locking
-   * mechanisms.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param pathRegularExpression Regular expression for the URI. May contain groups.
-   * @param callback The callback function to handle the REST call.
-   * @see OrthancPluginRegisterRestCallback()
-   *
-   * @note
-   * The regular expression is case sensitive and must follow the
-   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
-   *
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRestCallbackNoLock(
-    OrthancPluginContext*     context,
-    const char*               pathRegularExpression,
-    OrthancPluginRestCallback callback)
-  {
-    _OrthancPluginRestCallback params;
-    params.pathRegularExpression = pathRegularExpression;
-    params.callback = callback;
-    context->InvokeService(context, _OrthancPluginService_RegisterRestCallbackNoLock, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginOnStoredInstanceCallback callback;
-  } _OrthancPluginOnStoredInstanceCallback;
-
-  /**
-   * @brief Register a callback for received instances.
-   *
-   * This function registers a callback function that is called
-   * whenever a new DICOM instance is stored into the Orthanc core.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param callback The callback function.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnStoredInstanceCallback(
-    OrthancPluginContext*                  context,
-    OrthancPluginOnStoredInstanceCallback  callback)
-  {
-    _OrthancPluginOnStoredInstanceCallback params;
-    params.callback = callback;
-
-    context->InvokeService(context, _OrthancPluginService_RegisterOnStoredInstanceCallback, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    const void*              answer;
-    uint32_t                 answerSize;
-    const char*              mimeType;
-  } _OrthancPluginAnswerBuffer;
-
-  /**
-   * @brief Answer to a REST request.
-   *
-   * This function answers to a REST request with the content of a memory buffer.
-   * 
-   * @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 answer.
-   * @param answerSize Number of bytes of the answer.
-   * @param mimeType The MIME type of the answer.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginAnswerBuffer(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const void*              answer,
-    uint32_t                 answerSize,
-    const char*              mimeType)
-  {
-    _OrthancPluginAnswerBuffer params;
-    params.output = output;
-    params.answer = answer;
-    params.answerSize = answerSize;
-    params.mimeType = mimeType;
-    context->InvokeService(context, _OrthancPluginService_AnswerBuffer, &params);
-  }
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput*  output;
-    OrthancPluginPixelFormat  format;
-    uint32_t                  width;
-    uint32_t                  height;
-    uint32_t                  pitch;
-    const void*               buffer;
-  } _OrthancPluginCompressAndAnswerPngImage;
-
-  typedef struct
-  {
-    OrthancPluginRestOutput*  output;
-    OrthancPluginImageFormat  imageFormat;
-    OrthancPluginPixelFormat  pixelFormat;
-    uint32_t                  width;
-    uint32_t                  height;
-    uint32_t                  pitch;
-    const void*               buffer;
-    uint8_t                   quality;
-  } _OrthancPluginCompressAndAnswerImage;
-
-
-  /**
-   * @brief Answer to a REST request with a PNG image.
-   *
-   * This function answers to a REST request with a PNG image. The
-   * parameters of this function describe a memory buffer that
-   * contains an uncompressed image. The image will be automatically compressed
-   * as a PNG image by the core system of Orthanc.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param format The memory layout of the uncompressed image.
-   * @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 containing the uncompressed image.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerPngImage(
-    OrthancPluginContext*     context,
-    OrthancPluginRestOutput*  output,
-    OrthancPluginPixelFormat  format,
-    uint32_t                  width,
-    uint32_t                  height,
-    uint32_t                  pitch,
-    const void*               buffer)
-  {
-    _OrthancPluginCompressAndAnswerImage params;
-    params.output = output;
-    params.imageFormat = OrthancPluginImageFormat_Png;
-    params.pixelFormat = format;
-    params.width = width;
-    params.height = height;
-    params.pitch = pitch;
-    params.buffer = buffer;
-    params.quality = 0;  /* No quality for PNG */
-    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    const char*                 instanceId;
-  } _OrthancPluginGetDicomForInstance;
-
-  /**
-   * @brief Retrieve a DICOM instance using its Orthanc identifier.
-   * 
-   * Retrieve a DICOM instance using its Orthanc identifier. The DICOM
-   * 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. 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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetDicomForInstance(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 instanceId)
-  {
-    _OrthancPluginGetDicomForInstance params;
-    params.target = target;
-    params.instanceId = instanceId;
-    return context->InvokeService(context, _OrthancPluginService_GetDicomForInstance, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    const char*                 uri;
-  } _OrthancPluginRestApiGet;
-
-  /**
-   * @brief Make a GET call to the built-in Orthanc REST API.
-   * 
-   * Make a GET call to the built-in Orthanc REST API. 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.
-   * @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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGet(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 uri)
-  {
-    _OrthancPluginRestApiGet params;
-    params.target = target;
-    params.uri = uri;
-    return context->InvokeService(context, _OrthancPluginService_RestApiGet, &params);
-  }
-
-
-
-  /**
-   * @brief Make a GET call to the REST API, as tainted by the plugins.
-   * 
-   * Make a GET call to the Orthanc REST API, after all the plugins
-   * are applied. In other words, if some plugin overrides or adds the
-   * called URI to the built-in Orthanc REST API, this call will
-   * return the result provided by this plugin. 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.
-   * @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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiGetAfterPlugins(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 uri)
-  {
-    _OrthancPluginRestApiGet params;
-    params.target = target;
-    params.uri = uri;
-    return context->InvokeService(context, _OrthancPluginService_RestApiGetAfterPlugins, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    const char*                 uri;
-    const void*                 body;
-    uint32_t                    bodySize;
-  } _OrthancPluginRestApiPostPut;
-
-  /**
-   * @brief Make a POST call to the built-in Orthanc REST API.
-   * 
-   * Make a POST call to the built-in Orthanc REST API. 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 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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPost(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 uri,
-    const void*                 body,
-    uint32_t                    bodySize)
-  {
-    _OrthancPluginRestApiPostPut params;
-    params.target = target;
-    params.uri = uri;
-    params.body = body;
-    params.bodySize = bodySize;
-    return context->InvokeService(context, _OrthancPluginService_RestApiPost, &params);
-  }
-
-
-  /**
-   * @brief Make a POST call to the REST API, as tainted by the plugins.
-   * 
-   * Make a POST call to the Orthanc REST API, after all the plugins
-   * are applied. In other words, if some plugin overrides or adds the
-   * called URI to the built-in Orthanc REST API, this call will
-   * return the result provided by this plugin. 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 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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPostAfterPlugins(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 uri,
-    const void*                 body,
-    uint32_t                    bodySize)
-  {
-    _OrthancPluginRestApiPostPut params;
-    params.target = target;
-    params.uri = uri;
-    params.body = body;
-    params.bodySize = bodySize;
-    return context->InvokeService(context, _OrthancPluginService_RestApiPostAfterPlugins, &params);
-  }
-
-
-
-  /**
-   * @brief Make a DELETE call to the built-in Orthanc REST API.
-   * 
-   * Make a DELETE call to the built-in Orthanc REST API.
-   * 
-   * @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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDelete(
-    OrthancPluginContext*       context,
-    const char*                 uri)
-  {
-    return context->InvokeService(context, _OrthancPluginService_RestApiDelete, uri);
-  }
-
-
-  /**
-   * @brief Make a DELETE call to the REST API, as tainted by the plugins.
-   * 
-   * Make a DELETE call to the Orthanc REST API, after all the plugins
-   * are applied. In other words, if some plugin overrides or adds the
-   * called URI to the built-in Orthanc REST API, this call will
-   * return the result provided by this plugin. 
-   * 
-   * @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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiDeleteAfterPlugins(
-    OrthancPluginContext*       context,
-    const char*                 uri)
-  {
-    return context->InvokeService(context, _OrthancPluginService_RestApiDeleteAfterPlugins, uri);
-  }
-
-
-
-  /**
-   * @brief Make a PUT call to the built-in Orthanc REST API.
-   * 
-   * Make a PUT call to the built-in Orthanc REST API. 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 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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPut(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 uri,
-    const void*                 body,
-    uint32_t                    bodySize)
-  {
-    _OrthancPluginRestApiPostPut params;
-    params.target = target;
-    params.uri = uri;
-    params.body = body;
-    params.bodySize = bodySize;
-    return context->InvokeService(context, _OrthancPluginService_RestApiPut, &params);
-  }
-
-
-
-  /**
-   * @brief Make a PUT call to the REST API, as tainted by the plugins.
-   * 
-   * Make a PUT call to the Orthanc REST API, after all the plugins
-   * are applied. In other words, if some plugin overrides or adds the
-   * called URI to the built-in Orthanc REST API, this call will
-   * return the result provided by this plugin. 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 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
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRestApiPutAfterPlugins(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 uri,
-    const void*                 body,
-    uint32_t                    bodySize)
-  {
-    _OrthancPluginRestApiPostPut params;
-    params.target = target;
-    params.uri = uri;
-    params.body = body;
-    params.bodySize = bodySize;
-    return context->InvokeService(context, _OrthancPluginService_RestApiPutAfterPlugins, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    const char*              argument;
-  } _OrthancPluginOutputPlusArgument;
-
-  /**
-   * @brief Redirect a REST request.
-   *
-   * This function answers to a REST request by redirecting the user
-   * to another URI using HTTP status 301.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param redirection Where to redirect.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRedirect(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              redirection)
-  {
-    _OrthancPluginOutputPlusArgument params;
-    params.output = output;
-    params.argument = redirection;
-    context->InvokeService(context, _OrthancPluginService_Redirect, &params);
-  }
-
-
-
-  typedef struct
-  {
-    char**       result;
-    const char*  argument;
-  } _OrthancPluginRetrieveDynamicString;
-
-  /**
-   * @brief Look for a patient.
-   *
-   * Look for a patient stored in Orthanc, using its Patient ID tag (0x0010, 0x0020).
-   * This function uses the database index to run as fast as possible (it does not loop
-   * over all the stored patients).
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param patientID The Patient ID of interest.
-   * @return The NULL value if the patient is non-existent, or a string containing the 
-   * Orthanc ID of the patient. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupPatient(
-    OrthancPluginContext*  context,
-    const char*            patientID)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = patientID;
-
-    if (context->InvokeService(context, _OrthancPluginService_LookupPatient, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Look for a study.
-   *
-   * Look for a study stored in Orthanc, using its Study Instance UID tag (0x0020, 0x000d).
-   * This function uses the database index to run as fast as possible (it does not loop
-   * over all the stored studies).
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param studyUID The Study Instance UID of interest.
-   * @return The NULL value if the study is non-existent, or a string containing the 
-   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudy(
-    OrthancPluginContext*  context,
-    const char*            studyUID)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = studyUID;
-
-    if (context->InvokeService(context, _OrthancPluginService_LookupStudy, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Look for a study, using the accession number.
-   *
-   * Look for a study stored in Orthanc, using its Accession Number tag (0x0008, 0x0050).
-   * This function uses the database index to run as fast as possible (it does not loop
-   * over all the stored studies).
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param accessionNumber The Accession Number of interest.
-   * @return The NULL value if the study is non-existent, or a string containing the 
-   * Orthanc ID of the study. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupStudyWithAccessionNumber(
-    OrthancPluginContext*  context,
-    const char*            accessionNumber)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = accessionNumber;
-
-    if (context->InvokeService(context, _OrthancPluginService_LookupStudyWithAccessionNumber, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Look for a series.
-   *
-   * Look for a series stored in Orthanc, using its Series Instance UID tag (0x0020, 0x000e).
-   * This function uses the database index to run as fast as possible (it does not loop
-   * over all the stored series).
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param seriesUID The Series Instance UID of interest.
-   * @return The NULL value if the series is non-existent, or a string containing the 
-   * Orthanc ID of the series. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupSeries(
-    OrthancPluginContext*  context,
-    const char*            seriesUID)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = seriesUID;
-
-    if (context->InvokeService(context, _OrthancPluginService_LookupSeries, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Look for an instance.
-   *
-   * Look for an instance stored in Orthanc, using its SOP Instance UID tag (0x0008, 0x0018).
-   * This function uses the database index to run as fast as possible (it does not loop
-   * over all the stored instances).
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param sopInstanceUID The SOP Instance UID of interest.
-   * @return The NULL value if the instance is non-existent, or a string containing the 
-   * Orthanc ID of the instance. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginLookupInstance(
-    OrthancPluginContext*  context,
-    const char*            sopInstanceUID)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = sopInstanceUID;
-
-    if (context->InvokeService(context, _OrthancPluginService_LookupInstance, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    uint16_t                 status;
-  } _OrthancPluginSendHttpStatusCode;
-
-  /**
-   * @brief Send a HTTP status code.
-   *
-   * This function answers to a REST request by sending a HTTP status
-   * code (such as "400 - Bad Request"). Note that:
-   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
-   * - Redirections (status 301) must use ::OrthancPluginRedirect().
-   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
-   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param status The HTTP status code to be sent.
-   * @ingroup REST
-   * @see OrthancPluginSendHttpStatus()
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatusCode(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    uint16_t                 status)
-  {
-    _OrthancPluginSendHttpStatusCode params;
-    params.output = output;
-    params.status = status;
-    context->InvokeService(context, _OrthancPluginService_SendHttpStatusCode, &params);
-  }
-
-
-  /**
-   * @brief Signal that a REST request is not authorized.
-   *
-   * This function answers to a REST request by signaling that it is
-   * not authorized.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param realm The realm for the authorization process.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSendUnauthorized(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              realm)
-  {
-    _OrthancPluginOutputPlusArgument params;
-    params.output = output;
-    params.argument = realm;
-    context->InvokeService(context, _OrthancPluginService_SendUnauthorized, &params);
-  }
-
-
-  /**
-   * @brief Signal that this URI does not support this HTTP method.
-   *
-   * This function answers to a REST request by signaling that the
-   * queried URI does not support this method.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param allowedMethods The allowed methods for this URI (e.g. "GET,POST" after a PUT or a POST request).
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSendMethodNotAllowed(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              allowedMethods)
-  {
-    _OrthancPluginOutputPlusArgument params;
-    params.output = output;
-    params.argument = allowedMethods;
-    context->InvokeService(context, _OrthancPluginService_SendMethodNotAllowed, &params);
-  }
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    const char*              key;
-    const char*              value;
-  } _OrthancPluginSetHttpHeader;
-
-  /**
-   * @brief Set a cookie.
-   *
-   * This function sets a cookie in the HTTP client.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param cookie The cookie to be set.
-   * @param value The value of the cookie.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetCookie(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              cookie,
-    const char*              value)
-  {
-    _OrthancPluginSetHttpHeader params;
-    params.output = output;
-    params.key = cookie;
-    params.value = value;
-    context->InvokeService(context, _OrthancPluginService_SetCookie, &params);
-  }
-
-
-  /**
-   * @brief Set some HTTP header.
-   *
-   * This function sets a HTTP header in the HTTP answer.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param key The HTTP header to be set.
-   * @param value The value of the HTTP header.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpHeader(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              key,
-    const char*              value)
-  {
-    _OrthancPluginSetHttpHeader params;
-    params.output = output;
-    params.key = key;
-    params.value = value;
-    context->InvokeService(context, _OrthancPluginService_SetHttpHeader, &params);
-  }
-
-
-  typedef struct
-  {
-    char**                             resultStringToFree;
-    const char**                       resultString;
-    int64_t*                           resultInt64;
-    const char*                        key;
-    const OrthancPluginDicomInstance*  instance;
-    OrthancPluginInstanceOrigin*       resultOrigin;   /* New in Orthanc 0.9.5 SDK */
-  } _OrthancPluginAccessDicomInstance;
-
-
-  /**
-   * @brief Get the AET of a DICOM instance.
-   *
-   * This function returns the Application Entity Title (AET) of the
-   * DICOM modality from which a DICOM instance originates.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The AET if success, NULL if error.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceRemoteAet(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance)
-  {
-    const char* result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultString = &result;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceRemoteAet, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Get the size of a DICOM file.
-   *
-   * This function returns the number of bytes of the given DICOM instance.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The size of the file, -1 in case of error.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE int64_t OrthancPluginGetInstanceSize(
-    OrthancPluginContext*             context,
-    const OrthancPluginDicomInstance* instance)
-  {
-    int64_t size;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultInt64 = &size;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSize, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return -1;
-    }
-    else
-    {
-      return size;
-    }
-  }
-
-
-  /**
-   * @brief Get the data of a DICOM file.
-   *
-   * This function returns a pointer to the content of the given DICOM instance.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The pointer to the DICOM data, NULL in case of error.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE const void* OrthancPluginGetInstanceData(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance)
-  {
-    const char* result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultString = &result;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceData, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Get the DICOM tag hierarchy as a JSON file.
-   *
-   * This function returns a pointer to a newly created string
-   * containing a JSON file. This JSON file encodes the tag hierarchy
-   * of the given DICOM instance.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The NULL value in case of error, or a string containing the JSON file.
-   * This string must be freed by OrthancPluginFreeString().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceJson(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance)
-  {
-    char* result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultStringToFree = &result;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Get the DICOM tag hierarchy as a JSON file (with simplification).
-   *
-   * This function returns a pointer to a newly created string
-   * containing a JSON file. This JSON file encodes the tag hierarchy
-   * of the given DICOM instance. In contrast with
-   * ::OrthancPluginGetInstanceJson(), the returned JSON file is in
-   * its simplified version.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The NULL value in case of error, or a string containing the JSON file.
-   * This string must be freed by OrthancPluginFreeString().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceSimplifiedJson(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance)
-  {
-    char* result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultStringToFree = &result;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceSimplifiedJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Check whether a DICOM instance is associated with some metadata.
-   *
-   * This function checks whether the DICOM instance of interest is
-   * associated with some metadata. As of Orthanc 0.8.1, in the
-   * callbacks registered by
-   * ::OrthancPluginRegisterOnStoredInstanceCallback(), the only
-   * possibly available metadata are "ReceptionDate", "RemoteAET" and
-   * "IndexInSeries".
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @param metadata The metadata of interest.
-   * @return 1 if the metadata is present, 0 if it is absent, -1 in case of error.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE int  OrthancPluginHasInstanceMetadata(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance,
-    const char*                        metadata)
-  {
-    int64_t result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultInt64 = &result;
-    params.instance = instance;
-    params.key = metadata;
-
-    if (context->InvokeService(context, _OrthancPluginService_HasInstanceMetadata, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return -1;
-    }
-    else
-    {
-      return (result != 0);
-    }
-  }
-
-
-  /**
-   * @brief Get the value of some metadata associated with a given DICOM instance.
-   *
-   * This functions returns the value of some metadata that is associated with the DICOM instance of interest.
-   * Before calling this function, the existence of the metadata must have been checked with
-   * ::OrthancPluginHasInstanceMetadata().
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @param metadata The metadata of interest.
-   * @return The metadata value if success, NULL if error. Please note that the 
-   *         returned string belongs to the instance object and must NOT be 
-   *         deallocated. Please make a copy of the string if you wish to access 
-   *         it later.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetInstanceMetadata(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance,
-    const char*                        metadata)
-  {
-    const char* result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultString = &result;
-    params.instance = instance;
-    params.key = metadata;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceMetadata, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginStorageCreate  create;
-    OrthancPluginStorageRead    read;
-    OrthancPluginStorageRemove  remove;
-    OrthancPluginFree           free;
-  } _OrthancPluginRegisterStorageArea;
-
-  /**
-   * @brief Register a custom storage area.
-   *
-   * This function registers a custom storage area, to replace the
-   * built-in way Orthanc stores its files on the filesystem. This
-   * function must be called during the initialization of the plugin,
-   * i.e. inside the OrthancPluginInitialize() public function.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param create The callback function to store a file on the custom storage area.
-   * @param read The callback function to read a file from the custom storage area.
-   * @param remove The callback function to remove a file from the custom storage area.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterStorageArea(
-    OrthancPluginContext*       context,
-    OrthancPluginStorageCreate  create,
-    OrthancPluginStorageRead    read,
-    OrthancPluginStorageRemove  remove)
-  {
-    _OrthancPluginRegisterStorageArea params;
-    params.create = create;
-    params.read = read;
-    params.remove = remove;
-
-#ifdef  __cplusplus
-    params.free = ::free;
-#else
-    params.free = free;
-#endif
-
-    context->InvokeService(context, _OrthancPluginService_RegisterStorageArea, &params);
-  }
-
-
-
-  /**
-   * @brief Return the path to the Orthanc executable.
-   *
-   * This function returns the path to the Orthanc executable.
-   * 
-   * @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 path. This string must be freed by
-   * OrthancPluginFreeString().
-   **/
-  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancPath(OrthancPluginContext* context)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = NULL;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetOrthancPath, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Return the directory containing the Orthanc.
-   *
-   * This function returns the path to the directory containing the Orthanc executable.
-   * 
-   * @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 path. This string must be freed by
-   * OrthancPluginFreeString().
-   **/
-  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetOrthancDirectory(OrthancPluginContext* context)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = NULL;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetOrthancDirectory, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Return the path to the configuration file(s).
-   *
-   * This function returns the path to the configuration file(s) that
-   * was specified when starting Orthanc. Since version 0.9.1, this
-   * path can refer to a folder that stores a set of configuration
-   * files. This function is deprecated in favor of
-   * OrthancPluginGetConfiguration().
-   * 
-   * @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 path. This string must be freed by
-   * OrthancPluginFreeString().
-   * @see OrthancPluginGetConfiguration()
-   **/
-  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfigurationPath(OrthancPluginContext* context)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = NULL;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetConfigurationPath, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginOnChangeCallback callback;
-  } _OrthancPluginOnChangeCallback;
-
-  /**
-   * @brief Register a callback to monitor changes.
-   *
-   * This function registers a callback function that is called
-   * whenever a change happens to some DICOM resource.
-   *
-   * @warning If your change callback has to call the REST API of
-   * 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
-   * scripts.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param callback The callback function.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterOnChangeCallback(
-    OrthancPluginContext*          context,
-    OrthancPluginOnChangeCallback  callback)
-  {
-    _OrthancPluginOnChangeCallback params;
-    params.callback = callback;
-
-    context->InvokeService(context, _OrthancPluginService_RegisterOnChangeCallback, &params);
-  }
-
-
-
-  typedef struct
-  {
-    const char* plugin;
-    _OrthancPluginProperty property;
-    const char* value;
-  } _OrthancPluginSetPluginProperty;
-
-
-  /**
-   * @brief Set the URI where the plugin provides its Web interface.
-   *
-   * For plugins that come with a Web interface, this function
-   * declares the entry path where to find this interface. This
-   * information is notably used in the "Plugins" page of Orthanc
-   * Explorer.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param uri The root URI for this plugin.
-   **/ 
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetRootUri(
-    OrthancPluginContext*  context,
-    const char*            uri)
-  {
-    _OrthancPluginSetPluginProperty params;
-    params.plugin = OrthancPluginGetName();
-    params.property = _OrthancPluginProperty_RootUri;
-    params.value = uri;
-
-    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
-  }
-
-
-  /**
-   * @brief Set a description for this plugin.
-   *
-   * Set a description for this plugin. It is displayed in the
-   * "Plugins" page of Orthanc Explorer.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param description The description.
-   **/ 
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetDescription(
-    OrthancPluginContext*  context,
-    const char*            description)
-  {
-    _OrthancPluginSetPluginProperty params;
-    params.plugin = OrthancPluginGetName();
-    params.property = _OrthancPluginProperty_Description;
-    params.value = description;
-
-    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
-  }
-
-
-  /**
-   * @brief Extend the JavaScript code of Orthanc Explorer.
-   *
-   * Add JavaScript code to customize the default behavior of Orthanc
-   * Explorer. This can for instance be used to add new buttons.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param javascript The custom JavaScript code.
-   **/ 
-  ORTHANC_PLUGIN_INLINE void OrthancPluginExtendOrthancExplorer(
-    OrthancPluginContext*  context,
-    const char*            javascript)
-  {
-    _OrthancPluginSetPluginProperty params;
-    params.plugin = OrthancPluginGetName();
-    params.property = _OrthancPluginProperty_OrthancExplorer;
-    params.value = javascript;
-
-    context->InvokeService(context, _OrthancPluginService_SetPluginProperty, &params);
-  }
-
-
-  typedef struct
-  {
-    char**       result;
-    int32_t      property;
-    const char*  value;
-  } _OrthancPluginGlobalProperty;
-
-
-  /**
-   * @brief Get the value of a global property.
-   *
-   * Get the value of a global property that is stored in the Orthanc database. Global
-   * properties whose index is below 1024 are reserved by Orthanc.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param property The global property of interest.
-   * @param defaultValue The value to return, if the global property is unset.
-   * @return The value of the global property, or NULL in the case of an error. This
-   * string must be freed by OrthancPluginFreeString().
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetGlobalProperty(
-    OrthancPluginContext*  context,
-    int32_t                property,
-    const char*            defaultValue)
-  {
-    char* result;
-
-    _OrthancPluginGlobalProperty params;
-    params.result = &result;
-    params.property = property;
-    params.value = defaultValue;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetGlobalProperty, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Set the value of a global property.
-   *
-   * Set the value of a global property into the Orthanc
-   * database. Setting a global property can be used by plugins to
-   * save their internal parameters. Plugins are only allowed to set
-   * properties whose index are above or equal to 1024 (properties
-   * below 1024 are read-only and reserved by Orthanc).
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param property The global property of interest.
-   * @param value The value to be set in the global property.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Orthanc
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSetGlobalProperty(
-    OrthancPluginContext*  context,
-    int32_t                property,
-    const char*            value)
-  {
-    _OrthancPluginGlobalProperty params;
-    params.result = NULL;
-    params.property = property;
-    params.value = value;
-
-    return context->InvokeService(context, _OrthancPluginService_SetGlobalProperty, &params);
-  }
-
-
-
-  typedef struct
-  {
-    int32_t   *resultInt32;
-    uint32_t  *resultUint32;
-    int64_t   *resultInt64;
-    uint64_t  *resultUint64;
-  } _OrthancPluginReturnSingleValue;
-
-  /**
-   * @brief Get the number of command-line arguments.
-   *
-   * Retrieve the number of command-line arguments that were used to launch Orthanc.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @return The number of arguments.
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetCommandLineArgumentsCount(
-    OrthancPluginContext*  context)
-  {
-    uint32_t count = 0;
-
-    _OrthancPluginReturnSingleValue params;
-    memset(&params, 0, sizeof(params));
-    params.resultUint32 = &count;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgumentsCount, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return 0;
-    }
-    else
-    {
-      return count;
-    }
-  }
-
-
-
-  /**
-   * @brief Get the value of a command-line argument.
-   *
-   * Get the value of one of the command-line arguments that were used
-   * to launch Orthanc. The number of available arguments can be
-   * retrieved by OrthancPluginGetCommandLineArgumentsCount().
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param argument The index of the argument.
-   * @return The value of the argument, or NULL in the case of an error. This
-   * string must be freed by OrthancPluginFreeString().
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetCommandLineArgument(
-    OrthancPluginContext*  context,
-    uint32_t               argument)
-  {
-    char* result;
-
-    _OrthancPluginGlobalProperty params;
-    params.result = &result;
-    params.property = (int32_t) argument;
-    params.value = NULL;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetCommandLineArgument, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Get the expected version of the database schema.
-   *
-   * Retrieve the expected version of the database schema.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @return The version.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetExpectedDatabaseVersion(
-    OrthancPluginContext*  context)
-  {
-    uint32_t count = 0;
-
-    _OrthancPluginReturnSingleValue params;
-    memset(&params, 0, sizeof(params));
-    params.resultUint32 = &count;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetExpectedDatabaseVersion, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return 0;
-    }
-    else
-    {
-      return count;
-    }
-  }
-
-
-
-  /**
-   * @brief Return the content of the configuration file(s).
-   *
-   * This function returns the content of the configuration that is
-   * used by Orthanc, formatted as a JSON string.
-   * 
-   * @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 configuration. This string must be freed by
-   * OrthancPluginFreeString().
-   **/
-  ORTHANC_PLUGIN_INLINE char *OrthancPluginGetConfiguration(OrthancPluginContext* context)
-  {
-    char* result;
-
-    _OrthancPluginRetrieveDynamicString params;
-    params.result = &result;
-    params.argument = NULL;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetConfiguration, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    const char*              subType;
-    const char*              contentType;
-  } _OrthancPluginStartMultipartAnswer;
-
-  /**
-   * @brief Start an HTTP multipart answer.
-   *
-   * Initiates a HTTP multipart answer, as the result of a REST request.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @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(), OrthancPluginSendMultipartItem2()
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              subType,
-    const char*              contentType)
-  {
-    _OrthancPluginStartMultipartAnswer params;
-    params.output = output;
-    params.subType = subType;
-    params.contentType = contentType;
-    return context->InvokeService(context, _OrthancPluginService_StartMultipartAnswer, &params);
-  }
-
-
-  /**
-   * @brief Send an item as a part of some HTTP multipart answer.
-   *
-   * This function sends an item as a part of some HTTP multipart
-   * answer that was initiated by OrthancPluginStartMultipartAnswer().
-   * 
-   * @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.
-   * @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(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              answer,
-    uint32_t                 answerSize)
-  {
-    _OrthancPluginAnswerBuffer params;
-    params.output = output;
-    params.answer = answer;
-    params.answerSize = answerSize;
-    params.mimeType = NULL;
-    return context->InvokeService(context, _OrthancPluginService_SendMultipartItem, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*    target;
-    const void*                   source;
-    uint32_t                      size;
-    OrthancPluginCompressionType  compression;
-    uint8_t                       uncompress;
-  } _OrthancPluginBufferCompression;
-
-
-  /**
-   * @brief Compress or decompress a buffer.
-   *
-   * This function compresses or decompresses a buffer, using the
-   * 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. 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.
-   * @param uncompress If set to "0", the buffer must be compressed. 
-   * If set to "1", the buffer must be uncompressed.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginBufferCompression(
-    OrthancPluginContext*         context,
-    OrthancPluginMemoryBuffer*    target,
-    const void*                   source,
-    uint32_t                      size,
-    OrthancPluginCompressionType  compression,
-    uint8_t                       uncompress)
-  {
-    _OrthancPluginBufferCompression params;
-    params.target = target;
-    params.source = source;
-    params.size = size;
-    params.compression = compression;
-    params.uncompress = uncompress;
-
-    return context->InvokeService(context, _OrthancPluginService_BufferCompression, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    const char*                 path;
-  } _OrthancPluginReadFile;
-
-  /**
-   * @brief Read a file.
-   * 
-   * Read the content of a file on the filesystem, and returns it 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 path The path of the file to be read.
-   * @return 0 if success, or the error code if failure.
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginReadFile(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 path)
-  {
-    _OrthancPluginReadFile params;
-    params.target = target;
-    params.path = path;
-    return context->InvokeService(context, _OrthancPluginService_ReadFile, &params);
-  }
-
-
-
-  typedef struct
-  {
-    const char*  path;
-    const void*  data;
-    uint32_t     size;
-  } _OrthancPluginWriteFile;
-
-  /**
-   * @brief Write a file.
-   * 
-   * Write the content of a memory buffer to the filesystem.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param path The path of the file to be written.
-   * @param data The content of the memory buffer.
-   * @param size The size of the memory buffer.
-   * @return 0 if success, or the error code if failure.
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWriteFile(
-    OrthancPluginContext*  context,
-    const char*            path,
-    const void*            data,
-    uint32_t               size)
-  {
-    _OrthancPluginWriteFile params;
-    params.path = path;
-    params.data = data;
-    params.size = size;
-    return context->InvokeService(context, _OrthancPluginService_WriteFile, &params);
-  }
-
-
-
-  typedef struct
-  {
-    const char**            target;
-    OrthancPluginErrorCode  error;
-  } _OrthancPluginGetErrorDescription;
-
-  /**
-   * @brief Get the description of a given error code.
-   *
-   * This function returns the description of a given error code.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param error The error code of interest.
-   * @return The error description. This is a statically-allocated
-   * string, do not free it.
-   **/
-  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetErrorDescription(
-    OrthancPluginContext*    context,
-    OrthancPluginErrorCode   error)
-  {
-    const char* result = NULL;
-
-    _OrthancPluginGetErrorDescription params;
-    params.target = &result;
-    params.error = error;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetErrorDescription, &params) != OrthancPluginErrorCode_Success ||
-        result == NULL)
-    {
-      return "Unknown error code";
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    uint16_t                 status;
-    const char*              body;
-    uint32_t                 bodySize;
-  } _OrthancPluginSendHttpStatus;
-
-  /**
-   * @brief Send a HTTP status, with a custom body.
-   *
-   * This function answers to a HTTP request by sending a HTTP status
-   * code (such as "400 - Bad Request"), together with a body
-   * describing the error. The body will only be returned if the
-   * configuration option "HttpDescribeErrors" of Orthanc is set to "true".
-   * 
-   * Note that:
-   * - Successful requests (status 200) must use ::OrthancPluginAnswerBuffer().
-   * - Redirections (status 301) must use ::OrthancPluginRedirect().
-   * - Unauthorized access (status 401) must use ::OrthancPluginSendUnauthorized().
-   * - Methods not allowed (status 405) must use ::OrthancPluginSendMethodNotAllowed().
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param status The HTTP status code to be sent.
-   * @param body The body of the answer.
-   * @param bodySize The size of the body.
-   * @see OrthancPluginSendHttpStatusCode()
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSendHttpStatus(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    uint16_t                 status,
-    const char*              body,
-    uint32_t                 bodySize)
-  {
-    _OrthancPluginSendHttpStatus params;
-    params.output = output;
-    params.status = status;
-    params.body = body;
-    params.bodySize = bodySize;
-    context->InvokeService(context, _OrthancPluginService_SendHttpStatus, &params);
-  }
-
-
-
-  typedef struct
-  {
-    const OrthancPluginImage*  image;
-    uint32_t*                  resultUint32;
-    OrthancPluginPixelFormat*  resultPixelFormat;
-    void**                     resultBuffer;
-  } _OrthancPluginGetImageInfo;
-
-
-  /**
-   * @brief Return the pixel format of an image.
-   *
-   * This function returns the type of memory layout for the pixels of the given image.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image of interest.
-   * @return The pixel format.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginPixelFormat  OrthancPluginGetImagePixelFormat(
-    OrthancPluginContext*      context,
-    const OrthancPluginImage*  image)
-  {
-    OrthancPluginPixelFormat target;
-    
-    _OrthancPluginGetImageInfo params;
-    memset(&params, 0, sizeof(params));
-    params.image = image;
-    params.resultPixelFormat = &target;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetImagePixelFormat, &params) != OrthancPluginErrorCode_Success)
-    {
-      return OrthancPluginPixelFormat_Unknown;
-    }
-    else
-    {
-      return (OrthancPluginPixelFormat) target;
-    }
-  }
-
-
-
-  /**
-   * @brief Return the width of an image.
-   *
-   * This function returns the width of the given image.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image of interest.
-   * @return The width.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImageWidth(
-    OrthancPluginContext*      context,
-    const OrthancPluginImage*  image)
-  {
-    uint32_t width;
-    
-    _OrthancPluginGetImageInfo params;
-    memset(&params, 0, sizeof(params));
-    params.image = image;
-    params.resultUint32 = &width;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetImageWidth, &params) != OrthancPluginErrorCode_Success)
-    {
-      return 0;
-    }
-    else
-    {
-      return width;
-    }
-  }
-
-
-
-  /**
-   * @brief Return the height of an image.
-   *
-   * This function returns the height of the given image.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image of interest.
-   * @return The height.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImageHeight(
-    OrthancPluginContext*      context,
-    const OrthancPluginImage*  image)
-  {
-    uint32_t height;
-    
-    _OrthancPluginGetImageInfo params;
-    memset(&params, 0, sizeof(params));
-    params.image = image;
-    params.resultUint32 = &height;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetImageHeight, &params) != OrthancPluginErrorCode_Success)
-    {
-      return 0;
-    }
-    else
-    {
-      return height;
-    }
-  }
-
-
-
-  /**
-   * @brief Return the pitch of an image.
-   *
-   * This function returns the pitch of the given image. The pitch is
-   * defined as the number of bytes between 2 successive lines of the
-   * image in the memory buffer.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image of interest.
-   * @return The pitch.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetImagePitch(
-    OrthancPluginContext*      context,
-    const OrthancPluginImage*  image)
-  {
-    uint32_t pitch;
-    
-    _OrthancPluginGetImageInfo params;
-    memset(&params, 0, sizeof(params));
-    params.image = image;
-    params.resultUint32 = &pitch;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetImagePitch, &params) != OrthancPluginErrorCode_Success)
-    {
-      return 0;
-    }
-    else
-    {
-      return pitch;
-    }
-  }
-
-
-
-  /**
-   * @brief Return a pointer to the content of an image.
-   *
-   * This function returns a pointer to the memory buffer that
-   * contains the pixels of the image.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image of interest.
-   * @return The pointer.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE void*  OrthancPluginGetImageBuffer(
-    OrthancPluginContext*      context,
-    const OrthancPluginImage*  image)
-  {
-    void* target = NULL;
-
-    _OrthancPluginGetImageInfo params;
-    memset(&params, 0, sizeof(params));
-    params.resultBuffer = &target;
-    params.image = image;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetImageBuffer, &params) != OrthancPluginErrorCode_Success)
-    {
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-
-  typedef struct
-  {
-    OrthancPluginImage**       target;
-    const void*                data;
-    uint32_t                   size;
-    OrthancPluginImageFormat   format;
-  } _OrthancPluginUncompressImage;
-
-
-  /**
-   * @brief Decode a compressed image.
-   *
-   * This function decodes a compressed image from a memory buffer.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param data Pointer to a memory buffer containing the compressed image.
-   * @param size Size of the memory buffer containing the compressed image.
-   * @param format The file format of the compressed image.
-   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginUncompressImage(
-    OrthancPluginContext*      context,
-    const void*                data,
-    uint32_t                   size,
-    OrthancPluginImageFormat   format)
-  {
-    OrthancPluginImage* target = NULL;
-
-    _OrthancPluginUncompressImage params;
-    memset(&params, 0, sizeof(params));
-    params.target = &target;
-    params.data = data;
-    params.size = size;
-    params.format = format;
-
-    if (context->InvokeService(context, _OrthancPluginService_UncompressImage, &params) != OrthancPluginErrorCode_Success)
-    {
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-
-
-
-  typedef struct
-  {
-    OrthancPluginImage*   image;
-  } _OrthancPluginFreeImage;
-
-  /**
-   * @brief Free an image.
-   *
-   * This function frees an image that was decoded with OrthancPluginUncompressImage().
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeImage(
-    OrthancPluginContext* context, 
-    OrthancPluginImage*   image)
-  {
-    _OrthancPluginFreeImage params;
-    params.image = image;
-
-    context->InvokeService(context, _OrthancPluginService_FreeImage, &params);
-  }
-
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer* target;
-    OrthancPluginImageFormat   imageFormat;
-    OrthancPluginPixelFormat   pixelFormat;
-    uint32_t                   width;
-    uint32_t                   height;
-    uint32_t                   pitch;
-    const void*                buffer;
-    uint8_t                    quality;
-  } _OrthancPluginCompressImage;
-
-
-  /**
-   * @brief Encode a PNG image.
-   *
-   * This function compresses the given memory buffer containing an
-   * image using the PNG specification, and stores the result of the
-   * compression 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 format The memory layout of the uncompressed image.
-   * @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 containing the uncompressed image.
-   * @return 0 if success, or the error code if failure.
-   * @see OrthancPluginCompressAndAnswerPngImage()
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressPngImage(
-    OrthancPluginContext*         context,
-    OrthancPluginMemoryBuffer*    target,
-    OrthancPluginPixelFormat      format,
-    uint32_t                      width,
-    uint32_t                      height,
-    uint32_t                      pitch,
-    const void*                   buffer)
-  {
-    _OrthancPluginCompressImage params;
-    memset(&params, 0, sizeof(params));
-    params.target = target;
-    params.imageFormat = OrthancPluginImageFormat_Png;
-    params.pixelFormat = format;
-    params.width = width;
-    params.height = height;
-    params.pitch = pitch;
-    params.buffer = buffer;
-    params.quality = 0;  /* Unused for PNG */
-
-    return context->InvokeService(context, _OrthancPluginService_CompressImage, &params);
-  }
-
-
-  /**
-   * @brief Encode a JPEG image.
-   *
-   * This function compresses the given memory buffer containing an
-   * image using the JPEG specification, and stores the result of the
-   * compression 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 format The memory layout of the uncompressed image.
-   * @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 containing the uncompressed image.
-   * @param quality The quality of the JPEG encoding, between 1 (worst
-   * quality, best compression) and 100 (best quality, worst
-   * compression).
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCompressJpegImage(
-    OrthancPluginContext*         context,
-    OrthancPluginMemoryBuffer*    target,
-    OrthancPluginPixelFormat      format,
-    uint32_t                      width,
-    uint32_t                      height,
-    uint32_t                      pitch,
-    const void*                   buffer,
-    uint8_t                       quality)
-  {
-    _OrthancPluginCompressImage params;
-    memset(&params, 0, sizeof(params));
-    params.target = target;
-    params.imageFormat = OrthancPluginImageFormat_Jpeg;
-    params.pixelFormat = format;
-    params.width = width;
-    params.height = height;
-    params.pitch = pitch;
-    params.buffer = buffer;
-    params.quality = quality;
-
-    return context->InvokeService(context, _OrthancPluginService_CompressImage, &params);
-  }
-
-
-
-  /**
-   * @brief Answer to a REST request with a JPEG image.
-   *
-   * This function answers to a REST request with a JPEG image. The
-   * parameters of this function describe a memory buffer that
-   * contains an uncompressed image. The image will be automatically compressed
-   * as a JPEG image by the core system of Orthanc.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param format The memory layout of the uncompressed image.
-   * @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 containing the uncompressed image.
-   * @param quality The quality of the JPEG encoding, between 1 (worst
-   * quality, best compression) and 100 (best quality, worst
-   * compression).
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginCompressAndAnswerJpegImage(
-    OrthancPluginContext*     context,
-    OrthancPluginRestOutput*  output,
-    OrthancPluginPixelFormat  format,
-    uint32_t                  width,
-    uint32_t                  height,
-    uint32_t                  pitch,
-    const void*               buffer,
-    uint8_t                   quality)
-  {
-    _OrthancPluginCompressAndAnswerImage params;
-    params.output = output;
-    params.imageFormat = OrthancPluginImageFormat_Jpeg;
-    params.pixelFormat = format;
-    params.width = width;
-    params.height = height;
-    params.pitch = pitch;
-    params.buffer = buffer;
-    params.quality = quality;
-    context->InvokeService(context, _OrthancPluginService_CompressAndAnswerImage, &params);
-  }
-
-
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    OrthancPluginHttpMethod     method;
-    const char*                 url;
-    const char*                 username;
-    const char*                 password;
-    const void*                 body;
-    uint32_t                    bodySize;
-  } _OrthancPluginCallHttpClient;
-
-
-  /**
-   * @brief Issue a HTTP GET call.
-   * 
-   * Make a HTTP GET call to the given URL. The result to the query is
-   * stored into a newly allocated memory buffer. Favor
-   * OrthancPluginRestApiGet() if calling the built-in REST API of the
-   * Orthanc instance that hosts this plugin.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @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).
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpGet(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 url,
-    const char*                 username,
-    const char*                 password)
-  {
-    _OrthancPluginCallHttpClient params;
-    memset(&params, 0, sizeof(params));
-
-    params.target = target;
-    params.method = OrthancPluginHttpMethod_Get;
-    params.url = url;
-    params.username = username;
-    params.password = password;
-
-    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
-  }
-
-
-  /**
-   * @brief Issue a HTTP POST call.
-   * 
-   * Make a HTTP POST call to the given URL. The result to the query
-   * is stored into a newly allocated memory buffer. Favor
-   * OrthancPluginRestApiPost() if calling the built-in REST API of
-   * the Orthanc instance that hosts this plugin.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @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.
-   * @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).
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPost(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 url,
-    const void*                 body,
-    uint32_t                    bodySize,
-    const char*                 username,
-    const char*                 password)
-  {
-    _OrthancPluginCallHttpClient params;
-    memset(&params, 0, sizeof(params));
-
-    params.target = target;
-    params.method = OrthancPluginHttpMethod_Post;
-    params.url = url;
-    params.body = body;
-    params.bodySize = bodySize;
-    params.username = username;
-    params.password = password;
-
-    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
-  }
-
-
-  /**
-   * @brief Issue a HTTP PUT call.
-   * 
-   * Make a HTTP PUT call to the given URL. The result to the query is
-   * stored into a newly allocated memory buffer. Favor
-   * OrthancPluginRestApiPut() if calling the built-in REST API of the
-   * Orthanc instance that hosts this plugin.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @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.
-   * @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).
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPut(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    const char*                 url,
-    const void*                 body,
-    uint32_t                    bodySize,
-    const char*                 username,
-    const char*                 password)
-  {
-    _OrthancPluginCallHttpClient params;
-    memset(&params, 0, sizeof(params));
-
-    params.target = target;
-    params.method = OrthancPluginHttpMethod_Put;
-    params.url = url;
-    params.body = body;
-    params.bodySize = bodySize;
-    params.username = username;
-    params.password = password;
-
-    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
-  }
-
-
-  /**
-   * @brief Issue a HTTP DELETE call.
-   * 
-   * Make a HTTP DELETE call to the given URL. Favor
-   * OrthancPluginRestApiDelete() if calling the built-in REST API of
-   * the Orthanc instance that hosts this plugin.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @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).
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpDelete(
-    OrthancPluginContext*       context,
-    const char*                 url,
-    const char*                 username,
-    const char*                 password)
-  {
-    _OrthancPluginCallHttpClient params;
-    memset(&params, 0, sizeof(params));
-
-    params.method = OrthancPluginHttpMethod_Delete;
-    params.url = url;
-    params.username = username;
-    params.password = password;
-
-    return context->InvokeService(context, _OrthancPluginService_CallHttpClient, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginImage**       target;
-    const OrthancPluginImage*  source;
-    OrthancPluginPixelFormat   targetFormat;
-  } _OrthancPluginConvertPixelFormat;
-
-
-  /**
-   * @brief Change the pixel format of an image.
-   *
-   * This function creates a new image, changing the memory layout of the pixels.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param source The source image.
-   * @param targetFormat The target pixel format.
-   * @return The resulting image. It must be freed with OrthancPluginFreeImage().
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginImage *OrthancPluginConvertPixelFormat(
-    OrthancPluginContext*      context,
-    const OrthancPluginImage*  source,
-    OrthancPluginPixelFormat   targetFormat)
-  {
-    OrthancPluginImage* target = NULL;
-
-    _OrthancPluginConvertPixelFormat params;
-    params.target = &target;
-    params.source = source;
-    params.targetFormat = targetFormat;
-
-    if (context->InvokeService(context, _OrthancPluginService_ConvertPixelFormat, &params) != OrthancPluginErrorCode_Success)
-    {
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-
-
-  /**
-   * @brief Return the number of available fonts.
-   *
-   * This function returns the number of fonts that are built in the
-   * Orthanc core. These fonts can be used to draw texts on images
-   * through OrthancPluginDrawText().
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @return The number of fonts.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontsCount(
-    OrthancPluginContext*  context)
-  {
-    uint32_t count = 0;
-
-    _OrthancPluginReturnSingleValue params;
-    memset(&params, 0, sizeof(params));
-    params.resultUint32 = &count;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetFontsCount, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return 0;
-    }
-    else
-    {
-      return count;
-    }
-  }
-
-
-
-
-  typedef struct
-  {
-    uint32_t      fontIndex; /* in */
-    const char**  name; /* out */
-    uint32_t*     size; /* out */
-  } _OrthancPluginGetFontInfo;
-
-  /**
-   * @brief Return the name of a font.
-   *
-   * This function returns the name of a font that is built in the Orthanc core.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
-   * @return The font name. This is a statically-allocated string, do not free it.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetFontName(
-    OrthancPluginContext*  context,
-    uint32_t               fontIndex)
-  {
-    const char* result = NULL;
-
-    _OrthancPluginGetFontInfo params;
-    memset(&params, 0, sizeof(params));
-    params.name = &result;
-    params.fontIndex = fontIndex;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, &params) != OrthancPluginErrorCode_Success)
-    {
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Return the size of a font.
-   *
-   * This function returns the size of a font that is built in the Orthanc core.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
-   * @return The font size.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetFontSize(
-    OrthancPluginContext*  context,
-    uint32_t               fontIndex)
-  {
-    uint32_t result;
-
-    _OrthancPluginGetFontInfo params;
-    memset(&params, 0, sizeof(params));
-    params.size = &result;
-    params.fontIndex = fontIndex;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetFontInfo, &params) != OrthancPluginErrorCode_Success)
-    {
-      return 0;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginImage*   image;
-    uint32_t              fontIndex;
-    const char*           utf8Text;
-    int32_t               x;
-    int32_t               y;
-    uint8_t               r;
-    uint8_t               g;
-    uint8_t               b;
-  } _OrthancPluginDrawText;
-
-
-  /**
-   * @brief Draw text on an image.
-   *
-   * This function draws some text on some image.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param image The image upon which to draw the text.
-   * @param fontIndex The index of the font. This value must be less than OrthancPluginGetFontsCount().
-   * @param utf8Text The text to be drawn, encoded as an UTF-8 zero-terminated string.
-   * @param x The X position of the text over the image.
-   * @param y The Y position of the text over the image.
-   * @param r The value of the red color channel of the text.
-   * @param g The value of the green color channel of the text.
-   * @param b The value of the blue color channel of the text.
-   * @return 0 if success, other value if error.
-   * @ingroup Images
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginDrawText(
-    OrthancPluginContext*  context,
-    OrthancPluginImage*    image,
-    uint32_t               fontIndex,
-    const char*            utf8Text,
-    int32_t                x,
-    int32_t                y,
-    uint8_t                r,
-    uint8_t                g,
-    uint8_t                b)
-  {
-    _OrthancPluginDrawText params;
-    memset(&params, 0, sizeof(params));
-    params.image = image;
-    params.fontIndex = fontIndex;
-    params.utf8Text = utf8Text;
-    params.x = x;
-    params.y = y;
-    params.r = r;
-    params.g = g;
-    params.b = b;
-
-    return context->InvokeService(context, _OrthancPluginService_DrawText, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginStorageArea*   storageArea;
-    const char*                 uuid;
-    const void*                 content;
-    uint64_t                    size;
-    OrthancPluginContentType    type;
-  } _OrthancPluginStorageAreaCreate;
-
-
-  /**
-   * @brief Create a file inside the storage area.
-   *
-   * This function creates a new file inside the storage area that is
-   * currently used by Orthanc.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param storageArea The storage area.
-   * @param uuid The identifier of the file to be created.
-   * @param content The content to store in the newly created file.
-   * @param size The size of the content.
-   * @param type The type of the file content.
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaCreate(
-    OrthancPluginContext*       context,
-    OrthancPluginStorageArea*   storageArea,
-    const char*                 uuid,
-    const void*                 content,
-    uint64_t                    size,
-    OrthancPluginContentType    type)
-  {
-    _OrthancPluginStorageAreaCreate params;
-    params.storageArea = storageArea;
-    params.uuid = uuid;
-    params.content = content;
-    params.size = size;
-    params.type = type;
-
-    return context->InvokeService(context, _OrthancPluginService_StorageAreaCreate, &params);
-  }
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    OrthancPluginStorageArea*   storageArea;
-    const char*                 uuid;
-    OrthancPluginContentType    type;
-  } _OrthancPluginStorageAreaRead;
-
-
-  /**
-   * @brief Read a file from the storage area.
-   *
-   * This function reads the content of a given file from the storage
-   * area that is currently used by Orthanc.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
-   * @param storageArea The storage area.
-   * @param uuid The identifier of the file to be read.
-   * @param type The type of the file content.
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRead(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    OrthancPluginStorageArea*   storageArea,
-    const char*                 uuid,
-    OrthancPluginContentType    type)
-  {
-    _OrthancPluginStorageAreaRead params;
-    params.target = target;
-    params.storageArea = storageArea;
-    params.uuid = uuid;
-    params.type = type;
-
-    return context->InvokeService(context, _OrthancPluginService_StorageAreaRead, &params);
-  }
-
-
-  typedef struct
-  {
-    OrthancPluginStorageArea*   storageArea;
-    const char*                 uuid;
-    OrthancPluginContentType    type;
-  } _OrthancPluginStorageAreaRemove;
-
-  /**
-   * @brief Remove a file from the storage area.
-   *
-   * This function removes a given file from the storage area that is
-   * currently used by Orthanc.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param storageArea The storage area.
-   * @param uuid The identifier of the file to be removed.
-   * @param type The type of the file content.
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginStorageAreaRemove(
-    OrthancPluginContext*       context,
-    OrthancPluginStorageArea*   storageArea,
-    const char*                 uuid,
-    OrthancPluginContentType    type)
-  {
-    _OrthancPluginStorageAreaRemove params;
-    params.storageArea = storageArea;
-    params.uuid = uuid;
-    params.type = type;
-
-    return context->InvokeService(context, _OrthancPluginService_StorageAreaRemove, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginErrorCode*  target;
-    int32_t                  code;
-    uint16_t                 httpStatus;
-    const char*              message;
-  } _OrthancPluginRegisterErrorCode;
-  
-  /**
-   * @brief Declare a custom error code for this plugin.
-   *
-   * This function declares a custom error code that can be generated
-   * by this plugin. This declaration is used to enrich the body of
-   * the HTTP answer in the case of an error, and to set the proper
-   * HTTP status code.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param code The error code that is internal to this plugin.
-   * @param httpStatus The HTTP status corresponding to this error.
-   * @param message The description of the error.
-   * @return The error code that has been assigned inside the Orthanc core.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterErrorCode(
-    OrthancPluginContext*    context,
-    int32_t                  code,
-    uint16_t                 httpStatus,
-    const char*              message)
-  {
-    OrthancPluginErrorCode target;
-
-    _OrthancPluginRegisterErrorCode params;
-    params.target = &target;
-    params.code = code;
-    params.httpStatus = httpStatus;
-    params.message = message;
-
-    if (context->InvokeService(context, _OrthancPluginService_RegisterErrorCode, &params) == OrthancPluginErrorCode_Success)
-    {
-      return target;
-    }
-    else
-    {
-      /* There was an error while assigned the error. Use a generic code. */
-      return OrthancPluginErrorCode_Plugin;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    uint16_t                          group;
-    uint16_t                          element;
-    OrthancPluginValueRepresentation  vr;
-    const char*                       name;
-    uint32_t                          minMultiplicity;
-    uint32_t                          maxMultiplicity;
-  } _OrthancPluginRegisterDictionaryTag;
-  
-  /**
-   * @brief Register a new tag into the DICOM dictionary.
-   *
-   * 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.
-   * @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>").
-   * @return 0 if success, other value if error.
-   * @see OrthancPluginRegisterPrivateDictionaryTag()
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginRegisterDictionaryTag(
-    OrthancPluginContext*             context,
-    uint16_t                          group,
-    uint16_t                          element,
-    OrthancPluginValueRepresentation  vr,
-    const char*                       name,
-    uint32_t                          minMultiplicity,
-    uint32_t                          maxMultiplicity)
-  {
-    _OrthancPluginRegisterDictionaryTag params;
-    params.group = group;
-    params.element = element;
-    params.vr = vr;
-    params.name = name;
-    params.minMultiplicity = minMultiplicity;
-    params.maxMultiplicity = maxMultiplicity;
-
-    return context->InvokeService(context, _OrthancPluginService_RegisterDictionaryTag, &params);
-  }
-
-
-
-  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
-  {
-    OrthancPluginStorageArea*  storageArea;
-    OrthancPluginResourceType  level;
-  } _OrthancPluginReconstructMainDicomTags;
-
-  /**
-   * @brief Reconstruct the main DICOM tags.
-   *
-   * 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. A database transaction will be automatically setup.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param storageArea The storage area.
-   * @param level The type of the resources of interest.
-   * @return 0 if success, other value if error.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginReconstructMainDicomTags(
-    OrthancPluginContext*      context,
-    OrthancPluginStorageArea*  storageArea,
-    OrthancPluginResourceType  level)
-  {
-    _OrthancPluginReconstructMainDicomTags params;
-    params.level = level;
-    params.storageArea = storageArea;
-
-    return context->InvokeService(context, _OrthancPluginService_ReconstructMainDicomTags, &params);
-  }
-
-
-  typedef struct
-  {
-    char**                          result;
-    const char*                     instanceId;
-    const void*                     buffer;
-    uint32_t                        size;
-    OrthancPluginDicomToJsonFormat  format;
-    OrthancPluginDicomToJsonFlags   flags;
-    uint32_t                        maxStringLength;
-  } _OrthancPluginDicomToJson;
-
-
-  /**
-   * @brief Format a DICOM memory buffer as a JSON string.
-   *
-   * This function takes as input a memory buffer containing a DICOM
-   * file, and outputs a JSON string representing the tags of this
-   * DICOM file.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param buffer The memory buffer containing the DICOM file.
-   * @param size The size of the memory buffer.
-   * @param format The output format.
-   * @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
-   * string. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Toolbox
-   * @see OrthancPluginDicomInstanceToJson
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson(
-    OrthancPluginContext*           context,
-    const void*                     buffer,
-    uint32_t                        size,
-    OrthancPluginDicomToJsonFormat  format,
-    OrthancPluginDicomToJsonFlags   flags, 
-    uint32_t                        maxStringLength)
-  {
-    char* result;
-
-    _OrthancPluginDicomToJson params;
-    memset(&params, 0, sizeof(params));
-    params.result = &result;
-    params.buffer = buffer;
-    params.size = size;
-    params.format = format;
-    params.flags = flags;
-    params.maxStringLength = maxStringLength;
-
-    if (context->InvokeService(context, _OrthancPluginService_DicomBufferToJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Format a DICOM instance as a JSON string.
-   *
-   * This function formats a DICOM instance that is stored in Orthanc,
-   * and outputs a JSON string representing the tags of this DICOM
-   * instance.
-   *
-   * @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 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
-   * string. This string must be freed by OrthancPluginFreeString().
-   * @ingroup Toolbox
-   * @see OrthancPluginDicomInstanceToJson
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomInstanceToJson(
-    OrthancPluginContext*           context,
-    const char*                     instanceId,
-    OrthancPluginDicomToJsonFormat  format,
-    OrthancPluginDicomToJsonFlags   flags, 
-    uint32_t                        maxStringLength)
-  {
-    char* result;
-
-    _OrthancPluginDicomToJson params;
-    memset(&params, 0, sizeof(params));
-    params.result = &result;
-    params.instanceId = instanceId;
-    params.format = format;
-    params.flags = flags;
-    params.maxStringLength = maxStringLength;
-
-    if (context->InvokeService(context, _OrthancPluginService_DicomInstanceToJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  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 DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginInstanceOrigin OrthancPluginGetInstanceOrigin(
-    OrthancPluginContext*             context,
-    const 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 decode DICOM images,
-   * extending the built-in decoder of Orthanc that uses
-   * DCMTK. Starting with Orthanc 1.7.0, the exact behavior is
-   * affected by the configuration option
-   * "BuiltinDecoderTranscoderOrder" 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
-   * @see OrthancPluginGetInstanceDecodedFrame()
-   **/
-  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 void*                 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()
-   * @ingroup Toolbox
-   **/
-  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 void*                 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 suboperation.
-   * @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 void*                 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 void*                 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);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRestOutput* output;
-    const char*              details;
-    uint8_t                  log;
-  } _OrthancPluginSetHttpErrorDetails;
-
-  /**
-   * @brief Provide a detailed description for an HTTP error.
-   *
-   * This function sets the detailed description associated with an
-   * HTTP error. This description will be displayed in the "Details"
-   * field of the JSON body of the HTTP answer. It is only taken into
-   * consideration if the REST callback returns an error code that is
-   * different from "OrthancPluginErrorCode_Success", and if the
-   * "HttpDescribeErrors" configuration option of Orthanc is set to
-   * "true".
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param output The HTTP connection to the client application.
-   * @param details The details of the error message.
-   * @param log Whether to also write the detailed error to the Orthanc logs.
-   * @ingroup REST
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetHttpErrorDetails(
-    OrthancPluginContext*    context,
-    OrthancPluginRestOutput* output,
-    const char*              details,
-    uint8_t                  log)
-  {
-    _OrthancPluginSetHttpErrorDetails params;
-    params.output = output;
-    params.details = details;
-    params.log = log;
-    context->InvokeService(context, _OrthancPluginService_SetHttpErrorDetails, &params);
-  }
-
-
-
-  typedef struct
-  {
-    const char** result;
-    const char*  argument;
-  } _OrthancPluginRetrieveStaticString;
-
-  /**
-   * @brief Detect the MIME type of a file.
-   *
-   * This function returns the MIME type of a file by inspecting its extension.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param path Path to the file.
-   * @return The MIME type. This is a statically-allocated
-   * string, do not free it.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE const char* OrthancPluginAutodetectMimeType(
-    OrthancPluginContext*  context,
-    const char*            path)
-  {
-    const char* result = NULL;
-
-    _OrthancPluginRetrieveStaticString params;
-    params.result = &result;
-    params.argument = path;
-
-    if (context->InvokeService(context, _OrthancPluginService_AutodetectMimeType, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  typedef struct
-  {
-    const char*               name;
-    float                     value;
-    OrthancPluginMetricsType  type;
-  } _OrthancPluginSetMetricsValue;
-
-  /**
-   * @brief Set the value of a metrics.
-   *
-   * This function sets the value of a metrics to monitor the behavior
-   * of the plugin through tools such as Prometheus. The values of all
-   * the metrics are stored within the Orthanc context.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param name The name of the metrics to be set.
-   * @param value The value of the metrics.
-   * @param type The type of the metrics. This parameter is only taken into consideration
-   * the first time this metrics is set.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginSetMetricsValue(
-    OrthancPluginContext*     context,
-    const char*               name,
-    float                     value,
-    OrthancPluginMetricsType  type)
-  {
-    _OrthancPluginSetMetricsValue params;
-    params.name = name;
-    params.value = value;
-    params.type = type;
-    context->InvokeService(context, _OrthancPluginService_SetMetricsValue, &params);
-  }
-
-
-
-  typedef struct
-  {
-    OrthancPluginRefreshMetricsCallback  callback;
-  } _OrthancPluginRegisterRefreshMetricsCallback;
-
-  /**
-   * @brief Register a callback to refresh the metrics.
-   *
-   * This function registers a callback to refresh the metrics. The
-   * callback must make calls to OrthancPluginSetMetricsValue().
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param callback The callback function to handle the refresh.
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterRefreshMetricsCallback(
-    OrthancPluginContext*               context,
-    OrthancPluginRefreshMetricsCallback callback)
-  {
-    _OrthancPluginRegisterRefreshMetricsCallback params;
-    params.callback = callback;
-    context->InvokeService(context, _OrthancPluginService_RegisterRefreshMetricsCallback, &params);
-  }
-
-
-
-
-  typedef struct
-  {
-    char**                               target;
-    const void*                          dicom;
-    uint32_t                             dicomSize;
-    OrthancPluginDicomWebBinaryCallback  callback;
-  } _OrthancPluginEncodeDicomWeb;
-
-  /**
-   * @brief Convert a DICOM instance to DICOMweb JSON.
-   *
-   * This function converts a memory buffer containing a DICOM instance,
-   * into its DICOMweb JSON representation.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param dicom Pointer to the DICOM instance.
-   * @param dicomSize Size of the DICOM instance.
-   * @param callback Callback to set the value of the binary tags.
-   * @see OrthancPluginCreateDicom()
-   * @return The NULL value in case of error, or the JSON document. This string must
-   * be freed by OrthancPluginFreeString().
-   * @deprecated OrthancPluginEncodeDicomWebJson2()
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson(
-    OrthancPluginContext*                context,
-    const void*                          dicom,
-    uint32_t                             dicomSize,
-    OrthancPluginDicomWebBinaryCallback  callback)
-  {
-    char* target = NULL;
-    
-    _OrthancPluginEncodeDicomWeb params;
-    params.target = &target;
-    params.dicom = dicom;
-    params.dicomSize = dicomSize;
-    params.callback = callback;
-
-    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-
-  /**
-   * @brief Convert a DICOM instance to DICOMweb XML.
-   *
-   * This function converts a memory buffer containing a DICOM instance,
-   * into its DICOMweb XML representation.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param dicom Pointer to the DICOM instance.
-   * @param dicomSize Size of the DICOM instance.
-   * @param callback Callback to set the value of the binary tags.
-   * @return The NULL value in case of error, or the XML document. This string must
-   * be freed by OrthancPluginFreeString().
-   * @see OrthancPluginCreateDicom()
-   * @deprecated OrthancPluginEncodeDicomWebXml2()
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml(
-    OrthancPluginContext*                context,
-    const void*                          dicom,
-    uint32_t                             dicomSize,
-    OrthancPluginDicomWebBinaryCallback  callback)
-  {
-    char* target = NULL;
-    
-    _OrthancPluginEncodeDicomWeb params;
-    params.target = &target;
-    params.dicom = dicom;
-    params.dicomSize = dicomSize;
-    params.callback = callback;
-
-    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-  
-
-
-  typedef struct
-  {
-    char**                                target;
-    const void*                           dicom;
-    uint32_t                              dicomSize;
-    OrthancPluginDicomWebBinaryCallback2  callback;
-    void*                                 payload;
-  } _OrthancPluginEncodeDicomWeb2;
-
-  /**
-   * @brief Convert a DICOM instance to DICOMweb JSON.
-   *
-   * This function converts a memory buffer containing a DICOM instance,
-   * into its DICOMweb JSON representation.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param dicom Pointer to the DICOM instance.
-   * @param dicomSize Size of the DICOM instance.
-   * @param callback Callback to set the value of the binary tags.
-   * @param payload User payload.
-   * @return The NULL value in case of error, or the JSON document. This string must
-   * be freed by OrthancPluginFreeString().
-   * @see OrthancPluginCreateDicom()
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebJson2(
-    OrthancPluginContext*                 context,
-    const void*                           dicom,
-    uint32_t                              dicomSize,
-    OrthancPluginDicomWebBinaryCallback2  callback,
-    void*                                 payload)
-  {
-    char* target = NULL;
-    
-    _OrthancPluginEncodeDicomWeb2 params;
-    params.target = &target;
-    params.dicom = dicom;
-    params.dicomSize = dicomSize;
-    params.callback = callback;
-    params.payload = payload;
-
-    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebJson2, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-
-  /**
-   * @brief Convert a DICOM instance to DICOMweb XML.
-   *
-   * This function converts a memory buffer containing a DICOM instance,
-   * into its DICOMweb XML representation.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param dicom Pointer to the DICOM instance.
-   * @param dicomSize Size of the DICOM instance.
-   * @param callback Callback to set the value of the binary tags.
-   * @param payload User payload.
-   * @return The NULL value in case of error, or the XML document. This string must
-   * be freed by OrthancPluginFreeString().
-   * @see OrthancPluginCreateDicom()
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginEncodeDicomWebXml2(
-    OrthancPluginContext*                 context,
-    const void*                           dicom,
-    uint32_t                              dicomSize,
-    OrthancPluginDicomWebBinaryCallback2  callback,
-    void*                                 payload)
-  {
-    char* target = NULL;
-    
-    _OrthancPluginEncodeDicomWeb2 params;
-    params.target = &target;
-    params.dicom = dicom;
-    params.dicomSize = dicomSize;
-    params.callback = callback;
-    params.payload = payload;
-
-    if (context->InvokeService(context, _OrthancPluginService_EncodeDicomWebXml2, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-  
-
-
-  /**
-   * @brief Callback executed when a HTTP header is received during a chunked transfer.
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP client during a chunked HTTP transfer, as soon as it
-   * receives one HTTP header from the answer of the remote HTTP
-   * server.
-   *
-   * @see OrthancPluginChunkedHttpClient()
-   * @param answer The user payload, as provided by the calling plugin.
-   * @param key The key of the HTTP header.
-   * @param value The value of the HTTP header.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) (
-    void* answer,
-    const char* key,
-    const char* value);
-
-
-  /**
-   * @brief Callback executed when an answer chunk is received during a chunked transfer.
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP client during a chunked HTTP transfer, as soon as it
-   * receives one data chunk from the answer of the remote HTTP
-   * server.
-   *
-   * @see OrthancPluginChunkedHttpClient()
-   * @param answer The user payload, as provided by the calling plugin.
-   * @param data The content of the data chunk.
-   * @param size The size of the data chunk.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) (
-    void* answer,
-    const void* data,
-    uint32_t size);
-  
-
-  /**
-   * @brief Callback to know whether the request body is entirely read during a chunked transfer 
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP client during a chunked HTTP transfer, while reading
-   * the body of a POST or PUT request. The plugin must answer "1" as
-   * soon as the body is entirely read: The "request" data structure
-   * must act as an iterator.
-   *
-   * @see OrthancPluginChunkedHttpClient()
-   * @param request The user payload, as provided by the calling plugin.
-   * @return "1" if the body is over, or "0" if there is still data to be read.
-   * @ingroup Toolbox
-   **/
-  typedef uint8_t (*OrthancPluginChunkedClientRequestIsDone) (void* request);
-
-
-  /**
-   * @brief Callback to advance in the request body during a chunked transfer 
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP client during a chunked HTTP transfer, while reading
-   * the body of a POST or PUT request. This function asks the plugin
-   * to advance to the next chunk of data of the request body: The
-   * "request" data structure must act as an iterator.
-   *
-   * @see OrthancPluginChunkedHttpClient()
-   * @param request The user payload, as provided by the calling plugin.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientRequestNext) (void* request);
-
-
-  /**
-   * @brief Callback to read the current chunk of the request body during a chunked transfer 
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP client during a chunked HTTP transfer, while reading
-   * the body of a POST or PUT request. The plugin must provide the
-   * content of the current chunk of data of the request body.
-   *
-   * @see OrthancPluginChunkedHttpClient()
-   * @param request The user payload, as provided by the calling plugin.
-   * @return The content of the current request chunk.
-   * @ingroup Toolbox
-   **/
-  typedef const void* (*OrthancPluginChunkedClientRequestGetChunkData) (void* request);
-
-
-  /**
-   * @brief Callback to read the size of the current request chunk during a chunked transfer 
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP client during a chunked HTTP transfer, while reading
-   * the body of a POST or PUT request. The plugin must provide the
-   * size of the current chunk of data of the request body.
-   *
-   * @see OrthancPluginChunkedHttpClient()
-   * @param request The user payload, as provided by the calling plugin.
-   * @return The size of the current request chunk.
-   * @ingroup Toolbox
-   **/
-  typedef uint32_t (*OrthancPluginChunkedClientRequestGetChunkSize) (void* request);
-
-  
-  typedef struct
-  {
-    void*                                          answer;
-    OrthancPluginChunkedClientAnswerAddChunk       answerAddChunk;
-    OrthancPluginChunkedClientAnswerAddHeader      answerAddHeader;
-    uint16_t*                                      httpStatus;
-    OrthancPluginHttpMethod                        method;
-    const char*                                    url;
-    uint32_t                                       headersCount;
-    const char* const*                             headersKeys;
-    const char* const*                             headersValues;
-    void*                                          request;
-    OrthancPluginChunkedClientRequestIsDone        requestIsDone;
-    OrthancPluginChunkedClientRequestGetChunkData  requestChunkData;
-    OrthancPluginChunkedClientRequestGetChunkSize  requestChunkSize;
-    OrthancPluginChunkedClientRequestNext          requestNext;
-    const char*                                    username;
-    const char*                                    password;
-    uint32_t                                       timeout;
-    const char*                                    certificateFile;
-    const char*                                    certificateKeyFile;
-    const char*                                    certificateKeyPassword;
-    uint8_t                                        pkcs11;
-  } _OrthancPluginChunkedHttpClient;
-
-  
-  /**
-   * @brief Issue a HTTP call, using chunked HTTP transfers.
-   * 
-   * Make a HTTP call to the given URL using chunked HTTP
-   * transfers. The request body is provided as an iterator over data
-   * chunks. The answer is provided as a sequence of function calls
-   * with the individual HTTP headers and answer chunks.
-   * 
-   * Contrarily to OrthancPluginHttpClient() that entirely stores the
-   * request body and the answer body in memory buffers, this function
-   * uses chunked HTTP transfers. This results in a lower memory
-   * consumption. Pay attention to the fact that Orthanc servers with
-   * version <= 1.5.6 do not support chunked transfers: You must use
-   * OrthancPluginHttpClient() if contacting such older servers.
-   *
-   * 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 answer The user payload for the answer body. It will be provided to the callbacks for the answer.
-   * @param answerAddChunk Callback function to report a data chunk from the answer body.
-   * @param answerAddHeader Callback function to report an HTTP header sent by the remote server.
-   * @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 request The user payload containing the request body, and acting as an iterator.
-   * It will be provided to the callbacks for the request.
-   * @param requestIsDone Callback function to tell whether the request body is entirely read.
-   * @param requestChunkData Callback function to get the content of the current data chunk of the request body.
-   * @param requestChunkSize Callback function to get the size of the current data chunk of the request body.
-   * @param requestNext Callback function to advance to the next data chunk of the request body.
-   * @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 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 OrthancPluginHttpClient()
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginChunkedHttpClient(
-    OrthancPluginContext*                          context,
-    void*                                          answer,
-    OrthancPluginChunkedClientAnswerAddChunk       answerAddChunk,
-    OrthancPluginChunkedClientAnswerAddHeader      answerAddHeader,
-    uint16_t*                                      httpStatus,
-    OrthancPluginHttpMethod                        method,
-    const char*                                    url,
-    uint32_t                                       headersCount,
-    const char* const*                             headersKeys,
-    const char* const*                             headersValues,
-    void*                                          request,
-    OrthancPluginChunkedClientRequestIsDone        requestIsDone,
-    OrthancPluginChunkedClientRequestGetChunkData  requestChunkData,
-    OrthancPluginChunkedClientRequestGetChunkSize  requestChunkSize,
-    OrthancPluginChunkedClientRequestNext          requestNext,
-    const char*                                    username,
-    const char*                                    password,
-    uint32_t                                       timeout,
-    const char*                                    certificateFile,
-    const char*                                    certificateKeyFile,
-    const char*                                    certificateKeyPassword,
-    uint8_t                                        pkcs11)
-  {
-    _OrthancPluginChunkedHttpClient params;
-    memset(&params, 0, sizeof(params));
-
-    /* In common with OrthancPluginHttpClient() */
-    params.httpStatus = httpStatus;
-    params.method = method;
-    params.url = url;
-    params.headersCount = headersCount;
-    params.headersKeys = headersKeys;
-    params.headersValues = headersValues;
-    params.username = username;
-    params.password = password;
-    params.timeout = timeout;
-    params.certificateFile = certificateFile;
-    params.certificateKeyFile = certificateKeyFile;
-    params.certificateKeyPassword = certificateKeyPassword;
-    params.pkcs11 = pkcs11;
-
-    /* For chunked body/answer */
-    params.answer = answer;
-    params.answerAddChunk = answerAddChunk;
-    params.answerAddHeader = answerAddHeader;
-    params.request = request;
-    params.requestIsDone = requestIsDone;
-    params.requestChunkData = requestChunkData;
-    params.requestChunkSize = requestChunkSize;
-    params.requestNext = requestNext;
-
-    return context->InvokeService(context, _OrthancPluginService_ChunkedHttpClient, &params);
-  }
-
-
-
-  /**
-   * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer.
-   * @ingroup Callback
-   **/
-  typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader;
-
-
-
-  /**
-   * @brief Callback to create a reader to handle incoming chunked HTTP transfers.
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP server that supports chunked HTTP transfers. This
-   * callback is only invoked if the HTTP method is POST or PUT. The
-   * callback must create an user-specific "reader" object that will
-   * be fed with the body of the incoming body.
-   * 
-   * @see OrthancPluginRegisterChunkedRestCallback()
-   * @param reader Memory location that must be filled with the newly-created reader.
-   * @param url The URI that is accessed.
-   * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used.
-   * @return 0 if success, or the error code if failure.
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) (
-    OrthancPluginServerChunkedRequestReader**  reader,
-    const char*                                url,
-    const OrthancPluginHttpRequest*            request);
-
-  
-  /**
-   * @brief Callback invoked whenever a new data chunk is available during a chunked transfer.
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP server that supports chunked HTTP transfers. This callback
-   * is invoked as soon as a new data chunk is available for the request body.
-   * 
-   * @see OrthancPluginRegisterChunkedRestCallback()
-   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
-   * @param data The content of the data chunk.
-   * @param size The size of the data chunk.
-   * @return 0 if success, or the error code if failure.
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderAddChunk) (
-    OrthancPluginServerChunkedRequestReader* reader,
-    const void*                              data,
-    uint32_t                                 size);
-    
-
-  /**
-   * @brief Callback invoked whenever the request body is entirely received.
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP server that supports chunked HTTP transfers. This
-   * callback is invoked as soon as the full body of the HTTP request
-   * is available. The plugin can then send its answer thanks to the
-   * provided "output" object.
-   * 
-   * @see OrthancPluginRegisterChunkedRestCallback()
-   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
-   * @param output The HTTP connection to the client application.
-   * @return 0 if success, or the error code if failure.
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderExecute) (
-    OrthancPluginServerChunkedRequestReader* reader,
-    OrthancPluginRestOutput*                 output);
-    
-
-  /**
-   * @brief Callback invoked to release the resources associated with an incoming HTTP chunked transfer.
-   *
-   * Signature of a callback function that is called by Orthanc acting
-   * as a HTTP server that supports chunked HTTP transfers. This
-   * callback is invoked to release all the resources allocated by the
-   * given reader. Note that this function might be invoked even if
-   * the entire body was not read, to deal with client error or
-   * disconnection.
-   * 
-   * @see OrthancPluginRegisterChunkedRestCallback()
-   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
-   **/
-  typedef void (*OrthancPluginServerChunkedRequestReaderFinalize) (
-    OrthancPluginServerChunkedRequestReader* reader);
-  
-  typedef struct
-  {
-    const char*                                      pathRegularExpression;
-    OrthancPluginRestCallback                        getHandler;
-    OrthancPluginServerChunkedRequestReaderFactory   postHandler;
-    OrthancPluginRestCallback                        deleteHandler;
-    OrthancPluginServerChunkedRequestReaderFactory   putHandler;
-    OrthancPluginServerChunkedRequestReaderAddChunk  addChunk;
-    OrthancPluginServerChunkedRequestReaderExecute   execute;
-    OrthancPluginServerChunkedRequestReaderFinalize  finalize;
-  } _OrthancPluginChunkedRestCallback;
-
-
-  /**
-   * @brief Register a REST callback to handle chunked HTTP transfers.
-   *
-   * This function registers a REST callback against a regular
-   * expression for a URI. This function must be called during the
-   * initialization of the plugin, i.e. inside the
-   * OrthancPluginInitialize() public function.
-   *
-   * Contrarily to OrthancPluginRegisterRestCallback(), the callbacks
-   * will NOT be invoked in mutual exclusion, so it is up to the
-   * plugin to implement the required locking mechanisms.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
-   * @param getHandler The callback function to handle REST calls using the GET HTTP method.
-   * @param postHandler The callback function to handle REST calls using the GET POST method.
-   * @param deleteHandler The callback function to handle REST calls using the GET DELETE method.
-   * @param putHandler The callback function to handle REST calls using the GET PUT method.
-   * @param addChunk The callback invoked when a new chunk is available for the request body of a POST or PUT call.
-   * @param execute The callback invoked once the entire body of a POST or PUT call is read.
-   * @param finalize The callback invoked to release the resources associated with a POST or PUT call.
-   * @see OrthancPluginRegisterRestCallbackNoLock()
-   *
-   * @note
-   * The regular expression is case sensitive and must follow the
-   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
-   *
-   * @ingroup Callbacks
-   **/
-  ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback(
-    OrthancPluginContext*                            context,
-    const char*                                      pathRegularExpression,
-    OrthancPluginRestCallback                        getHandler,
-    OrthancPluginServerChunkedRequestReaderFactory   postHandler,
-    OrthancPluginRestCallback                        deleteHandler,
-    OrthancPluginServerChunkedRequestReaderFactory   putHandler,
-    OrthancPluginServerChunkedRequestReaderAddChunk  addChunk,
-    OrthancPluginServerChunkedRequestReaderExecute   execute,
-    OrthancPluginServerChunkedRequestReaderFinalize  finalize)
-  {
-    _OrthancPluginChunkedRestCallback params;
-    params.pathRegularExpression = pathRegularExpression;
-    params.getHandler = getHandler;
-    params.postHandler = postHandler;
-    params.deleteHandler = deleteHandler;
-    params.putHandler = putHandler;
-    params.addChunk = addChunk;
-    params.execute = execute;
-    params.finalize = finalize;
-
-    context->InvokeService(context, _OrthancPluginService_RegisterChunkedRestCallback, &params);
-  }
-
-
-
-
-
-  typedef struct
-  {
-    char**       result;
-    uint16_t     group;
-    uint16_t     element;
-    const char*  privateCreator;
-  } _OrthancPluginGetTagName;
-
-  /**
-   * @brief Returns the symbolic name of a DICOM tag.
-   *
-   * This function makes a lookup to the dictionary of DICOM tags that
-   * are known to Orthanc, and returns the symbolic name of a DICOM tag.
-   *
-   * @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 privateCreator For private tags, the name of the private creator (can be NULL).
-   * @return NULL in the case of an error, or a newly allocated string
-   * containing the path. This string must be freed by
-   * OrthancPluginFreeString().
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName(
-    OrthancPluginContext*  context,
-    uint16_t               group,
-    uint16_t               element,
-    const char*            privateCreator)
-  {
-    char* result;
-
-    _OrthancPluginGetTagName params;
-    params.result = &result;
-    params.group = group;
-    params.element = element;
-    params.privateCreator = privateCreator;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetTagName, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-
-  /**
-   * @brief Callback executed by the storage commitment SCP.
-   *
-   * Signature of a factory function that creates an object to handle
-   * one incoming storage commitment request.
-   *
-   * @remark The factory receives the list of the SOP class/instance
-   * UIDs of interest to the remote storage commitment SCU. This gives
-   * the factory the possibility to start some prefetch process
-   * upfront in the background, before the handler object is actually
-   * queried about the status of these DICOM instances.
-   *
-   * @param handler Output variable where the factory puts the handler object it created.
-   * @param jobId ID of the Orthanc job that is responsible for handling 
-   * the storage commitment request. This job will successively look for the
-   * status of all the individual queried DICOM instances.
-   * @param transactionUid UID of the storage commitment transaction
-   * provided by the storage commitment SCU. It contains the value of the
-   * (0008,1195) DICOM tag.
-   * @param sopClassUids Array of the SOP class UIDs (0008,0016) that are queried by the SCU.
-   * @param sopInstanceUids Array of the SOP instance UIDs (0008,0018) that are queried by the SCU.
-   * @param countInstances Number of DICOM instances that are queried. This is the size
-   * of the `sopClassUids` and `sopInstanceUids` arrays.
-   * @param remoteAet The AET of the storage commitment SCU.
-   * @param calledAet The AET used by the SCU to contact the storage commitment SCP (i.e. Orthanc).
-   * @return 0 if success, other value if error.
-   * @ingroup DicomCallbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentFactory) (
-    void**              handler /* out */,
-    const char*         jobId,
-    const char*         transactionUid,
-    const char* const*  sopClassUids,
-    const char* const*  sopInstanceUids,
-    uint32_t            countInstances,
-    const char*         remoteAet,
-    const char*         calledAet);
-
-  
-  /**
-   * @brief Callback to free one storage commitment SCP handler.
-   * 
-   * Signature of a callback function that releases the resources
-   * allocated by the factory of the storage commitment SCP. The
-   * handler is the return value of a previous call to the
-   * OrthancPluginStorageCommitmentFactory() callback.
-   *
-   * @param handler The handler object to be destructed.
-   * @ingroup DicomCallbacks
-   **/
-  typedef void (*OrthancPluginStorageCommitmentDestructor) (void* handler);
-
-
-  /**
-   * @brief Callback to get the status of one DICOM instance in the
-   * storage commitment SCP.
-   *
-   * Signature of a callback function that is successively invoked for
-   * each DICOM instance that is queried by the remote storage
-   * commitment SCU.  The function must be tought of as a method of
-   * the handler object that was created by a previous call to the
-   * OrthancPluginStorageCommitmentFactory() callback. After each call
-   * to this method, the progress of the associated Orthanc job is
-   * updated.
-   * 
-   * @param target Output variable where to put the status for the queried instance.
-   * @param handler The handler object associated with this storage commitment request.
-   * @param sopClassUid The SOP class UID (0008,0016) of interest.
-   * @param sopInstanceUid The SOP instance UID (0008,0018) of interest.
-   * @ingroup DicomCallbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginStorageCommitmentLookup) (
-    OrthancPluginStorageCommitmentFailureReason* target,
-    void* handler,
-    const char* sopClassUid,
-    const char* sopInstanceUid);
-    
-    
-  typedef struct
-  {
-    OrthancPluginStorageCommitmentFactory     factory;
-    OrthancPluginStorageCommitmentDestructor  destructor;
-    OrthancPluginStorageCommitmentLookup      lookup;
-  } _OrthancPluginRegisterStorageCommitmentScpCallback;
-
-  /**
-   * @brief Register a callback to handle incoming requests to the storage commitment SCP.
-   *
-   * This function registers a callback to handle storage commitment SCP requests.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param factory Factory function that creates the handler object
-   * for incoming storage commitment requests.
-   * @param destructor Destructor function to destroy the handler object.
-   * @param lookup Callback method to get the status of one DICOM instance.
-   * @return 0 if success, other value if error.
-   * @ingroup DicomCallbacks
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterStorageCommitmentScpCallback(
-    OrthancPluginContext*                     context,
-    OrthancPluginStorageCommitmentFactory     factory,
-    OrthancPluginStorageCommitmentDestructor  destructor,
-    OrthancPluginStorageCommitmentLookup      lookup)
-  {
-    _OrthancPluginRegisterStorageCommitmentScpCallback params;
-    params.factory = factory;
-    params.destructor = destructor;
-    params.lookup = lookup;
-    return context->InvokeService(context, _OrthancPluginService_RegisterStorageCommitmentScpCallback, &params);
-  }
-  
-
-
-  /**
-   * @brief Callback to filter incoming DICOM instances received by Orthanc.
-   *
-   * Signature of a callback function that is triggered whenever
-   * Orthanc receives a new DICOM instance (e.g. through REST API or
-   * DICOM protocol), and that answers whether this DICOM instance
-   * should be accepted or discarded by Orthanc.
-   *
-   * Note that the metadata information is not available
-   * (i.e. GetInstanceMetadata() should not be used on "instance").
-   *
-   * @param instance The received DICOM instance.
-   * @return 0 to discard the instance, 1 to store the instance, -1 if error.
-   * @ingroup Callback
-   **/
-  typedef int32_t (*OrthancPluginIncomingDicomInstanceFilter) (
-    const OrthancPluginDicomInstance* instance);
-
-
-  typedef struct
-  {
-    OrthancPluginIncomingDicomInstanceFilter callback;
-  } _OrthancPluginIncomingDicomInstanceFilter;
-
-  /**
-   * @brief Register a callback to filter incoming DICOM instance.
-   *
-   * This function registers a custom callback to filter incoming
-   * DICOM instances received by Orthanc (either through the REST API
-   * or through the DICOM protocol).
-   *
-   * @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 OrthancPluginRegisterIncomingDicomInstanceFilter(
-    OrthancPluginContext*                     context,
-    OrthancPluginIncomingDicomInstanceFilter  callback)
-  {
-    _OrthancPluginIncomingDicomInstanceFilter params;
-    params.callback = callback;
-
-    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingDicomInstanceFilter, &params);
-  }
-
-
-  /**
-   * @brief Get the transfer syntax of a DICOM file.
-   *
-   * This function returns a pointer to a newly created string that
-   * contains the transfer syntax UID of the DICOM instance. The empty
-   * string might be returned if this information is unknown.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The NULL value in case of error, or a string containing the
-   * transfer syntax UID. This string must be freed by OrthancPluginFreeString().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceTransferSyntaxUid(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance)
-  {
-    char* result;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultStringToFree = &result;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceTransferSyntaxUid, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Check whether the DICOM file has pixel data.
-   *
-   * This function returns a Boolean value indicating whether the
-   * DICOM instance contains the pixel data (7FE0,0010) tag.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return "1" if the DICOM instance contains pixel data, or "0" if
-   * the tag is missing, or "-1" in the case of an error.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE int32_t OrthancPluginHasInstancePixelData(
-    OrthancPluginContext*             context,
-    const OrthancPluginDicomInstance* instance)
-  {
-    int64_t hasPixelData;
-
-    _OrthancPluginAccessDicomInstance params;
-    memset(&params, 0, sizeof(params));
-    params.resultInt64 = &hasPixelData;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_HasInstancePixelData, &params) != OrthancPluginErrorCode_Success ||
-        hasPixelData < 0 ||
-        hasPixelData > 1)
-    {
-      /* Error */
-      return -1;
-    }
-    else
-    {
-      return (hasPixelData != 0);
-    }
-  }
-
-
-
-
-
-
-  typedef struct
-  {
-    OrthancPluginDicomInstance**  target;
-    const void*                   buffer;
-    uint32_t                      size;
-    const char*                   transferSyntax;
-  } _OrthancPluginCreateDicomInstance;
-
-  /**
-   * @brief Parse a DICOM instance.
-   *
-   * This function parses a memory buffer that contains a DICOM
-   * file. The function returns a new pointer to a data structure that
-   * is managed by the Orthanc core.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param buffer The memory buffer containing the DICOM instance.
-   * @param size The size of the memory buffer.
-   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginCreateDicomInstance(
-    OrthancPluginContext*  context,
-    const void*            buffer,
-    uint32_t               size)
-  {
-    OrthancPluginDicomInstance* target = NULL;
-
-    _OrthancPluginCreateDicomInstance params;
-    params.target = &target;
-    params.buffer = buffer;
-    params.size = size;
-
-    if (context->InvokeService(context, _OrthancPluginService_CreateDicomInstance, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-  typedef struct
-  {
-    OrthancPluginDicomInstance*   dicom;
-  } _OrthancPluginFreeDicomInstance;
-
-  /**
-   * @brief Free a DICOM instance.
-   *
-   * This function frees a DICOM instance that was parsed using
-   * OrthancPluginCreateDicomInstance().
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param dicom The DICOM instance.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreeDicomInstance(
-    OrthancPluginContext*        context, 
-    OrthancPluginDicomInstance*  dicom)
-  {
-    _OrthancPluginFreeDicomInstance params;
-    params.dicom = dicom;
-
-    context->InvokeService(context, _OrthancPluginService_FreeDicomInstance, &params);
-  }
-
-
-  typedef struct
-  {
-    uint32_t*                             targetUint32;
-    OrthancPluginMemoryBuffer*            targetBuffer;
-    OrthancPluginImage**                  targetImage;
-    char**                                targetStringToFree;
-    const OrthancPluginDicomInstance*     instance;
-    uint32_t                              frameIndex;
-    OrthancPluginDicomToJsonFormat        format;
-    OrthancPluginDicomToJsonFlags         flags;
-    uint32_t                              maxStringLength;
-    OrthancPluginDicomWebBinaryCallback2  dicomWebCallback;
-    void*                                 dicomWebPayload;
-  } _OrthancPluginAccessDicomInstance2;
-
-  /**
-   * @brief Get the number of frames in a DICOM instance.
-   *
-   * This function returns the number of frames that are part of a
-   * DICOM image managed by the Orthanc core.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @return The number of frames (will be zero in the case of an error).
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetInstanceFramesCount(
-    OrthancPluginContext*             context,
-    const OrthancPluginDicomInstance* instance)
-  {
-    uint32_t count;
-
-    _OrthancPluginAccessDicomInstance2 params;
-    memset(&params, 0, sizeof(params));
-    params.targetUint32 = &count;
-    params.instance = instance;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceFramesCount, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return 0;
-    }
-    else
-    {
-      return count;
-    }
-  }
-
-
-  /**
-   * @brief Get the raw content of a frame in a DICOM instance.
-   *
-   * This function returns a memory buffer containing the raw content
-   * of a frame in a DICOM instance that is managed by the Orthanc
-   * core. This is notably useful for compressed transfer syntaxes, as
-   * it gives access to the embedded files (such as JPEG, JPEG-LS or
-   * JPEG2k). The Orthanc core transparently reassembles the fragments
-   * to extract the raw frame.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
-   * @param instance The instance of interest.
-   * @param frameIndex The index of the frame of interest.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginGetInstanceRawFrame(
-    OrthancPluginContext*             context,
-    OrthancPluginMemoryBuffer*        target,
-    const OrthancPluginDicomInstance* instance,
-    uint32_t                          frameIndex)
-  {
-    _OrthancPluginAccessDicomInstance2 params;
-    memset(&params, 0, sizeof(params));
-    params.targetBuffer = target;
-    params.instance = instance;
-    params.frameIndex = frameIndex;
-
-    return context->InvokeService(context, _OrthancPluginService_GetInstanceRawFrame, &params);
-  }
-
-
-  /**
-   * @brief Decode one frame from a DICOM instance.
-   *
-   * This function decodes one frame of a DICOM image that is managed
-   * by the Orthanc core.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The instance of interest.
-   * @param frameIndex The index of the frame of interest.
-   * @return The uncompressed image. It must be freed with OrthancPluginFreeImage().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginImage* OrthancPluginGetInstanceDecodedFrame(
-    OrthancPluginContext*             context,
-    const OrthancPluginDicomInstance* instance,
-    uint32_t                          frameIndex)
-  {
-    OrthancPluginImage* target = NULL;
-
-    _OrthancPluginAccessDicomInstance2 params;
-    memset(&params, 0, sizeof(params));
-    params.targetImage = &target;
-    params.instance = instance;
-    params.frameIndex = frameIndex;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDecodedFrame, &params) != OrthancPluginErrorCode_Success)
-    {
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-  
-  /**
-   * @brief Parse and transcode a DICOM instance.
-   *
-   * This function parses a memory buffer that contains a DICOM file,
-   * then transcodes it to the given transfer syntax. The function
-   * returns a new pointer to a data structure that is managed by the
-   * Orthanc core.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param buffer The memory buffer containing the DICOM instance.
-   * @param size The size of the memory buffer.
-   * @param transferSyntax The transfer syntax UID for the transcoding.
-   * @return The newly allocated DICOM instance. It must be freed with OrthancPluginFreeDicomInstance().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginDicomInstance* OrthancPluginTranscodeDicomInstance(
-    OrthancPluginContext*  context,
-    const void*            buffer,
-    uint32_t               size,
-    const char*            transferSyntax)
-  {
-    OrthancPluginDicomInstance* target = NULL;
-
-    _OrthancPluginCreateDicomInstance params;
-    params.target = &target;
-    params.buffer = buffer;
-    params.size = size;
-    params.transferSyntax = transferSyntax;
-
-    if (context->InvokeService(context, _OrthancPluginService_TranscodeDicomInstance, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-  /**
-   * @brief Writes a DICOM instance to a memory buffer.
-   *
-   * This function returns a memory buffer containing the
-   * serialization of a DICOM instance that is managed by the Orthanc
-   * core.
-   * 
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
-   * @param instance The instance of interest.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSerializeDicomInstance(
-    OrthancPluginContext*             context,
-    OrthancPluginMemoryBuffer*        target,
-    const OrthancPluginDicomInstance* instance)
-  {
-    _OrthancPluginAccessDicomInstance2 params;
-    memset(&params, 0, sizeof(params));
-    params.targetBuffer = target;
-    params.instance = instance;
-
-    return context->InvokeService(context, _OrthancPluginService_SerializeDicomInstance, &params);
-  }
-  
-
-  /**
-   * @brief Format a DICOM memory buffer as a JSON string.
-   *
-   * This function takes as DICOM instance managed by the Orthanc
-   * core, and outputs a JSON string representing the tags of this
-   * DICOM file.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The DICOM instance of interest.
-   * @param format The output format.
-   * @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
-   * string. This string must be freed by OrthancPluginFreeString().
-   * @ingroup DicomInstance
-   * @see OrthancPluginDicomBufferToJson
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceAdvancedJson(
-    OrthancPluginContext*              context,
-    const OrthancPluginDicomInstance*  instance,
-    OrthancPluginDicomToJsonFormat     format,
-    OrthancPluginDicomToJsonFlags      flags, 
-    uint32_t                           maxStringLength)
-  {
-    char* result = NULL;
-
-    _OrthancPluginAccessDicomInstance2 params;
-    memset(&params, 0, sizeof(params));
-    params.targetStringToFree = &result;
-    params.instance = instance;
-    params.format = format;
-    params.flags = flags;
-    params.maxStringLength = maxStringLength;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceAdvancedJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return result;
-    }
-  }
-
-
-  /**
-   * @brief Convert a DICOM instance to DICOMweb JSON.
-   *
-   * This function converts a DICOM instance that is managed by the
-   * Orthanc core, into its DICOMweb JSON representation.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The DICOM instance of interest.
-   * @param callback Callback to set the value of the binary tags.
-   * @param payload User payload.
-   * @return The NULL value in case of error, or the JSON document. This string must
-   * be freed by OrthancPluginFreeString().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebJson(
-    OrthancPluginContext*                 context,
-    const OrthancPluginDicomInstance*     instance,
-    OrthancPluginDicomWebBinaryCallback2  callback,
-    void*                                 payload)
-  {
-    char* target = NULL;
-    
-    _OrthancPluginAccessDicomInstance2 params;
-    params.targetStringToFree = &target;
-    params.instance = instance;
-    params.dicomWebCallback = callback;
-    params.dicomWebPayload = payload;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebJson, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-  
-
-  /**
-   * @brief Convert a DICOM instance to DICOMweb XML.
-   *
-   * This function converts a DICOM instance that is managed by the
-   * Orthanc core, into its DICOMweb XML representation.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param instance The DICOM instance of interest.
-   * @param callback Callback to set the value of the binary tags.
-   * @param payload User payload.
-   * @return The NULL value in case of error, or the XML document. This string must
-   * be freed by OrthancPluginFreeString().
-   * @ingroup DicomInstance
-   **/
-  ORTHANC_PLUGIN_INLINE char* OrthancPluginGetInstanceDicomWebXml(
-    OrthancPluginContext*                 context,
-    const OrthancPluginDicomInstance*     instance,
-    OrthancPluginDicomWebBinaryCallback2  callback,
-    void*                                 payload)
-  {
-    char* target = NULL;
-    
-    _OrthancPluginAccessDicomInstance2 params;
-    params.targetStringToFree = &target;
-    params.instance = instance;
-    params.dicomWebCallback = callback;
-    params.dicomWebPayload = payload;
-
-    if (context->InvokeService(context, _OrthancPluginService_GetInstanceDicomWebXml, &params) != OrthancPluginErrorCode_Success)
-    {
-      /* Error */
-      return NULL;
-    }
-    else
-    {
-      return target;
-    }
-  }
-
-
-
-  /**
-   * @brief Signature of a callback function to transcode a DICOM instance.
-   * @param transcoded Target memory buffer. It must be allocated by the
-   * plugin using OrthancPluginCreateMemoryBuffer().
-   * @param buffer Memory buffer containing the source DICOM instance.
-   * @param size Size of the source memory buffer.
-   * @param allowedSyntaxes A C array of possible transfer syntaxes UIDs for the
-   * result of the transcoding. The plugin must choose by itself the 
-   * transfer syntax that will be used for the resulting DICOM image.
-   * @param countSyntaxes The number of transfer syntaxes that are contained
-   * in the "allowedSyntaxes" array.
-   * @param allowNewSopInstanceUid Whether the transcoding plugin can select
-   * a transfer syntax that will change the SOP instance UID (or, in other 
-   * terms, whether the plugin can transcode using lossy compression).
-   * @return 0 if success (i.e. image successfully transcoded and stored into
-   * "transcoded"), or the error code if failure.
-   * @ingroup Callbacks
-   **/
-  typedef OrthancPluginErrorCode (*OrthancPluginTranscoderCallback) (
-    OrthancPluginMemoryBuffer* transcoded /* out */,
-    const void*                buffer,
-    uint64_t                   size,
-    const char* const*         allowedSyntaxes,
-    uint32_t                   countSyntaxes,
-    uint8_t                    allowNewSopInstanceUid);
-
-
-  typedef struct
-  {
-    OrthancPluginTranscoderCallback callback;
-  } _OrthancPluginTranscoderCallback;
-
-  /**
-   * @brief Register a callback to handle the transcoding of DICOM images.
-   *
-   * This function registers a custom callback to transcode DICOM
-   * images, extending the built-in transcoder of Orthanc that uses
-   * DCMTK. The exact behavior is affected by the configuration option
-   * "BuiltinDecoderTranscoderOrder" 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 OrthancPluginRegisterTranscoderCallback(
-    OrthancPluginContext*            context,
-    OrthancPluginTranscoderCallback  callback)
-  {
-    _OrthancPluginTranscoderCallback params;
-    params.callback = callback;
-
-    return context->InvokeService(context, _OrthancPluginService_RegisterTranscoderCallback, &params);
-  }
-  
-
-
-  typedef struct
-  {
-    OrthancPluginMemoryBuffer*  target;
-    uint32_t                    size;
-  } _OrthancPluginCreateMemoryBuffer;
-
-  /**
-   * @brief Create a memory buffer.
-   *
-   * This function creates a memory buffer that is managed by the
-   * Orthanc core. The main use case of this function is for plugins
-   * that act as DICOM transcoders.
-   * 
-   * Your plugin should never call "free()" on the resulting memory
-   * buffer, as the C library that is used by the plugin is in general
-   * not the same as the one used by the Orthanc core.
-   *
-   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
-   * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
-   * @param size Size of the memory buffer to be created.
-   * @return 0 if success, or the error code if failure.
-   * @ingroup Toolbox
-   **/
-  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginCreateMemoryBuffer(
-    OrthancPluginContext*       context,
-    OrthancPluginMemoryBuffer*  target,
-    uint32_t                    size)
-  {
-    _OrthancPluginCreateMemoryBuffer params;
-    params.target = target;
-    params.size = size;
-
-    return context->InvokeService(context, _OrthancPluginService_CreateMemoryBuffer, &params);
-  }
-  
-
-#ifdef  __cplusplus
-}
-#endif
-
-
-/** @} */
-
--- a/Plugins/Samples/AutomatedJpeg2kCompression/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-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)
--- a/Plugins/Samples/AutomatedJpeg2kCompression/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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(const 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/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(Basic)
-
-set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
-include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
-
-add_library(PluginTest SHARED Plugin.c)
--- a/Plugins/Samples/Basic/Plugin.c	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,554 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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.h>
-#include <stdio.h>
-
-static OrthancPluginContext* context = NULL;
-
-static OrthancPluginErrorCode customError;
-
-
-OrthancPluginErrorCode Callback1(OrthancPluginRestOutput* output,
-                                 const char* url,
-                                 const OrthancPluginHttpRequest* request)
-{
-  char buffer[1024];
-  uint32_t i;
-
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    /**
-     * NB: Calling "OrthancPluginSendMethodNotAllowed(context, output,
-     * "GET");" is preferable. This is a sample to demonstrate
-     * "OrthancPluginSetHttpErrorDetails()". 
-     **/
-    OrthancPluginSetHttpErrorDetails(context, output, "This Callback1() can only be used by a GET call", 1 /* log */);
-    return OrthancPluginErrorCode_ParameterOutOfRange;
-  }
-  
-  sprintf(buffer, "Callback on URL [%s] with body [%s]\n", url, (const char*) request->body);
-  OrthancPluginLogWarning(context, buffer);
-
-  OrthancPluginSetCookie(context, output, "hello", "world");
-  OrthancPluginAnswerBuffer(context, output, buffer, strlen(buffer), "text/plain");
-
-  OrthancPluginLogWarning(context, "");    
-
-  for (i = 0; i < request->groupsCount; i++)
-  {
-    sprintf(buffer, "  REGEX GROUP %d = [%s]", i, request->groups[i]);
-    OrthancPluginLogWarning(context, buffer);    
-  }
-
-  OrthancPluginLogWarning(context, "");    
-
-  for (i = 0; i < request->getCount; i++)
-  {
-    sprintf(buffer, "  GET [%s] = [%s]", request->getKeys[i], request->getValues[i]);
-    OrthancPluginLogWarning(context, buffer);    
-  }
-
-  OrthancPluginLogWarning(context, "");
-
-  for (i = 0; i < request->headersCount; i++)
-  {
-    sprintf(buffer, "  HEADERS [%s] = [%s]", request->headersKeys[i], request->headersValues[i]);
-    OrthancPluginLogWarning(context, buffer);    
-  }
-
-  OrthancPluginLogWarning(context, "");
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-OrthancPluginErrorCode Callback2(OrthancPluginRestOutput* output,
-                                 const char* url,
-                                 const OrthancPluginHttpRequest* request)
-{
-  /* Answer with a sample 16bpp image. */
-
-  uint16_t buffer[256 * 256];
-  uint32_t x, y, value;
-
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-  }
-  else
-  {
-    value = 0;
-    for (y = 0; y < 256; y++)
-    {
-      for (x = 0; x < 256; x++, value++)
-      {
-        buffer[value] = value;
-      }
-    }
-
-    OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale16,
-                                           256, 256, sizeof(uint16_t) * 256, buffer);
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-OrthancPluginErrorCode Callback3(OrthancPluginRestOutput* output,
-                                 const char* url,
-                                 const OrthancPluginHttpRequest* request)
-{
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-  }
-  else
-  {
-    OrthancPluginMemoryBuffer dicom;
-    if (!OrthancPluginGetDicomForInstance(context, &dicom, request->groups[0]))
-    {
-      /* No error, forward the DICOM file */
-      OrthancPluginAnswerBuffer(context, output, dicom.data, dicom.size, "application/dicom");
-
-      /* Free memory */
-      OrthancPluginFreeMemoryBuffer(context, &dicom);
-    }
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-OrthancPluginErrorCode Callback4(OrthancPluginRestOutput* output,
-                                 const char* url,
-                                 const OrthancPluginHttpRequest* request)
-{
-  /* Answer with a sample 8bpp image. */
-
-  uint8_t  buffer[256 * 256];
-  uint32_t x, y, value;
-
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-  }
-  else
-  {
-    value = 0;
-    for (y = 0; y < 256; y++)
-    {
-      for (x = 0; x < 256; x++, value++)
-      {
-        buffer[value] = x;
-      }
-    }
-
-    OrthancPluginCompressAndAnswerPngImage(context, output, OrthancPluginPixelFormat_Grayscale8,
-                                           256, 256, 256, buffer);
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-OrthancPluginErrorCode Callback5(OrthancPluginRestOutput* output,
-                                 const char* url,
-                                 const OrthancPluginHttpRequest* request)
-{
-  /**
-   * Demonstration the difference between the
-   * "OrthancPluginRestApiXXX()" and the
-   * "OrthancPluginRestApiXXXAfterPlugins()" mechanisms to forward
-   * REST calls.
-   *
-   * # curl http://localhost:8042/forward/built-in/system
-   * # curl http://localhost:8042/forward/plugins/system
-   * # curl http://localhost:8042/forward/built-in/plugin/image
-   *   => FAILURE (because the "/plugin/image" URI is implemented by this plugin)
-   * # curl http://localhost:8042/forward/plugins/plugin/image  => SUCCESS
-   **/
-
-  OrthancPluginMemoryBuffer tmp;
-  int isBuiltIn, error;
-
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-    return OrthancPluginErrorCode_Success;
-  }
-
-  isBuiltIn = strcmp("plugins", request->groups[0]);
- 
-  if (isBuiltIn)
-  {
-    error = OrthancPluginRestApiGet(context, &tmp, request->groups[1]);
-  }
-  else
-  {
-    error = OrthancPluginRestApiGetAfterPlugins(context, &tmp, request->groups[1]);
-  }
-
-  if (error)
-  {
-    return OrthancPluginErrorCode_InternalError;
-  }
-  else
-  {
-    OrthancPluginAnswerBuffer(context, output, tmp.data, tmp.size, "application/octet-stream");
-    OrthancPluginFreeMemoryBuffer(context, &tmp);
-    return OrthancPluginErrorCode_Success;
-  }
-}
-
-
-OrthancPluginErrorCode CallbackCreateDicom(OrthancPluginRestOutput* output,
-                                           const char* url,
-                                           const OrthancPluginHttpRequest* request)
-{
-  const char* pathLocator = "\"Path\" : \"";
-  char info[1024];
-  char *id, *eos;
-  OrthancPluginMemoryBuffer tmp;
-
-  if (request->method != OrthancPluginHttpMethod_Post)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "POST");
-  }
-  else
-  {
-    /* Make POST request to create a new DICOM instance */
-    sprintf(info, "{\"PatientName\":\"Test\"}");
-    OrthancPluginRestApiPost(context, &tmp, "/tools/create-dicom", info, strlen(info));
-
-    /**
-     * Recover the ID of the created instance is constructed by a
-     * quick-and-dirty parsing of a JSON string.
-     **/
-    id = strstr((char*) tmp.data, pathLocator) + strlen(pathLocator);
-    eos = strchr(id, '\"');
-    eos[0] = '\0';
-
-    /* Delete the newly created DICOM instance. */
-    OrthancPluginRestApiDelete(context, id);
-    OrthancPluginFreeMemoryBuffer(context, &tmp);
-
-    /* Set some cookie */
-    OrthancPluginSetCookie(context, output, "hello", "world");
-
-    /* Set some HTTP header */
-    OrthancPluginSetHttpHeader(context, output, "Cache-Control", "max-age=0, no-cache");
-    
-    OrthancPluginAnswerBuffer(context, output, "OK\n", 3, "text/plain");
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-void DicomWebBinaryCallback(
-  OrthancPluginDicomWebNode*          node,
-  OrthancPluginDicomWebSetBinaryNode  setter,
-  uint32_t                            levelDepth,
-  const uint16_t*                     levelTagGroup,
-  const uint16_t*                     levelTagElement,
-  const uint32_t*                     levelIndex,
-  uint16_t                            tagGroup,
-  uint16_t                            tagElement,
-  OrthancPluginValueRepresentation    vr)
-{
-  setter(node, OrthancPluginDicomWebBinaryMode_BulkDataUri, "HelloURI");
-}
-
-
-OrthancPluginErrorCode OnStoredCallback(const OrthancPluginDicomInstance* instance,
-                                        const char* instanceId)
-{
-  char buffer[256];
-  FILE* fp;
-  char* json;
-  static int first = 1;
-
-  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);  
-
-  fp = fopen("PluginReceivedInstance.dcm", "wb");
-  fwrite(OrthancPluginGetInstanceData(context, instance),
-         OrthancPluginGetInstanceSize(context, instance), 1, fp);
-  fclose(fp);
-
-  json = OrthancPluginGetInstanceSimplifiedJson(context, instance);
-  if (first)
-  {
-    printf("[%s]\n", json);
-  }
-  OrthancPluginFreeString(context, json);
-
-  if (OrthancPluginHasInstanceMetadata(context, instance, "ReceptionDate"))
-  {
-    printf("Received on [%s]\n", OrthancPluginGetInstanceMetadata(context, instance, "ReceptionDate"));
-  }
-  else
-  {
-    OrthancPluginLogError(context, "Instance has no reception date, should never happen!");
-  }
-
-  json = OrthancPluginEncodeDicomWebXml(context,
-                                        OrthancPluginGetInstanceData(context, instance),
-                                        OrthancPluginGetInstanceSize(context, instance),
-                                        DicomWebBinaryCallback);
-  if (first)
-  {
-    printf("[%s]\n", json);
-    first = 0;    /* Only print the first DICOM instance */
-  }
-  OrthancPluginFreeString(context, json);
-  
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
-                                        OrthancPluginResourceType resourceType,
-                                        const char* resourceId)
-{
-  char info[1024];
-
-  OrthancPluginMemoryBuffer tmp;
-  memset(&tmp, 0, sizeof(tmp));
-
-  sprintf(info, "Change %d on resource %s of type %d", changeType,
-          (resourceId == NULL ? "<none>" : resourceId), resourceType);
-  OrthancPluginLogWarning(context, info);
-
-  switch (changeType)
-  {
-    case OrthancPluginChangeType_NewInstance:
-    {
-      sprintf(info, "/instances/%s/metadata/AnonymizedFrom", resourceId);
-      if (OrthancPluginRestApiGet(context, &tmp, info) == 0)
-      {
-        sprintf(info, "  Instance %s comes from the anonymization of instance", resourceId);
-        strncat(info, (const char*) tmp.data, tmp.size);
-        OrthancPluginLogWarning(context, info);
-        OrthancPluginFreeMemoryBuffer(context, &tmp);
-      }
-
-      break;
-    }
-
-    case OrthancPluginChangeType_OrthancStarted:
-    {
-      OrthancPluginSetMetricsValue(context, "sample_started", 1, OrthancPluginMetricsType_Default); 
-
-      /* Make REST requests to the built-in Orthanc API */
-      OrthancPluginRestApiGet(context, &tmp, "/changes");
-      OrthancPluginFreeMemoryBuffer(context, &tmp);
-      OrthancPluginRestApiGet(context, &tmp, "/changes?limit=1");
-      OrthancPluginFreeMemoryBuffer(context, &tmp);
-
-      /* Play with PUT by defining a new target modality. */
-      sprintf(info, "[ \"STORESCP\", \"localhost\", 2000 ]");
-      OrthancPluginRestApiPut(context, &tmp, "/modalities/demo", info, strlen(info));
-
-      break;
-    }
-
-    case OrthancPluginChangeType_OrthancStopped:
-      OrthancPluginLogWarning(context, "Orthanc has stopped");
-      break;
-
-    default:
-      break;
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-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 */
-  }
-}
-
-
-static void RefreshMetrics()
-{
-  static unsigned int count = 0;
-  OrthancPluginSetMetricsValue(context, "sample_counter", 
-                               (float) (count++), OrthancPluginMetricsType_Default); 
-}
-
-
-static int32_t FilterIncomingDicomInstance(const OrthancPluginDicomInstance* instance)
-{
-  char buf[1024];
-  char* s;
-  int32_t hasPixelData;
-
-  s = OrthancPluginGetInstanceTransferSyntaxUid(context, instance);
-  sprintf(buf, "Incoming transfer syntax: %s", s);
-  OrthancPluginFreeString(context, s);
-  OrthancPluginLogWarning(context, buf);
-
-  hasPixelData = OrthancPluginHasInstancePixelData(context, instance);
-  sprintf(buf, "Incoming has pixel data: %d", hasPixelData);
-  OrthancPluginLogWarning(context, buf);
-
-  /* Reject all instances without pixel data */
-  return hasPixelData;
-}
-
-
-ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
-{
-  char info[1024], *s;
-  int counter, i;
-  OrthancPluginDictionaryEntry entry;
-
-  context = c;
-  OrthancPluginLogWarning(context, "Sample plugin is initializing");
-
-  /* Check the version of the Orthanc core */
-  if (OrthancPluginCheckVersion(c) == 0)
-  {
-    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;
-  }
-
-  /* Print some information about Orthanc */
-  sprintf(info, "The version of Orthanc is '%s'", context->orthancVersion);
-  OrthancPluginLogWarning(context, info);
-
-  s = OrthancPluginGetOrthancPath(context);
-  sprintf(info, "  Path to Orthanc: %s", s);
-  OrthancPluginLogWarning(context, info);
-  OrthancPluginFreeString(context, s);
-
-  s = OrthancPluginGetOrthancDirectory(context);
-  sprintf(info, "  Directory of Orthanc: %s", s);
-  OrthancPluginLogWarning(context, info);
-  OrthancPluginFreeString(context, s);
-
-  s = OrthancPluginGetConfiguration(context);
-  sprintf(info, "  Content of the configuration file:\n");
-  OrthancPluginLogWarning(context, info);
-  OrthancPluginLogWarning(context, s);
-  OrthancPluginFreeString(context, s);
-
-  /* Print the command-line arguments of Orthanc */
-  counter = OrthancPluginGetCommandLineArgumentsCount(context);
-  for (i = 0; i < counter; i++)
-  {
-    s = OrthancPluginGetCommandLineArgument(context, i);
-    sprintf(info, "  Command-line argument %d: \"%s\"", i, s);
-    OrthancPluginLogWarning(context, info);
-    OrthancPluginFreeString(context, s);    
-  }
-
-  /* Register the callbacks */
-  OrthancPluginRegisterRestCallback(context, "/(plu.*)/hello", Callback1);
-  OrthancPluginRegisterRestCallback(context, "/plu.*/image", Callback2);
-  OrthancPluginRegisterRestCallback(context, "/plugin/instances/([^/]+)/info", Callback3);
-  OrthancPluginRegisterRestCallback(context, "/instances/([^/]+)/preview", Callback4);
-  OrthancPluginRegisterRestCallback(context, "/forward/(built-in)(/.+)", Callback5);
-  OrthancPluginRegisterRestCallback(context, "/forward/(plugins)(/.+)", Callback5);
-  OrthancPluginRegisterRestCallback(context, "/plugin/create", CallbackCreateDicom);
-
-  OrthancPluginRegisterOnStoredInstanceCallback(context, OnStoredCallback);
-  OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
-  OrthancPluginRegisterIncomingHttpRequestFilter(context, FilterIncomingHttpRequest);
-  OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetrics);
-  OrthancPluginRegisterIncomingDicomInstanceFilter(context, FilterIncomingDicomInstance);
-    
-  
-  /* Declare several properties of the plugin */
-  OrthancPluginSetRootUri(context, "/plugin/hello");
-  OrthancPluginSetDescription(context, "This is the description of the sample plugin that can be seen in Orthanc Explorer.");
-  OrthancPluginExtendOrthancExplorer(context, "alert('Hello Orthanc! From sample plugin with love.');");
-
-  customError = OrthancPluginRegisterErrorCode(context, 4, 402, "Hello world");
-  
-  OrthancPluginRegisterDictionaryTag(context, 0x0014, 0x1020, OrthancPluginValueRepresentation_DA,
-                                     "ValidationExpiryDate", 1, 1);
-
-  OrthancPluginLookupDictionary(context, &entry, "ValidationExpiryDate");
-  OrthancPluginLookupDictionary(context, &entry, "0010-0010");
-
-  return 0;
-}
-
-
-ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-{
-  OrthancPluginLogWarning(context, "Sample plugin is finalizing");
-}
-
-
-ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-{
-  return "sample";
-}
-
-
-ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-{
-  return "1.0";
-}
-
--- a/Plugins/Samples/Common/DicomDatasetReader.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,173 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  }
-}
--- a/Plugins/Samples/Common/DicomDatasetReader.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Plugins/Samples/Common/DicomPath.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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"
-
-#include <boost/lexical_cast.hpp>
-
-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::Prefix& DicomPath::GetPrefixItem(size_t depth)
-  {
-    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);
-  }
-
-
-  std::string DicomPath::Format() const
-  {
-    std::string s;
-      
-    for (size_t i = 0; i < GetPrefixLength(); i++)
-    {
-      s += (GetPrefixTag(i).FormatHexadecimal() + " / " +
-            boost::lexical_cast<std::string>(i) + " / ");
-    }
-
-    return s + GetFinalTag().FormatHexadecimal();
-  }
-}
--- a/Plugins/Samples/Common/DicomPath.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-
-    Prefix& GetPrefixItem(size_t depth);
-
-  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;
-    }
-
-    void SetPrefixIndex(size_t depth,
-                        size_t value)
-    {
-      GetPrefixItem(depth).second = value;
-    }
-    
-    const DicomTag& GetFinalTag() const
-    {
-      return finalTag_;
-    }
-
-    void SetFinalTag(const DicomTag& tag)
-    {
-      finalTag_ = tag;
-    }
-
-    std::string Format() const;
-  };
-}
--- a/Plugins/Samples/Common/DicomTag.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-    }
-  }
-
-
-  std::string DicomTag::FormatHexadecimal() const
-  {
-    char buf[16];
-    sprintf(buf, "(%04x,%04x)", group_, element_);
-    return buf;
-  }
-}
--- a/Plugins/Samples/Common/DicomTag.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,109 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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>
-#include <string>
-
-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);
-    }
-
-    std::string FormatHexadecimal() const;
-  };
-
-
-  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);
-}
--- a/Plugins/Samples/Common/ExportedSymbols.list	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-# This is the list of the symbols that must be exported by Orthanc
-# plugins, if targeting OS X
-
-_OrthancPluginInitialize
-_OrthancPluginFinalize
-_OrthancPluginGetName
-_OrthancPluginGetVersion
--- a/Plugins/Samples/Common/FullOrthancDataset.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-    }
-  }
-}
--- a/Plugins/Samples/Common/FullOrthancDataset.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-
-    FullOrthancDataset* Clone() const
-    {
-      return new FullOrthancDataset(this->root_);
-    }
-  };
-}
--- a/Plugins/Samples/Common/IDicomDataset.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Plugins/Samples/Common/IOrthancConnection.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  }
-}
--- a/Plugins/Samples/Common/IOrthancConnection.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Plugins/Samples/Common/OrthancHttpConnection.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  }
-}
--- a/Plugins/Samples/Common/OrthancHttpConnection.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Plugins/Samples/Common/OrthancPluginConnection.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-
-    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;
-
-    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;
-
-    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;
-
-    if (!::OrthancPlugins::RestApiDelete(uri, false))
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
-    }
-  }
-}
--- a/Plugins/Samples/Common/OrthancPluginConnection.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-  {
-  public:
-    virtual void RestApiGet(std::string& result,
-                            const std::string& uri);
-
-    virtual void RestApiPost(std::string& result,
-                             const std::string& uri,
-                             const std::string& body);
-
-    virtual void RestApiPut(std::string& result,
-                            const std::string& uri,
-                            const std::string& body);
-
-    virtual void RestApiDelete(const std::string& uri);
-  };
-}
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3395 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <boost/algorithm/string/predicate.hpp>
-#include <boost/move/unique_ptr.hpp>
-#include <boost/thread.hpp>
-#include <json/reader.h>
-#include <json/writer.h>
-
-
-#if !ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 0)
-static const OrthancPluginErrorCode OrthancPluginErrorCode_NullPointer = OrthancPluginErrorCode_Plugin;
-#endif
-
-
-namespace OrthancPlugins
-{
-  static OrthancPluginContext* globalContext_ = NULL;
-
-
-  void SetGlobalContext(OrthancPluginContext* context)
-  {
-    if (context == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer);
-    }
-    else if (globalContext_ == NULL)
-    {
-      globalContext_ = context;
-    }
-    else
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
-    }
-  }
-
-
-  bool HasGlobalContext()
-  {
-    return globalContext_ != NULL;
-  }
-
-
-  OrthancPluginContext* GetGlobalContext()
-  {
-    if (globalContext_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(BadSequenceOfCalls);
-    }
-    else
-    {
-      return globalContext_;
-    }
-  }
-
-
-  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()
-  {
-    buffer_.data = NULL;
-    buffer_.size = 0;
-  }
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  MemoryBuffer::MemoryBuffer(const void* buffer,
-                             size_t size)
-  {
-    uint32_t s = static_cast<uint32_t>(size);
-    if (static_cast<size_t>(s) != size)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
-    }
-    else if (OrthancPluginCreateMemoryBuffer(GetGlobalContext(), &buffer_, s) !=
-             OrthancPluginErrorCode_Success)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NotEnoughMemory);
-    }
-    else
-    {
-      memcpy(buffer_.data, buffer, size);
-    }
-  }
-#endif
-
-
-  void MemoryBuffer::Clear()
-  {
-    if (buffer_.data != NULL)
-    {
-      OrthancPluginFreeMemoryBuffer(GetGlobalContext(), &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;
-  }
-
-
-  void MemoryBuffer::Swap(MemoryBuffer& other)
-  {
-    std::swap(buffer_.data, other.buffer_.data);
-    std::swap(buffer_.size, other.buffer_.size);
-  }
-
-
-  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))
-    {
-      LogError("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(GetGlobalContext(), &buffer_, uri.c_str()));
-    }
-    else
-    {
-      return CheckHttp(OrthancPluginRestApiGet(GetGlobalContext(), &buffer_, uri.c_str()));
-    }
-  }
-
-  bool MemoryBuffer::RestApiGet(const std::string& uri,
-                                const std::map<std::string, std::string>& httpHeaders,
-                                bool applyPlugins)
-  {
-    Clear();
-
-    std::vector<const char*> headersKeys;
-    std::vector<const char*> headersValues;
-    
-    for (std::map<std::string, std::string>::const_iterator
-           it = httpHeaders.begin(); it != httpHeaders.end(); it++)
-    {
-      headersKeys.push_back(it->first.c_str());
-      headersValues.push_back(it->second.c_str());
-    }
-
-    return CheckHttp(OrthancPluginRestApiGet2(
-                       GetGlobalContext(), &buffer_, uri.c_str(), httpHeaders.size(),
-                       (headersKeys.empty() ? NULL : &headersKeys[0]),
-                       (headersValues.empty() ? NULL : &headersValues[0]), applyPlugins));
-  }
-
-  bool MemoryBuffer::RestApiPost(const std::string& uri,
-                                 const void* body,
-                                 size_t bodySize,
-                                 bool applyPlugins)
-  {
-    Clear();
-    
-    // Cast for compatibility with Orthanc SDK <= 1.5.6
-    const char* b = reinterpret_cast<const char*>(body);
-
-    if (applyPlugins)
-    {
-      return CheckHttp(OrthancPluginRestApiPostAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize));
-    }
-    else
-    {
-      return CheckHttp(OrthancPluginRestApiPost(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize));
-    }
-  }
-
-
-  bool MemoryBuffer::RestApiPut(const std::string& uri,
-                                const void* body,
-                                size_t bodySize,
-                                bool applyPlugins)
-  {
-    Clear();
-
-    // Cast for compatibility with Orthanc SDK <= 1.5.6
-    const char* b = reinterpret_cast<const char*>(body);
-
-    if (applyPlugins)
-    {
-      return CheckHttp(OrthancPluginRestApiPutAfterPlugins(GetGlobalContext(), &buffer_, uri.c_str(), b, bodySize));
-    }
-    else
-    {
-      return CheckHttp(OrthancPluginRestApiPut(GetGlobalContext(), &buffer_, uri.c_str(), b, 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(GetGlobalContext(), &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(GetGlobalContext(), &buffer_, s.c_str(), pixelData.GetObject(), flags));
-  }
-
-
-  void MemoryBuffer::ReadFile(const std::string& path)
-  {
-    Clear();
-    Check(OrthancPluginReadFile(GetGlobalContext(), &buffer_, path.c_str()));
-  }
-
-
-  void MemoryBuffer::GetDicomQuery(const OrthancPluginWorklistQuery* query)
-  {
-    Clear();
-    Check(OrthancPluginWorklistGetDicomQuery(GetGlobalContext(), &buffer_, query));
-  }
-
-
-  void OrthancString::Assign(char* str)
-  {
-    Clear();
-
-    if (str != NULL)
-    {
-      str_ = str;
-    }
-  }
-
-
-  void OrthancString::Clear()
-  {
-    if (str_ != NULL)
-    {
-      OrthancPluginFreeString(GetGlobalContext(), 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)
-    {
-      LogError("Cannot convert an empty memory buffer to JSON");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-
-    Json::Reader reader;
-    if (!reader.parse(str_, target))
-    {
-      LogError("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;
-    str.Assign(OrthancPluginDicomBufferToJson
-               (GetGlobalContext(), 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(GetGlobalContext(), &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(GetGlobalContext(), &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(GetGlobalContext(), &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(GetGlobalContext(), &buffer_, instanceId.c_str()));
-  }
-
-
-  bool HttpDelete(const std::string& url,
-                  const std::string& username,
-                  const std::string& password)
-  {
-    OrthancPluginErrorCode error = OrthancPluginHttpDelete
-      (GetGlobalContext(), 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);
-    }
-  }
-
-
-  void LogError(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogError(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void LogWarning(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void LogInfo(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void OrthancConfiguration::LoadConfiguration()
-  {
-    OrthancString str;
-    str.Assign(OrthancPluginGetConfiguration(GetGlobalContext()));
-
-    if (str.GetContent() == NULL)
-    {
-      LogError("Cannot access the Orthanc configuration");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-
-    str.ToJson(configuration_);
-
-    if (configuration_.type() != Json::objectValue)
-    {
-      LogError("Unable to read the Orthanc configuration");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-  }
-    
-
-  OrthancConfiguration::OrthancConfiguration()
-  {
-    LoadConfiguration();
-  }
-
-
-  OrthancConfiguration::OrthancConfiguration(bool loadConfiguration)
-  {
-    if (loadConfiguration)
-    {
-      LoadConfiguration();
-    }
-    else
-    {
-      configuration_ = Json::objectValue;
-    }
-  }
-
-
-  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.path_ = GetPath(key);
-
-    if (!configuration_.isMember(key))
-    {
-      target.configuration_ = Json::objectValue;
-    }
-    else
-    {
-      if (configuration_[key].type() != Json::objectValue)
-      {
-        LogError("The configuration section \"" + target.path_ +
-                 "\" is not an associative array as expected");
-
-        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)
-    {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a string as expected");
-
-      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:
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not an integer as expected");
-
-        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)
-    {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a positive integer as expected");
-
-      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)
-    {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a Boolean as expected");
-
-      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:
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not an integer as expected");
-
-        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;
-    }
-
-    LogError("The configuration option \"" + GetPath(key) +
-             "\" is not a list of strings as expected");
-
-    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)
-    {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a string as expected");
-
-      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
-      {
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not a dictionary mapping strings to strings");
-
-        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-      }
-    }
-  }
-
-
-  void OrthancImage::Clear()
-  {
-    if (image_ != NULL)
-    {
-      OrthancPluginFreeImage(GetGlobalContext(), image_);
-      image_ = NULL;
-    }
-  }
-
-
-  void OrthancImage::CheckImageAvailable() const
-  {
-    if (image_ == NULL)
-    {
-      LogError("Trying to access a NULL image");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-  }
-
-
-  OrthancImage::OrthancImage() :
-    image_(NULL)
-  {
-  }
-
-
-  OrthancImage::OrthancImage(OrthancPluginImage*  image) :
-    image_(image)
-  {
-  }
-
-
-  OrthancImage::OrthancImage(OrthancPluginPixelFormat  format,
-                             uint32_t                  width,
-                             uint32_t                  height)
-  {
-    image_ = OrthancPluginCreateImage(GetGlobalContext(), format, width, height);
-
-    if (image_ == NULL)
-    {
-      LogError("Cannot create an image");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-  }
-
-
-  OrthancImage::OrthancImage(OrthancPluginPixelFormat  format,
-                             uint32_t                  width,
-                             uint32_t                  height,
-                             uint32_t                  pitch,
-                             void*                     buffer)
-  {
-    image_ = OrthancPluginCreateImageAccessor
-      (GetGlobalContext(), format, width, height, pitch, buffer);
-
-    if (image_ == NULL)
-    {
-      LogError("Cannot create an image accessor");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-  }
-
-  void OrthancImage::UncompressPngImage(const void* data,
-                                        size_t size)
-  {
-    Clear();
-
-    image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Png);
-
-    if (image_ == NULL)
-    {
-      LogError("Cannot uncompress a PNG image");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-  }
-
-
-  void OrthancImage::UncompressJpegImage(const void* data,
-                                         size_t size)
-  {
-    Clear();
-    image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg);
-    if (image_ == NULL)
-    {
-      LogError("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(GetGlobalContext(), data, size, frame);
-    if (image_ == NULL)
-    {
-      LogError("Cannot uncompress a DICOM image");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-  }
-
-
-  OrthancPluginPixelFormat OrthancImage::GetPixelFormat() const
-  {
-    CheckImageAvailable();
-    return OrthancPluginGetImagePixelFormat(GetGlobalContext(), image_);
-  }
-
-
-  unsigned int OrthancImage::GetWidth() const
-  {
-    CheckImageAvailable();
-    return OrthancPluginGetImageWidth(GetGlobalContext(), image_);
-  }
-
-
-  unsigned int OrthancImage::GetHeight() const
-  {
-    CheckImageAvailable();
-    return OrthancPluginGetImageHeight(GetGlobalContext(), image_);
-  }
-
-
-  unsigned int OrthancImage::GetPitch() const
-  {
-    CheckImageAvailable();
-    return OrthancPluginGetImagePitch(GetGlobalContext(), image_);
-  }
-
-
-  void* OrthancImage::GetBuffer() const
-  {
-    CheckImageAvailable();
-    return OrthancPluginGetImageBuffer(GetGlobalContext(), image_);
-  }
-
-
-  void OrthancImage::CompressPngImage(MemoryBuffer& target) const
-  {
-    CheckImageAvailable();
-
-    OrthancPlugins::MemoryBuffer answer;
-    OrthancPluginCompressPngImage(GetGlobalContext(), *answer, GetPixelFormat(),
-                                  GetWidth(), GetHeight(), GetPitch(), GetBuffer());
-
-    target.Swap(answer);
-  }
-
-
-  void OrthancImage::CompressJpegImage(MemoryBuffer& target,
-                                       uint8_t quality) const
-  {
-    CheckImageAvailable();
-
-    OrthancPlugins::MemoryBuffer answer;
-    OrthancPluginCompressJpegImage(GetGlobalContext(), *answer, GetPixelFormat(),
-                                   GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
-
-    target.Swap(answer);
-  }
-
-
-  void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output) const
-  {
-    CheckImageAvailable();
-    OrthancPluginCompressAndAnswerPngImage(GetGlobalContext(), output, GetPixelFormat(),
-                                           GetWidth(), GetHeight(), GetPitch(), GetBuffer());
-  }
-
-
-  void OrthancImage::AnswerJpegImage(OrthancPluginRestOutput* output,
-                                     uint8_t quality) const
-  {
-    CheckImageAvailable();
-    OrthancPluginCompressAndAnswerJpegImage(GetGlobalContext(), output, GetPixelFormat(),
-                                            GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
-  }
-
-
-  OrthancPluginImage* OrthancImage::Release()
-  {
-    CheckImageAvailable();
-    OrthancPluginImage* tmp = image_;
-    image_ = NULL;
-    return tmp;
-  }
-
-
-#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
-  FindMatcher::FindMatcher(const OrthancPluginWorklistQuery* worklist) :
-    matcher_(NULL),
-    worklist_(worklist)
-  {
-    if (worklist_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
-    }
-  }
-
-
-  void FindMatcher::SetupDicom(const void*  query,
-                               uint32_t     size)
-  {
-    worklist_ = NULL;
-
-    matcher_ = OrthancPluginCreateFindMatcher(GetGlobalContext(), query, size);
-    if (matcher_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-  }
-
-
-  FindMatcher::~FindMatcher()
-  {
-    // The "worklist_" field
-
-    if (matcher_ != NULL)
-    {
-      OrthancPluginFreeFindMatcher(GetGlobalContext(), matcher_);
-    }
-  }
-
-
-
-  bool FindMatcher::IsMatch(const void*  dicom,
-                            uint32_t     size) const
-  {
-    int32_t result;
-
-    if (matcher_ != NULL)
-    {
-      result = OrthancPluginFindMatcherIsMatch(GetGlobalContext(), matcher_, dicom, size);
-    }
-    else if (worklist_ != NULL)
-    {
-      result = OrthancPluginWorklistIsMatch(GetGlobalContext(), 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 */
-
-  void AnswerJson(const Json::Value& value,
-                  OrthancPluginRestOutput* output
-    )
-  {
-    Json::StyledWriter writer;
-    std::string bodyString = writer.write(value);
-
-    OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json");
-  }
-
-  void AnswerString(const std::string& answer,
-                    const char* mimeType,
-                    OrthancPluginRestOutput* output
-    )
-  {
-    OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType);
-  }
-
-  void AnswerHttpError(uint16_t httpError, OrthancPluginRestOutput *output)
-  {
-    OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError);
-  }
-
-  void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods)
-  {
-    OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods);
-  }
-
-  bool RestApiGetString(std::string& result,
-                        const std::string& uri,
-                        bool applyPlugins)
-  {
-    MemoryBuffer answer;
-    if (!answer.RestApiGet(uri, applyPlugins))
-    {
-      return false;
-    }
-    else
-    {
-      answer.ToString(result);
-      return true;
-    }
-  }
-
-  bool RestApiGetString(std::string& result,
-                        const std::string& uri,
-                        const std::map<std::string, std::string>& httpHeaders,
-                        bool applyPlugins)
-  {
-    MemoryBuffer answer;
-    if (!answer.RestApiGet(uri, httpHeaders, applyPlugins))
-    {
-      return false;
-    }
-    else
-    {
-      answer.ToString(result);
-      return true;
-    }
-  }
-
-
-
-  bool RestApiGet(Json::Value& result,
-                  const std::string& uri,
-                  bool applyPlugins)
-  {
-    MemoryBuffer answer;
-
-    if (!answer.RestApiGet(uri, applyPlugins))
-    {
-      return false;
-    }
-    else
-    {
-      if (!answer.IsEmpty())
-      {
-        answer.ToJson(result);
-      }
-      return true;
-    }
-  }
-
-
-  bool RestApiPost(std::string& result,
-                   const std::string& uri,
-                   const void* body,
-                   size_t bodySize,
-                   bool applyPlugins)
-  {
-    MemoryBuffer answer;
-
-    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
-    {
-      return false;
-    }
-    else
-    {
-      if (!answer.IsEmpty())
-      {
-        result.assign(answer.GetData(), answer.GetSize());
-      }
-      return true;
-    }
-  }
-
-
-  bool RestApiPost(Json::Value& result,
-                   const std::string& uri,
-                   const void* body,
-                   size_t bodySize,
-                   bool applyPlugins)
-  {
-    MemoryBuffer answer;
-
-    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
-    {
-      return false;
-    }
-    else
-    {
-      if (!answer.IsEmpty())
-      {
-        answer.ToJson(result);
-      }
-      return true;
-    }
-  }
-
-
-  bool RestApiPost(Json::Value& result,
-                   const std::string& uri,
-                   const Json::Value& body,
-                   bool applyPlugins)
-  {
-    Json::FastWriter writer;
-    return RestApiPost(result, uri, writer.write(body), applyPlugins);
-  }
-
-
-  bool RestApiPut(Json::Value& result,
-                  const std::string& uri,
-                  const void* body,
-                  size_t bodySize,
-                  bool applyPlugins)
-  {
-    MemoryBuffer answer;
-
-    if (!answer.RestApiPut(uri, body, bodySize, applyPlugins))
-    {
-      return false;
-    }
-    else
-    {
-      if (!answer.IsEmpty()) // i.e, on a PUT to metadata/..., orthanc returns an empty response
-      {
-        answer.ToJson(result);
-      }
-      return true;
-    }
-  }
-
-
-  bool RestApiPut(Json::Value& result,
-                  const std::string& uri,
-                  const Json::Value& body,
-                  bool applyPlugins)
-  {
-    Json::FastWriter writer;
-    return RestApiPut(result, uri, writer.write(body), applyPlugins);
-  }
-
-
-  bool RestApiDelete(const std::string& uri,
-                     bool applyPlugins)
-  {
-    OrthancPluginErrorCode error;
-
-    if (applyPlugins)
-    {
-      error = OrthancPluginRestApiDeleteAfterPlugins(GetGlobalContext(), uri.c_str());
-    }
-    else
-    {
-      error = OrthancPluginRestApiDelete(GetGlobalContext(), 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(unsigned int major,
-                                   unsigned int minor,
-                                   unsigned int revision)
-  {
-    LogError("Your version of the Orthanc core (" +
-             std::string(GetGlobalContext()->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)");
-  }
-
-
-  bool CheckMinimalOrthancVersion(unsigned int major,
-                                  unsigned int minor,
-                                  unsigned int revision)
-  {
-    if (!HasGlobalContext())
-    {
-      LogError("Bad Orthanc context in the plugin");
-      return false;
-    }
-
-    if (!strcmp(GetGlobalContext()->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
-      (GetGlobalContext()->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 ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
-  const char* AutodetectMimeType(const std::string& path)
-  {
-    const char* mime = OrthancPluginAutodetectMimeType(GetGlobalContext(), path.c_str());
-
-    if (mime == NULL)
-    {
-      // Should never happen, just for safety
-      return "application/octet-stream";
-    }
-    else
-    {
-      return mime;
-    }
-  }
-#endif
-
-
-#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
-    {
-      LogError("Inexistent peer: " + name);
-      ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
-    }
-  }
-
-
-  OrthancPeers::OrthancPeers() :
-    peers_(NULL),
-    timeout_(0)
-  {
-    peers_ = OrthancPluginGetPeers(GetGlobalContext());
-
-    if (peers_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
-    }
-
-    uint32_t count = OrthancPluginGetPeersCount(GetGlobalContext(), peers_);
-
-    for (uint32_t i = 0; i < count; i++)
-    {
-      const char* name = OrthancPluginGetPeerName(GetGlobalContext(), peers_, i);
-      if (name == NULL)
-      {
-        OrthancPluginFreePeers(GetGlobalContext(), peers_);
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
-      }
-
-      index_[name] = i;
-    }
-  }
-
-
-  OrthancPeers::~OrthancPeers()
-  {
-    if (peers_ != NULL)
-    {
-      OrthancPluginFreePeers(GetGlobalContext(), 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(GetGlobalContext(), 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(GetGlobalContext(), 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(GetGlobalContext(), 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);
-    }
-
-    OrthancPlugins::MemoryBuffer answer;
-    uint16_t status;
-    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-      (GetGlobalContext(), *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.Swap(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;
-
-    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;
-
-    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;
-
-    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;
-
-    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);
-    }
-
-    OrthancPlugins::MemoryBuffer answer;
-    uint16_t status;
-    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-      (GetGlobalContext(), *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.Swap(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);
-    }
-
-    OrthancPlugins::MemoryBuffer answer;
-    uint16_t status;
-    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-      (GetGlobalContext(), *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)
-    {
-      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);
-    }
-
-    OrthancPlugins::MemoryBuffer answer;
-    uint16_t status;
-    OrthancPluginErrorCode code = OrthancPluginCallPeerApi
-      (GetGlobalContext(), *answer, NULL, &status, peers_,
-       static_cast<uint32_t>(index), OrthancPluginHttpMethod_Delete, uri.c_str(),
-       0, NULL, NULL, NULL, 0, timeout_);
-
-    if (code == OrthancPluginErrorCode_Success)
-    {
-      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
-
-
-
-
-
-  /******************************************************************
-   ** JOBS
-   ******************************************************************/
-
-#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(OrthancJob* job)
-  {
-    if (job == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
-    }
-
-    OrthancPluginJob* orthanc = OrthancPluginCreateJob(
-      GetGlobalContext(), 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(OrthancJob* job,
-                                 int priority)
-  {
-    if (job == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
-    }
-
-    OrthancPluginJob* orthanc = Create(job);
-
-    char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority);
-
-    if (id == NULL)
-    {
-      LogError("Plugin cannot submit job");
-      OrthancPluginFreeJob(GetGlobalContext(), orthanc);
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
-    }
-    else
-    {
-      std::string tmp(id);
-      tmp.assign(id);
-      OrthancPluginFreeString(GetGlobalContext(), id);
-
-      return tmp;
-    }
-  }
-
-
-  void OrthancJob::SubmitAndWait(Json::Value& result,
-                                 OrthancJob* job /* takes ownership */,
-                                 int priority)
-  {
-    std::string id = Submit(job, priority);
-
-    for (;;)
-    {
-      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
-
-      Json::Value status;
-      if (!RestApiGet(status, "/jobs/" + id, false) ||
-          !status.isMember("State") ||
-          status["State"].type() != Json::stringValue)
-      {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem);        
-      }
-
-      const std::string state = status["State"].asString();
-      if (state == "Success")
-      {
-        if (status.isMember("Content"))
-        {
-          result = status["Content"];
-        }
-        else
-        {
-          result = Json::objectValue;
-        }
-
-        return;
-      }
-      else if (state == "Running")
-      {
-        continue;
-      }
-      else if (!status.isMember("ErrorCode") ||
-               status["ErrorCode"].type() != Json::intValue)
-      {
-        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError);
-      }
-      else
-      {
-        if (!status.isMember("ErrorDescription") ||
-            status["ErrorDescription"].type() != Json::stringValue)
-        {
-          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());
-        }
-        else
-        {
-#if HAS_ORTHANC_EXCEPTION == 1
-          throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()),
-                                          status["ErrorDescription"].asString());
-#else
-          LogError("Exception while executing the job: " + status["ErrorDescription"].asString());
-          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());          
-#endif
-        }
-      }
-    }
-  }
-
-
-  void OrthancJob::SubmitFromRestApiPost(OrthancPluginRestOutput* output,
-                                         const Json::Value& body,
-                                         OrthancJob* job)
-  {
-    static const char* KEY_SYNCHRONOUS = "Synchronous";
-    static const char* KEY_ASYNCHRONOUS = "Asynchronous";
-    static const char* KEY_PRIORITY = "Priority";
-
-    boost::movelib::unique_ptr<OrthancJob> protection(job);
-  
-    if (body.type() != Json::objectValue)
-    {
-#if HAS_ORTHANC_EXCEPTION == 1
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                      "Expected a JSON object in the body");
-#else
-      LogError("Expected a JSON object in the body");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-#endif
-    }
-
-    bool synchronous = true;
-  
-    if (body.isMember(KEY_SYNCHRONOUS))
-    {
-      if (body[KEY_SYNCHRONOUS].type() != Json::booleanValue)
-      {
-#if HAS_ORTHANC_EXCEPTION == 1
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                        "Option \"" + std::string(KEY_SYNCHRONOUS) +
-                                        "\" must be Boolean");
-#else
-        LogError("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean");
-        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-#endif
-      }
-      else
-      {
-        synchronous = body[KEY_SYNCHRONOUS].asBool();
-      }
-    }
-
-    if (body.isMember(KEY_ASYNCHRONOUS))
-    {
-      if (body[KEY_ASYNCHRONOUS].type() != Json::booleanValue)
-      {
-#if HAS_ORTHANC_EXCEPTION == 1
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                        "Option \"" + std::string(KEY_ASYNCHRONOUS) +
-                                        "\" must be Boolean");
-#else
-        LogError("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean");
-        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-#endif
-      }
-      else
-      {
-        synchronous = !body[KEY_ASYNCHRONOUS].asBool();
-      }
-    }
-
-    int priority = 0;
-
-    if (body.isMember(KEY_PRIORITY))
-    {
-      if (body[KEY_PRIORITY].type() != Json::booleanValue)
-      {
-#if HAS_ORTHANC_EXCEPTION == 1
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
-                                        "Option \"" + std::string(KEY_PRIORITY) +
-                                        "\" must be an integer");
-#else
-        LogError("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer");
-        ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-#endif
-      }
-      else
-      {
-        priority = !body[KEY_PRIORITY].asInt();
-      }
-    }
-  
-    Json::Value result;
-
-    if (synchronous)
-    {
-      OrthancPlugins::OrthancJob::SubmitAndWait(result, protection.release(), priority);
-    }
-    else
-    {
-      std::string id = OrthancPlugins::OrthancJob::Submit(protection.release(), priority);
-
-      result = Json::objectValue;
-      result["ID"] = id;
-      result["Path"] = "/jobs/" + id;
-    }
-
-    std::string s = result.toStyledString();
-    OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(),
-                              s.size(), "application/json");
-  }
-
-#endif
-
-
-
-
-  /******************************************************************
-   ** METRICS
-   ******************************************************************/
-
-#if HAS_ORTHANC_PLUGIN_METRICS == 1
-  MetricsTimer::MetricsTimer(const char* name) :
-    name_(name)
-  {
-    start_ = boost::posix_time::microsec_clock::universal_time();
-  }
-  
-  MetricsTimer::~MetricsTimer()
-  {
-    const boost::posix_time::ptime stop = boost::posix_time::microsec_clock::universal_time();
-    const boost::posix_time::time_duration diff = stop - start_;
-    OrthancPluginSetMetricsValue(GetGlobalContext(), name_.c_str(), static_cast<float>(diff.total_milliseconds()),
-                                 OrthancPluginMetricsType_Timer);
-  }
-#endif
-
-
-
-
-  /******************************************************************
-   ** HTTP CLIENT
-   ******************************************************************/
-
-#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
-  class HttpClient::RequestBodyWrapper : public boost::noncopyable
-  {
-  private:
-    static RequestBodyWrapper& GetObject(void* body)
-    {
-      assert(body != NULL);
-      return *reinterpret_cast<RequestBodyWrapper*>(body);
-    }
-
-    IRequestBody&  body_;
-    bool           done_;
-    std::string    chunk_;
-
-  public:
-    RequestBodyWrapper(IRequestBody& body) :
-      body_(body),
-      done_(false)
-    {
-    }      
-
-    static uint8_t IsDone(void* body)
-    {
-      return GetObject(body).done_;
-    }
-    
-    static const void* GetChunkData(void* body)
-    {
-      return GetObject(body).chunk_.c_str();
-    }
-    
-    static uint32_t GetChunkSize(void* body)
-    {
-      return static_cast<uint32_t>(GetObject(body).chunk_.size());
-    }
-
-    static OrthancPluginErrorCode Next(void* body)
-    {
-      RequestBodyWrapper& that = GetObject(body);
-        
-      if (that.done_)
-      {
-        return OrthancPluginErrorCode_BadSequenceOfCalls;
-      }
-      else
-      {
-        try
-        {
-          that.done_ = !that.body_.ReadNextChunk(that.chunk_);
-          return OrthancPluginErrorCode_Success;
-        }
-        catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
-        {
-          return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
-        }
-        catch (...)
-        {
-          return OrthancPluginErrorCode_InternalError;
-        }
-      }
-    }    
-  };
-
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-  static OrthancPluginErrorCode AnswerAddHeaderCallback(void* answer,
-                                                        const char* key,
-                                                        const char* value)
-  {
-    assert(answer != NULL && key != NULL && value != NULL);
-
-    try
-    {
-      reinterpret_cast<HttpClient::IAnswer*>(answer)->AddHeader(key, value);
-      return OrthancPluginErrorCode_Success;
-    }
-    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
-    {
-      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
-    }
-    catch (...)
-    {
-      return OrthancPluginErrorCode_Plugin;
-    }
-  }
-#endif
-
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-  static OrthancPluginErrorCode AnswerAddChunkCallback(void* answer,
-                                                       const void* data,
-                                                       uint32_t size)
-  {
-    assert(answer != NULL);
-
-    try
-    {
-      reinterpret_cast<HttpClient::IAnswer*>(answer)->AddChunk(data, size);
-      return OrthancPluginErrorCode_Success;
-    }
-    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
-    {
-      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
-    }
-    catch (...)
-    {
-      return OrthancPluginErrorCode_Plugin;
-    }
-  }
-#endif
-
-
-  HttpClient::HttpClient() :
-    httpStatus_(0),
-    method_(OrthancPluginHttpMethod_Get),
-    timeout_(0),
-    pkcs11_(false),
-    chunkedBody_(NULL),
-    allowChunkedTransfers_(true)
-  {
-  }
-
-
-  void HttpClient::AddHeaders(const HttpHeaders& headers)
-  {
-    for (HttpHeaders::const_iterator it = headers.begin();
-         it != headers.end(); ++it)
-    {
-      headers_[it->first] = it->second;
-    }
-  }
-
-  
-  void HttpClient::SetCredentials(const std::string& username,
-                                  const std::string& password)
-  {
-    username_ = username;
-    password_ = password;
-  }
-
-  
-  void HttpClient::ClearCredentials()
-  {
-    username_.empty();
-    password_.empty();
-  }
-
-
-  void HttpClient::SetCertificate(const std::string& certificateFile,
-                                  const std::string& keyFile,
-                                  const std::string& keyPassword)
-  {
-    certificateFile_ = certificateFile;
-    certificateKeyFile_ = keyFile;
-    certificateKeyPassword_ = keyPassword;
-  }
-
-  
-  void HttpClient::ClearCertificate()
-  {
-    certificateFile_.clear();
-    certificateKeyFile_.clear();
-    certificateKeyPassword_.clear();
-  }
-
-
-  void HttpClient::ClearBody()
-  {
-    fullBody_.clear();
-    chunkedBody_ = NULL;
-  }
-
-  
-  void HttpClient::SwapBody(std::string& body)
-  {
-    fullBody_.swap(body);
-    chunkedBody_ = NULL;
-  }
-
-  
-  void HttpClient::SetBody(const std::string& body)
-  {
-    fullBody_ = body;
-    chunkedBody_ = NULL;
-  }
-
-  
-  void HttpClient::SetBody(IRequestBody& body)
-  {
-    fullBody_.clear();
-    chunkedBody_ = &body;
-  }
-
-
-  namespace
-  {
-    class HeadersWrapper : public boost::noncopyable
-    {
-    private:
-      std::vector<const char*>  headersKeys_;
-      std::vector<const char*>  headersValues_;
-
-    public:
-      HeadersWrapper(const HttpClient::HttpHeaders& headers)
-      {
-        headersKeys_.reserve(headers.size());
-        headersValues_.reserve(headers.size());
-
-        for (HttpClient::HttpHeaders::const_iterator it = headers.begin(); it != headers.end(); ++it)
-        {
-          headersKeys_.push_back(it->first.c_str());
-          headersValues_.push_back(it->second.c_str());
-        }
-      }
-
-      void AddStaticString(const char* key,
-                           const char* value)
-      {
-        headersKeys_.push_back(key);
-        headersValues_.push_back(value);
-      }
-
-      uint32_t GetCount() const
-      {
-        return headersKeys_.size();
-      }
-
-      const char* const* GetKeys() const
-      {
-        return headersKeys_.empty() ? NULL : &headersKeys_[0];
-      }
-
-      const char* const* GetValues() const
-      {
-        return headersValues_.empty() ? NULL : &headersValues_[0];
-      }
-    };
-
-
-    class MemoryRequestBody : public HttpClient::IRequestBody
-    {
-    private:
-      std::string  body_;
-      bool         done_;
-
-    public:
-      MemoryRequestBody(const std::string& body) :
-        body_(body),
-        done_(false)
-      {
-        if (body_.empty())
-        {
-          done_ = true;
-        }
-      }
-
-      virtual bool ReadNextChunk(std::string& chunk)
-      {
-        if (done_)
-        {
-          return false;
-        }
-        else
-        {
-          chunk.swap(body_);
-          done_ = true;
-          return true;
-        }
-      }
-    };
-
-
-    // This class mimics Orthanc::ChunkedBuffer
-    class ChunkedBuffer : public boost::noncopyable
-    {
-    private:
-      typedef std::list<std::string*>  Content;
-
-      Content  content_;
-      size_t   size_;
-
-    public:
-      ChunkedBuffer() :
-        size_(0)
-      {
-      }
-
-      ~ChunkedBuffer()
-      {
-        Clear();
-      }
-
-      void Clear()
-      {
-        for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
-        {
-          assert(*it != NULL);
-          delete *it;
-        }
-
-        content_.clear();
-      }
-
-      void Flatten(std::string& target) const
-      {
-        target.resize(size_);
-
-        size_t pos = 0;
-
-        for (Content::const_iterator it = content_.begin(); it != content_.end(); ++it)
-        {
-          assert(*it != NULL);
-          size_t s = (*it)->size();
-
-          if (s != 0)
-          {
-            memcpy(&target[pos], (*it)->c_str(), s);
-            pos += s;
-          }
-        }
-
-        assert(size_ == 0 ||
-               pos == target.size());
-      }
-
-      void AddChunk(const void* data,
-                    size_t size)
-      {
-        content_.push_back(new std::string(reinterpret_cast<const char*>(data), size));
-        size_ += size;
-      }
-
-      void AddChunk(const std::string& chunk)
-      {
-        content_.push_back(new std::string(chunk));
-        size_ += chunk.size();
-      }
-    };
-
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-    class MemoryAnswer : public HttpClient::IAnswer
-    {
-    private:
-      HttpClient::HttpHeaders  headers_;
-      ChunkedBuffer            body_;
-
-    public:
-      const HttpClient::HttpHeaders& GetHeaders() const
-      {
-        return headers_;
-      }
-
-      const ChunkedBuffer& GetBody() const
-      {
-        return body_;
-      }
-
-      virtual void AddHeader(const std::string& key,
-                             const std::string& value)
-      {
-        headers_[key] = value;
-      }
-
-      virtual void AddChunk(const void* data,
-                            size_t size)
-      {
-        body_.AddChunk(data, size);
-      }
-    };
-#endif
-  }
-
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-  void HttpClient::ExecuteWithStream(uint16_t& httpStatus,
-                                     IAnswer& answer,
-                                     IRequestBody& body) const
-  {
-    HeadersWrapper h(headers_);
-
-    if (method_ == OrthancPluginHttpMethod_Post ||
-        method_ == OrthancPluginHttpMethod_Put)
-    {
-      // Automatically set the "Transfer-Encoding" header if absent
-      bool found = false;
-
-      for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
-      {
-        if (boost::iequals(it->first, "Transfer-Encoding"))
-        {
-          found = true;
-          break;
-        }
-      }
-
-      if (!found)
-      {
-        h.AddStaticString("Transfer-Encoding", "chunked");
-      }
-    }
-
-    RequestBodyWrapper request(body);
-        
-    OrthancPluginErrorCode error = OrthancPluginChunkedHttpClient(
-      GetGlobalContext(),
-      &answer,
-      AnswerAddChunkCallback,
-      AnswerAddHeaderCallback,
-      &httpStatus,
-      method_,
-      url_.c_str(),
-      h.GetCount(),
-      h.GetKeys(),
-      h.GetValues(),
-      &request,
-      RequestBodyWrapper::IsDone,
-      RequestBodyWrapper::GetChunkData,
-      RequestBodyWrapper::GetChunkSize,
-      RequestBodyWrapper::Next,
-      username_.empty() ? NULL : username_.c_str(),
-      password_.empty() ? NULL : password_.c_str(),
-      timeout_,
-      certificateFile_.empty() ? NULL : certificateFile_.c_str(),
-      certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
-      certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
-      pkcs11_ ? 1 : 0);
-
-    if (error != OrthancPluginErrorCode_Success)
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
-    }
-  }
-#endif    
-
-
-  void HttpClient::ExecuteWithoutStream(uint16_t& httpStatus,
-                                        HttpHeaders& answerHeaders,
-                                        std::string& answerBody,
-                                        const std::string& body) const
-  {
-    HeadersWrapper headers(headers_);
-
-    MemoryBuffer answerBodyBuffer, answerHeadersBuffer;
-
-    OrthancPluginErrorCode error = OrthancPluginHttpClient(
-      GetGlobalContext(),
-      *answerBodyBuffer,
-      *answerHeadersBuffer,
-      &httpStatus,
-      method_,
-      url_.c_str(),
-      headers.GetCount(),
-      headers.GetKeys(),
-      headers.GetValues(),
-      body.empty() ? NULL : body.c_str(),
-      body.size(),
-      username_.empty() ? NULL : username_.c_str(),
-      password_.empty() ? NULL : password_.c_str(),
-      timeout_,
-      certificateFile_.empty() ? NULL : certificateFile_.c_str(),
-      certificateFile_.empty() ? NULL : certificateKeyFile_.c_str(),
-      certificateFile_.empty() ? NULL : certificateKeyPassword_.c_str(),
-      pkcs11_ ? 1 : 0);
-
-    if (error != OrthancPluginErrorCode_Success)
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(error);
-    }
-
-    Json::Value v;
-    answerHeadersBuffer.ToJson(v);
-
-    if (v.type() != Json::objectValue)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-
-    Json::Value::Members members = v.getMemberNames();
-    answerHeaders.clear();
-
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const Json::Value& h = v[members[i]];
-      if (h.type() != Json::stringValue)
-      {
-        ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-      }
-      else
-      {
-        answerHeaders[members[i]] = h.asString();
-      }
-    }
-
-    answerBodyBuffer.ToString(answerBody);
-  }
-
-
-  void HttpClient::Execute(IAnswer& answer)
-  {
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-    if (allowChunkedTransfers_)
-    {
-      if (chunkedBody_ != NULL)
-      {
-        ExecuteWithStream(httpStatus_, answer, *chunkedBody_);
-      }
-      else
-      {
-        MemoryRequestBody wrapper(fullBody_);
-        ExecuteWithStream(httpStatus_, answer, wrapper);
-      }
-
-      return;
-    }
-#endif
-    
-    // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
-    // transfers are disabled. This results in higher memory usage
-    // (all chunks from the answer body are sent at once)
-
-    HttpHeaders answerHeaders;
-    std::string answerBody;
-    Execute(answerHeaders, answerBody);
-
-    for (HttpHeaders::const_iterator it = answerHeaders.begin(); 
-         it != answerHeaders.end(); ++it)
-    {
-      answer.AddHeader(it->first, it->second);      
-    }
-
-    if (!answerBody.empty())
-    {
-      answer.AddChunk(answerBody.c_str(), answerBody.size());
-    }
-  }
-
-
-  void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
-                           std::string& answerBody /* out */)
-  {
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-    if (allowChunkedTransfers_)
-    {
-      MemoryAnswer answer;
-      Execute(answer);
-      answerHeaders = answer.GetHeaders();
-      answer.GetBody().Flatten(answerBody);
-      return;
-    }
-#endif
-    
-    // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
-    // transfers are disabled. This results in higher memory usage
-    // (all chunks from the request body are sent at once)
-
-    if (chunkedBody_ != NULL)
-    {
-      ChunkedBuffer buffer;
-      
-      std::string chunk;
-      while (chunkedBody_->ReadNextChunk(chunk))
-      {
-        buffer.AddChunk(chunk);
-      }
-
-      std::string body;
-      buffer.Flatten(body);
-
-      ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, body);
-    }
-    else
-    {
-      ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, fullBody_);
-    }
-  }
-
-
-  void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
-                           Json::Value& answerBody /* out */)
-  {
-    std::string body;
-    Execute(answerHeaders, body);
-    
-    Json::Reader reader;
-    if (!reader.parse(body, answerBody))
-    {
-      LogError("Cannot convert HTTP answer body to JSON");
-      ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-    }
-  }
-
-
-  void HttpClient::Execute()
-  {
-    HttpHeaders answerHeaders;
-    std::string body;
-    Execute(answerHeaders, body);
-  }
-
-#endif  /* HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1 */
-
-
-
-
-
-  /******************************************************************
-   ** CHUNKED HTTP SERVER
-   ******************************************************************/
-
-  namespace Internals
-  {
-    void NullRestCallback(OrthancPluginRestOutput* output,
-                          const char* url,
-                          const OrthancPluginHttpRequest* request)
-    {
-    }
-  
-    IChunkedRequestReader *NullChunkedRestCallback(const char* url,
-                                                   const OrthancPluginHttpRequest* request)
-    {
-      return NULL;
-    }
-
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
-
-    OrthancPluginErrorCode ChunkedRequestReaderAddChunk(
-      OrthancPluginServerChunkedRequestReader* reader,
-      const void*                              data,
-      uint32_t                                 size)
-    {
-      try
-      {
-        if (reader == NULL)
-        {
-          return OrthancPluginErrorCode_InternalError;
-        }
-
-        reinterpret_cast<IChunkedRequestReader*>(reader)->AddChunk(data, size);
-        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;
-      }
-    }
-
-    
-    OrthancPluginErrorCode ChunkedRequestReaderExecute(
-      OrthancPluginServerChunkedRequestReader* reader,
-      OrthancPluginRestOutput*                 output)
-    {
-      try
-      {
-        if (reader == NULL)
-        {
-          return OrthancPluginErrorCode_InternalError;
-        }
-
-        reinterpret_cast<IChunkedRequestReader*>(reader)->Execute(output);
-        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;
-      }
-    }
-
-    
-    void ChunkedRequestReaderFinalize(
-      OrthancPluginServerChunkedRequestReader* reader)
-    {
-      if (reader != NULL)
-      {
-        delete reinterpret_cast<IChunkedRequestReader*>(reader);
-      }
-    }
-
-#else
-    
-    OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
-                                                    const char* url,
-                                                    const OrthancPluginHttpRequest* request,
-                                                    RestCallback         GetHandler,
-                                                    ChunkedRestCallback  PostHandler,
-                                                    RestCallback         DeleteHandler,
-                                                    ChunkedRestCallback  PutHandler)
-    {
-      try
-      {
-        std::string allowed;
-
-        if (GetHandler != Internals::NullRestCallback)
-        {
-          allowed += "GET";
-        }
-
-        if (PostHandler != Internals::NullChunkedRestCallback)
-        {
-          if (!allowed.empty())
-          {
-            allowed += ",";
-          }
-        
-          allowed += "POST";
-        }
-
-        if (DeleteHandler != Internals::NullRestCallback)
-        {
-          if (!allowed.empty())
-          {
-            allowed += ",";
-          }
-        
-          allowed += "DELETE";
-        }
-
-        if (PutHandler != Internals::NullChunkedRestCallback)
-        {
-          if (!allowed.empty())
-          {
-            allowed += ",";
-          }
-        
-          allowed += "PUT";
-        }
-      
-        switch (request->method)
-        {
-          case OrthancPluginHttpMethod_Get:
-            if (GetHandler == Internals::NullRestCallback)
-            {
-              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
-            }
-            else
-            {
-              GetHandler(output, url, request);
-            }
-
-            break;
-
-          case OrthancPluginHttpMethod_Post:
-            if (PostHandler == Internals::NullChunkedRestCallback)
-            {
-              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
-            }
-            else
-            {
-              boost::movelib::unique_ptr<IChunkedRequestReader> reader(PostHandler(url, request));
-              if (reader.get() == NULL)
-              {
-                ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-              }
-              else
-              {
-                reader->AddChunk(request->body, request->bodySize);
-                reader->Execute(output);
-              }
-            }
-
-            break;
-
-          case OrthancPluginHttpMethod_Delete:
-            if (DeleteHandler == Internals::NullRestCallback)
-            {
-              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
-            }
-            else
-            {
-              DeleteHandler(output, url, request);
-            }
-
-            break;
-
-          case OrthancPluginHttpMethod_Put:
-            if (PutHandler == Internals::NullChunkedRestCallback)
-            {
-              OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowed.c_str());
-            }
-            else
-            {
-              boost::movelib::unique_ptr<IChunkedRequestReader> reader(PutHandler(url, request));
-              if (reader.get() == NULL)
-              {
-                ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-              }
-              else
-              {
-                reader->AddChunk(request->body, request->bodySize);
-                reader->Execute(output);
-              }
-            }
-
-            break;
-
-          default:
-            ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-        }
-
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
-      {
-#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
-        if (HasGlobalContext() &&
-            e.HasDetails())
-        {
-          // The "false" instructs Orthanc not to log the detailed
-          // error message. This is to avoid duplicating the details,
-          // because "OrthancException" already does it on construction.
-          OrthancPluginSetHttpErrorDetails
-            (GetGlobalContext(), output, e.GetDetails(), false);
-        }
-#endif
-
-        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return OrthancPluginErrorCode_BadFileFormat;
-      }
-      catch (...)
-      {
-        return OrthancPluginErrorCode_Plugin;
-      }
-    }
-#endif
-  }
-
-
-#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
-  OrthancPluginErrorCode IStorageCommitmentScpHandler::Lookup(
-    OrthancPluginStorageCommitmentFailureReason* target,
-    void* rawHandler,
-    const char* sopClassUid,
-    const char* sopInstanceUid)
-  {
-    assert(target != NULL &&
-           rawHandler != NULL);
-      
-    try
-    {
-      IStorageCommitmentScpHandler& handler = *reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
-      *target = handler.Lookup(sopClassUid, sopInstanceUid);
-      return OrthancPluginErrorCode_Success;
-    }
-    catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
-    {
-      return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
-    }
-    catch (...)
-    {
-      return OrthancPluginErrorCode_Plugin;
-    }
-  }
-#endif
-
-
-#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
-  void IStorageCommitmentScpHandler::Destructor(void* rawHandler)
-  {
-    assert(rawHandler != NULL);
-    delete reinterpret_cast<IStorageCommitmentScpHandler*>(rawHandler);
-  }
-#endif
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
-  DicomInstance::DicomInstance(const OrthancPluginDicomInstance* instance) :
-    toFree_(false),
-    instance_(instance)
-  {
-  }
-#else
-  DicomInstance::DicomInstance(OrthancPluginDicomInstance* instance) :
-    toFree_(false),
-    instance_(instance)
-  {
-  }
-#endif
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  DicomInstance::DicomInstance(const void* buffer,
-                               size_t size) :
-    toFree_(true),
-    instance_(OrthancPluginCreateDicomInstance(GetGlobalContext(), buffer, size))
-  {
-    if (instance_ == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(NullPointer);
-    }
-  }
-#endif
-
-
-  DicomInstance::~DicomInstance()
-  {
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    if (toFree_ &&
-        instance_ != NULL)
-    {
-      OrthancPluginFreeDicomInstance(
-        GetGlobalContext(), const_cast<OrthancPluginDicomInstance*>(instance_));
-    }
-#endif
-  }
-
-  
-  std::string DicomInstance::GetRemoteAet() const
-  {
-    const char* s = OrthancPluginGetInstanceRemoteAet(GetGlobalContext(), instance_);
-    if (s == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-    }
-    else
-    {
-      return std::string(s);
-    }
-  }
-
-
-  void DicomInstance::GetJson(Json::Value& target) const
-  {
-    OrthancString s;
-    s.Assign(OrthancPluginGetInstanceJson(GetGlobalContext(), instance_));
-    s.ToJson(target);
-  }
-  
-
-  void DicomInstance::GetSimplifiedJson(Json::Value& target) const
-  {
-    OrthancString s;
-    s.Assign(OrthancPluginGetInstanceSimplifiedJson(GetGlobalContext(), instance_));
-    s.ToJson(target);
-  }
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
-  std::string DicomInstance::GetTransferSyntaxUid() const
-  {
-    OrthancString s;
-    s.Assign(OrthancPluginGetInstanceTransferSyntaxUid(GetGlobalContext(), instance_));
-
-    std::string result;
-    s.ToString(result);
-    return result;
-  }
-#endif
-
-  
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
-  bool DicomInstance::HasPixelData() const
-  {
-    int32_t result = OrthancPluginHasInstancePixelData(GetGlobalContext(), instance_);
-    if (result < 0)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-    }
-    else
-    {
-      return (result != 0);
-    }
-  }
-#endif
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)  
-  void DicomInstance::GetRawFrame(std::string& target,
-                                  unsigned int frameIndex) const
-  {
-    MemoryBuffer buffer;
-    OrthancPluginErrorCode code = OrthancPluginGetInstanceRawFrame(
-      GetGlobalContext(), *buffer, instance_, frameIndex);
-
-    if (code == OrthancPluginErrorCode_Success)
-    {
-      buffer.ToString(target);
-    }
-    else
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
-    }
-  }
-#endif
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)  
-  OrthancImage* DicomInstance::GetDecodedFrame(unsigned int frameIndex) const
-  {
-    OrthancPluginImage* image = OrthancPluginGetInstanceDecodedFrame(
-      GetGlobalContext(), instance_, frameIndex);
-
-    if (image == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-    }
-    else
-    {
-      return new OrthancImage(image);
-    }
-  }
-#endif  
-
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  void DicomInstance::Serialize(std::string& target) const
-  {
-    MemoryBuffer buffer;
-    OrthancPluginErrorCode code = OrthancPluginSerializeDicomInstance(
-      GetGlobalContext(), *buffer, instance_);
-
-    if (code == OrthancPluginErrorCode_Success)
-    {
-      buffer.ToString(target);
-    }
-    else
-    {
-      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
-    }
-  }
-#endif
-  
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-  DicomInstance* DicomInstance::Transcode(const void* buffer,
-                                          size_t size,
-                                          const std::string& transferSyntax)
-  {
-    OrthancPluginDicomInstance* instance = OrthancPluginTranscodeDicomInstance(
-      GetGlobalContext(), buffer, size, transferSyntax.c_str());
-
-    if (instance == NULL)
-    {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(Plugin);
-    }
-    else
-    {
-      boost::movelib::unique_ptr<DicomInstance> result(new DicomInstance(instance));
-      result->toFree_ = true;
-      return result.release();
-    }
-  }
-#endif
-}
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1240 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <boost/date_time/posix_time/posix_time.hpp>
-#include <json/value.h>
-#include <vector>
-#include <list>
-#include <set>
-#include <map>
-
-
-
-/**
- * The definition of ORTHANC_PLUGINS_VERSION_IS_ABOVE below is for
- * backward compatibility with Orthanc SDK <= 1.3.0.
- * 
- *   $ hg diff -r Orthanc-1.3.0:Orthanc-1.3.1 ../../../Plugins/Include/orthanc/OrthancCPlugin.h
- *
- **/
-#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 !defined(ORTHANC_FRAMEWORK_VERSION_IS_ABOVE)
-#define ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(major, minor, revision)      \
-  (ORTHANC_VERSION_MAJOR > major ||                                     \
-   (ORTHANC_VERSION_MAJOR == major &&                                   \
-    (ORTHANC_VERSION_MINOR > minor ||                                   \
-     (ORTHANC_VERSION_MINOR == minor &&                                 \
-      ORTHANC_VERSION_REVISION >= 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
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
-#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  1
-#else
-#  define HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS  0
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4)
-#  define HAS_ORTHANC_PLUGIN_METRICS  1
-#else
-#  define HAS_ORTHANC_PLUGIN_METRICS  0
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 1, 0)
-#  define HAS_ORTHANC_PLUGIN_HTTP_CLIENT  1
-#else
-#  define HAS_ORTHANC_PLUGIN_HTTP_CLIENT  0
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT  1
-#else
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT  0
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 7)
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  1
-#else
-#  define HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER  0
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 0)
-#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  1
-#else
-#  define HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP  0
-#endif
-
-
-
-namespace OrthancPlugins
-{
-  typedef void (*RestCallback) (OrthancPluginRestOutput* output,
-                                const char* url,
-                                const OrthancPluginHttpRequest* request);
-
-  void SetGlobalContext(OrthancPluginContext* context);
-
-  bool HasGlobalContext();
-
-  OrthancPluginContext* GetGlobalContext();
-
-  
-  class OrthancImage;
-
-
-  class MemoryBuffer : public boost::noncopyable
-  {
-  private:
-    OrthancPluginMemoryBuffer  buffer_;
-
-    void Check(OrthancPluginErrorCode code);
-
-    bool CheckHttp(OrthancPluginErrorCode code);
-
-  public:
-    MemoryBuffer();
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    // This constructor makes a copy of the given buffer in the memory
-    // handled by the Orthanc core
-    MemoryBuffer(const void* buffer,
-                 size_t size);
-#endif
-
-    ~MemoryBuffer()
-    {
-      Clear();
-    }
-
-    OrthancPluginMemoryBuffer* operator*()
-    {
-      return &buffer_;
-    }
-
-    // This transfers ownership from "other" to "this"
-    void Assign(OrthancPluginMemoryBuffer& other);
-
-    void Swap(MemoryBuffer& 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;
-    }
-
-    bool IsEmpty() const
-    {
-      return GetSize() == 0 || GetData() == NULL;
-    }
-
-    void Clear();
-
-    void ToString(std::string& target) const;
-
-    void ToJson(Json::Value& target) const;
-
-    bool RestApiGet(const std::string& uri,
-                    bool applyPlugins);
-
-    bool RestApiGet(const std::string& uri,
-                    const std::map<std::string, std::string>& httpHeaders,
-                    bool applyPlugins);
-
-    bool RestApiPost(const std::string& uri,
-                     const void* body,
-                     size_t bodySize,
-                     bool applyPlugins);
-
-    bool RestApiPut(const std::string& uri,
-                    const void* 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:
-    char*   str_;
-
-    void Clear();
-
-  public:
-    OrthancString() :
-      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:
-    Json::Value  configuration_;  // Necessarily a Json::objectValue
-    std::string  path_;
-
-    std::string GetPath(const std::string& key) const;
-
-    void LoadConfiguration();
-    
-  public:
-    OrthancConfiguration();
-
-    explicit OrthancConfiguration(bool load);
-
-    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:
-    OrthancPluginImage*    image_;
-
-    void Clear();
-
-    void CheckImageAvailable() const;
-
-  public:
-    OrthancImage();
-
-    explicit OrthancImage(OrthancPluginImage* image);
-
-    OrthancImage(OrthancPluginPixelFormat  format,
-                 uint32_t                  width,
-                 uint32_t                  height);
-
-    OrthancImage(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() const;
-
-    unsigned int GetWidth() const;
-
-    unsigned int GetHeight() const;
-
-    unsigned int GetPitch() const;
-    
-    void* GetBuffer() const;
-
-    const OrthancPluginImage* GetObject() const
-    {
-      return image_;
-    }
-
-    void CompressPngImage(MemoryBuffer& target) const;
-
-    void CompressJpegImage(MemoryBuffer& target,
-                           uint8_t quality) const;
-
-    void AnswerPngImage(OrthancPluginRestOutput* output) const;
-
-    void AnswerJpegImage(OrthancPluginRestOutput* output,
-                         uint8_t quality) const;
-    
-    void* GetWriteableBuffer();
-
-    OrthancPluginImage* Release();
-  };
-
-
-#if HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1
-  class FindMatcher : public boost::noncopyable
-  {
-  private:
-    OrthancPluginFindMatcher*          matcher_;
-    const OrthancPluginWorklistQuery*  worklist_;
-
-    void SetupDicom(const void*            query,
-                    uint32_t               size);
-
-  public:
-    explicit FindMatcher(const OrthancPluginWorklistQuery*  worklist);
-
-    FindMatcher(const void*  query,
-                uint32_t     size)
-    {
-      SetupDicom(query, size);
-    }
-
-    explicit FindMatcher(const MemoryBuffer&  dicom)
-    {
-      SetupDicom(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,
-                  const std::string& uri,
-                  bool applyPlugins);
-
-  bool RestApiGetString(std::string& result,
-                        const std::string& uri,
-                        bool applyPlugins);
-
-  bool RestApiGetString(std::string& result,
-                        const std::string& uri,
-                        const std::map<std::string, std::string>& httpHeaders,
-                        bool applyPlugins);
-
-  bool RestApiPost(std::string& result,
-                   const std::string& uri,
-                   const void* body,
-                   size_t bodySize,
-                   bool applyPlugins);
-
-  bool RestApiPost(Json::Value& result,
-                   const std::string& uri,
-                   const void* body,
-                   size_t bodySize,
-                   bool applyPlugins);
-
-  bool RestApiPost(Json::Value& result,
-                   const std::string& uri,
-                   const Json::Value& body,
-                   bool applyPlugins);
-
-  inline bool RestApiPost(Json::Value& result,
-                          const std::string& uri,
-                          const std::string& body,
-                          bool applyPlugins)
-  {
-    return RestApiPost(result, uri, body.empty() ? NULL : body.c_str(),
-                       body.size(), applyPlugins);
-  }
-
-  inline bool RestApiPost(Json::Value& result,
-                          const std::string& uri,
-                          const MemoryBuffer& body,
-                          bool applyPlugins)
-  {
-    return RestApiPost(result, uri, body.GetData(),
-                       body.GetSize(), applyPlugins);
-  }
-
-  bool RestApiPut(Json::Value& result,
-                  const std::string& uri,
-                  const void* body,
-                  size_t bodySize,
-                  bool applyPlugins);
-
-  bool RestApiPut(Json::Value& result,
-                  const std::string& uri,
-                  const Json::Value& body,
-                  bool applyPlugins);
-
-  inline bool RestApiPut(Json::Value& result,
-                         const std::string& uri,
-                         const std::string& body,
-                         bool applyPlugins)
-  {
-    return RestApiPut(result, uri, body.empty() ? NULL : body.c_str(),
-                      body.size(), applyPlugins);
-  }
-
-  bool RestApiDelete(const std::string& uri,
-                     bool applyPlugins);
-
-  bool HttpDelete(const std::string& url,
-                  const std::string& username,
-                  const std::string& password);
-
-  void AnswerJson(const Json::Value& value,
-                  OrthancPluginRestOutput* output);
-
-  void AnswerString(const std::string& answer,
-                    const char* mimeType,
-                    OrthancPluginRestOutput* output);
-
-  void AnswerHttpError(uint16_t httpError,
-                       OrthancPluginRestOutput* output);
-
-  void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods);
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
-  const char* AutodetectMimeType(const std::string& path);
-#endif
-
-  void LogError(const std::string& message);
-
-  void LogWarning(const std::string& message);
-
-  void LogInfo(const std::string& message);
-
-  void ReportMinimalOrthancVersion(unsigned int major,
-                                   unsigned int minor,
-                                   unsigned int revision);
-  
-  bool CheckMinimalOrthancVersion(unsigned int major,
-                                  unsigned int minor,
-                                  unsigned int revision);
-
-
-  namespace Internals
-  {
-    template <RestCallback Callback>
-    static OrthancPluginErrorCode Protect(OrthancPluginRestOutput* output,
-                                          const char* url,
-                                          const OrthancPluginHttpRequest* request)
-    {
-      try
-      {
-        Callback(output, url, request);
-        return OrthancPluginErrorCode_Success;
-      }
-      catch (ORTHANC_PLUGINS_EXCEPTION_CLASS& e)
-      {
-#if HAS_ORTHANC_EXCEPTION == 1 && HAS_ORTHANC_PLUGIN_EXCEPTION_DETAILS == 1
-        if (HasGlobalContext() &&
-            e.HasDetails())
-        {
-          // The "false" instructs Orthanc not to log the detailed
-          // error message. This is to avoid duplicating the details,
-          // because "OrthancException" already does it on construction.
-          OrthancPluginSetHttpErrorDetails
-            (GetGlobalContext(), output, e.GetDetails(), false);
-        }
-#endif
-
-        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return OrthancPluginErrorCode_BadFileFormat;
-      }
-      catch (...)
-      {
-        return OrthancPluginErrorCode_Plugin;
-      }
-    }
-  }
-
-  
-  template <RestCallback Callback>
-  void RegisterRestCallback(const std::string& uri,
-                            bool isThreadSafe)
-  {
-    if (isThreadSafe)
-    {
-      OrthancPluginRegisterRestCallbackNoLock
-        (GetGlobalContext(), uri.c_str(), Internals::Protect<Callback>);
-    }
-    else
-    {
-      OrthancPluginRegisterRestCallback
-        (GetGlobalContext(), 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;
-
-    OrthancPluginPeers   *peers_;
-    Index                 index_;
-    uint32_t              timeout_;
-
-    size_t GetPeerIndex(const std::string& name) const;
-
-  public:
-    OrthancPeers();
-
-    ~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(OrthancJob* job /* takes ownership */);
-
-    static std::string Submit(OrthancJob* job /* takes ownership */,
-                              int priority);
-
-    static void SubmitAndWait(Json::Value& result,
-                              OrthancJob* job /* takes ownership */,
-                              int priority);
-
-    // Submit a job from a POST on the REST API with the same
-    // conventions as in the Orthanc core (according to the
-    // "Synchronous" and "Priority" options)
-    static void SubmitFromRestApiPost(OrthancPluginRestOutput* output,
-                                      const Json::Value& body,
-                                      OrthancJob* job);
-  };
-#endif
-
-
-#if HAS_ORTHANC_PLUGIN_METRICS == 1
-  inline void SetMetricsValue(char* name,
-                              float value)
-  {
-    OrthancPluginSetMetricsValue(GetGlobalContext(), name,
-                                 value, OrthancPluginMetricsType_Default);
-  }
-
-  class MetricsTimer : public boost::noncopyable
-  {
-  private:
-    std::string               name_;
-    boost::posix_time::ptime  start_;
-
-  public:
-    explicit MetricsTimer(const char* name);
-
-    ~MetricsTimer();
-  };
-#endif
-
-
-#if HAS_ORTHANC_PLUGIN_HTTP_CLIENT == 1
-  class HttpClient : public boost::noncopyable
-  {
-  public:
-    typedef std::map<std::string, std::string>  HttpHeaders;
-
-    class IRequestBody : public boost::noncopyable
-    {
-    public:
-      virtual ~IRequestBody()
-      {
-      }
-
-      virtual bool ReadNextChunk(std::string& chunk) = 0;
-    };
-
-
-    class IAnswer : public boost::noncopyable
-    {
-    public:
-      virtual ~IAnswer()
-      {
-      }
-
-      virtual void AddHeader(const std::string& key,
-                             const std::string& value) = 0;
-
-      virtual void AddChunk(const void* data,
-                            size_t size) = 0;
-    };
-
-
-  private:
-    class RequestBodyWrapper;
-
-    uint16_t                 httpStatus_;
-    OrthancPluginHttpMethod  method_;
-    std::string              url_;
-    HttpHeaders              headers_;
-    std::string              username_;
-    std::string              password_;
-    uint32_t                 timeout_;
-    std::string              certificateFile_;
-    std::string              certificateKeyFile_;
-    std::string              certificateKeyPassword_;
-    bool                     pkcs11_;
-    std::string              fullBody_;
-    IRequestBody*            chunkedBody_;
-    bool                     allowChunkedTransfers_;
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-    void ExecuteWithStream(uint16_t& httpStatus,  // out
-                           IAnswer& answer,       // out
-                           IRequestBody& body) const;
-#endif
-
-    void ExecuteWithoutStream(uint16_t& httpStatus,        // out
-                              HttpHeaders& answerHeaders,  // out
-                              std::string& answerBody,     // out
-                              const std::string& body) const;
-    
-  public:
-    HttpClient();
-
-    uint16_t GetHttpStatus() const
-    {
-      return httpStatus_;
-    }
-
-    void SetMethod(OrthancPluginHttpMethod method)
-    {
-      method_ = method;
-    }
-
-    const std::string& GetUrl() const
-    {
-      return url_;
-    }
-
-    void SetUrl(const std::string& url)
-    {
-      url_ = url;
-    }
-
-    void SetHeaders(const HttpHeaders& headers)
-    {
-      headers_ = headers;
-    }
-
-    void AddHeader(const std::string& key,
-                   const std::string& value)
-    {
-      headers_[key] = value;
-    }
-
-    void AddHeaders(const HttpHeaders& headers);
-
-    void SetCredentials(const std::string& username,
-                        const std::string& password);
-
-    void ClearCredentials();
-
-    void SetTimeout(unsigned int timeout)  // 0 for default timeout
-    {
-      timeout_ = timeout;
-    }
-
-    void SetCertificate(const std::string& certificateFile,
-                        const std::string& keyFile,
-                        const std::string& keyPassword);
-
-    void ClearCertificate();
-
-    void SetPkcs11(bool pkcs11)
-    {
-      pkcs11_ = pkcs11;
-    }
-
-    void ClearBody();
-
-    void SwapBody(std::string& body);
-
-    void SetBody(const std::string& body);
-
-    void SetBody(IRequestBody& body);
-
-    // This function can be used to disable chunked transfers if the
-    // remote server is Orthanc with a version <= 1.5.6.
-    void SetChunkedTransfersAllowed(bool allow)
-    {
-      allowChunkedTransfers_ = allow;
-    }
-
-    bool IsChunkedTransfersAllowed() const
-    {
-      return allowChunkedTransfers_;
-    }
-
-    void Execute(IAnswer& answer);
-
-    void Execute(HttpHeaders& answerHeaders /* out */,
-                 std::string& answerBody /* out */);
-
-    void Execute(HttpHeaders& answerHeaders /* out */,
-                 Json::Value& answerBody /* out */);
-
-    void Execute();
-  };
-#endif
-
-
-
-  class IChunkedRequestReader : public boost::noncopyable
-  {
-  public:
-    virtual ~IChunkedRequestReader()
-    {
-    }
-
-    virtual void AddChunk(const void* data,
-                          size_t size) = 0;
-
-    virtual void Execute(OrthancPluginRestOutput* output) = 0;
-  };
-
-
-  typedef IChunkedRequestReader* (*ChunkedRestCallback) (const char* url,
-                                                         const OrthancPluginHttpRequest* request);
-
-
-  namespace Internals
-  {
-    void NullRestCallback(OrthancPluginRestOutput* output,
-                          const char* url,
-                          const OrthancPluginHttpRequest* request);
-  
-    IChunkedRequestReader *NullChunkedRestCallback(const char* url,
-                                                   const OrthancPluginHttpRequest* request);
-
-
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
-    template <ChunkedRestCallback Callback>
-    static OrthancPluginErrorCode ChunkedProtect(OrthancPluginServerChunkedRequestReader** reader,
-                                                const char* url,
-                                                const OrthancPluginHttpRequest* request)
-    {
-      try
-      {
-        if (reader == NULL)
-        {
-          return OrthancPluginErrorCode_InternalError;
-        }
-        else
-        {
-          *reader = reinterpret_cast<OrthancPluginServerChunkedRequestReader*>(Callback(url, request));
-          if (*reader == NULL)
-          {
-            return OrthancPluginErrorCode_Plugin;
-          }
-          else
-          {
-            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;
-      }
-    }
-
-    OrthancPluginErrorCode ChunkedRequestReaderAddChunk(
-      OrthancPluginServerChunkedRequestReader* reader,
-      const void*                              data,
-      uint32_t                                 size);
-
-    OrthancPluginErrorCode ChunkedRequestReaderExecute(
-      OrthancPluginServerChunkedRequestReader* reader,
-      OrthancPluginRestOutput*                 output);
-
-    void ChunkedRequestReaderFinalize(
-      OrthancPluginServerChunkedRequestReader* reader);
-
-#else  
-
-    OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
-                                                    const char* url,
-                                                    const OrthancPluginHttpRequest* request,
-                                                    RestCallback GetHandler,
-                                                    ChunkedRestCallback PostHandler,
-                                                    RestCallback DeleteHandler,
-                                                    ChunkedRestCallback PutHandler);
-
-    template<
-      RestCallback         GetHandler,
-      ChunkedRestCallback  PostHandler,
-      RestCallback         DeleteHandler,
-      ChunkedRestCallback  PutHandler
-      >
-    inline OrthancPluginErrorCode ChunkedRestCompatibility(OrthancPluginRestOutput* output,
-                                                           const char* url,
-                                                           const OrthancPluginHttpRequest* request)
-    {
-      return ChunkedRestCompatibility(output, url, request, GetHandler,
-                                      PostHandler, DeleteHandler, PutHandler);
-    }
-#endif
-  }
-
-
-
-  // NB: We use a templated class instead of a templated function, because
-  // default values are only available in functions since C++11
-  template<
-    RestCallback         GetHandler    = Internals::NullRestCallback,
-    ChunkedRestCallback  PostHandler   = Internals::NullChunkedRestCallback,
-    RestCallback         DeleteHandler = Internals::NullRestCallback,
-    ChunkedRestCallback  PutHandler    = Internals::NullChunkedRestCallback
-    >
-  class ChunkedRestRegistration : public boost::noncopyable
-  {
-  public:
-    static void Apply(const std::string& uri)
-    {
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_SERVER == 1
-      OrthancPluginRegisterChunkedRestCallback(
-        GetGlobalContext(), uri.c_str(),
-        GetHandler == Internals::NullRestCallback         ? NULL : Internals::Protect<GetHandler>,
-        PostHandler == Internals::NullChunkedRestCallback ? NULL : Internals::ChunkedProtect<PostHandler>,
-        DeleteHandler == Internals::NullRestCallback      ? NULL : Internals::Protect<DeleteHandler>,
-        PutHandler == Internals::NullChunkedRestCallback  ? NULL : Internals::ChunkedProtect<PutHandler>,
-        Internals::ChunkedRequestReaderAddChunk,
-        Internals::ChunkedRequestReaderExecute,
-        Internals::ChunkedRequestReaderFinalize);
-#else
-      OrthancPluginRegisterRestCallbackNoLock(
-        GetGlobalContext(), uri.c_str(), 
-        Internals::ChunkedRestCompatibility<GetHandler, PostHandler, DeleteHandler, PutHandler>);
-#endif
-    }
-  };
-
-  
-
-#if HAS_ORTHANC_PLUGIN_STORAGE_COMMITMENT_SCP == 1
-  class IStorageCommitmentScpHandler : public boost::noncopyable
-  {
-  public:
-    virtual ~IStorageCommitmentScpHandler()
-    {
-    }
-    
-    virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
-                                                               const std::string& sopInstanceUid) = 0;
-    
-    static OrthancPluginErrorCode Lookup(OrthancPluginStorageCommitmentFailureReason* target,
-                                         void* rawHandler,
-                                         const char* sopClassUid,
-                                         const char* sopInstanceUid);
-
-    static void Destructor(void* rawHandler);
-  };
-#endif
-
-
-  class DicomInstance : public boost::noncopyable
-  {
-  private:
-    bool toFree_;
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
-    const OrthancPluginDicomInstance*  instance_;
-#else
-    OrthancPluginDicomInstance*  instance_;
-#endif
-    
-  public:
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)    
-    DicomInstance(const OrthancPluginDicomInstance* instance);
-#else
-    DicomInstance(OrthancPluginDicomInstance* instance);
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    DicomInstance(const void* buffer,
-                  size_t size);
-#endif
-
-    ~DicomInstance();
-
-    std::string GetRemoteAet() const;
-
-    const void* GetBuffer() const
-    {
-      return OrthancPluginGetInstanceData(GetGlobalContext(), instance_);
-    }
-
-    size_t GetSize() const
-    {
-      return static_cast<size_t>(OrthancPluginGetInstanceSize(GetGlobalContext(), instance_));
-    }
-
-    void GetJson(Json::Value& target) const;
-
-    void GetSimplifiedJson(Json::Value& target) const;
-
-    OrthancPluginInstanceOrigin GetOrigin() const
-    {
-      return OrthancPluginGetInstanceOrigin(GetGlobalContext(), instance_);
-    }
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
-    std::string GetTransferSyntaxUid() const;
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 6, 1)
-    bool HasPixelData() const;
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    unsigned int GetFramesCount() const
-    {
-      return OrthancPluginGetInstanceFramesCount(GetGlobalContext(), instance_);
-    }
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    void GetRawFrame(std::string& target,
-                     unsigned int frameIndex) const;
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    OrthancImage* GetDecodedFrame(unsigned int frameIndex) const;
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    void Serialize(std::string& target) const;
-#endif
-
-#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 7, 0)
-    static DicomInstance* Transcode(const void* buffer,
-                                    size_t size,
-                                    const std::string& transferSyntax);
-#endif
-  };
-}
--- a/Plugins/Samples/Common/OrthancPluginException.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-set(ORTHANC_ROOT ${SAMPLES_ROOT}/../..)
-include(CheckIncludeFiles)
-include(CheckIncludeFileCXX)
-include(CheckLibraryExists)
-include(FindPythonInterp)
-include(${ORTHANC_ROOT}/Resources/CMake/AutoGeneratedCode.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
-
-
-if (CMAKE_COMPILER_IS_GNUCXX)
-  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic")
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic")
-endif()
-
-
-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 pthread)
-endif()
-
-include_directories(${SAMPLES_ROOT}/../Include/)
-
-if (MSVC)
-  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(${SAMPLES_ROOT}/../../Resources/ThirdParty/VisualStudio/)
-  endif()
-endif()
-
-add_definitions(-DHAS_ORTHANC_EXCEPTION=0)
--- a/Plugins/Samples/Common/SimplifiedOrthancDataset.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-    }
-  }
-}
--- a/Plugins/Samples/Common/SimplifiedOrthancDataset.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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;
-  };
-}
--- a/Plugins/Samples/Common/VersionScript.map	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-# This is a version-script for Orthanc plugins
-
-{
-global:
-  OrthancPluginInitialize;
-  OrthancPluginFinalize;
-  OrthancPluginGetName;
-  OrthancPluginGetVersion;
-
-local:
-  *;
-};
--- a/Plugins/Samples/ConnectivityChecks/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(ConnectivityChecks)
-
-SET(PLUGIN_NAME "connectivity-checks" CACHE STRING "Name of the plugin")
-SET(PLUGIN_VERSION "mainline" CACHE STRING "Version of the plugin")
-
-include(${CMAKE_CURRENT_SOURCE_DIR}/../../../Resources/CMake/OrthancFrameworkParameters.cmake)
-include(${CMAKE_CURRENT_SOURCE_DIR}/../../../Resources/CMake/OrthancFrameworkConfiguration.cmake)
-
-include(JavaScriptLibraries.cmake)
-
-if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-  execute_process(
-    COMMAND 
-    ${PYTHON_EXECUTABLE} ${ORTHANC_ROOT}/Resources/WindowsResources.py
-    ${PLUGIN_VERSION} ConnectivityChecks ConnectivityChecks.dll "Orthanc plugin to serve additional folders"
-    ERROR_VARIABLE Failure
-    OUTPUT_FILE ${AUTOGENERATED_DIR}/ConnectivityChecks.rc
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while computing the version information: ${Failure}")
-  endif()
-
-  list(APPEND ADDITIONAL_RESOURCES ${AUTOGENERATED_DIR}/ConnectivityChecks.rc)
-endif()  
-
-EmbedResources(
-  --framework-path=${CMAKE_CURRENT_SOURCE_DIR}/../../../Core
-  WEB_RESOURCES  ${CMAKE_CURRENT_SOURCE_DIR}/WebResources
-  LIBRARIES      ${JAVASCRIPT_LIBS_DIR}
-  )
-
-add_definitions(
-  -DHAS_ORTHANC_EXCEPTION=1
-  -DORTHANC_PLUGIN_NAME="${PLUGIN_NAME}"
-  -DORTHANC_PLUGIN_VERSION="${PLUGIN_VERSION}"
-  )
-
-include_directories(
-  ${ORTHANC_ROOT}/Plugins/Include/
-  )
-
-add_library(ConnectivityChecks SHARED
-  ${ADDITIONAL_RESOURCES}
-  ${AUTOGENERATED_SOURCES}
-  ${ORTHANC_CORE_SOURCES_DEPENDENCIES}
-  ${ORTHANC_ROOT}/Core/Enumerations.cpp
-  ${ORTHANC_ROOT}/Core/Logging.cpp
-  ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
-  ${ORTHANC_ROOT}/Core/Toolbox.cpp
-  Plugin.cpp
-  )
-
-set_target_properties(
-  ConnectivityChecks PROPERTIES 
-  VERSION ${PLUGIN_VERSION} 
-  SOVERSION ${PLUGIN_VERSION}
-  )
-
-install(
-  TARGETS ConnectivityChecks
-  DESTINATION .
-  )
--- a/Plugins/Samples/ConnectivityChecks/JavaScriptLibraries.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-set(BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
-
-DownloadPackage(
-  "da0189f7c33bf9f652ea65401e0a3dc9"
-  "${BASE_URL}/dicom-web/bootstrap-4.3.1.zip"
-  "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1")
-
-DownloadPackage(
-  "8242afdc5bd44105d9dc9e6535315484"
-  "${BASE_URL}/dicom-web/vuejs-2.6.10.tar.gz"
-  "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10")
-
-DownloadPackage(
-  "3e2b4e1522661f7fcf8ad49cb933296c"
-  "${BASE_URL}/dicom-web/axios-0.19.0.tar.gz"
-  "${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0")
-
-DownloadFile(
-  "220afd743d9e9643852e31a135a9f3ae"
-  "${BASE_URL}/jquery-3.4.1.min.js")
-
-
-set(JAVASCRIPT_LIBS_DIR  ${CMAKE_CURRENT_BINARY_DIR}/javascript-libs)
-file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR})
-
-file(COPY
-  ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js
-  ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.map
-  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/js/bootstrap.min.js
-  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/js/bootstrap.min.js.map
-  ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js
-  ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/jquery-3.4.1.min.js
-  DESTINATION
-  ${JAVASCRIPT_LIBS_DIR}/js
-  )
-
-file(COPY
-  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css
-  ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css.map
-  DESTINATION
-  ${JAVASCRIPT_LIBS_DIR}/css
-  )
--- a/Plugins/Samples/ConnectivityChecks/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 <EmbeddedResources.h>
-#include <orthanc/OrthancCPlugin.h>
-
-#include "../../../Core/OrthancException.h"
-#include "../../../Core/SystemToolbox.h"
-
-#define ROOT_URI "/connectivity-checks"
-
-
-static OrthancPluginContext* context_ = NULL;
-
-
-template <Orthanc::EmbeddedResources::DirectoryResourceId DIRECTORY>
-static OrthancPluginErrorCode ServeStaticResource(OrthancPluginRestOutput* output,
-                                                  const char* url,
-                                                  const OrthancPluginHttpRequest* request)
-{
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
-    return OrthancPluginErrorCode_Success;
-  }
-
-  std::string path = "/" + std::string(request->groups[0]);
-  std::string mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path));
-
-  try
-  {
-    std::string s;
-    Orthanc::EmbeddedResources::GetDirectoryResource(s, DIRECTORY, path.c_str());
-
-    const char* resource = s.size() ? s.c_str() : NULL;
-    OrthancPluginAnswerBuffer(context_, output, resource, s.size(), mime.c_str());
-  }
-  catch (Orthanc::OrthancException&)
-  {
-    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
-    OrthancPluginLogError(context_, s.c_str());
-    OrthancPluginSendHttpStatusCode(context_, output, 404);
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-
-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[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;
-    }
-
-    /* Register the callbacks */
-    OrthancPluginSetDescription(context_, "Utilities to check connectivity to DICOM modalities, DICOMweb servers and Orthanc peers.");
-    OrthancPluginSetRootUri(context_, ROOT_URI "/app/index.html");
-    OrthancPluginRegisterRestCallback(context_, ROOT_URI "/libs/(.*)", ServeStaticResource<Orthanc::EmbeddedResources::LIBRARIES>);
-    OrthancPluginRegisterRestCallback(context_, ROOT_URI "/app/(.*)", ServeStaticResource<Orthanc::EmbeddedResources::WEB_RESOURCES>);
- 
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return ORTHANC_PLUGIN_NAME;
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return ORTHANC_PLUGIN_VERSION;
-  }
-}
--- a/Plugins/Samples/ConnectivityChecks/WebResources/app.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-new Vue({
-  el: '#app',
-  data: {
-    dicomNodes: {},
-    peers: [],
-    canTestPeers: false,
-    dicomWebServers: []
-  },
-  methods: {
-    toggle: function (todo) {
-      todo.done = !todo.done
-    },
-
-    testDicomModalities: function () {
-      console.log('testing DICOM modalities');
-      axios
-        .get('../../modalities?expand')
-        .then(response => {
-          this.dicomNodes = response.data;
-          for (let alias of Object.keys(this.dicomNodes)) {
-            this.dicomNodes[alias]['alias'] = alias;
-            this.dicomNodes[alias]['status'] = 'testing';
-            axios
-              .post('../../modalities/' + alias + '/echo')
-              .then(response => {
-                this.dicomNodes[alias]['status'] = 'ok';
-                this.$forceUpdate();
-              })
-              .catch(response => {
-                this.dicomNodes[alias]['status'] = 'ko';
-                this.$forceUpdate();
-              })
-                }
-        })
-    },
-
-    testOrthancPeers: function () {
-      console.log('testing Orthanc peers');
-      axios
-        .get('../../peers?expand')
-        .then(response => {
-          this.peers = response.data;
-          for (let alias of Object.keys(this.peers)) {
-            this.peers[alias]['alias'] = alias;
-
-            if (this.canTestPeers) {
-              this.peers[alias]['status'] = 'testing';
-              axios
-                .get('../../peers/' + alias + '/system') // introduced in ApiVersion 5 only !
-                .then(response => {
-                  this.peers[alias]['status'] = 'ok';
-                  this.$forceUpdate();
-                })
-                .catch(response => {
-                  this.peers[alias]['status'] = 'ko';
-                  this.$forceUpdate();
-                })
-                  }
-            else {
-              this.peers[alias]['status'] = 'unknown';
-              this.$forceUpdate();
-            }
-          }
-        })
-    },
-
-    testDicomWebServers: function () {
-      console.log('testing Dicom-web servers');
-      axios
-        .get('../../dicom-web/servers?expand')
-        .then(response => {
-          this.dicomWebServers = response.data;
-          for (let alias of Object.keys(this.dicomWebServers)) {
-            this.dicomWebServers[alias]['alias'] = alias;
-            this.dicomWebServers[alias]['status'] = 'testing';
-
-            // perform a dummy qido-rs to test the connectivity
-            axios
-              .post('../../dicom-web/servers/' + alias + '/qido', {
-                'Uri' : '/studies',
-                'Arguments' : {
-                  '00100010' : 'CONNECTIVITY^CHECKS'
-                }
-              })
-              .then(response => {
-                this.dicomWebServers[alias]['status'] = 'ok';
-                this.$forceUpdate();
-              })
-              .catch(response => {
-                this.dicomWebServers[alias]['status'] = 'ko';
-                this.$forceUpdate();
-              })
-                }
-        })
-    },
-
-  },
-  computed: {
-  },
-  mounted() {
-    axios
-      .get('../../system')
-      .then(response => {
-        this.canTestPeers = response.data.ApiVersion >= 5;
-        this.testDicomModalities();
-        if (this.canTestPeers) {
-          this.testOrthancPeers();
-        }
-        this.testDicomWebServers();
-      })
-  }
-})
--- a/Plugins/Samples/ConnectivityChecks/WebResources/index.html	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="UTF-8">
-
-    <link rel="stylesheet" href="../libs/css/bootstrap.min.css">
-
-    <title>Orthanc Connectivity checks</title>
-    <link rel="stylesheet" href="style.css" type="text/css">
-  </head>
-
-  <body>
-    <div id="app" class="container-fluid">
-      <h2>DICOM nodes</h2>
-      <table class="table">
-        <thead>
-          <tr>
-            <th scope="col">Alias</th>
-            <th scope="col">AET</th>
-            <th scope="col">Host</th>
-            <th scope="col">Port</th>
-            <th scope="col">Status</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="node in dicomNodes">
-            <th scope="row">{{node.alias}}</th>
-            <td>{{node.AET}}</td>
-            <td>{{node.Host}}</td>
-            <td>{{node.Port}}</td>
-            <td v-if="node.status=='ok'" class="connected">Connected</td>
-            <td v-if="node.status=='ko'" class="disconnected">Disconnected</td>
-            <td v-if="node.status=='testing'">
-              <div class="spinner-border" role="status">
-                <span class="sr-only">Testing...</span>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-
-      <h2>Orthanc peers</h2>
-      <table class="table" v-if="canTestPeers">
-        <thead>
-          <tr>
-            <th scope="col">Alias</th>
-            <th scope="col">Url</th>
-            <th scope="col">Status</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="node in peers">
-            <th scope="row">{{node.alias}}</th>
-            <td>{{node.Url}}</td>
-            <td v-if="node.status=='ok'" class="connected">Connected</td>
-            <td v-if="node.status=='ko'" class="disconnected">Disconnected</td>
-            <td v-if="node.status=='unknown'" class="unknown">
-              Can not test the peers connectivity with this version of Orthanc
-            </td>
-            <td v-if="node.status=='testing'">
-              <div class="spinner-border" role="status">
-                <span class="sr-only">Testing...</span>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-
-      <h2>DicomWeb servers</h2>
-      <table class="table">
-        <thead>
-          <tr>
-            <th scope="col">Alias</th>
-            <th scope="col">Url</th>
-            <th scope="col">Status</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="node in dicomWebServers">
-            <th scope="row">{{node.alias}}</th>
-            <td>{{node.Url}}</td>
-            <td v-if="node.status=='ok'" class="connected">Connected</td>
-            <td v-if="node.status=='ko'" class="disconnected">Disconnected</td>
-            <td v-if="node.status=='testing'">
-              <div class="spinner-border" role="status">
-                <span class="sr-only">Testing...</span>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <script src="../libs/js/jquery-3.4.1.min.js" type="text/javascript"></script>
-    <script src="../libs/js/bootstrap.min.js" type="text/javascript"></script>
-    <script src="../libs/js/axios.min.js" type="text/javascript"></script>
-    <script src="../libs/js/vue.min.js" type="text/javascript"></script>
-    <script src="app.js" type="text/javascript"></script>
-  </body>
-</html>
--- a/Plugins/Samples/ConnectivityChecks/WebResources/style.css	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-.connected {
-    background-color: darkgreen;
-    color: white;
-}
-
-.disconnected {
-  background-color: darkred;
-    color: white;
-}
-
-.unknown {
-  background-color: gold;
-}
--- a/Plugins/Samples/CustomImageDecoder/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-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)
--- a/Plugins/Samples/CustomImageDecoder/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,81 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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/ModalityWorklists/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-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
-  )
--- a/Plugins/Samples/ModalityWorklists/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,268 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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 "../../../Core/Compatibility.h"
-#include "../Common/OrthancPluginCppWrapper.h"
-
-#include <boost/filesystem.hpp>
-#include <json/value.h>
-#include <json/reader.h>
-#include <string.h>
-#include <iostream>
-#include <algorithm>
-
-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;
-  dicom.ReadFile(path);
-
-  if (matcher.IsMatch(dicom))
-  {
-    // This DICOM file matches the worklist query, add it to the answers
-    OrthancPluginErrorCode code = OrthancPluginWorklistAddAnswer
-      (OrthancPlugins::GetGlobalContext(), answers, query, dicom.GetData(), dicom.GetSize());
-
-    if (code != OrthancPluginErrorCode_Success)
-    {
-      OrthancPlugins::LogError("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;
-  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("Received worklist query from remote modality " +
-                          std::string(issuerAet) + ":\n" + json.toStyledString());
-
-  if (!filterIssuerAet_)
-  {
-    return new OrthancPlugins::FindMatcher(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;
-    modified.CreateDicom(json, OrthancPluginCreateDicomFlags_None);
-    
-    return new OrthancPlugins::FindMatcher(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::unique_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("Worklist matched: " + it->path().string());
-              matchedWorklistCount++;
-            }
-          }
-        }
-      }
-
-      std::ostringstream message;
-      message << "Worklist C-Find: parsed " << parsedFilesCount << " files, found " << matchedWorklistCount << " match(es)";
-      OrthancPlugins::LogInfo(message.str());
-
-    }
-    catch (fs::filesystem_error&)
-    {
-      OrthancPlugins::LogError("Inexistent folder while scanning for worklists: " + source.string());
-      return OrthancPluginErrorCode_DirectoryExpected;
-    }
-
-    // Uncomment the following line if too many answers are to be returned
-    // OrthancPluginMarkWorklistAnswersIncomplete(OrthancPlugins::GetGlobalContext(), answers);
-
-    return OrthancPluginErrorCode_Success;
-  }
-  catch (OrthancPlugins::PluginException& e)
-  {
-    return e.GetErrorCode();
-  }
-}
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
-  {
-    OrthancPlugins::SetGlobalContext(c);
-
-    /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(c) == 0)
-    {
-      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      return -1;
-    }
-
-    OrthancPlugins::LogWarning("Sample worklist plugin is initializing");
-    OrthancPluginSetDescription(c, "Serve DICOM modality worklists from a folder with Orthanc.");
-
-    OrthancPlugins::OrthancConfiguration configuration;
-
-    OrthancPlugins::OrthancConfiguration worklists;
-    configuration.GetSection(worklists, "Worklists");
-
-    bool enabled = worklists.GetBooleanValue("Enable", false);
-    if (enabled)
-    {
-      if (worklists.LookupStringValue(folder_, "Database"))
-      {
-        OrthancPlugins::LogWarning("The database of worklists will be read from folder: " + folder_);
-        OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback);
-      }
-      else
-      {
-        OrthancPlugins::LogError("The configuration option \"Worklists.Database\" must contain a path");
-        return -1;
-      }
-
-      filterIssuerAet_ = worklists.GetBooleanValue("FilterIssuerAet", false);
-    }
-    else
-    {
-      OrthancPlugins::LogWarning("Worklist server is disabled by the configuration file");
-    }
-
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-    OrthancPlugins::LogWarning("Sample worklist plugin is finalizing");
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "worklists";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return MODALITY_WORKLISTS_VERSION;
-  }
-}
--- a/Plugins/Samples/ModalityWorklists/README	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-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
--- a/Plugins/Samples/ModalityWorklists/WorklistsDatabase/Generate.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-#!/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 sorted(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/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-More contributed samples of plugins can be found and added in
-the "OrthancContributed" repository on GitHub:
-https://github.com/jodogne/OrthancContributed/tree/master/Plugins
--- a/Plugins/Samples/ServeFolders/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(ServeFolders)
-
-SET(SERVE_FOLDERS_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(${CMAKE_SOURCE_DIR}/../Common/OrthancPlugins.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/JsonCppConfiguration.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/BoostConfiguration.cmake)
-
-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}")
-
-set_target_properties(ServeFolders PROPERTIES 
-  VERSION ${SERVE_FOLDERS_VERSION} 
-  SOVERSION ${SERVE_FOLDERS_VERSION})
-
-install(
-  TARGETS ServeFolders
-  RUNTIME DESTINATION lib    # Destination for Windows
-  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
-  )
--- a/Plugins/Samples/ServeFolders/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,465 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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 <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 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 void SetHttpHeaders(OrthancPluginRestOutput* output)
-{
-  if (!allowCache_)
-  {
-    // http://stackoverflow.com/a/2068407/881731
-    OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
-    OrthancPluginSetHttpHeader(context, output, "Cache-Control", "no-cache, no-store, must-revalidate");
-    OrthancPluginSetHttpHeader(context, output, "Pragma", "no-cache");
-    OrthancPluginSetHttpHeader(context, output, "Expires", "0");
-  }
-}
-
-
-static void RegisterDefaultExtensions()
-{
-  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 std::string GetMimeType(const std::string& path)
-{
-  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())
-  {
-    return found->second;
-  }
-  else
-  {
-    OrthancPlugins::LogWarning("ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
-    return "application/octet-stream";
-  }
-}
-
-
-static bool LookupFolder(std::string& folder,
-                         OrthancPluginRestOutput* output,
-                         const OrthancPluginHttpRequest* request)
-{
-  const std::string uri = request->groups[0];
-
-  std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
-  if (found == folders_.end())
-  {
-    OrthancPlugins::LogError("Unknown URI in plugin server-folders: " + uri);
-    OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
-    return false;
-  }
-  else
-  {
-    folder = found->second;
-    return true;
-  }
-}
-
-
-static void Answer(OrthancPluginRestOutput* output,
-                   const char* content,
-                   size_t size,
-                   const std::string& mime)
-{
-  if (generateETag_)
-  {
-    OrthancPlugins::OrthancString md5;
-    md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), content, size));
-
-    std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
-    OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
-  }
-
-  SetHttpHeaders(output);
-  OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), 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(OrthancPlugins::GetGlobalContext(), output, "GET");
-    return;
-  }
-
-  std::string folder;
-
-  if (LookupFolder(folder, output, request))
-  {
-    const fs::path item(request->groups[1]);
-    const fs::path parent((fs::path(folder) / item).parent_path());
-
-    if (item.filename().string() == "index.html" &&
-        fs::is_directory(parent) &&
-        !fs::is_regular_file(fs::path(folder) / item))
-    {
-      // On-the-fly generation of an "index.html" 
-      std::string s;
-      s += "<html>\n";
-      s += "  <body>\n";
-      s += "    <ul>\n";
-
-      fs::directory_iterator end;
-
-      for (fs::directory_iterator it(parent) ; it != end; ++it)
-      {
-        if (fs::is_directory(it->status()))
-        {
-          std::string f = it->path().filename().string();
-          s += "      <li><a href=\"" + f + "/index.html\">" + f + "/</a></li>\n";
-        }
-      }
-
-      for (fs::directory_iterator it(parent) ; 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 f = it->path().filename().string();
-          s += "      <li><a href=\"" + f + "\">" + f + "</a></li>\n";
-        }
-      }
-
-      s += "    </ul>\n";
-      s += "  </body>\n";
-      s += "</html>\n";
-
-      Answer(output, s.c_str(), s.size(), "text/html");
-    }
-    else
-    {
-      std::string path = folder + "/" + item.string();
-      std::string mime = GetMimeType(path);
-
-      OrthancPlugins::MemoryBuffer content;
-
-      try
-      {
-        content.ReadFile(path);
-      }
-      catch (...)
-      {
-        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(OrthancPlugins::GetGlobalContext(),
-                                 output, "Last-Modified", t.c_str());
-
-      Answer(output, content.GetData(), content.GetSize(), mime);
-    }
-  }
-}
-
-
-void ListServedFolders(OrthancPluginRestOutput* output,
-                       const char* url,
-                       const OrthancPluginHttpRequest* request)
-{
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "GET");
-    return;
-  }
-
-  std::string s = "<html><body><h1>Additional folders served by Orthanc</h1>\n";
-
-  if (folders_.empty())
-  {
-    s += "<p>Empty section <tt>ServeFolders</tt> in your configuration file: No additional folder is served.</p>\n";
-  }
-  else
-  {
-    s += "<ul>\n";
-    for (std::map<std::string, std::string>::const_iterator
-           it = folders_.begin(); it != folders_.end(); ++it)
-    {
-      // The URI is relative to INDEX_URI ("/app/plugin-serve-folders.html")
-      s += "<li><a href=\"../" + it->first + "/index.html\">" + it->first + "</li>\n";
-    }
-    
-    s += "</ul>\n";
-  }
-
-  s += "</body></html>\n";
-
-  Answer(output, s.c_str(), s.size(), "text/html");
-}
-
-
-static void ConfigureFolders(const Json::Value& folders)
-{
-  if (folders.type() != Json::objectValue)
-  {
-    OrthancPlugins::LogError("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("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("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("Trying to 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>(regex.c_str(), true);
-    }
-  }
-}
-
-
-static void ConfigureExtensions(const Json::Value& extensions)
-{
-  if (extensions.type() != Json::objectValue)
-  {
-    OrthancPlugins::LogError("The list of extensions is badly formatted (must be a JSON object)");
-    ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
-  }
-
-  Json::Value::Members members = extensions.getMemberNames();
-
-  for (Json::Value::Members::const_iterator 
-         it = members.begin(); it != members.end(); ++it)
-  {
-    if (extensions[*it].type() != Json::stringValue)
-    {
-      OrthancPlugins::LogError("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("ServeFolders: Removing MIME type for file extension \"." +
-                                 name + "\"");
-    }
-    else
-    {
-      OrthancPlugins::LogWarning("ServeFolders: Associating file extension \"." + name + 
-                                 "\" with MIME type \"" + mime + "\"");
-    }
-  }  
-}
-
-
-static void ReadConfiguration()
-{
-  OrthancPlugins::OrthancConfiguration configuration;
-
-  {
-    OrthancPlugins::OrthancConfiguration globalConfiguration;
-    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("ServeFolders: Requesting the HTTP client to " +
-                                 std::string(tmp ? "enable" : "disable") + 
-                                 " its caching mechanism");
-    }
-
-    if (configuration.LookupBooleanValue(tmp, "GenerateETag"))
-    {
-      generateETag_ = tmp;
-      OrthancPlugins::LogWarning("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("ServeFolders: Empty configuration file: "
-                               "No additional folder will be served!");
-  }
-}
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
-  {
-    OrthancPlugins::SetGlobalContext(context);
-
-    /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(context) == 0)
-    {
-      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      return -1;
-    }
-
-    RegisterDefaultExtensions();
-    OrthancPluginSetDescription(context, "Serve additional folders with the HTTP server of Orthanc.");
-    OrthancPluginSetRootUri(context, INDEX_URI);
-    OrthancPlugins::RegisterRestCallback<ListServedFolders>(INDEX_URI, true);
-
-    try
-    {
-      ReadConfiguration();
-    }
-    catch (OrthancPlugins::PluginException& e)
-    {
-      OrthancPlugins::LogError("Error while initializing the ServeFolders plugin: " + 
-                               std::string(e.What(context)));
-    }
-
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "serve-folders";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return SERVE_FOLDERS_VERSION;
-  }
-}
--- a/Plugins/Samples/ServeFolders/README	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-ServeFolders plugin
-===================
-
-This sample plugin enables Orthanc to serve additional folders using
-its embedded Web server.
-
-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/serve-folders.html
--- a/Plugins/Samples/StorageArea/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(Basic)
-
-set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
-include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
-
-add_library(PluginTest SHARED Plugin.cpp)
--- a/Plugins/Samples/StorageArea/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,164 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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.h>
-#include <stdio.h>
-#include <string>
-
-static OrthancPluginContext* context = NULL;
-
-
-static std::string GetPath(const char* uuid)
-{
-  return "plugin_" + std::string(uuid);
-}
-
-
-static OrthancPluginErrorCode StorageCreate(const char* uuid,
-                                            const void* content,
-                                            int64_t size,
-                                            OrthancPluginContentType type)
-{
-  std::string path = GetPath(uuid);
-
-  FILE* fp = fopen(path.c_str(), "wb");
-  if (!fp)
-  {
-    return OrthancPluginErrorCode_StorageAreaPlugin;
-  }
-
-  bool ok = fwrite(content, size, 1, fp) == 1;
-  fclose(fp);
-
-  return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_StorageAreaPlugin;
-}
-
-
-static OrthancPluginErrorCode StorageRead(void** content,
-                                          int64_t* size,
-                                          const char* uuid,
-                                          OrthancPluginContentType type)
-{
-  std::string path = GetPath(uuid);
-
-  FILE* fp = fopen(path.c_str(), "rb");
-  if (!fp)
-  {
-    return OrthancPluginErrorCode_StorageAreaPlugin;
-  }
-
-  if (fseek(fp, 0, SEEK_END) < 0)
-  {
-    fclose(fp);
-    return OrthancPluginErrorCode_StorageAreaPlugin;
-  }
-
-  *size = ftell(fp);
-
-  if (fseek(fp, 0, SEEK_SET) < 0)
-  {
-    fclose(fp);
-    return OrthancPluginErrorCode_StorageAreaPlugin;
-  }
-
-  bool ok = true;
-
-  if (*size == 0)
-  {
-    *content = NULL;
-  }
-  else
-  {
-    *content = malloc(*size);
-    if (*content == NULL ||
-        fread(*content, *size, 1, fp) != 1)
-    {
-      ok = false;
-    }
-  }
-
-  fclose(fp);
-
-  return ok ? OrthancPluginErrorCode_Success : OrthancPluginErrorCode_StorageAreaPlugin;
-}
-
-
-static OrthancPluginErrorCode StorageRemove(const char* uuid,
-                                            OrthancPluginContentType type)
-{
-  std::string path = GetPath(uuid);
-
-  if (remove(path.c_str()) == 0)
-  {
-    return OrthancPluginErrorCode_Success;
-  }
-  else
-  {
-    return OrthancPluginErrorCode_StorageAreaPlugin;
-  }
-}
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
-  {
-    context = c;
-    OrthancPluginLogWarning(context, "Storage plugin is initializing");
-
-    /* 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",
-              c->orthancVersion,
-              ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-              ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      OrthancPluginLogError(context, info);
-      return -1;
-    }
-
-    OrthancPluginRegisterStorageArea(context, StorageCreate, StorageRead, StorageRemove);
-
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-    OrthancPluginLogWarning(context, "Storage plugin is finalizing");
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "storage";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return "1.0";
-  }
-}
--- a/Plugins/Samples/StorageCommitmentScp/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(StorageCommitmentScp)
-
-SET(PLUGIN_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(StorageCommitmentScp SHARED 
-  Plugin.cpp
-  ../Common/OrthancPluginCppWrapper.cpp
-  ${JSONCPP_SOURCES}
-  ${BOOST_SOURCES}
-  )
-
-message("Setting the version of the plugin to ${PLUGIN_VERSION}")
-add_definitions(
-  -DPLUGIN_VERSION="${PLUGIN_VERSION}"
-  )
-
-set_target_properties(StorageCommitmentScp PROPERTIES 
-  VERSION ${PLUGIN_VERSION} 
-  SOVERSION ${PLUGIN_VERSION})
-
-install(
-  TARGETS StorageCommitmentScp
-  RUNTIME DESTINATION lib    # Destination for Windows
-  LIBRARY DESTINATION share/orthanc/plugins    # Destination for Linux
-  )
--- a/Plugins/Samples/StorageCommitmentScp/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,116 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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 <json/value.h>
-#include <json/reader.h>
-
-
-
-class StorageCommitmentSample : public OrthancPlugins::IStorageCommitmentScpHandler
-{
-private:
-  int count_;
-  
-public:
-  StorageCommitmentSample() : count_(0)
-  {
-  }
-  
-  virtual OrthancPluginStorageCommitmentFailureReason Lookup(const std::string& sopClassUid,
-                                                             const std::string& sopInstanceUid)
-  {
-    printf("?? [%s] [%s]\n", sopClassUid.c_str(), sopInstanceUid.c_str());
-    if (count_++ % 2 == 0)
-      return OrthancPluginStorageCommitmentFailureReason_Success;
-    else
-      return OrthancPluginStorageCommitmentFailureReason_NoSuchObjectInstance;
-  }
-};
-
-
-static OrthancPluginErrorCode StorageCommitmentScp(void**              handler /* out */,
-                                                   const char*         jobId,
-                                                   const char*         transactionUid,
-                                                   const char* const*  sopClassUids,
-                                                   const char* const*  sopInstanceUids,
-                                                   uint32_t            countInstances,
-                                                   const char*         remoteAet,
-                                                   const char*         calledAet)
-{
-  /*std::string s;
-    OrthancPlugins::RestApiPost(s, "/jobs/" + std::string(jobId) + "/pause", NULL, 0, false);*/
-  
-  printf("[%s] [%s] [%s] [%s]\n", jobId, transactionUid, remoteAet, calledAet);
-
-  for (uint32_t i = 0; i < countInstances; i++)
-  {
-    printf("++ [%s] [%s]\n", sopClassUids[i], sopInstanceUids[i]);
-  }
-
-  *handler = new StorageCommitmentSample;
-  return OrthancPluginErrorCode_Success;
-}
-
-
-extern "C"
-{
-  ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
-  {
-    OrthancPlugins::SetGlobalContext(c);
-
-    /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(c) == 0)
-    {
-      OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
-                                                  ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
-                                                  ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      return -1;
-    }
-
-    OrthancPluginSetDescription(c, "Sample storage commitment SCP plugin.");
-
-    OrthancPluginRegisterStorageCommitmentScpCallback(
-      c, StorageCommitmentScp,
-      OrthancPlugins::IStorageCommitmentScpHandler::Destructor,
-      OrthancPlugins::IStorageCommitmentScpHandler::Lookup);
-    
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return "storage-commitment-scp";
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return PLUGIN_VERSION;
-  }
-}
--- a/Plugins/Samples/WebSkeleton/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(WebSkeleton)
-
-SET(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
-SET(RESOURCES_ROOT ${CMAKE_SOURCE_DIR}/StaticResources)
-
-set(SAMPLES_ROOT ${CMAKE_SOURCE_DIR}/..)
-include(${SAMPLES_ROOT}/Common/OrthancPlugins.cmake)
-
-include(Framework/Framework.cmake)
-
-add_library(WebSkeleton SHARED 
-  ${AUTOGENERATED_SOURCES}
-  )
--- a/Plugins/Samples/WebSkeleton/Configuration.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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
-
-#define ORTHANC_PLUGIN_NAME  "web-skeleton"
-
-#define ORTHANC_PLUGIN_VERSION "1.0"
-
-#define ORTHANC_PLUGIN_WEB_ROOT  "/plugin-web-skeleton/"
--- a/Plugins/Samples/WebSkeleton/Framework/EmbedResources.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,393 +0,0 @@
-# Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 sys
-import os
-import os.path
-import pprint
-import re
-
-UPCASE_CHECK = True
-ARGS = []
-for i in range(len(sys.argv)):
-    if not sys.argv[i].startswith('--'):
-        ARGS.append(sys.argv[i])
-    elif sys.argv[i].lower() == '--no-upcase-check':
-        UPCASE_CHECK = False
-
-if len(ARGS) < 2 or len(ARGS) % 2 != 0:
-    print ('Usage:')
-    print ('python %s [--no-upcase-check] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
-    exit(-1)
-
-TARGET_BASE_FILENAME = ARGS[1]
-SOURCES = ARGS[2:]
-
-try:
-    # Make sure the destination directory exists
-    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
-except:
-    pass
-
-
-#####################################################################
-## Read each resource file
-#####################################################################
-
-def CheckNoUpcase(s):
-    global UPCASE_CHECK
-    if (UPCASE_CHECK and
-        re.search('[A-Z]', s) != None):
-        raise Exception("Path in a directory with an upcase letter: %s" % s)
-
-resources = {}
-
-counter = 0
-i = 0
-while i < len(SOURCES):
-    resourceName = SOURCES[i].upper()
-    pathName = SOURCES[i + 1]
-
-    if not os.path.exists(pathName):
-        raise Exception("Non existing path: %s" % pathName)
-
-    if resourceName in resources:
-        raise Exception("Twice the same resource: " + resourceName)
-    
-    if os.path.isdir(pathName):
-        # The resource is a directory: Recursively explore its files
-        content = {}
-        for root, dirs, files in os.walk(pathName):
-            dirs.sort()
-            files.sort()
-            base = os.path.relpath(root, pathName)
-            for f in files:
-                if f.find('~') == -1:  # Ignore Emacs backup files
-                    if base == '.':
-                        r = f
-                    else:
-                        r = os.path.join(base, f)
-
-                    CheckNoUpcase(r)
-                    r = '/' + r.replace('\\', '/')
-                    if r in content:
-                        raise Exception("Twice the same filename (check case): " + r)
-
-                    content[r] = {
-                        'Filename' : os.path.join(root, f),
-                        'Index' : counter
-                        }
-                    counter += 1
-
-        resources[resourceName] = {
-            'Type' : 'Directory',
-            'Files' : content
-            }
-
-    elif os.path.isfile(pathName):
-        resources[resourceName] = {
-            'Type' : 'File',
-            'Index' : counter,
-            'Filename' : pathName
-            }
-        counter += 1
-
-    else:
-        raise Exception("Not a regular file, nor a directory: " + pathName)
-
-    i += 2
-
-#pprint.pprint(resources)
-
-
-#####################################################################
-## Write .h header
-#####################################################################
-
-header = open(TARGET_BASE_FILENAME + '.h', 'w')
-
-header.write("""
-#pragma once
-
-#include <string>
-#include <list>
-
-namespace Orthanc
-{
-  namespace EmbeddedResources
-  {
-    enum FileResourceId
-    {
-""")
-
-isFirst = True
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        if isFirst:
-            isFirst = False
-        else:    
-            header.write(',\n')
-        header.write('      %s' % name)
-
-header.write("""
-    };
-
-    enum DirectoryResourceId
-    {
-""")
-
-isFirst = True
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        if isFirst:
-            isFirst = False
-        else:    
-            header.write(',\n')
-        header.write('      %s' % name)
-
-header.write("""
-    };
-
-    const void* GetFileResourceBuffer(FileResourceId id);
-    size_t GetFileResourceSize(FileResourceId id);
-    void GetFileResource(std::string& result, FileResourceId id);
-
-    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
-    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
-    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
-
-    void ListResources(std::list<std::string>& result, DirectoryResourceId id);
-  }
-}
-""")
-header.close()
-
-
-
-#####################################################################
-## Write the resource content in the .cpp source
-#####################################################################
-
-PYTHON_MAJOR_VERSION = sys.version_info[0]
-
-def WriteResource(cpp, item):
-    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
-
-    f = open(item['Filename'], "rb")
-    content = f.read()
-    f.close()
-
-    # http://stackoverflow.com/a/1035360
-    pos = 0
-    for b in content:
-        if PYTHON_MAJOR_VERSION == 2:
-            c = ord(b[0])
-        else:
-            c = b
-
-        if pos > 0:
-            cpp.write(', ')
-
-        if (pos % 16) == 0:
-            cpp.write('\n    ')
-
-        if c < 0:
-            raise Exception("Internal error")
-
-        cpp.write("0x%02x" % c)
-        pos += 1
-
-    # Zero-size array are disallowed, so we put one single void character in it.
-    if pos == 0:
-        cpp.write('  0')
-
-    cpp.write('  };\n')
-    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
-
-
-cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
-
-print os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
-
-cpp.write("""
-#include "%s.h"
-
-#include <stdint.h>
-#include <string.h>
-#include <stdexcept>
-
-namespace Orthanc
-{
-  namespace EmbeddedResources
-  {
-""" % (os.path.basename(TARGET_BASE_FILENAME)))
-
-
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        WriteResource(cpp, resources[name])
-    else:
-        for f in resources[name]['Files']:
-            WriteResource(cpp, resources[name]['Files'][f])
-
-
-
-#####################################################################
-## Write the accessors to the file resources in .cpp
-#####################################################################
-
-cpp.write("""
-    const void* GetFileResourceBuffer(FileResourceId id)
-    {
-      switch (id)
-      {
-""")
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        cpp.write('      case %s:\n' % name)
-        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
-
-cpp.write("""
-      default:
-        throw std::runtime_error("Unknown resource");
-      }
-    }
-
-    size_t GetFileResourceSize(FileResourceId id)
-    {
-      switch (id)
-      {
-""")
-
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        cpp.write('      case %s:\n' % name)
-        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
-
-cpp.write("""
-      default:
-        throw std::runtime_error("Unknown resource");
-      }
-    }
-""")
-
-
-
-#####################################################################
-## Write the accessors to the directory resources in .cpp
-#####################################################################
-
-cpp.write("""
-    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
-    {
-      switch (id)
-      {
-""")
-
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        cpp.write('      case %s:\n' % name)
-        isFirst = True
-        for path in resources[name]['Files']:
-            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
-            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
-        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
-
-cpp.write("""      default:
-        throw std::runtime_error("Unknown resource");
-      }
-    }
-
-    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
-    {
-      switch (id)
-      {
-""")
-
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        cpp.write('      case %s:\n' % name)
-        isFirst = True
-        for path in resources[name]['Files']:
-            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
-            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
-        cpp.write('        throw std::runtime_error("Unknown path in a directory resource");\n\n')
-
-cpp.write("""      default:
-        throw std::runtime_error("Unknown resource");
-      }
-    }
-""")
-
-
-
-
-#####################################################################
-## List the resources in a directory
-#####################################################################
-
-cpp.write("""
-    void ListResources(std::list<std::string>& result, DirectoryResourceId id)
-    {
-      result.clear();
-
-      switch (id)
-      {
-""")
-
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        cpp.write('      case %s:\n' % name)
-        for path in sorted(resources[name]['Files']):
-            cpp.write('        result.push_back("%s");\n' % path)
-        cpp.write('        break;\n\n')
-
-cpp.write("""      default:
-        throw std::runtime_error("Unknown resource");
-      }
-    }
-""")
-
-
-
-
-#####################################################################
-## Write the convenience wrappers in .cpp
-#####################################################################
-
-cpp.write("""
-    void GetFileResource(std::string& result, FileResourceId id)
-    {
-      size_t size = GetFileResourceSize(id);
-      result.resize(size);
-      if (size > 0)
-        memcpy(&result[0], GetFileResourceBuffer(id), size);
-    }
-
-    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
-    {
-      size_t size = GetDirectoryResourceSize(id, path);
-      result.resize(size);
-      if (size > 0)
-        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
-    }
-  }
-}
-""")
-cpp.close()
--- a/Plugins/Samples/WebSkeleton/Framework/Framework.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-# Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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/>.
-
-
-if (STANDALONE_BUILD)
-  add_definitions(-DORTHANC_PLUGIN_STANDALONE=1)
-
-  set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
-  set(AUTOGENERATED_SOURCES "${AUTOGENERATED_DIR}/EmbeddedResources.cpp")
-
-  file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
-  include_directories(${AUTOGENERATED_DIR})
-
-  set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
-  add_custom_command(
-    OUTPUT
-    "${AUTOGENERATED_DIR}/EmbeddedResources.h"
-    "${AUTOGENERATED_DIR}/EmbeddedResources.cpp"
-    COMMAND 
-    python
-    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
-    "${AUTOGENERATED_DIR}/EmbeddedResources"
-    STATIC_RESOURCES
-    ${RESOURCES_ROOT}
-    DEPENDS
-    "${CMAKE_CURRENT_SOURCE_DIR}/Framework/EmbedResources.py"
-    ${STATIC_RESOURCES}
-    )
-
-else()
-  add_definitions(
-    -DORTHANC_PLUGIN_STANDALONE=0
-    -DORTHANC_PLUGIN_RESOURCES_ROOT="${RESOURCES_ROOT}"
-    )
-endif()
-
-
-list(APPEND AUTOGENERATED_SOURCES
-  "${CMAKE_CURRENT_SOURCE_DIR}/Framework/Plugin.cpp"
-  )
--- a/Plugins/Samples/WebSkeleton/Framework/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,272 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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 "../Configuration.h"
-
-#include <orthanc/OrthancCPlugin.h>
-#include <string>
-#include <stdexcept>
-#include <algorithm>
-#include <sys/stat.h>
-
-#if ORTHANC_PLUGIN_STANDALONE == 1
-// This is an auto-generated file for standalone builds
-#include <EmbeddedResources.h>
-#endif
-
-static OrthancPluginContext* context = NULL;
-
-
-static const char* GetMimeType(const std::string& path)
-{
-  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 == ".json")
-  {
-    return "application/json";
-  }
-  else if (extension == ".xml")
-  {
-    return "application/xml";
-  }
-  else if (extension == ".wasm")
-  {
-    return "application/wasm";
-  }
-  else if (extension == ".png")
-  {
-    return "image/png";
-  }
-  else if (extension == ".jpg" || extension == ".jpeg")
-  {
-    return "image/jpeg";
-  }
-  else
-  {
-    std::string s = "Unknown MIME type for extension: " + extension;
-    OrthancPluginLogWarning(context, s.c_str());
-    return "application/octet-stream";
-  }
-}
-
-
-static bool ReadFile(std::string& content,
-                     const std::string& path)
-{
-  struct stat s;
-  if (stat(path.c_str(), &s) != 0 ||
-      !(s.st_mode & S_IFREG))
-  {
-    // Either the path does not exist, or it is not a regular file
-    return false;
-  }
-
-  FILE* fp = fopen(path.c_str(), "rb");
-  if (fp == NULL)
-  {
-    return false;
-  }
-
-  long size;
-
-  if (fseek(fp, 0, SEEK_END) == -1 ||
-      (size = ftell(fp)) < 0)
-  {
-    fclose(fp);
-    return false;
-  }
-
-  content.resize(size);
-      
-  if (fseek(fp, 0, SEEK_SET) == -1)
-  {
-    fclose(fp);
-    return false;
-  }
-
-  bool ok = true;
-
-  if (size > 0 &&
-      fread(&content[0], size, 1, fp) != 1)
-  {
-    ok = false;
-  }
-
-  fclose(fp);
-
-  return ok;
-}
-
-
-#if ORTHANC_PLUGIN_STANDALONE == 1
-static OrthancPluginErrorCode ServeStaticResource(OrthancPluginRestOutput* output,
-                                                  const char* url,
-                                                  const OrthancPluginHttpRequest* request)
-{
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-    return OrthancPluginErrorCode_Success;
-  }
-
-  std::string path = "/" + std::string(request->groups[0]);
-  const char* mime = GetMimeType(path);
-
-  try
-  {
-    std::string s;
-    Orthanc::EmbeddedResources::GetDirectoryResource
-      (s, Orthanc::EmbeddedResources::STATIC_RESOURCES, path.c_str());
-
-    const char* resource = s.size() ? s.c_str() : NULL;
-    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
-  }
-  catch (std::runtime_error&)
-  {
-    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
-    OrthancPluginLogError(context, s.c_str());
-    OrthancPluginSendHttpStatusCode(context, output, 404);
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-#endif
-
-
-#if ORTHANC_PLUGIN_STANDALONE == 0
-static OrthancPluginErrorCode ServeFolder(OrthancPluginRestOutput* output,
-                                          const char* url,
-                                          const OrthancPluginHttpRequest* request)
-{
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-    return OrthancPluginErrorCode_Success;
-  }
-
-  std::string path = ORTHANC_PLUGIN_RESOURCES_ROOT "/" + std::string(request->groups[0]);
-  const char* mime = GetMimeType(path);
-
-  std::string s;
-  if (ReadFile(s, path))
-  {
-    const char* resource = s.size() ? s.c_str() : NULL;
-    OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime);
-  }
-  else
-  {
-    std::string s = "Unknown static resource in plugin: " + std::string(request->groups[0]);
-    OrthancPluginLogError(context, s.c_str());
-    OrthancPluginSendHttpStatusCode(context, output, 404);
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-#endif
-
-
-static OrthancPluginErrorCode RedirectRoot(OrthancPluginRestOutput* output,
-                                           const char* url,
-                                           const OrthancPluginHttpRequest* request)
-{
-  if (request->method != OrthancPluginHttpMethod_Get)
-  {
-    OrthancPluginSendMethodNotAllowed(context, output, "GET");
-  }
-  else
-  {
-    OrthancPluginRedirect(context, output, ORTHANC_PLUGIN_WEB_ROOT "index.html");
-  }
-
-  return OrthancPluginErrorCode_Success;
-}
-
-
-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[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;
-    }
-
-    /* Register the callbacks */
-
-#if ORTHANC_PLUGIN_STANDALONE == 1
-    OrthancPluginLogInfo(context, "Serving static resources (standalone build)");
-    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeStaticResource);
-#else
-    OrthancPluginLogInfo(context, "Serving resources from folder: " ORTHANC_PLUGIN_RESOURCES_ROOT);
-    OrthancPluginRegisterRestCallback(context, ORTHANC_PLUGIN_WEB_ROOT "(.*)", ServeFolder);
-#endif
-
-    OrthancPluginRegisterRestCallback(context, "/", RedirectRoot);
- 
-    return 0;
-  }
-
-
-  ORTHANC_PLUGINS_API void OrthancPluginFinalize()
-  {
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
-  {
-    return ORTHANC_PLUGIN_NAME;
-  }
-
-
-  ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
-  {
-    return ORTHANC_PLUGIN_VERSION;
-  }
-}
--- a/Plugins/Samples/WebSkeleton/NOTES.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-This is a sample Orthanc plugin that serves static resources (HTML,
-JavaScript, CSS, images...).
-
-The resources to serve must be stored in the folder "StaticResources".
-
-The folder "Framework" contains a reusable framework for any plugin
-whose sole objective is to serve static resources.
--- a/Plugins/Samples/WebSkeleton/StaticResources/index.html	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-<!doctype html>
-
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>Web Skeleton</title>
-  </head>
-
-  <body>
-    <h1>Web Skeleton</h1>
-    <p>
-      This is a sample skeleton for Orthanc showing how to create a
-      plugin that serves static HTML resources.
-    </p>
-  </body>
-</html>
--- a/Resources/CMake/AutoGeneratedCode.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/../EmbedResources.py"
-  CACHE INTERNAL "Path to the EmbedResources.py script from Orthanc")
-set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
-set(AUTOGENERATED_SOURCES)
-
-file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
-include_directories(${AUTOGENERATED_DIR})
-
-macro(EmbedResources)
-  # Convert a semicolon separated list to a whitespace separated string
-  set(SCRIPT_OPTIONS)
-  set(SCRIPT_ARGUMENTS)
-  set(DEPENDENCIES)
-  set(IS_PATH_NAME false)
-
-  set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
-
-  # Loop over the arguments of the function
-  foreach(arg ${ARGN})
-    # Extract the first character of the argument
-    string(SUBSTRING "${arg}" 0 1 FIRST_CHAR)
-    if (${FIRST_CHAR} STREQUAL "-")
-      # If the argument starts with a dash "-", this is an option to
-      # EmbedResources.py
-      if (${arg} MATCHES "--target=.*")
-        # Does the argument starts with "--target="?
-        string(SUBSTRING "${arg}" 9 -1 TARGET)  # 9 is the length of "--target="
-        set(TARGET_BASE "${AUTOGENERATED_DIR}/${TARGET}")
-      else()
-        list(APPEND SCRIPT_OPTIONS ${arg})
-      endif()
-    else()
-      if (${IS_PATH_NAME})
-        list(APPEND SCRIPT_ARGUMENTS "${arg}")
-        list(APPEND DEPENDENCIES "${arg}")
-        set(IS_PATH_NAME false)
-      else()
-        list(APPEND SCRIPT_ARGUMENTS "${arg}")
-        set(IS_PATH_NAME true)
-      endif()
-    endif()
-  endforeach()
-
-  add_custom_command(
-    OUTPUT
-    "${TARGET_BASE}.h"
-    "${TARGET_BASE}.cpp"
-    COMMAND ${PYTHON_EXECUTABLE} ${EMBED_RESOURCES_PYTHON}
-            ${SCRIPT_OPTIONS} "${TARGET_BASE}" ${SCRIPT_ARGUMENTS}
-    DEPENDS
-    ${EMBED_RESOURCES_PYTHON}
-    ${DEPENDENCIES}
-    )
-
-  list(APPEND AUTOGENERATED_SOURCES
-    "${TARGET_BASE}.cpp"
-    ) 
-endmacro()
--- a/Resources/CMake/BoostConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,395 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_BOOST)
-  set(BOOST_STATIC 1)
-else()
-  include(FindBoost)
-
-  set(BOOST_STATIC 0)
-  #set(Boost_DEBUG 1)
-  #set(Boost_USE_STATIC_LIBS ON)
-
-  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()
-
-  
-  # Patch by xnox to fix issue #166 (CMake find_boost version is now
-  # broken with newer boost/cmake)
-  # https://bitbucket.org/sjodogne/orthanc/issues/166/
-  if (POLICY CMP0093)
-    set(BOOST144 1.44)
-  else()
-    set(BOOST144 104400)
-  endif()
-  
-  
-  # Boost releases 1.44 through 1.47 supply both V2 and V3 filesystem
-  # http://www.boost.org/doc/libs/1_46_1/libs/filesystem/v3/doc/index.htm
-  if (${Boost_VERSION} LESS ${BOOST144})
-    add_definitions(
-      -DBOOST_HAS_FILESYSTEM_V3=0
-      )
-  else()
-    add_definitions(
-      -DBOOST_HAS_FILESYSTEM_V3=1
-      -DBOOST_FILESYSTEM_VERSION=3
-      )
-  endif()
-
-  include_directories(${Boost_INCLUDE_DIRS})
-  link_libraries(${Boost_LIBRARIES})
-endif()
-
-
-if (BOOST_STATIC)
-  ##
-  ## Parameters for static compilation of Boost 
-  ##
-  
-  set(BOOST_NAME boost_1_69_0)
-  set(BOOST_VERSION 1.69.0)
-  set(BOOST_BCP_SUFFIX bcpdigest-1.5.6)
-  set(BOOST_MD5 "579bccc0ea4d1a261c1d0c5e27446c3d")
-  set(BOOST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/${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}")
-
-
-  ##
-  ## Patching boost
-  ## 
-
-  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
-    )
-
-  if (FirstRun AND Failure)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-
-
-  ##
-  ## Generic configuration of Boost
-  ## 
-
-  if (CMAKE_COMPILER_IS_GNUCXX)
-    add_definitions(-isystem ${BOOST_SOURCES_DIR})
-  endif()
-
-  include_directories(
-    BEFORE ${BOOST_SOURCES_DIR}
-    )
-
-  if (ORTHANC_BUILDING_FRAMEWORK_LIBRARY)
-    add_definitions(
-      # Packaging Boost inside the Orthanc Framework DLL
-      -DBOOST_ALL_DYN_LINK
-      -DBOOST_THREAD_BUILD_DLL
-      #-DBOOST_REGEX_BUILD_DLL
-      )
-  else()
-    add_definitions(
-      # Static build of Boost (this was the only possibility in
-      # Orthanc <= 1.7.1)
-      -DBOOST_ALL_NO_LIB 
-      -DBOOST_ALL_NOLIB 
-      -DBOOST_DATE_TIME_NO_LIB 
-      -DBOOST_THREAD_BUILD_LIB
-      -DBOOST_PROGRAM_OPTIONS_NO_LIB
-      -DBOOST_REGEX_NO_LIB
-      -DBOOST_SYSTEM_NO_LIB
-      -DBOOST_LOCALE_NO_LIB
-      )
-  endif()
-
-  add_definitions(
-    # In static builds, explicitly prevent Boost from using the system
-    # locale in lexical casts. This is notably important if
-    # "boost::lexical_cast<double>()" is applied to strings containing
-    # "," instead of "." as decimal separators. Check out function
-    # "OrthancStone::LinearAlgebra::ParseVector()".
-    -DBOOST_LEXICAL_CAST_ASSUME_C_LOCALE
-    )
-
-  set(BOOST_SOURCES
-    ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp
-    )
-
-  if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase" OR
-      "${CMAKE_SYSTEM_NAME}" STREQUAL "Android")
-    add_definitions(
-      -DBOOST_SYSTEM_USE_STRERROR=1
-      )
-  endif()
-
-  
-  ##
-  ## 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()
-
-    # Fix for error: "boost_1_69_0/boost/chrono/detail/inlined/mac/thread_clock.hpp:54:28: 
-    # error: use of undeclared identifier 'pthread_mach_thread_np'"
-    # https://github.com/envoyproxy/envoy/pull/1785
-    if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
-      add_definitions(-D_DARWIN_C_SOURCE=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
-    )
-
-
-  ##
-  ## 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()
-
-
-  ##
-  ## Configuration of boost::locale
-  ## 
-
-  if (NOT ENABLE_LOCALE)
-    message("boost::locale is disabled")
-  else()
-    set(BOOST_ICU_SOURCES
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/boundary.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/codecvt.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/collator.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/conversion.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/date_time.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/formatter.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/icu_backend.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/numeric.cpp
-      ${BOOST_SOURCES_DIR}/libs/locale/src/icu/time_zone.cpp
-      )
-
-    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")
-      add_definitions(
-        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
-        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
-        )
-      
-      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
-        )
-
-      if (BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
-          BOOST_LOCALE_BACKEND STREQUAL "libiconv")
-        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
-      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
-        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
-        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
-      else()
-        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
-      endif()
-
-    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
-      add_definitions(
-        -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
-        -DBOOST_LOCALE_NO_STD_BACKEND=1
-        )
-      
-      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
-        )
-
-      if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten" OR
-          BOOST_LOCALE_BACKEND STREQUAL "gcc" OR
-          BOOST_LOCALE_BACKEND STREQUAL "libiconv")
-        # In WebAssembly or asm.js, we rely on the version of iconv
-        # that is shipped with the stdlib
-        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
-      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
-        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
-        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
-      else()
-        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
-      endif()
-
-    elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
-      add_definitions(
-        -DBOOST_LOCALE_NO_POSIX_BACKEND=1
-        -DBOOST_LOCALE_NO_STD_BACKEND=1
-        )
-
-      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
-        )
-
-      # Starting with release 0.8.2, Orthanc statically links against
-      # libiconv on Windows. Indeed, the "WCONV" library of Windows XP
-      # seems not to support properly several codepages (notably
-      # "Latin3", "Hebrew", and "Arabic"). Set "BOOST_LOCALE_BACKEND"
-      # to "wconv" to use WCONV anyway.
-
-      if (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
-        add_definitions(-DBOOST_LOCALE_WITH_ICONV=1)
-      elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
-        add_definitions(-DBOOST_LOCALE_WITH_ICU=1)
-        list(APPEND BOOST_SOURCES ${BOOST_ICU_SOURCES})
-      elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv")
-        message("Using Window's wconv")
-        add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
-      else()
-        message(FATAL_ERROR "Unsupported value for BOOST_LOCALE_BACKEND on Windows: ${BOOST_LOCALE_BACKEND}")
-      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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-#!/bin/bash
-
-set -e
-set -u
-
-## Starting with version 0.6.2, Orthanc is shipped with a subset of the
-## Boost libraries that is generated with the BCP tool:
-##
-## http://www.boost.org/doc/libs/1_54_0/tools/bcp/doc/html/index.html
-##
-## This script generates this subset.
-##
-## 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 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 between 1.4.0 and 1.4.2: Boost 1.67.0
-##   - Orthanc between 1.5.0 and 1.5.4: Boost 1.68.0
-##   - Orthanc >= 1.5.5: Boost 1.69.0
-
-BOOST_VERSION=1_69_0
-ORTHANC_VERSION=1.5.6
-
-rm -rf /tmp/boost_${BOOST_VERSION}
-rm -rf /tmp/bcp/boost_${BOOST_VERSION}
-
-cd /tmp
-echo "Uncompressing the sources of Boost ${BOOST_VERSION}..."
-tar xfz ./boost_${BOOST_VERSION}.tar.gz 
-
-echo "Generating the subset..."
-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 chrono /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..."
-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
--- a/Resources/CMake/CivetwebConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_CIVETWEB)
-
-  ## WARNING: "civetweb-1.12.tar.gz" comes with a subfolder
-  ## "civetweb-1.12/test/nonlatin" that cannot be removed by "hg purge
-  ## --all" on Windows hosts. We thus created a custom
-  ## "civetweb-1.12-fixed.tar.gz" as follows:
-  ##
-  ##  $ cd /tmp
-  ##  $ wget http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.12.tar.gz
-  ##  $ tar xvf civetweb-1.12.tar.gz
-  ##  $ rm -rf civetweb-1.12/src/third_party/ civetweb-1.12/test/
-  ##  $ tar cvfz civetweb-1.12-fixed.tar.gz civetweb-1.12
-  ##
-  
-  set(CIVETWEB_SOURCES_DIR ${CMAKE_BINARY_DIR}/civetweb-1.12)
-  set(CIVETWEB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/civetweb-1.12-fixed.tar.gz")
-  set(CIVETWEB_MD5 "016ed7cd26cbc46b5941f0cbfb2e4ac8")
-
-  if (IS_DIRECTORY "${CIVETWEB_SOURCES_DIR}")
-    set(FirstRun OFF)
-  else()
-    set(FirstRun ON)
-  endif()
-
-  DownloadPackage(${CIVETWEB_MD5} ${CIVETWEB_URL} "${CIVETWEB_SOURCES_DIR}")
-
-  execute_process(
-    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-    ${ORTHANC_ROOT}/Resources/Patches/civetweb-1.12.patch
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-    RESULT_VARIABLE Failure
-    )
-
-  if (FirstRun AND Failure)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-  
-  include_directories(
-    ${CIVETWEB_SOURCES_DIR}/include
-    )
-
-  set(CIVETWEB_SOURCES
-    ${CIVETWEB_SOURCES_DIR}/src/civetweb.c
-    )
-
-  # New in Orthanc 1.6.0: Enable support of compression in civetweb
-  set_source_files_properties(
-    ${CIVETWEB_SOURCES}
-    PROPERTIES COMPILE_DEFINITIONS
-    "USE_ZLIB=1")
-  
-  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()
-
-  source_group(ThirdParty\\Civetweb REGULAR_EXPRESSION ${CIVETWEB_SOURCES_DIR}/.*)
-
-  add_definitions(
-    -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=1
-    )
-
-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)
-
-  add_definitions(
-    -DCIVETWEB_HAS_DISABLE_KEEP_ALIVE=0
-    )
-endif()
--- a/Resources/CMake/Compiler.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,263 +0,0 @@
-# This file sets all the compiler-related flags
-
-include(CheckLibraryExists)
-
-if ((CMAKE_CROSSCOMPILING AND NOT
-      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") 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_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-  # Cache the environment variables "LSB_CC" and "LSB_CXX" for further
-  # use by "ExternalProject" in CMake
-  SET(CMAKE_LSB_CC $ENV{LSB_CC} CACHE STRING "")
-  SET(CMAKE_LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
-endif()
-
-
-if (CMAKE_COMPILER_IS_GNUCXX)
-  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 -Wno-long-long -Wno-variadic-macros")
-
-  if (CMAKE_CROSSCOMPILING)
-    # http://stackoverflow.com/a/3543845/881731
-    set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> -O coff -I<CMAKE_CURRENT_SOURCE_DIR> <SOURCE> <OBJECT>")
-  endif()
-
-elseif (MSVC)
-  # Use static runtime under Visual Studio
-  # http://www.cmake.org/Wiki/CMake_FAQ#Dynamic_Replace
-  # http://stackoverflow.com/a/6510446
-  foreach(flag_var
-    CMAKE_C_FLAGS_DEBUG
-    CMAKE_CXX_FLAGS_DEBUG
-    CMAKE_C_FLAGS_RELEASE 
-    CMAKE_CXX_FLAGS_RELEASE
-    CMAKE_C_FLAGS_MINSIZEREL 
-    CMAKE_CXX_FLAGS_MINSIZEREL 
-    CMAKE_C_FLAGS_RELWITHDEBINFO 
-    CMAKE_CXX_FLAGS_RELWITHDEBINFO) 
-    string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
-    string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}")
-  endforeach(flag_var)
-
-  # Add /Zm256 compiler option to Visual Studio to fix PCH errors
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256")
-
-  # New in Orthanc 1.5.5
-  if (MSVC_MULTIPLE_PROCESSES)
-    # "If you omit the processMax argument in the /MP option, the
-    # compiler obtains the number of effective processors from the
-    # operating system, and then creates one process per effective
-    # processor"
-    # https://blog.kitware.com/cmake-building-with-all-your-cores/
-    # https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes
-    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
-    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
-  endif()
-    
-  add_definitions(
-    -D_CRT_SECURE_NO_WARNINGS=1
-    -D_CRT_SECURE_NO_DEPRECATE=1
-    )
-
-  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" OR
-    ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
-
-  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
-      NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
-    # The "--no-undefined" linker flag makes the shared libraries
-    # (plugins ModalityWorklists and ServeFolders) fail to compile on
-    # OpenBSD, and make the PostgreSQL plugin complain about missing
-    # "environ" global variable in 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")
-  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(pthread)
-
-  if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
-    link_libraries(rt)
-  endif()
-
-  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
-      )
-  endif()
-
-elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-  if (MSVC)
-    message("MSVC compiler version = " ${MSVC_VERSION} "\n")
-    # Starting Visual Studio 2013 (version 1800), it is not possible
-    # to target Windows XP anymore
-    if (MSVC_VERSION LESS 1800)
-      add_definitions(
-        -DWINVER=0x0501
-        -D_WIN32_WINNT=0x0501
-        )
-    endif()
-  else()
-    add_definitions(
-      -DWINVER=0x0501
-      -D_WIN32_WINNT=0x0501
-      )
-  endif()
-
-  add_definitions(
-    -D_CRT_SECURE_NO_WARNINGS=1
-    )
-  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}")
-
-    if (DYNAMIC_MINGW_STDLIB)
-    else()
-      # This is a patch for MinGW64
-      SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_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++")
-    endif()
-
-    CHECK_LIBRARY_EXISTS(winpthread pthread_create "" HAVE_WIN_PTHREAD)
-    if (HAVE_WIN_PTHREAD)
-      if (DYNAMIC_MINGW_STDLIB)
-      else()
-        # This line is necessary to compile with recent versions of MinGW,
-        # otherwise "libwinpthread-1.dll" is not statically linked.
-        SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic")
-      endif()
-      add_definitions(-DHAVE_WIN_PTHREAD=1)
-    else()
-      add_definitions(-DHAVE_WIN_PTHREAD=0)
-    endif()
-  endif()
-
-elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
-  SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -exported_symbols_list ${ORTHANC_ROOT}/Plugins/Samples/Common/ExportedSymbols.list")
-
-  add_definitions(
-    -D_XOPEN_SOURCE=1
-    )
-  link_libraries(iconv)
-
-elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-  message("Building using Emscripten (for WebAssembly or asm.js targets)")
-
-  # 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.
-  # Setting this option to "ON" fixes error: "shared:ERROR:
-  # BINARYEN_TRAP_MODE is not supported by the LLVM wasm backend" if
-  # using the "upstream" backend of Emscripten.
-  if (NOT EMSCRIPTEN_SET_LLVM_WASM_BACKEND)
-    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s BINARYEN_TRAP_MODE='\"clamp\"'")
-  endif()
-
-  set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'")
-  
-elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
-
-else()
-  message("Unknown target platform: ${CMAKE_SYSTEM_NAME}")
-  message(FATAL_ERROR "Support your platform here")
-endif()
-
-
-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_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()
-
-
-if (STATIC_BUILD)
-  add_definitions(-DORTHANC_STATIC=1)
-else()
-  add_definitions(-DORTHANC_STATIC=0)
-endif()
--- a/Resources/CMake/DcmtkConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,268 +0,0 @@
-if (NOT DEFINED ENABLE_DCMTK_NETWORKING)
-  set(ENABLE_DCMTK_NETWORKING ON)
-endif()
-
-if (STATIC_BUILD OR NOT USE_SYSTEM_DCMTK)
-  if (DCMTK_STATIC_VERSION STREQUAL "3.6.0")
-    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.0.cmake)   
-  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.2")
-    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.2.cmake)
-  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.4")
-    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.4.cmake)
-  elseif (DCMTK_STATIC_VERSION STREQUAL "3.6.5")
-    include(${CMAKE_CURRENT_LIST_DIR}/DcmtkConfigurationStatic-3.6.5.cmake)
-  else()
-    message(FATAL_ERROR "Unsupported version of DCMTK: ${DCMTK_STATIC_VERSION}")
-  endif()
-
-
-  ##
-  ## Commands shared by all versions of DCMTK
-  ##
-
-  AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmdata/libsrc DCMTK_SOURCES)
-  AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/ofstd/libsrc DCMTK_SOURCES)
-
-  LIST(REMOVE_ITEM DCMTK_SOURCES 
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdictbi.cc
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/mkdeftag.cc
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
-    )
-
-  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-    message(${DCMTK_SOURCES_DIR})
-    list(REMOVE_ITEM DCMTK_SOURCES 
-      ${DCMTK_SOURCES_DIR}/ofstd/libsrc/offilsys.cc
-      )
-  endif()
-
-  if (ENABLE_DCMTK_NETWORKING)
-    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmnet/libsrc DCMTK_SOURCES)
-    include_directories(
-      ${DCMTK_SOURCES_DIR}/dcmnet/include
-      )
-  endif()
-
-  if (ENABLE_DCMTK_TRANSCODING)
-    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimgle/libsrc DCMTK_SOURCES)
-    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmimage/libsrc DCMTK_SOURCES)
-    include_directories(
-      ${DCMTK_SOURCES_DIR}/dcmimage/include
-      )
-  endif()
-  
-  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)
-    AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/dcmjpeg/libijg16 DCMTK_SOURCES)
-    include_directories(
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/include
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg16
-      ${DCMTK_SOURCES_DIR}/dcmimgle/include
-      )
-    list(REMOVE_ITEM DCMTK_SOURCES 
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/ddpiimpl.cc
-
-      # Solves linking problem in WebAssembly: "wasm-ld: error:
-      # duplicate symbol: jaritab" (modification in Orthanc 1.5.9)
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg8/jaricom.c
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg12/jaricom.c
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libijg24/jaricom.c
-      )
-
-    if (NOT ENABLE_DCMTK_TRANSCODING)
-      list(REMOVE_ITEM DCMTK_SOURCES 
-        # 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()
-  endif()
-
-
-  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(
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/include
-      ${DCMTK_SOURCES_DIR}/dcmjpls/include
-      ${DCMTK_SOURCES_DIR}/dcmjpls/libcharls
-      )
-    list(APPEND DCMTK_SOURCES 
-      ${DCMTK_SOURCES_DIR}/dcmjpeg/libsrc/djrplol.cc
-      )
-
-    if (NOT ENABLE_DCMTK_TRANSCODING)
-      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
-        )
-    endif()
-  endif()
-
-  
-  # This fixes crashes related to the destruction of the DCMTK OFLogger
-  # http://support.dcmtk.org/docs-snapshot/file_macros.html
-  add_definitions(
-    -DLOG4CPLUS_DISABLE_FATAL=1
-    -DDCMTK_VERSION_NUMBER=${DCMTK_VERSION_NUMBER}
-    )
-
-
-  if (NOT ENABLE_DCMTK_LOG)
-    # Disable logging internal to DCMTK
-    # https://groups.google.com/d/msg/orthanc-users/v2SzzAmY948/VxT1QVGiBAAJ
-    add_definitions(
-      -DDCMTK_LOG4CPLUS_DISABLE_FATAL=1
-      -DDCMTK_LOG4CPLUS_DISABLE_ERROR=1
-      -DDCMTK_LOG4CPLUS_DISABLE_WARN=1
-      -DDCMTK_LOG4CPLUS_DISABLE_INFO=1
-      -DDCMTK_LOG4CPLUS_DISABLE_DEBUG=1
-      )
-  endif()
-
-  include_directories(
-    #${DCMTK_SOURCES_DIR}
-    ${DCMTK_SOURCES_DIR}/config/include
-    ${DCMTK_SOURCES_DIR}/ofstd/include
-    ${DCMTK_SOURCES_DIR}/oflog/include
-    ${DCMTK_SOURCES_DIR}/dcmdata/include
-    )
-
-  source_group(ThirdParty\\Dcmtk REGULAR_EXPRESSION ${DCMTK_SOURCES_DIR}/.*)
-
-  if (STANDALONE_BUILD)
-    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()
-    set(DCMTK_USE_EMBEDDED_DICTIONARIES 0)
-  endif()
-
-
-else()
-  if (CMAKE_CROSSCOMPILING AND
-      "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
-
-    CHECK_INCLUDE_FILE_CXX(dcmtk/dcmdata/dcfilefo.h HAVE_DCMTK_H)
-    if (NOT HAVE_DCMTK_H)
-      message(FATAL_ERROR "Please install the libdcmtk-dev package")
-    endif()
-
-    CHECK_LIBRARY_EXISTS(dcmdata "dcmDataDict" "" HAVE_DCMTK_LIB)
-    if (NOT HAVE_DCMTK_LIB)
-      message(FATAL_ERROR "Please install the libdcmtk package")
-    endif()  
-
-    find_path(DCMTK_INCLUDE_DIRS dcmtk/config/osconfig.h
-      /usr/include
-      )
-
-    link_libraries(dcmdata dcmnet dcmjpeg oflog ofstd)
-
-  else()
-    # The following line allows one to manually add libraries at the
-    # command-line, which is necessary for Ubuntu/Debian packages
-    set(tmp "${DCMTK_LIBRARIES}")
-    include(FindDCMTK)
-    list(APPEND DCMTK_LIBRARIES "${tmp}")
-
-    include_directories(${DCMTK_INCLUDE_DIRS})
-  endif()
-
-  add_definitions(
-    -DHAVE_CONFIG_H=1
-    )
-
-  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 libdcmtk*-dev")
-  endif()
-
-  message("DCMTK configuration file: ${DCMTK_CONFIGURATION_FILE}")
-  
-  # Autodetection of the version of DCMTK
-  file(STRINGS
-    "${DCMTK_CONFIGURATION_FILE}" 
-    DCMTK_VERSION_NUMBER1 REGEX
-    ".*PACKAGE_VERSION .*")    
-
-  string(REGEX REPLACE
-    ".*PACKAGE_VERSION.*\"([0-9]*)\\.([0-9]*)\\.([0-9]*)\"$"
-    "\\1\\2\\3" 
-    DCMTK_VERSION_NUMBER 
-    ${DCMTK_VERSION_NUMBER1})
-
-  set(DCMTK_USE_EMBEDDED_DICTIONARIES 0)
-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()
-
-    if (CMAKE_CROSSCOMPILING AND
-        "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
-      # Remove the sysroot prefix
-      file(RELATIVE_PATH tmp ${CMAKE_FIND_ROOT_PATH} ${DCMTK_DICTIONARY_DIR_AUTO})
-      set(DCMTK_DICTIONARY_DIR_AUTO /${tmp} CACHE INTERNAL "")
-    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/DcmtkConfigurationStatic-3.6.0.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-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://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.0.zip")
-SET(DCMTK_MD5 "219ad631b82031806147e4abbfba4fa4")
-
-if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
-  set(FirstRun OFF)
-else()
-  set(FirstRun ON)
-endif()
-
-DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
-
-
-if (FirstRun)
-  # 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()
-  message("The patches for DCMTK have already been applied")
-endif()
-
-
-# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
-IF (CMAKE_CROSSCOMPILING)
-  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_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})
-
-CONFIGURE_FILE(
-  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
-  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
-
-
-
-# Source for the logging facility of DCMTK
-AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
-    )
-
-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)
-    # This is a patch for DCMTK 3.6.0 and MinGW64
-    execute_process(
-      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.0-mingw64.patch
-      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-      RESULT_VARIABLE Failure
-      )
-
-    if (Failure AND FirstRun)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
-  endif()
-endif()
--- a/Resources/CMake/DcmtkConfigurationStatic-3.6.2.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,194 +0,0 @@
-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://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.2.tar.gz")
-SET(DCMTK_MD5 "d219a4152772985191c9b89d75302d12")
-
-macro(DCMTK_UNSET)
-endmacro()
-
-macro(DCMTK_UNSET_CACHE)
-endmacro()
-
-set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
-set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
-
-if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
-else()
-  set(DCMTK_WITH_THREADS ON)
-endif()
-
-add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
-
-if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
-  set(FirstRun OFF)
-else()
-  set(FirstRun ON)
-endif()
-
-DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
-
-
-if (FirstRun)
-  # "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.patch
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-    RESULT_VARIABLE Failure
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-
-  configure_file(
-    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-dcdict_orthanc.cc
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
-    COPYONLY)
-else()
-  message("The patches for DCMTK have already been applied")
-endif()
-
-
-# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
-IF (CMAKE_CROSSCOMPILING)
-  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 "")
-
-  if (FirstRun)
-    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 (Failure)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
-  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_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})
-
-CONFIGURE_FILE(
-  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
-  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
-
-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()
-
-
-# Source for the logging facility of DCMTK
-AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
-    )
-
-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
-    )
-endif()
-
-
-#set_source_files_properties(${DCMTK_SOURCES}
-#  PROPERTIES COMPILE_DEFINITIONS
-#  "PACKAGE_VERSION=\"${DCMTK_PACKAGE_VERSION}\";PACKAGE_VERSION_NUMBER=\"${DCMTK_VERSION_NUMBER}\"")
-
-
-# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
-# default since this does not seem to be appropriate (anymore) for
-# most modern operating systems. In order to change this default, the
-# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
-# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
-# be defined to change this setting at compilation time (see
-# macros.txt for details).
-# https://forum.dcmtk.org/viewtopic.php?t=4632
-add_definitions(
-  -DDISABLE_NAGLE_ALGORITHM=1
-  )
--- a/Resources/CMake/DcmtkConfigurationStatic-3.6.4.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-SET(DCMTK_VERSION_NUMBER 364)
-SET(DCMTK_PACKAGE_VERSION "3.6.4")
-SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.4)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.4.tar.gz")
-SET(DCMTK_MD5 "97597439a2ae7a39086066318db5f3bc")
-
-macro(DCMTK_UNSET)
-endmacro()
-
-macro(DCMTK_UNSET_CACHE)
-endmacro()
-
-set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
-set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
-
-if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
-else()
-  set(DCMTK_WITH_THREADS ON)
-endif()
-
-add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
-
-if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
-  set(FirstRun OFF)
-else()
-  set(FirstRun ON)
-endif()
-
-DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
-
-
-if (FirstRun)
-  # Apply the patches
-  execute_process(
-    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.4.patch
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-    RESULT_VARIABLE Failure
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-
-  configure_file(
-    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-dcdict_orthanc.cc
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
-    COPYONLY)
-else()
-  message("The patches for DCMTK have already been applied")
-endif()
-
-
-include_directories(
-  ${DCMTK_SOURCES_DIR}/dcmiod/include
-  )
-
-
-# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
-IF (CMAKE_CROSSCOMPILING)
-  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 "")
-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_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})
-
-CONFIGURE_FILE(
-  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
-  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
-
-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()
-
-
-# Source for the logging facility of DCMTK
-AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
-    )
-
-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
-    )
-endif()
-
-
-# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
-# default since this does not seem to be appropriate (anymore) for
-# most modern operating systems. In order to change this default, the
-# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
-# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
-# be defined to change this setting at compilation time (see
-# macros.txt for details).
-# https://forum.dcmtk.org/viewtopic.php?t=4632
-add_definitions(
-  -DDISABLE_NAGLE_ALGORITHM=1
-  )
--- a/Resources/CMake/DcmtkConfigurationStatic-3.6.5.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-SET(DCMTK_VERSION_NUMBER 365)
-SET(DCMTK_PACKAGE_VERSION "3.6.5")
-SET(DCMTK_SOURCES_DIR ${CMAKE_BINARY_DIR}/dcmtk-3.6.5)
-SET(DCMTK_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dcmtk-3.6.5.tar.gz")
-SET(DCMTK_MD5 "e19707f64ee5695c496b9c1e48e39d07")
-
-macro(DCMTK_UNSET)
-endmacro()
-
-macro(DCMTK_UNSET_CACHE)
-endmacro()
-
-set(DCMTK_BINARY_DIR ${DCMTK_SOURCES_DIR}/)
-set(DCMTK_CMAKE_INCLUDE ${DCMTK_SOURCES_DIR}/)
-
-if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-  set(DCMTK_WITH_THREADS OFF)  # Disable thread support in wasm/asm.js
-else()
-  set(DCMTK_WITH_THREADS ON)
-endif()
-
-add_definitions(-DDCMTK_INSIDE_LOG4CPLUS=1)
-
-if (IS_DIRECTORY "${DCMTK_SOURCES_DIR}")
-  set(FirstRun OFF)
-else()
-  set(FirstRun ON)
-endif()
-
-DownloadPackage(${DCMTK_MD5} ${DCMTK_URL} "${DCMTK_SOURCES_DIR}")
-
-
-if (FirstRun)
-  # Apply the patches
-  execute_process(
-    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-3.6.5.patch
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-    RESULT_VARIABLE Failure
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-
-  configure_file(
-    ${ORTHANC_ROOT}/Resources/Patches/dcmtk-dcdict_orthanc.cc
-    ${DCMTK_SOURCES_DIR}/dcmdata/libsrc/dcdict_orthanc.cc
-    COPYONLY)
-else()
-  message("The patches for DCMTK have already been applied")
-endif()
-
-
-include_directories(
-  ${DCMTK_SOURCES_DIR}/dcmiod/include
-  )
-
-
-# C_CHAR_UNSIGNED *must* be set before calling "GenerateDCMTKConfigure.cmake"
-IF (CMAKE_CROSSCOMPILING)
-  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 "")
-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_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})
-
-CONFIGURE_FILE(
-  ${DCMTK_SOURCES_DIR}/CMake/osconfig.h.in
-  ${DCMTK_SOURCES_DIR}/config/include/dcmtk/config/osconfig.h)
-
-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()
-
-
-# Source for the logging facility of DCMTK
-AUX_SOURCE_DIRECTORY(${DCMTK_SOURCES_DIR}/oflog/libsrc DCMTK_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" 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
-    )
-
-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
-    )
-endif()
-
-
-# Starting with DCMTK 3.6.2, the Nagle algorithm is not disabled by
-# default since this does not seem to be appropriate (anymore) for
-# most modern operating systems. In order to change this default, the
-# environment variable NO_TCPDELAY can be set to "1" (see envvars.txt
-# for details). Alternatively, the macro DISABLE_NAGLE_ALGORITHM can
-# be defined to change this setting at compilation time (see
-# macros.txt for details).
-# https://forum.dcmtk.org/viewtopic.php?t=4632
-add_definitions(
-  -DDISABLE_NAGLE_ALGORITHM=1
-  )
-
-
-if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-  # For compatibility with Windows XP, avoid using fiber-local-storage
-  # in log4cplus, but use thread-local-storage instead. Otherwise,
-  # Windows XP complains about missing "FlsGetValue()" in KERNEL32.dll
-  add_definitions(
-    -DDCMTK_LOG4CPLUS_AVOID_WIN32_FLS
-    )
-
-  if (CMAKE_COMPILER_IS_GNUCXX OR             # MinGW
-      "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")  # MSVC for 32bit (*)
-
-    # (*) With multithreaded logging enabled, Visual Studio 2008 fails
-    # with error: ".\dcmtk-3.6.5\oflog\libsrc\globinit.cc(422) : error
-    # C2664: 'dcmtk::log4cplus::thread::impl::tls_init' : cannot
-    # convert parameter 1 from 'void (__stdcall *)(void *)' to
-    # 'dcmtk::log4cplus::thread::impl::tls_init_cleanup_func_type'"
-    #   None of the functions with this name in scope match the target type
-
-    add_definitions(
-      -DDCMTK_LOG4CPLUS_SINGLE_THREADED
-      )
-  endif()
-endif()
--- a/Resources/CMake/DownloadOrthancFramework.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,497 +0,0 @@
-# Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 "system" AND
-     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 \"system\", \"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")
-      set(ORTHANC_FRAMEWORK_MAJOR 999)
-      set(ORTHANC_FRAMEWORK_MINOR 999)
-      set(ORTHANC_FRAMEWORK_REVISION 999)
-
-    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.1")
-        set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
-        set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.0")
-        set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1")
-        set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
-        set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3")
-        set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4")
-        set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5")
-        set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6")
-        set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7")
-        set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8")
-        set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0")
-        set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1")
-        set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.0")
-        set(ORTHANC_FRAMEWORK_MD5 "ce5f689e852b01d3672bd3d2f952a5ef")
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.1")
-        set(ORTHANC_FRAMEWORK_MD5 "3c171217f930abe80246997bdbcaf7cc")
-
-      # Below this point are development snapshots that were used to
-      # release some plugin, before an official release of the Orthanc
-      # framework was available. Here is the command to be used to
-      # generate a proper archive:
-      #
-      #   $ hg archive /tmp/Orthanc-`hg id -i | sed 's/\+//'`.tar.gz
-      #
-      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df")
-        # DICOMweb 1.1 (framework pre-1.6.0)
-        set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645")
-      endif()
-    endif()
-  endif()
-
-elseif (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
-  message("Using the Orthanc framework from a path of the filesystem. Assuming mainline version.")
-  set(ORTHANC_FRAMEWORK_MAJOR 999)
-  set(ORTHANC_FRAMEWORK_MINOR 999)
-  set(ORTHANC_FRAMEWORK_REVISION 999)
-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://hg.orthanc-server.com/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://orthanc.osimis.io/ThirdPartyDownloads/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()
-
-
-
-##
-## Case of the Orthanc framework installed as a shared library in a
-## GNU/Linux distribution (typically Debian)
-##
-
-if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
-  set(ORTHANC_FRAMEWORK_LIBDIR "" CACHE PATH "")
-
-  if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND
-      CMAKE_COMPILER_IS_GNUCXX) # MinGW
-    set(DYNAMIC_MINGW_STDLIB ON)   # Disable static linking against libc (to throw exceptions)
-    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++")
-    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++")
-  endif()
-  
-  include(CheckIncludeFile)
-  include(CheckIncludeFileCXX)
-  include(FindPythonInterp)
-  include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
-  include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
-  include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
-  set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py)
-
-  if (NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows" AND
-      NOT ORTHANC_FRAMEWORK_STATIC)
-    # Look for mandatory dependency JsonCpp (cf. JsonCppConfiguration.cmake)
-    find_path(JSONCPP_INCLUDE_DIR json/reader.h
-      /usr/include/jsoncpp
-      /usr/local/include/jsoncpp
-      )
-
-    message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}")
-    include_directories(${JSONCPP_INCLUDE_DIR})
-    link_libraries(jsoncpp)
-
-    CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H)
-    if (NOT HAVE_JSONCPP_H)
-      message(FATAL_ERROR "Please install the libjsoncpp-dev package")
-    endif()
-
-    # Look for mandatory dependency Boost (cf. BoostConfiguration.cmake)
-    include(FindBoost)
-    find_package(Boost COMPONENTS filesystem thread system date_time regex)
-
-    if (NOT Boost_FOUND)
-      message(FATAL_ERROR "Unable to locate Boost on this system")
-    endif()
-    
-    include_directories(${Boost_INCLUDE_DIRS})
-    link_libraries(${Boost_LIBRARIES})
-
-    # Optional component - Lua
-    if (ENABLE_LUA)
-      include(FindLua)
-
-      if (NOT LUA_FOUND)
-        message(FATAL_ERROR "Please install the liblua-dev package")
-      endif()
-
-      include_directories(${LUA_INCLUDE_DIR})
-      link_libraries(${LUA_LIBRARIES})
-    endif()
-
-    # Optional component - SQLite
-    if (ENABLE_SQLITE)    
-      CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H)
-      if (NOT HAVE_SQLITE_H)
-        message(FATAL_ERROR "Please install the libsqlite3-dev package")
-      endif()
-      link_libraries(sqlite3)
-    endif()
-  endif()
-
-  # Optional component - Google Test
-  if (ENABLE_GOOGLE_TEST)
-    set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
-    set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
-    mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
-    include(${CMAKE_CURRENT_LIST_DIR}/GoogleTestConfiguration.cmake)
-  endif()
-
-  # Look for Orthanc framework shared library
-  include(CheckCXXSymbolExists)
-
-  if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
-    set(ORTHANC_FRAMEWORK_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT})
-    include_directories(${ORTHANC_FRAMEWORK_ROOT}/..)
-  else()
-    find_path(ORTHANC_FRAMEWORK_INCLUDE_DIR OrthancFramework.h
-      /usr/include/orthanc-framework
-      /usr/local/include/orthanc-framework
-      ${ORTHANC_FRAMEWORK_ROOT}
-      )
-  endif()
-  
-  message("Orthanc framework include dir: ${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
-  include_directories(${ORTHANC_FRAMEWORK_INCLUDE_DIR})
-  
-  set(CMAKE_REQUIRED_INCLUDES "${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
-
-  if (NOT "${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "")
-    set(CMAKE_REQUIRED_LIBRARIES "-L${ORTHANC_FRAMEWORK_LIBDIR} -lOrthancFramework")
-  else()
-    set(CMAKE_REQUIRED_LIBRARIES "OrthancFramework")
-  endif()
-  
-  check_cxx_symbol_exists("Orthanc::InitializeFramework" "OrthancFramework.h" HAVE_ORTHANC_FRAMEWORK)
-  if(NOT HAVE_ORTHANC_FRAMEWORK)
-    message(FATAL_ERROR "Cannot find the Orthanc framework")
-  endif()
-
-  if (NOT "${ORTHANC_FRAMEWORK_ROOT}" STREQUAL "")
-    include_directories(${ORTHANC_FRAMEWORK_ROOT})
-  endif()
-
-  if (NOT "${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "")
-    link_directories(${ORTHANC_FRAMEWORK_LIBDIR})
-  endif()
-endif()
--- a/Resources/CMake/DownloadPackage.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-macro(GetUrlFilename TargetVariable Url)
-  string(REGEX REPLACE "^.*/" "" ${TargetVariable} "${Url}")
-endmacro()
-
-
-macro(GetUrlExtension TargetVariable Url)
-  #string(REGEX REPLACE "^.*/[^.]*\\." "" TMP "${Url}")
-  string(REGEX REPLACE "^.*\\." "" TMP "${Url}")
-  string(TOLOWER "${TMP}" "${TargetVariable}")
-endmacro()
-
-
-
-##
-## Setup the patch 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()
-
-
-
-##
-## Check the existence of the required decompression tools
-##
-
-if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
-  find_program(ZIP_EXECUTABLE 7z 
-    PATHS 
-    "$ENV{ProgramFiles}/7-Zip"
-    "$ENV{ProgramW6432}/7-Zip"
-    )
-
-  if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND")
-    message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
-  endif()
-
-else()
-  find_program(UNZIP_EXECUTABLE unzip)
-  if (${UNZIP_EXECUTABLE} MATCHES "UNZIP_EXECUTABLE-NOTFOUND")
-    message(FATAL_ERROR "Please install the 'unzip' package")
-  endif()
-
-  find_program(TAR_EXECUTABLE tar)
-  if (${TAR_EXECUTABLE} MATCHES "TAR_EXECUTABLE-NOTFOUND")
-    message(FATAL_ERROR "Please install the 'tar' package")
-  endif()
-
-  find_program(GUNZIP_EXECUTABLE gunzip)
-  if (${GUNZIP_EXECUTABLE} MATCHES "GUNZIP_EXECUTABLE-NOTFOUND")
-    message(FATAL_ERROR "Please install the 'gzip' package")
-  endif()
-endif()
-
-
-macro(DownloadFile MD5 Url)
-  GetUrlFilename(TMP_FILENAME "${Url}")
-
-  set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}")
-  if (NOT EXISTS "${TMP_PATH}")
-    message("Downloading ${Url}")
-
-    # This fixes issue 6: "I think cmake shouldn't download the
-    # packages which are not in the system, it should stop and let
-    # user know."
-    # https://code.google.com/p/orthanc/issues/detail?id=6
-    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()
-
-    if ("${MD5}" STREQUAL "no-check")
-      message(WARNING "Not checking the MD5 of: ${Url}")
-      file(DOWNLOAD "${Url}" "${TMP_PATH}"
-        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
-        STATUS Failure)
-    else()
-      file(DOWNLOAD "${Url}" "${TMP_PATH}"
-        SHOW_PROGRESS TIMEOUT 300 INACTIVITY_TIMEOUT 60
-        EXPECTED_MD5 "${MD5}" STATUS Failure)
-    endif()
-
-    list(GET Failure 0 Status)
-    if (NOT Status EQUAL 0)
-      message(FATAL_ERROR "Cannot download file: ${Url}")
-    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()
-endmacro()
-
-
-macro(DownloadPackage MD5 Url TargetDirectory)
-  if (NOT IS_DIRECTORY "${TargetDirectory}")
-    DownloadFile("${MD5}" "${Url}")
-    
-    GetUrlExtension(TMP_EXTENSION "${Url}")
-    #message(${TMP_EXTENSION})
-    message("Uncompressing ${TMP_FILENAME}")
-
-    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
-
-      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}
-          RESULT_VARIABLE Failure
-          OUTPUT_QUIET
-          )
-
-        if (Failure)
-          message(FATAL_ERROR "Error while running the uncompression tool")
-        endif()
-
-        if ("${TMP_EXTENSION}" STREQUAL "tgz")
-          string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}")
-        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(
-          COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_FILENAME2}
-          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-          RESULT_VARIABLE Failure
-          OUTPUT_QUIET
-          )
-      elseif ("${TMP_EXTENSION}" STREQUAL "zip")
-        execute_process(
-          COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_PATH}
-          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-          RESULT_VARIABLE Failure
-          OUTPUT_QUIET
-          )
-      else()
-        message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
-      endif()
-
-    else()
-      if ("${TMP_EXTENSION}" STREQUAL "zip")
-        execute_process(
-          COMMAND sh -c "${UNZIP_EXECUTABLE} -q ${TMP_PATH}"
-          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-          RESULT_VARIABLE Failure
-        )
-      elseif (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz"))
-        #message("tar xvfz ${TMP_PATH}")
-        execute_process(
-          COMMAND sh -c "${TAR_EXECUTABLE} xfz ${TMP_PATH}"
-          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-          RESULT_VARIABLE Failure
-          )
-      elseif ("${TMP_EXTENSION}" STREQUAL "bz2")
-        execute_process(
-          COMMAND sh -c "${TAR_EXECUTABLE} xfj ${TMP_PATH}"
-          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 "Unsupported package extension: ${TMP_EXTENSION}")
-      endif()
-    endif()
-   
-    if (Failure)
-      message(FATAL_ERROR "Error while running the uncompression tool")
-    endif()
-
-    if (NOT IS_DIRECTORY "${TargetDirectory}")
-      message(FATAL_ERROR "The package was not uncompressed at the proper location. Check the CMake instructions.")
-    endif()
-  endif()
-endmacro()
-
-
-
-macro(DownloadCompressedFile MD5 Url TargetFile)
-  if (NOT EXISTS "${TargetFile}")
-    DownloadFile("${MD5}" "${Url}")
-    
-    GetUrlExtension(TMP_EXTENSION "${Url}")
-    #message(${TMP_EXTENSION})
-    message("Uncompressing ${TMP_FILENAME}")
-
-    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
-
-      if ("${TMP_EXTENSION}" STREQUAL "gz")
-        execute_process(
-          # "-so" writes uncompressed file to stdout
-          COMMAND ${ZIP_EXECUTABLE} e -so -y ${TMP_PATH}
-          OUTPUT_FILE "${TargetFile}"
-          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-          RESULT_VARIABLE Failure
-          OUTPUT_QUIET
-          )
-
-        if (Failure)
-          message(FATAL_ERROR "Error while running the uncompression tool")
-        endif()
-
-      else()
-        message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
-      endif()
-
-    else()
-      if ("${TMP_EXTENSION}" STREQUAL "gz")
-        execute_process(
-          COMMAND sh -c "${GUNZIP_EXECUTABLE} -c ${TMP_PATH}"
-          OUTPUT_FILE "${TargetFile}"
-          RESULT_VARIABLE Failure
-          )
-      else()
-        message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
-      endif()
-    endif()
-   
-    if (Failure)
-      message(FATAL_ERROR "Error while running the uncompression tool")
-    endif()
-
-    if (NOT EXISTS "${TargetFile}")
-      message(FATAL_ERROR "The file was not uncompressed at the proper location. Check the CMake instructions.")
-    endif()
-  endif()
-endmacro()
--- a/Resources/CMake/GoogleTestConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-if (USE_GOOGLE_TEST_DEBIAN_PACKAGE)
-  find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR
-    NAMES src/gtest-all.cc
-    PATHS
-    ${CROSSTOOL_NG_IMAGE}/usr/src/gtest
-    ${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest
-    PATH_SUFFIXES src
-    )
-
-  find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR
-    NAMES gtest.h
-    PATHS
-    ${CROSSTOOL_NG_IMAGE}/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(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/googletest-release-1.8.1)
-  set(GOOGLE_TEST_URL "http://orthanc.osimis.io/ThirdPartyDownloads/gtest-1.8.1.tar.gz")
-  set(GOOGLE_TEST_MD5 "2e6fbeb6a91310a16efe181886c59596")
-
-  DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}")
-
-  include_directories(
-    ${GOOGLE_TEST_SOURCES_DIR}/googletest
-    ${GOOGLE_TEST_SOURCES_DIR}/googletest/include
-    ${GOOGLE_TEST_SOURCES_DIR}
-    )
-
-  set(GOOGLE_TEST_SOURCES
-    ${GOOGLE_TEST_SOURCES_DIR}/googletest/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)
-  if (NOT GTEST_FOUND)
-    message(FATAL_ERROR "Unable to find GoogleTest")
-  endif()
-
-  include_directories(${GTEST_INCLUDE_DIRS})
-
-  # 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-set(JSONCPP_CXX11 OFF)
-
-if (STATIC_BUILD OR NOT USE_SYSTEM_JSONCPP)
-  if (USE_LEGACY_JSONCPP)
-    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-0.10.7)
-    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/jsoncpp-0.10.7.tar.gz")
-    set(JSONCPP_MD5 "3a8072ca6a1fa9cbaf7715ae625f134f")
-    add_definitions(-DORTHANC_LEGACY_JSONCPP=1)
-  else()
-    set(JSONCPP_SOURCES_DIR ${CMAKE_BINARY_DIR}/jsoncpp-1.8.4)
-    set(JSONCPP_URL "http://orthanc.osimis.io/ThirdPartyDownloads/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}")
-
-  set(JSONCPP_SOURCES
-    ${JSONCPP_SOURCES_DIR}/src/lib_json/json_reader.cpp
-    ${JSONCPP_SOURCES_DIR}/src/lib_json/json_value.cpp
-    ${JSONCPP_SOURCES_DIR}/src/lib_json/json_writer.cpp
-    )
-
-  include_directories(
-    ${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()
-  find_path(JSONCPP_INCLUDE_DIR json/reader.h
-    /usr/include/jsoncpp
-    /usr/local/include/jsoncpp
-    )
-
-  message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}")
-  include_directories(${JSONCPP_INCLUDE_DIR})
-  link_libraries(jsoncpp)
-
-  CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H)
-  if (NOT HAVE_JSONCPP_H)
-    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)
-    message("Switching to C++11 standard in gcc, as version of JsonCpp is >= 1.0.0")
-    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wno-deprecated-declarations")
-  elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
-    message("Switching to C++11 standard in 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,339 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_CURL)
-  SET(CURL_SOURCES_DIR ${CMAKE_BINARY_DIR}/curl-7.64.0)
-  SET(CURL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/curl-7.64.0.tar.gz")
-  SET(CURL_MD5 "a026740d599a32bcbbe6e70679397899")
-
-  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.64.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}/.*)
-
-  add_definitions(
-    -DBUILDING_LIBCURL=1
-    -DCURL_STATICLIB=1
-    -DCURL_DISABLE_LDAPS=1
-    -DCURL_DISABLE_LDAP=1
-    -DCURL_DISABLE_DICT=1
-    -DCURL_DISABLE_FILE=1
-    -DCURL_DISABLE_FTP=1
-    -DCURL_DISABLE_GOPHER=1
-    -DCURL_DISABLE_LDAP=1
-    -DCURL_DISABLE_LDAPS=1
-    -DCURL_DISABLE_POP3=1
-    #-DCURL_DISABLE_PROXY=1
-    -DCURL_DISABLE_RTSP=1
-    -DCURL_DISABLE_TELNET=1
-    -DCURL_DISABLE_TFTP=1
-    )
-
-  if (ENABLE_SSL)
-    add_definitions(
-      #-DHAVE_LIBSSL=1
-      -DUSE_OPENSSL=1
-      -DHAVE_OPENSSL_ENGINE_H=1
-      -DUSE_SSLEAY=1
-      )
-  endif()
-
-  if (NOT EXISTS "${CURL_SOURCES_DIR}/lib/vauth/vauth/vauth.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/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" OR
-      ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
-    if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
-      SET(TMP_OS "x86_64")
-    else()
-      SET(TMP_OS "x86")
-    endif()
-
-    set_property(
-      SOURCE ${CURL_SOURCES}
-      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)
-
-    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)
-
-    list(APPEND 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
-        )
-      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()
-
-elseif (CMAKE_CROSSCOMPILING AND
-    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
-
-  CHECK_INCLUDE_FILE_CXX(curl/curl.h HAVE_CURL_H)
-  if (NOT HAVE_CURL_H)
-    message(FATAL_ERROR "Please install the libcurl-dev package")
-  endif()
-
-  CHECK_LIBRARY_EXISTS(curl "curl_easy_init" "" HAVE_CURL_LIB)
-  if (NOT HAVE_CURL_LIB)
-    message(FATAL_ERROR "Please install the libcurl package")
-  endif()  
-  
-  link_libraries(curl)
-
-else()
-  include(FindCURL)
-  include_directories(${CURL_INCLUDE_DIRS})
-  link_libraries(${CURL_LIBRARIES})
-
-  if (NOT ${CURL_FOUND})
-    message(FATAL_ERROR "Unable to find LibCurl")
-  endif()
-endif()
--- a/Resources/CMake/LibIconvConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-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://orthanc.osimis.io/ThirdPartyDownloads/libiconv-1.15.tar.gz")
-  set(LIBICONV_MD5 "ace8b5f2db42f7b3b3057585e80d9808")
-
-  DownloadPackage(${LIBICONV_MD5} ${LIBICONV_URL} "${LIBICONV_SOURCES_DIR}")
-
-  # 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)
-
-  # https://groups.google.com/d/msg/android-ndk/AS1nkxnk6m4/EQm09hD1tigJ
-  add_definitions(
-    -DBUILDING_LIBICONV=1
-    -DIN_LIBRARY=1
-    -DLIBDIR=""
-    -DICONV_CONST=
-    #-DENABLE_EXTRA=1
-    )
-
-  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)
-
-  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
-    )
-
-  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
-    )
-
-  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()
-
-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()
--- a/Resources/CMake/LibIcuConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-
-# Check out: ../ThirdParty/icu/README.txt
-
-# http://userguide.icu-project.org/packaging
-# http://userguide.icu-project.org/howtouseicu
-
-message("Using libicu")
-
-if (STATIC_BUILD OR NOT USE_SYSTEM_LIBICU)
-  include(${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/icu/Version.cmake)
-  DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
-
-  # Use the gzip-compressed data
-  DownloadFile(${LIBICU_DATA_COMPRESSED_MD5} ${LIBICU_DATA_URL})
-  set(LIBICU_RESOURCES
-    LIBICU_DATA  ${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${LIBICU_DATA}
-    )
-
-  set_source_files_properties(
-    ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
-    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
-    )
-
-  include_directories(BEFORE
-    ${LIBICU_SOURCES_DIR}/source/common
-    ${LIBICU_SOURCES_DIR}/source/i18n
-    )
-
-  aux_source_directory(${LIBICU_SOURCES_DIR}/source/common LIBICU_SOURCES)
-  aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n LIBICU_SOURCES)
-
-  add_definitions(
-    #-DU_COMBINED_IMPLEMENTATION
-    #-DU_DEF_ICUDATA_ENTRY_POINT=icudt63l_dat
-    #-DU_LIB_SUFFIX_C_NAME=l
-
-    #-DUCONFIG_NO_SERVICE=1
-    -DU_COMMON_IMPLEMENTATION
-    -DU_STATIC_IMPLEMENTATION
-    -DU_ENABLE_DYLOAD=0
-    -DU_HAVE_STD_STRING=1
-    -DU_I18N_IMPLEMENTATION
-    -DU_IO_IMPLEMENTATION
-    -DU_STATIC_IMPLEMENTATION=1
-    #-DU_CHARSET_IS_UTF8
-    -DUNISTR_FROM_STRING_EXPLICIT=
-
-    -DORTHANC_STATIC_ICU=1
-    -DORTHANC_ICU_DATA_MD5="${LIBICU_DATA_UNCOMPRESSED_MD5}"
-    )
-
-  if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
-    set_source_files_properties(
-      ${LIBICU_SOURCES_DIR}/source/common/locmap.c
-      PROPERTIES COMPILE_DEFINITIONS "LOCALE_SNAME=0x0000005c"
-      )
-  endif()
-
-  source_group(ThirdParty\\libicu REGULAR_EXPRESSION ${LIBICU_SOURCES_DIR}/.*)
-
-else() 
-  CHECK_INCLUDE_FILE_CXX(unicode/uvernum.h HAVE_ICU_H)
-  if (NOT HAVE_ICU_H)
-    message(FATAL_ERROR "Please install the libicu-dev package")
-  endif()
-
-  find_library(LIBICU_PATH_1 NAMES icuuc)
-  find_library(LIBICU_PATH_2 NAMES icui18n)
-
-  if (NOT LIBICU_PATH_1 OR 
-      NOT LIBICU_PATH_2)
-    message(FATAL_ERROR "Please install the libicu-dev package")
-  else()
-    link_libraries(${LIBICU_PATH_1} ${LIBICU_PATH_2})
-  endif()
-
-  add_definitions(
-    -DORTHANC_STATIC_ICU=0
-    )
-endif()
--- a/Resources/CMake/LibJpegConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_LIBJPEG)
-  set(LIBJPEG_SOURCES_DIR ${CMAKE_BINARY_DIR}/jpeg-9c)
-  DownloadPackage(
-    "93c62597eeef81a84d988bccbda1e990"
-    "http://orthanc.osimis.io/ThirdPartyDownloads/jpegsrc.v9c.tar.gz"
-    "${LIBJPEG_SOURCES_DIR}")
-
-  include_directories(
-    ${LIBJPEG_SOURCES_DIR}
-    )
-
-  list(APPEND LIBJPEG_SOURCES 
-    ${LIBJPEG_SOURCES_DIR}/jaricom.c
-    ${LIBJPEG_SOURCES_DIR}/jcapimin.c
-    ${LIBJPEG_SOURCES_DIR}/jcapistd.c
-    ${LIBJPEG_SOURCES_DIR}/jcarith.c
-    ${LIBJPEG_SOURCES_DIR}/jccoefct.c
-    ${LIBJPEG_SOURCES_DIR}/jccolor.c
-    ${LIBJPEG_SOURCES_DIR}/jcdctmgr.c
-    ${LIBJPEG_SOURCES_DIR}/jchuff.c
-    ${LIBJPEG_SOURCES_DIR}/jcinit.c
-    ${LIBJPEG_SOURCES_DIR}/jcmarker.c
-    ${LIBJPEG_SOURCES_DIR}/jcmaster.c
-    ${LIBJPEG_SOURCES_DIR}/jcomapi.c
-    ${LIBJPEG_SOURCES_DIR}/jcparam.c
-    ${LIBJPEG_SOURCES_DIR}/jcprepct.c
-    ${LIBJPEG_SOURCES_DIR}/jcsample.c
-    ${LIBJPEG_SOURCES_DIR}/jctrans.c
-    ${LIBJPEG_SOURCES_DIR}/jdapimin.c
-    ${LIBJPEG_SOURCES_DIR}/jdapistd.c
-    ${LIBJPEG_SOURCES_DIR}/jdarith.c
-    ${LIBJPEG_SOURCES_DIR}/jdatadst.c
-    ${LIBJPEG_SOURCES_DIR}/jdatasrc.c
-    ${LIBJPEG_SOURCES_DIR}/jdcoefct.c
-    ${LIBJPEG_SOURCES_DIR}/jdcolor.c
-    ${LIBJPEG_SOURCES_DIR}/jddctmgr.c
-    ${LIBJPEG_SOURCES_DIR}/jdhuff.c
-    ${LIBJPEG_SOURCES_DIR}/jdinput.c
-    ${LIBJPEG_SOURCES_DIR}/jcmainct.c
-    ${LIBJPEG_SOURCES_DIR}/jdmainct.c
-    ${LIBJPEG_SOURCES_DIR}/jdmarker.c
-    ${LIBJPEG_SOURCES_DIR}/jdmaster.c
-    ${LIBJPEG_SOURCES_DIR}/jdmerge.c
-    ${LIBJPEG_SOURCES_DIR}/jdpostct.c
-    ${LIBJPEG_SOURCES_DIR}/jdsample.c
-    ${LIBJPEG_SOURCES_DIR}/jdtrans.c
-    ${LIBJPEG_SOURCES_DIR}/jerror.c
-    ${LIBJPEG_SOURCES_DIR}/jfdctflt.c
-    ${LIBJPEG_SOURCES_DIR}/jfdctfst.c
-    ${LIBJPEG_SOURCES_DIR}/jfdctint.c
-    ${LIBJPEG_SOURCES_DIR}/jidctflt.c
-    ${LIBJPEG_SOURCES_DIR}/jidctfst.c
-    ${LIBJPEG_SOURCES_DIR}/jidctint.c
-    #${LIBJPEG_SOURCES_DIR}/jmemansi.c
-    #${LIBJPEG_SOURCES_DIR}/jmemdos.c
-    #${LIBJPEG_SOURCES_DIR}/jmemmac.c
-    ${LIBJPEG_SOURCES_DIR}/jmemmgr.c
-    #${LIBJPEG_SOURCES_DIR}/jmemname.c
-    ${LIBJPEG_SOURCES_DIR}/jmemnobs.c
-    ${LIBJPEG_SOURCES_DIR}/jquant1.c
-    ${LIBJPEG_SOURCES_DIR}/jquant2.c
-    ${LIBJPEG_SOURCES_DIR}/jutils.c
-
-    # ${LIBJPEG_SOURCES_DIR}/rdbmp.c
-    # ${LIBJPEG_SOURCES_DIR}/rdcolmap.c
-    # ${LIBJPEG_SOURCES_DIR}/rdgif.c
-    # ${LIBJPEG_SOURCES_DIR}/rdppm.c
-    # ${LIBJPEG_SOURCES_DIR}/rdrle.c
-    # ${LIBJPEG_SOURCES_DIR}/rdswitch.c
-    # ${LIBJPEG_SOURCES_DIR}/rdtarga.c
-    # ${LIBJPEG_SOURCES_DIR}/transupp.c
-    # ${LIBJPEG_SOURCES_DIR}/wrbmp.c
-    # ${LIBJPEG_SOURCES_DIR}/wrgif.c
-    # ${LIBJPEG_SOURCES_DIR}/wrppm.c
-    # ${LIBJPEG_SOURCES_DIR}/wrrle.c
-    # ${LIBJPEG_SOURCES_DIR}/wrtarga.c
-    )
-
-  configure_file(
-    ${LIBJPEG_SOURCES_DIR}/jconfig.txt
-    ${LIBJPEG_SOURCES_DIR}/jconfig.h COPYONLY
-    )
-
-  source_group(ThirdParty\\libjpeg REGULAR_EXPRESSION ${LIBJPEG_SOURCES_DIR}/.*)
-
-else()
-  include(FindJPEG)
-
-  if (NOT ${JPEG_FOUND})
-    message(FATAL_ERROR "Unable to find libjpeg")
-  endif()
-
-  include_directories(${JPEG_INCLUDE_DIR})
-  link_libraries(${JPEG_LIBRARIES})
-endif()
--- a/Resources/CMake/LibP11Configuration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,72 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_LIBP11)
-  SET(LIBP11_SOURCES_DIR ${CMAKE_BINARY_DIR}/libp11-0.4.0)
-  SET(LIBP11_URL "http://orthanc.osimis.io/ThirdPartyDownloads/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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_LIBPNG)
-  SET(LIBPNG_SOURCES_DIR ${CMAKE_BINARY_DIR}/libpng-1.6.36)
-  SET(LIBPNG_URL "http://orthanc.osimis.io/ThirdPartyDownloads/libpng-1.6.36.tar.gz")
-  SET(LIBPNG_MD5 "65afdeaa05f5ec14e31d9276143012e9")
-
-  DownloadPackage(${LIBPNG_MD5} ${LIBPNG_URL} "${LIBPNG_SOURCES_DIR}")
-
-  include_directories(
-    ${LIBPNG_SOURCES_DIR}
-    )
-
-  configure_file(
-    ${LIBPNG_SOURCES_DIR}/scripts/pnglibconf.h.prebuilt
-    ${LIBPNG_SOURCES_DIR}/pnglibconf.h
-    )
-
-  set(LIBPNG_SOURCES
-    #${LIBPNG_SOURCES_DIR}/example.c
-    ${LIBPNG_SOURCES_DIR}/png.c
-    ${LIBPNG_SOURCES_DIR}/pngerror.c
-    ${LIBPNG_SOURCES_DIR}/pngget.c
-    ${LIBPNG_SOURCES_DIR}/pngmem.c
-    ${LIBPNG_SOURCES_DIR}/pngpread.c
-    ${LIBPNG_SOURCES_DIR}/pngread.c
-    ${LIBPNG_SOURCES_DIR}/pngrio.c
-    ${LIBPNG_SOURCES_DIR}/pngrtran.c
-    ${LIBPNG_SOURCES_DIR}/pngrutil.c
-    ${LIBPNG_SOURCES_DIR}/pngset.c
-    #${LIBPNG_SOURCES_DIR}/pngtest.c
-    ${LIBPNG_SOURCES_DIR}/pngtrans.c
-    ${LIBPNG_SOURCES_DIR}/pngwio.c
-    ${LIBPNG_SOURCES_DIR}/pngwrite.c
-    ${LIBPNG_SOURCES_DIR}/pngwtran.c
-    ${LIBPNG_SOURCES_DIR}/pngwutil.c
-    )
-
-  add_definitions(
-    -DPNG_NO_CONFIG_H=1
-    -DPNG_NO_CONSOLE_IO=1
-    -DPNG_NO_STDIO=1
-    # The following declaration avoids "__declspec(dllexport)" in
-    # libpng to prevent publicly exposing its symbols by the DLLs
-    -DPNG_IMPEXP=
-    )
-
-  source_group(ThirdParty\\libpng REGULAR_EXPRESSION ${LIBPNG_SOURCES_DIR}/.*)
-
-else()
-  include(FindPNG)
-
-  if (NOT ${PNG_FOUND})
-    message(FATAL_ERROR "Unable to find libpng")
-  endif()
-
-  include_directories(${PNG_INCLUDE_DIRS})
-  link_libraries(${PNG_LIBRARIES})
-  add_definitions(${PNG_DEFINITIONS})
-endif()
--- a/Resources/CMake/LuaConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,138 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_LUA)
-  SET(LUA_SOURCES_DIR ${CMAKE_BINARY_DIR}/lua-5.3.5)
-  SET(LUA_MD5 "4f4b4f323fd3514a68e0ab3da8ce3455")
-  SET(LUA_URL "http://orthanc.osimis.io/ThirdPartyDownloads/lua-5.3.5.tar.gz")
-
-  DownloadPackage(${LUA_MD5} ${LUA_URL} "${LUA_SOURCES_DIR}")
-
-  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")
-
-      if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
-          ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
-        add_definitions(-DLUA_USE_LINUX=1)
-      elseif (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
-        add_definitions(
-          -DLUA_USE_LINUX=1
-          -DLUA_USE_READLINE=1
-          )
-      elseif (${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
-        add_definitions(-DLUA_USE_POSIX=1)
-      endif()
-
-    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(
-        -DLUA_USE_MACOSX=1
-        -DLUA_DL_DYLD=1       # Enable loading of shared libraries (for Apple OS X)
-        )
-      
-    else()
-      message(FATAL_ERROR "Support your platform here")
-    endif()
-  endif()
-
-  add_definitions(
-    -DLUA_COMPAT_5_2=1
-    )
-
-  include_directories(
-    ${LUA_SOURCES_DIR}/src
-    )
-
-  set(LUA_SOURCES
-    # Don't compile the Lua command-line
-    #${LUA_SOURCES_DIR}/src/lua.c
-    #${LUA_SOURCES_DIR}/src/luac.c
-
-    # Core Lua
-    ${LUA_SOURCES_DIR}/src/lapi.c
-    ${LUA_SOURCES_DIR}/src/lcode.c
-    ${LUA_SOURCES_DIR}/src/lctype.c
-    ${LUA_SOURCES_DIR}/src/ldebug.c
-    ${LUA_SOURCES_DIR}/src/ldo.c
-    ${LUA_SOURCES_DIR}/src/ldump.c
-    ${LUA_SOURCES_DIR}/src/lfunc.c
-    ${LUA_SOURCES_DIR}/src/lgc.c
-    ${LUA_SOURCES_DIR}/src/llex.c
-    ${LUA_SOURCES_DIR}/src/lmem.c
-    ${LUA_SOURCES_DIR}/src/lobject.c
-    ${LUA_SOURCES_DIR}/src/lopcodes.c
-    ${LUA_SOURCES_DIR}/src/lparser.c
-    ${LUA_SOURCES_DIR}/src/lstate.c
-    ${LUA_SOURCES_DIR}/src/lstring.c
-    ${LUA_SOURCES_DIR}/src/ltable.c
-    ${LUA_SOURCES_DIR}/src/ltm.c
-    ${LUA_SOURCES_DIR}/src/lundump.c
-    ${LUA_SOURCES_DIR}/src/lvm.c
-    ${LUA_SOURCES_DIR}/src/lzio.c
-
-    # Base Lua modules
-    ${LUA_SOURCES_DIR}/src/lauxlib.c
-    ${LUA_SOURCES_DIR}/src/lbaselib.c
-    ${LUA_SOURCES_DIR}/src/lbitlib.c
-    ${LUA_SOURCES_DIR}/src/lcorolib.c
-    ${LUA_SOURCES_DIR}/src/ldblib.c
-    ${LUA_SOURCES_DIR}/src/liolib.c
-    ${LUA_SOURCES_DIR}/src/lmathlib.c
-    ${LUA_SOURCES_DIR}/src/loadlib.c
-    ${LUA_SOURCES_DIR}/src/loslib.c
-    ${LUA_SOURCES_DIR}/src/lstrlib.c
-    ${LUA_SOURCES_DIR}/src/ltablib.c
-    ${LUA_SOURCES_DIR}/src/lutf8lib.c
-
-    ${LUA_SOURCES_DIR}/src/linit.c
-    )
-
-  source_group(ThirdParty\\Lua REGULAR_EXPRESSION ${LUA_SOURCES_DIR}/.*)
-
-elseif (CMAKE_CROSSCOMPILING AND
-    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
-
-  set(LUA_VERSIONS 5.3 5.2 5.1)
-
-  unset(LUA_VERSION)
-  foreach(version IN ITEMS ${LUA_VERSIONS})
-    CHECK_INCLUDE_FILE(lua${version}/lua.h HAVE_LUA${version}_H)
-    if (HAVE_LUA${version}_H)
-      set(LUA_VERSION ${version})
-      break()
-    endif()
-  endforeach()
-
-  if (NOT LUA_VERSION)
-    message(FATAL_ERROR "Please install the liblua-dev package")
-  endif()
-  
-  CHECK_LIBRARY_EXISTS(lua${LUA_VERSION} "lua_call" "${LUA_LIB_DIR}" HAVE_LUA_LIB)
-  if (NOT HAVE_LUA_LIB)
-    message(FATAL_ERROR "Please install the liblua package")
-  endif()  
-
-  include_directories(${CROSSTOOL_NG_IMAGE}/usr/include/lua${LUA_VERSION})
-  link_libraries(lua${LUA_VERSION})
-
-else()
-  include(FindLua)
-
-  if (NOT LUA_FOUND)
-    message(FATAL_ERROR "Please install the liblua-dev package")
-  endif()
-
-  include_directories(${LUA_INCLUDE_DIR})
-  link_libraries(${LUA_LIBRARIES})
-endif()
--- a/Resources/CMake/MongooseConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,95 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_MONGOOSE)
-  SET(MONGOOSE_SOURCES_DIR ${CMAKE_BINARY_DIR}/mongoose)
-
-  if (IS_DIRECTORY "${MONGOOSE_SOURCES_DIR}")
-    set(FirstRun OFF)
-  else()
-    set(FirstRun ON)
-  endif()
-
-  if (0)
-    # Use Mongoose 3.1
-    DownloadPackage(
-      "e718fc287b4eb1bd523be3fa00942bb0"
-      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.1.tgz"
-      "${MONGOOSE_SOURCES_DIR}")
-    
-    add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
-    set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.1-patch.diff)
-
-  else() 
-    # Use Mongoose 3.8
-    DownloadPackage(
-      "7e3296295072792cdc3c633f9404e0c3"
-      "http://orthanc.osimis.io/ThirdPartyDownloads/mongoose-3.8.tgz"
-      "${MONGOOSE_SOURCES_DIR}")
-    
-    add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
-    set(MONGOOSE_PATCH ${ORTHANC_ROOT}/Resources/Patches/mongoose-3.8-patch.diff)
-  endif()
-
-  # Patch mongoose
-  execute_process(
-    COMMAND ${PATCH_EXECUTABLE} -N mongoose.c 
-    INPUT_FILE ${MONGOOSE_PATCH}
-    WORKING_DIRECTORY ${MONGOOSE_SOURCES_DIR}
-    RESULT_VARIABLE Failure
-    )
-
-  if (Failure AND FirstRun)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-
-  include_directories(
-    ${MONGOOSE_SOURCES_DIR}
-    )
-
-  set(MONGOOSE_SOURCES
-    ${MONGOOSE_SOURCES_DIR}/mongoose.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 mongoose
-      )
-  endif()
-
-
-  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-    if (CMAKE_COMPILER_IS_GNUCXX)
-      # This is a patch for MinGW64
-      add_definitions(-D_TIMESPEC_DEFINED=1)
-    endif()
-  endif()
-
-  source_group(ThirdParty\\Mongoose REGULAR_EXPRESSION ${MONGOOSE_SOURCES_DIR}/.*)
-
-else()
-  CHECK_INCLUDE_FILE_CXX(mongoose.h HAVE_MONGOOSE_H)
-  if (NOT HAVE_MONGOOSE_H)
-    message(FATAL_ERROR "Please install the mongoose-devel package")
-  endif()
-
-  CHECK_LIBRARY_EXISTS(mongoose mg_start "" HAVE_MONGOOSE_LIB)
-  if (NOT HAVE_MONGOOSE_LIB)
-    message(FATAL_ERROR "Please install the mongoose-devel package")
-  endif()
-
-  if (SYSTEM_MONGOOSE_USE_CALLBACKS)
-    add_definitions(-DMONGOOSE_USE_CALLBACKS=1)
-  else()
-    add_definitions(-DMONGOOSE_USE_CALLBACKS=0)
-  endif()
-
-  link_libraries(mongoose)
-endif()
--- a/Resources/CMake/OpenSslConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_OPENSSL)
-  if (OPENSSL_STATIC_VERSION STREQUAL "1.0.2")
-    include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfigurationStatic-1.0.2.cmake)
-  elseif (OPENSSL_STATIC_VERSION STREQUAL "1.1.1")
-    include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfigurationStatic-1.1.1.cmake)
-  else()
-    message(FATAL_ERROR "Unsupported version of OpenSSL: ${OPENSSL_STATIC_VERSION}")
-  endif()
-
-  source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*)
-
-elseif (CMAKE_CROSSCOMPILING AND
-    "${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg")
-
-  CHECK_INCLUDE_FILE_CXX(openssl/opensslv.h HAVE_OPENSSL_H)
-  if (NOT HAVE_OPENSSL_H)
-    message(FATAL_ERROR "Please install the libopenssl-dev package")
-  endif()
-
-  CHECK_LIBRARY_EXISTS(crypto "OPENSSL_init" "" HAVE_OPENSSL_CRYPTO_LIB)
-  if (NOT HAVE_OPENSSL_CRYPTO_LIB)
-    message(FATAL_ERROR "Please install the libopenssl package")
-  endif()  
-  
-  CHECK_LIBRARY_EXISTS(ssl "SSL_library_init" "" HAVE_OPENSSL_SSL_LIB)
-  if (NOT HAVE_OPENSSL_SSL_LIB)
-    message(FATAL_ERROR "Please install the libopenssl package")
-  endif()  
-  
-  link_libraries(crypto ssl)
-
-else()
-  include(FindOpenSSL)
-
-  if (NOT ${OPENSSL_FOUND})
-    message(FATAL_ERROR "Unable to find OpenSSL")
-  endif()
-
-  include_directories(${OPENSSL_INCLUDE_DIR})
-  link_libraries(${OPENSSL_LIBRARIES})
-endif()
--- a/Resources/CMake/OpenSslConfigurationStatic-1.0.2.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,332 +0,0 @@
-SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.0.2p)
-SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.0.2p.tar.gz")
-SET(OPENSSL_MD5 "ac5eb30bf5798aa14b1ae6d0e7da58df")
-
-if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
-  set(FirstRun OFF)
-else()
-  set(FirstRun ON)
-endif()
-
-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
-  -DOPENSSL_NO_ASM
-  -DOPENSSL_NO_DYNAMIC_ENGINE
-  -DNO_WINDOWS_BRAINDEATH
-
-  -DOPENSSL_NO_BF 
-  -DOPENSSL_NO_CAMELLIA
-  -DOPENSSL_NO_CAST 
-  -DOPENSSL_NO_EC_NISTP_64_GCC_128
-  -DOPENSSL_NO_GMP
-  -DOPENSSL_NO_GOST
-  -DOPENSSL_NO_HW
-  -DOPENSSL_NO_JPAKE
-  -DOPENSSL_NO_IDEA
-  -DOPENSSL_NO_KRB5 
-  -DOPENSSL_NO_MD2 
-  -DOPENSSL_NO_MDC2 
-  #-DOPENSSL_NO_MD4   # MD4 is necessary for MariaDB/MySQL client
-  -DOPENSSL_NO_RC2 
-  -DOPENSSL_NO_RC4 
-  -DOPENSSL_NO_RC5 
-  -DOPENSSL_NO_RFC3779
-  -DOPENSSL_NO_SCTP
-  -DOPENSSL_NO_STORE
-  -DOPENSSL_NO_SEED
-  -DOPENSSL_NO_WHIRLPOOL
-  -DOPENSSL_NO_RIPEMD
-  )
-
-include_directories(
-  ${OPENSSL_SOURCES_DIR}
-  ${OPENSSL_SOURCES_DIR}/crypto
-  ${OPENSSL_SOURCES_DIR}/crypto/asn1
-  ${OPENSSL_SOURCES_DIR}/crypto/modes
-  ${OPENSSL_SOURCES_DIR}/crypto/evp
-  ${OPENSSL_SOURCES_DIR}/include
-  )
-
-set(OPENSSL_SOURCES_SUBDIRS
-  ${OPENSSL_SOURCES_DIR}/crypto
-  ${OPENSSL_SOURCES_DIR}/crypto/aes
-  ${OPENSSL_SOURCES_DIR}/crypto/asn1
-  ${OPENSSL_SOURCES_DIR}/crypto/bio
-  ${OPENSSL_SOURCES_DIR}/crypto/bn
-  ${OPENSSL_SOURCES_DIR}/crypto/buffer
-  ${OPENSSL_SOURCES_DIR}/crypto/cmac
-  ${OPENSSL_SOURCES_DIR}/crypto/cms
-  ${OPENSSL_SOURCES_DIR}/crypto/comp
-  ${OPENSSL_SOURCES_DIR}/crypto/conf
-  ${OPENSSL_SOURCES_DIR}/crypto/des
-  ${OPENSSL_SOURCES_DIR}/crypto/dh
-  ${OPENSSL_SOURCES_DIR}/crypto/dsa
-  ${OPENSSL_SOURCES_DIR}/crypto/dso
-  ${OPENSSL_SOURCES_DIR}/crypto/engine
-  ${OPENSSL_SOURCES_DIR}/crypto/err
-  ${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
-  ${OPENSSL_SOURCES_DIR}/crypto/ocsp
-  ${OPENSSL_SOURCES_DIR}/crypto/pem
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs12
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7
-  ${OPENSSL_SOURCES_DIR}/crypto/pqueue
-  ${OPENSSL_SOURCES_DIR}/crypto/rand
-  ${OPENSSL_SOURCES_DIR}/crypto/rsa
-  ${OPENSSL_SOURCES_DIR}/crypto/sha
-  ${OPENSSL_SOURCES_DIR}/crypto/srp
-  ${OPENSSL_SOURCES_DIR}/crypto/stack
-  ${OPENSSL_SOURCES_DIR}/crypto/ts
-  ${OPENSSL_SOURCES_DIR}/crypto/txt_db
-  ${OPENSSL_SOURCES_DIR}/crypto/ui
-  ${OPENSSL_SOURCES_DIR}/crypto/x509
-  ${OPENSSL_SOURCES_DIR}/crypto/x509v3
-  ${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()
-
-list(REMOVE_ITEM OPENSSL_SOURCES
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_unix.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_vms.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c
-  ${OPENSSL_SOURCES_DIR}/crypto/armcap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bf/bfs.cpp
-  ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_rtcp.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bn/exp.c
-  ${OPENSSL_SOURCES_DIR}/crypto/conf/cnf_save.c
-  ${OPENSSL_SOURCES_DIR}/crypto/conf/test.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/des.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/des3s.cpp
-  ${OPENSSL_SOURCES_DIR}/crypto/des/des_opts.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/dess.cpp
-  ${OPENSSL_SOURCES_DIR}/crypto/des/read_pwd.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/speed.c
-  ${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
-  ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/rand/randtest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/x509v3/tabtest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/x509v3/v3conf.c
-  ${OPENSSL_SOURCES_DIR}/ssl/ssl_task.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_nyi.c
-  ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_dgram.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bn/bntest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bn/expspeed.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bn/exptest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/engine/enginetest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/evp/evp_test.c
-  ${OPENSSL_SOURCES_DIR}/crypto/hmac/hmactest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/md5/md5.c
-  ${OPENSSL_SOURCES_DIR}/crypto/md5/md5test.c
-  ${OPENSSL_SOURCES_DIR}/crypto/o_dir_test.c
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/dec.c
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/enc.c
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/sign.c
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/verify.c
-  ${OPENSSL_SOURCES_DIR}/crypto/rsa/rsa_test.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha1.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha1t.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha1test.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha256t.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/sha512t.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sha/shatest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/srp/srptest.c
-
-  ${OPENSSL_SOURCES_DIR}/crypto/bn/divtest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bn/bnspeed.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/destest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/dh/p192.c
-  ${OPENSSL_SOURCES_DIR}/crypto/dh/p512.c
-  ${OPENSSL_SOURCES_DIR}/crypto/dh/p1024.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/rpw.c
-  ${OPENSSL_SOURCES_DIR}/ssl/ssltest.c
-  ${OPENSSL_SOURCES_DIR}/crypto/dsa/dsagen.c
-  ${OPENSSL_SOURCES_DIR}/crypto/dsa/dsatest.c
-  ${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")
-
-  if (ENABLE_OPENSSL_ENGINES)
-    link_libraries(crypt32)
-  endif()
-endif()
--- a/Resources/CMake/OpenSslConfigurationStatic-1.1.1.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-SET(OPENSSL_SOURCES_DIR ${CMAKE_BINARY_DIR}/openssl-1.1.1g)
-SET(OPENSSL_URL "http://orthanc.osimis.io/ThirdPartyDownloads/openssl-1.1.1g.tar.gz")
-SET(OPENSSL_MD5 "76766e98997660138cdaf13a187bd234")
-
-if (IS_DIRECTORY "${OPENSSL_SOURCES_DIR}")
-  set(FirstRun OFF)
-else()
-  set(FirstRun ON)
-endif()
-
-DownloadPackage(${OPENSSL_MD5} ${OPENSSL_URL} "${OPENSSL_SOURCES_DIR}")
-
-if (FirstRun)
-  file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/buildinf.h "
-#define DATE \"\"
-#define PLATFORM \"\"
-#define compiler_flags \"\"
-")
-  file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/bn_conf.h "")
-  file(WRITE ${OPENSSL_SOURCES_DIR}/crypto/dso_conf.h "")
-
-  configure_file(
-    ${ORTHANC_ROOT}/Resources/Patches/openssl-1.1.1-conf.h.in
-    ${OPENSSL_SOURCES_DIR}/include/openssl/opensslconf.h
-    )
-
-  # Apply the patches
-  execute_process(
-    COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-    ${ORTHANC_ROOT}/Resources/Patches/openssl-1.1.1g.patch
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-    RESULT_VARIABLE Failure
-    )
-
-  if (Failure)
-    message(FATAL_ERROR "Error while patching a file")
-  endif()
-else()
-  message("The patches for OpenSSL have already been applied")
-endif()
-
-add_definitions(
-  -DOPENSSL_THREADS
-  -DOPENSSL_IA32_SSE2
-  -DOPENSSL_NO_ASM
-  -DOPENSSL_NO_DYNAMIC_ENGINE
-  -DOPENSSL_NO_DEVCRYPTOENG
-
-  -DOPENSSL_NO_BF 
-  -DOPENSSL_NO_CAMELLIA
-  -DOPENSSL_NO_CAST 
-  -DOPENSSL_NO_EC_NISTP_64_GCC_128
-  -DOPENSSL_NO_GMP
-  -DOPENSSL_NO_GOST
-  -DOPENSSL_NO_HW
-  -DOPENSSL_NO_JPAKE
-  -DOPENSSL_NO_IDEA
-  -DOPENSSL_NO_KRB5 
-  -DOPENSSL_NO_MD2 
-  -DOPENSSL_NO_MDC2 
-  #-DOPENSSL_NO_MD4   # MD4 is necessary for MariaDB/MySQL client
-  -DOPENSSL_NO_RC2 
-  -DOPENSSL_NO_RC4 
-  -DOPENSSL_NO_RC5 
-  -DOPENSSL_NO_RFC3779
-  -DOPENSSL_NO_SCTP
-  -DOPENSSL_NO_STORE
-  -DOPENSSL_NO_SEED
-  -DOPENSSL_NO_WHIRLPOOL
-  -DOPENSSL_NO_RIPEMD
-  -DOPENSSL_NO_AFALGENG
-
-  -DOPENSSLDIR="/usr/local/ssl"
-  )
-
-
-include_directories(
-  ${OPENSSL_SOURCES_DIR}
-  ${OPENSSL_SOURCES_DIR}/crypto
-  ${OPENSSL_SOURCES_DIR}/crypto/asn1
-  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448
-  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32
-  ${OPENSSL_SOURCES_DIR}/crypto/evp
-  ${OPENSSL_SOURCES_DIR}/crypto/include
-  ${OPENSSL_SOURCES_DIR}/crypto/modes
-  ${OPENSSL_SOURCES_DIR}/include
-  )
-
-
-set(OPENSSL_SOURCES_SUBDIRS
-  ${OPENSSL_SOURCES_DIR}/crypto
-  ${OPENSSL_SOURCES_DIR}/crypto/aes
-  ${OPENSSL_SOURCES_DIR}/crypto/aria
-  ${OPENSSL_SOURCES_DIR}/crypto/asn1
-  ${OPENSSL_SOURCES_DIR}/crypto/async
-  ${OPENSSL_SOURCES_DIR}/crypto/async/arch
-  ${OPENSSL_SOURCES_DIR}/crypto/bio
-  ${OPENSSL_SOURCES_DIR}/crypto/blake2
-  ${OPENSSL_SOURCES_DIR}/crypto/bn
-  ${OPENSSL_SOURCES_DIR}/crypto/buffer
-  ${OPENSSL_SOURCES_DIR}/crypto/chacha
-  ${OPENSSL_SOURCES_DIR}/crypto/cmac
-  ${OPENSSL_SOURCES_DIR}/crypto/cms
-  ${OPENSSL_SOURCES_DIR}/crypto/comp
-  ${OPENSSL_SOURCES_DIR}/crypto/conf
-  ${OPENSSL_SOURCES_DIR}/crypto/ct
-  ${OPENSSL_SOURCES_DIR}/crypto/des
-  ${OPENSSL_SOURCES_DIR}/crypto/dh
-  ${OPENSSL_SOURCES_DIR}/crypto/dsa
-  ${OPENSSL_SOURCES_DIR}/crypto/dso
-  ${OPENSSL_SOURCES_DIR}/crypto/ec
-  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448
-  ${OPENSSL_SOURCES_DIR}/crypto/ec/curve448/arch_32
-  ${OPENSSL_SOURCES_DIR}/crypto/err
-  ${OPENSSL_SOURCES_DIR}/crypto/evp
-  ${OPENSSL_SOURCES_DIR}/crypto/hmac
-  ${OPENSSL_SOURCES_DIR}/crypto/kdf
-  ${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
-  ${OPENSSL_SOURCES_DIR}/crypto/ocsp
-  ${OPENSSL_SOURCES_DIR}/crypto/pem
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs12
-  ${OPENSSL_SOURCES_DIR}/crypto/pkcs7
-  ${OPENSSL_SOURCES_DIR}/crypto/poly1305
-  ${OPENSSL_SOURCES_DIR}/crypto/pqueue
-  ${OPENSSL_SOURCES_DIR}/crypto/rand
-  ${OPENSSL_SOURCES_DIR}/crypto/ripemd
-  ${OPENSSL_SOURCES_DIR}/crypto/rsa
-  ${OPENSSL_SOURCES_DIR}/crypto/sha
-  ${OPENSSL_SOURCES_DIR}/crypto/siphash
-  ${OPENSSL_SOURCES_DIR}/crypto/sm2
-  ${OPENSSL_SOURCES_DIR}/crypto/sm3
-  ${OPENSSL_SOURCES_DIR}/crypto/sm4
-  ${OPENSSL_SOURCES_DIR}/crypto/srp
-  ${OPENSSL_SOURCES_DIR}/crypto/stack
-  ${OPENSSL_SOURCES_DIR}/crypto/store
-  ${OPENSSL_SOURCES_DIR}/crypto/ts
-  ${OPENSSL_SOURCES_DIR}/crypto/txt_db
-  ${OPENSSL_SOURCES_DIR}/crypto/ui
-  ${OPENSSL_SOURCES_DIR}/crypto/x509
-  ${OPENSSL_SOURCES_DIR}/crypto/x509v3
-  ${OPENSSL_SOURCES_DIR}/ssl
-  ${OPENSSL_SOURCES_DIR}/ssl/record
-  ${OPENSSL_SOURCES_DIR}/ssl/statem
-  )
-
-if (ENABLE_OPENSSL_ENGINES)
-  add_definitions(
-    #-DENGINESDIR="/usr/local/lib/engines-1.1"  # On GNU/Linux
-    -DENGINESDIR="."
-    )
-
-  list(APPEND OPENSSL_SOURCES_SUBDIRS
-    ${OPENSSL_SOURCES_DIR}/engines
-    ${OPENSSL_SOURCES_DIR}/crypto/engine
-    )
-else()
-  add_definitions(-DOPENSSL_NO_ENGINE)
-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()
-
-list(REMOVE_ITEM OPENSSL_SOURCES
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_nyi.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_unix.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_vms.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_win32.c
-  ${OPENSSL_SOURCES_DIR}/crypto/LPdir_wince.c
-  ${OPENSSL_SOURCES_DIR}/crypto/aes/aes_x86core.c
-  ${OPENSSL_SOURCES_DIR}/crypto/armcap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/bio/bss_dgram.c
-  ${OPENSSL_SOURCES_DIR}/crypto/des/ncbc_enc.c
-  ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256.c
-  ${OPENSSL_SOURCES_DIR}/crypto/ec/ecp_nistz256_table.c
-  ${OPENSSL_SOURCES_DIR}/crypto/engine/eng_devcrypto.c
-  ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_base2_44.c  # Cannot be compiled with MinGW
-  ${OPENSSL_SOURCES_DIR}/crypto/poly1305/poly1305_ieee754.c  # Cannot be compiled with MinGW
-  ${OPENSSL_SOURCES_DIR}/crypto/ppccap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/s390xcap.c
-  ${OPENSSL_SOURCES_DIR}/crypto/sparcv9cap.c
-  ${OPENSSL_SOURCES_DIR}/engines/e_afalg.c  # Cannot be compiled with MinGW
-  )
-
-# Check out "${OPENSSL_SOURCES_DIR}/Configurations/README": "This is
-# default if no option is specified, it works on any supported
-# system." It is mandatory to define it as a macro, as it is used by
-# all the source files that include OpenSSL (e.g. "Core/Toolbox.cpp"
-# or curl)
-add_definitions(-DTHIRTY_TWO_BIT)
-
-
-if (NOT CMAKE_COMPILER_IS_GNUCXX OR
-    "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows" OR
-    "${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-  # Disable the use of a gcc extension, that is neither available on
-  # MinGW, nor on LSB
-  add_definitions(
-    -DOPENSSL_NO_CRYPTO_MDEBUG_BACKTRACE
-    )
-endif()
-
-
-if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
-  set(OPENSSL_DEFINITIONS
-    "${OPENSSL_DEFINITIONS};OPENSSL_SYSNAME_WIN32;SO_WIN32;WIN32_LEAN_AND_MEAN;L_ENDIAN;NO_WINDOWS_BRAINDEATH")
-  
-  if (ENABLE_OPENSSL_ENGINES)
-    link_libraries(crypt32)
-  endif()
-
-  add_definitions(
-    -DOPENSSL_RAND_SEED_OS  # ${OPENSSL_SOURCES_DIR}/crypto/rand/rand_win.c
-    )
- 
-elseif ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
-  add_definitions(
-    # In order for "crypto/mem_sec.c" to compile on LSB
-    -DOPENSSL_NO_SECURE_MEMORY
-
-    # The "OPENSSL_RAND_SEED_OS" value implies a syscall() to
-    # "__NR_getrandom" (i.e. system call "getentropy(2)") in
-    # "rand_unix.c", which is not available in LSB.
-    -DOPENSSL_RAND_SEED_DEVRANDOM
-    )
-
-else()
-  # Fixes error "OpenSSL error: error:2406C06E:random number
-  # generator:RAND_DRBG_instantiate:error retrieving entropy" that was
-  # present in Orthanc 1.6.0, if statically linking on Ubuntu 18.04
-  add_definitions(
-    -DOPENSSL_RAND_SEED_OS
-    )
-endif()
-
-
-set_source_files_properties(
-  ${OPENSSL_SOURCES}
-    PROPERTIES COMPILE_DEFINITIONS
-    "${OPENSSL_DEFINITIONS};DSO_NONE"
-    )
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,705 +0,0 @@
-##
-## 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(OPENSSL_STATIC_VERSION 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(BOOST_LOCALE_BACKEND 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
-    -DORTHANC_ENABLE_DCMTK_TRANSCODING=0
-    )
-  unset(DCMTK_DICTIONARY_DIR CACHE)
-  unset(DCMTK_VERSION CACHE)
-  unset(USE_DCMTK_362_PRIVATE_DIC CACHE)
-  unset(USE_SYSTEM_DCMTK CACHE)
-  unset(ENABLE_DCMTK_JPEG CACHE)
-  unset(ENABLE_DCMTK_JPEG_LOSSLESS CACHE)
-  unset(DCMTK_STATIC_VERSION CACHE)
-  unset(ENABLE_DCMTK_LOG CACHE)
-endif()
-
-
-#####################################################################
-## List of source files
-#####################################################################
-
-set(ORTHANC_CORE_SOURCES_INTERNAL
-  ${ORTHANC_ROOT}/Core/Cache/MemoryCache.cpp
-  ${ORTHANC_ROOT}/Core/Cache/MemoryObjectCache.cpp
-  ${ORTHANC_ROOT}/Core/Cache/MemoryStringCache.cpp
-  ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
-  ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
-  ${ORTHANC_ROOT}/Core/EnumerationDictionary.h
-  ${ORTHANC_ROOT}/Core/Enumerations.cpp
-  ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp
-  ${ORTHANC_ROOT}/Core/HttpServer/MultipartStreamReader.cpp
-  ${ORTHANC_ROOT}/Core/HttpServer/StringMatcher.cpp
-  ${ORTHANC_ROOT}/Core/Logging.cpp
-  ${ORTHANC_ROOT}/Core/OrthancFramework.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
-      )
-    set(ORTHANC_ENABLE_CIVETWEB 1)
-  else()
-    include(${CMAKE_CURRENT_LIST_DIR}/MongooseConfiguration.cmake)
-  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/HttpServer.cpp
-    ${ORTHANC_ROOT}/Core/HttpServer/HttpStreamTranscoder.cpp
-    ${ORTHANC_ROOT}/Core/HttpServer/HttpToolbox.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()
-
-if (ORTHANC_ENABLE_CIVETWEB)
-  add_definitions(-DORTHANC_ENABLE_CIVETWEB=1)
-else()
-  add_definitions(-DORTHANC_ENABLE_CIVETWEB=0)
-endif()
-
-if (ORTHANC_ENABLE_MONGOOSE)
-  add_definitions(-DORTHANC_ENABLE_MONGOOSE=1)
-else()
-  add_definitions(-DORTHANC_ENABLE_MONGOOSE=0)
-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
-##
-
-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(BOOST_LOCALE_BACKEND CACHE)
-  else()
-    if (BOOST_LOCALE_BACKEND STREQUAL "gcc")
-    elseif (BOOST_LOCALE_BACKEND STREQUAL "libiconv")
-      include(${CMAKE_CURRENT_LIST_DIR}/LibIconvConfiguration.cmake)
-    elseif (BOOST_LOCALE_BACKEND STREQUAL "icu")
-      include(${CMAKE_CURRENT_LIST_DIR}/LibIcuConfiguration.cmake)
-    elseif (BOOST_LOCALE_BACKEND STREQUAL "wconv")
-      message("Using Microsoft Window's wconv")
-    else()
-      message(FATAL_ERROR "Invalid value for BOOST_LOCALE_BACKEND: ${BOOST_LOCALE_BACKEND}")
-    endif()
-  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 one 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/DicomWebJsonVisitor.cpp
-    ${ORTHANC_ROOT}/Core/DicomParsing/FromDcmtkBridge.cpp
-    ${ORTHANC_ROOT}/Core/DicomParsing/ParsedDicomDir.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/DicomAssociation.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomAssociationParameters.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomControlUserConnection.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/DicomStoreUserConnection.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/GetScp.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()
-
-  # New in Orthanc 1.6.0
-  if (ENABLE_DCMTK_TRANSCODING)
-    add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=1)
-    list(APPEND ORTHANC_DICOM_SOURCES_INTERNAL
-      ${ORTHANC_ROOT}/Core/DicomParsing/DcmtkTranscoder.cpp
-      ${ORTHANC_ROOT}/Core/DicomParsing/IDicomTranscoder.cpp
-      ${ORTHANC_ROOT}/Core/DicomParsing/MemoryBufferTranscoder.cpp
-      )
-  else()
-    add_definitions(-DORTHANC_ENABLE_DCMTK_TRANSCODING=0)
-  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_BUILDING_FRAMEWORK_LIBRARY)
-  add_definitions(-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=1)
-else()
-  add_definitions(-DORTHANC_BUILDING_FRAMEWORK_LIBRARY=0)
-endif()
-
-
-if (ORTHANC_SANDBOXED)
-  add_definitions(
-    -DORTHANC_SANDBOXED=1
-    )
-
-  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-    set(ORTHANC_ENABLE_LOGGING ON)
-    set(ORTHANC_ENABLE_LOGGING_STDIO ON)
-  else()
-    set(ORTHANC_ENABLE_LOGGING OFF)
-  endif()
-  
-else()
-  set(ORTHANC_ENABLE_LOGGING ON)
-  set(ORTHANC_ENABLE_LOGGING_STDIO OFF)
-
-  add_definitions(
-    -DORTHANC_SANDBOXED=0
-    )
-
-  list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
-    ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp
-    ${ORTHANC_ROOT}/Core/FileBuffer.cpp
-    ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp
-    ${ORTHANC_ROOT}/Core/MetricsRegistry.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 (ORTHANC_ENABLE_LOGGING)
-  add_definitions(-DORTHANC_ENABLE_LOGGING=1)
-else()
-  add_definitions(-DORTHANC_ENABLE_LOGGING=0)
-endif()
-
-if (ORTHANC_ENABLE_LOGGING_STDIO)
-  add_definitions(-DORTHANC_ENABLE_LOGGING_STDIO=1)
-else()
-  add_definitions(-DORTHANC_ENABLE_LOGGING_STDIO=0)
-endif()
-
-
-
-#####################################################################
-## Configuration of Orthanc versioning macros (new in Orthanc 1.5.0)
-#####################################################################
-
-if (ORTHANC_VERSION STREQUAL "mainline")
-  set(ORTHANC_VERSION_MAJOR "999")
-  set(ORTHANC_VERSION_MINOR "999")
-  set(ORTHANC_VERSION_REVISION "999")
-else()
-  string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\1" ORTHANC_VERSION_MAJOR    ${ORTHANC_VERSION})
-  string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\2" ORTHANC_VERSION_MINOR    ${ORTHANC_VERSION})
-  string(REGEX REPLACE "^([0-9]*)\\.([0-9]*)\\.([0-9]*)$" "\\3" ORTHANC_VERSION_REVISION ${ORTHANC_VERSION})
-
-  if (NOT ORTHANC_VERSION STREQUAL
-      "${ORTHANC_VERSION_MAJOR}.${ORTHANC_VERSION_MINOR}.${ORTHANC_VERSION_REVISION}")
-    message(FATAL_ERROR "Error in the (x.y.z) format of the Orthanc version: ${ORTHANC_VERSION}")
-  endif()
-endif()
-
-add_definitions(
-  -DORTHANC_VERSION_MAJOR=${ORTHANC_VERSION_MAJOR}
-  -DORTHANC_VERSION_MINOR=${ORTHANC_VERSION_MINOR}
-  -DORTHANC_VERSION_REVISION=${ORTHANC_VERSION_REVISION}
-  )
-
-
-
-#####################################################################
-## 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}
-  ${LIBICU_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()
-
-
-if (NOT "${LIBICU_RESOURCES}" STREQUAL "" OR
-    NOT "${DCMTK_DICTIONARIES}" STREQUAL "")
-  EmbedResources(
-    --namespace=Orthanc.FrameworkResources
-    --target=OrthancFrameworkResources
-    --framework-path=${ORTHANC_ROOT}/Core
-    ${LIBICU_RESOURCES}
-    ${DCMTK_DICTIONARIES}
-    )
-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()
--- a/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-#####################################################################
-## 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 "7")
-
-
-#####################################################################
-## 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)")
-
-# Generic parameters of the build
-set(ENABLE_CIVETWEB ON CACHE BOOL "Use Civetweb instead of Mongoose (Mongoose was the default embedded HTTP server in Orthanc <= 1.5.1)")
-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_LIBICU ON CACHE BOOL "Use the system version of libicu")
-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(DCMTK_STATIC_VERSION "3.6.5" CACHE STRING "Version of DCMTK to be used in static builds (can be \"3.6.0\", \"3.6.2\", \"3.6.4\", or \"3.6.5\")")
-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_LOG ON CACHE BOOL "Enable logging internal to 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(BOOST_LOCALE_BACKEND "libiconv" CACHE STRING "Back-end for locales that is used by Boost (can be \"gcc\", \"libiconv\", \"icu\", or \"wconv\" on Windows)")
-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 LSB and old versions of Visual Studio)")
-set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
-set(MSVC_MULTIPLE_PROCESSES OFF CACHE BOOL "Add the /MP option to build with multiple processes if using Visual Studio")
-set(EMSCRIPTEN_SET_LLVM_WASM_BACKEND OFF CACHE BOOL "Sets the compiler flags required to use the LLVM Web Assembly backend in emscripten")
-set(OPENSSL_STATIC_VERSION "1.1.1" CACHE STRING "Version of OpenSSL to be used in static builds (can be \"1.0.2\", or \"1.1.1\")")
-
-mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
-mark_as_advanced(SYSTEM_MONGOOSE_USE_CALLBACKS)
-mark_as_advanced(USE_PUGIXML)
-mark_as_advanced(USE_DCMTK_362_PRIVATE_DIC)
-
-
-#####################################################################
-## 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_DCMTK_TRANSCODING OFF CACHE INTERNAL "Enable DICOM transcoding in DCMTK")
-set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL")
-
-set(ORTHANC_SANDBOXED OFF CACHE INTERNAL
-  "Whether Orthanc runs inside a sandboxed environment (such as Google NaCl or WebAssembly)")
-
-set(ORTHANC_BUILDING_FRAMEWORK_LIBRARY OFF CACHE INTERNAL
-  "Whether we are in the process of building the Orthanc Framework shared library")
-
-#
-# 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_PUGIXML)
-  set(PUGIXML_SOURCES_DIR ${CMAKE_BINARY_DIR}/pugixml-1.9)
-  set(PUGIXML_MD5 "7286ee2ed11376b6b780ced19fae0b64")
-  set(PUGIXML_URL "http://orthanc.osimis.io/ThirdPartyDownloads/pugixml-1.9.tar.gz")
-
-  DownloadPackage(${PUGIXML_MD5} ${PUGIXML_URL} "${PUGIXML_SOURCES_DIR}")
-
-  include_directories(
-    ${PUGIXML_SOURCES_DIR}/src
-    )
-
-  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)
-endif()
--- a/Resources/CMake/SQLiteConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-if (APPLE)
-  # Under OS X, the binaries must always be linked against the
-  # system-wide version of SQLite. Otherwise, if some Orthanc plugin
-  # also uses its own version of SQLite (such as orthanc-webviewer),
-  # this results in a crash in "sqlite3_mutex_enter(db->mutex);" (the
-  # mutex is not initialized), probably because the EXE and the DYNLIB
-  # share the same memory location for this mutex.
-  set(SQLITE_STATIC OFF)
-
-elseif (STATIC_BUILD OR NOT USE_SYSTEM_SQLITE)
-  set(SQLITE_STATIC ON)
-else()
-  set(SQLITE_STATIC OFF)
-endif()
-
-
-if (SQLITE_STATIC)
-  SET(SQLITE_SOURCES_DIR ${CMAKE_BINARY_DIR}/sqlite-amalgamation-3270100)
-  SET(SQLITE_MD5 "16717b26358ba81f0bfdac07addc77da")
-  SET(SQLITE_URL "http://orthanc.osimis.io/ThirdPartyDownloads/sqlite-amalgamation-3270100.zip")
-
-  set(ORTHANC_SQLITE_VERSION 3027001)
-
-  DownloadPackage(${SQLITE_MD5} ${SQLITE_URL} "${SQLITE_SOURCES_DIR}")
-
-  set(SQLITE_SOURCES
-    ${SQLITE_SOURCES_DIR}/sqlite3.c
-    )
-
-  add_definitions(
-    # For SQLite to run in the "Serialized" thread-safe mode
-    # http://www.sqlite.org/threadsafe.html
-    -DSQLITE_THREADSAFE=1  
-    -DSQLITE_OMIT_LOAD_EXTENSION  # Disable SQLite plugins
-    )
-
-  include_directories(
-    ${SQLITE_SOURCES_DIR}
-    )
-
-  source_group(ThirdParty\\SQLite REGULAR_EXPRESSION ${SQLITE_SOURCES_DIR}/.*)
-
-else()
-  CHECK_INCLUDE_FILE(sqlite3.h HAVE_SQLITE_H)
-  if (NOT HAVE_SQLITE_H)
-    message(FATAL_ERROR "Please install the libsqlite3-dev package")
-  endif()
-
-  find_path(SQLITE_INCLUDE_DIR
-    NAMES sqlite3.h
-    PATHS
-    /usr/include
-    /usr/local/include
-    )
-  message("SQLite include dir: ${SQLITE_INCLUDE_DIR}")
-
-  # 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_NUMBER2 ${SQLITE_VERSION_NUMBER1})
-
-  # Remove the trailing spaces to convert the string to a proper integer
-  string(STRIP ${SQLITE_VERSION_NUMBER2} ORTHANC_SQLITE_VERSION)
-
-  message("Detected version of SQLite: ${ORTHANC_SQLITE_VERSION}")
-
-  IF (${ORTHANC_SQLITE_VERSION} LESS 3007000)
-    # "sqlite3_create_function_v2" is not defined in SQLite < 3.7.0
-    message(FATAL_ERROR "SQLite version must be above 3.7.0. Please set the CMake variable USE_SYSTEM_SQLITE to OFF.")
-  ENDIF()
-
-  link_libraries(sqlite3)
-endif()
-
-
-add_definitions(
-  -DORTHANC_SQLITE_VERSION=${ORTHANC_SQLITE_VERSION}
-  )
--- a/Resources/CMake/Uninstall.cmake.in	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-# Code taken from the CMake FAQ
-# http://www.cmake.org/Wiki/CMake_FAQ#Can_I_do_.22make_uninstall.22_with_CMake.3F
-
-if (NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
-  message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"")
-endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt")
-
-file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files)
-string(REGEX REPLACE "\n" ";" files "${files}")
-list(REVERSE files)
-foreach (file ${files})
-  message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"")
-  if (EXISTS "$ENV{DESTDIR}${file}")
-    execute_process(
-      COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}"
-      OUTPUT_VARIABLE rm_out
-      RESULT_VARIABLE rm_retval
-      )
-    if(NOT ${rm_retval} EQUAL 0)
-      message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"")
-    endif (NOT ${rm_retval} EQUAL 0)
-  else (EXISTS "$ENV{DESTDIR}${file}")
-    message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.")
-  endif (EXISTS "$ENV{DESTDIR}${file}")
-endforeach(file)
--- a/Resources/CMake/UuidConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-
-  if (STATIC_BUILD OR NOT USE_SYSTEM_UUID)
-    SET(E2FSPROGS_SOURCES_DIR ${CMAKE_BINARY_DIR}/e2fsprogs-1.44.5)
-    SET(E2FSPROGS_URL "http://orthanc.osimis.io/ThirdPartyDownloads/e2fsprogs-1.44.5.tar.gz")
-    SET(E2FSPROGS_MD5 "8d78b11d04d26c0b2dd149529441fa80")
-
-    if (IS_DIRECTORY "${E2FSPROGS_SOURCES_DIR}")
-      set(FirstRun OFF)
-    else()
-      set(FirstRun ON)
-    endif()
-
-    DownloadPackage(${E2FSPROGS_MD5} ${E2FSPROGS_URL} "${E2FSPROGS_SOURCES_DIR}")
-
-    
-    ##
-    ## Patch for OS X, in order to be compatible with Cocoa (used in Stone)
-    ## 
-
-    execute_process(
-      COMMAND ${PATCH_EXECUTABLE} -p0 -N -i
-      ${ORTHANC_ROOT}/Resources/Patches/e2fsprogs-1.44.5-apple.patch
-      WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-      RESULT_VARIABLE Failure
-      )
-
-    if (FirstRun AND Failure)
-      message(FATAL_ERROR "Error while patching a file")
-    endif()
-
-
-    include_directories(
-      BEFORE ${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()
-
-    find_library(LIBUUID uuid
-      PATHS
-      /usr/lib
-      /usr/local/lib
-      )
-
-    check_library_exists(${LIBUUID} uuid_generate_random "" HAVE_LIBUUID)
-    if (NOT HAVE_LIBUUID)
-      message(FATAL_ERROR "Unable to find the uuid library")
-    endif()
-    
-    link_libraries(${LIBUUID})
-  endif()
-
-endif()
--- a/Resources/CMake/VisualStudioPrecompiledHeaders.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-macro(ADD_VISUAL_STUDIO_PRECOMPILED_HEADERS PrecompiledHeaders PrecompiledSource Sources Target)
-  get_filename_component(PrecompiledBasename ${PrecompiledHeaders} NAME_WE)
-  set(PrecompiledBinary "${PrecompiledBasename}_${CMAKE_BUILD_TYPE}_${CMAKE_GENERATOR_PLATFORM}.pch")
-
-  set_source_files_properties(${PrecompiledSource}
-    PROPERTIES COMPILE_FLAGS "/Yc\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
-    OBJECT_OUTPUTS "${PrecompiledBinary}")
-
-  set_source_files_properties(${${Sources}}
-    PROPERTIES COMPILE_FLAGS "/Yu\"${PrecompiledHeaders}\" /FI\"${PrecompiledHeaders}\" /Fp\"${PrecompiledBinary}\""
-    OBJECT_DEPENDS "${PrecompiledBinary}")
-
-  set(${Target} ${PrecompiledSource})
-endmacro()
--- a/Resources/CMake/ZlibConfiguration.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-if (STATIC_BUILD OR NOT USE_SYSTEM_ZLIB)
-  SET(ZLIB_SOURCES_DIR ${CMAKE_BINARY_DIR}/zlib-1.2.11)
-  SET(ZLIB_URL "http://orthanc.osimis.io/ThirdPartyDownloads/zlib-1.2.11.tar.gz")
-  SET(ZLIB_MD5 "1c9f62f0778697a09d36121ead88e08e")
-
-  DownloadPackage(${ZLIB_MD5} ${ZLIB_URL} "${ZLIB_SOURCES_DIR}")
-
-  include_directories(
-    ${ZLIB_SOURCES_DIR}
-    )
-
-  list(APPEND ZLIB_SOURCES 
-    ${ZLIB_SOURCES_DIR}/adler32.c
-    ${ZLIB_SOURCES_DIR}/compress.c
-    ${ZLIB_SOURCES_DIR}/crc32.c 
-    ${ZLIB_SOURCES_DIR}/deflate.c 
-    ${ZLIB_SOURCES_DIR}/infback.c 
-    ${ZLIB_SOURCES_DIR}/inffast.c 
-    ${ZLIB_SOURCES_DIR}/inflate.c 
-    ${ZLIB_SOURCES_DIR}/inftrees.c 
-    ${ZLIB_SOURCES_DIR}/trees.c 
-    ${ZLIB_SOURCES_DIR}/uncompr.c 
-    ${ZLIB_SOURCES_DIR}/zutil.c
-    )
-
-  if (NOT ORTHANC_SANDBOXED)
-    # The source files below require access to the filesystem
-    list(APPEND ZLIB_SOURCES
-      ${ZLIB_SOURCES_DIR}/gzlib.c 
-      ${ZLIB_SOURCES_DIR}/gzclose.c 
-      ${ZLIB_SOURCES_DIR}/gzread.c 
-      ${ZLIB_SOURCES_DIR}/gzwrite.c 
-      )
-  endif()
-
-  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()
--- a/Resources/Configuration.json	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,576 +0,0 @@
-{
-  /**
-   * General configuration of Orthanc
-   **/
-
-  // The logical name of this instance of Orthanc. This one is
-  // 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). 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.
-  "IndexDirectory" : "OrthancStorage",
-
-  // Path to the directory where Orthanc stores its large temporary
-  // files. The content of this folder can be safely deleted if
-  // Orthanc once stopped. The folder must exist. The corresponding
-  // filesystem must be properly sized, given that for instance a ZIP
-  // archive of DICOM images created by a job can weight several GBs,
-  // and that there might be up to "min(JobsHistorySize,
-  // MediaArchiveSize)" archives to be stored simultaneously. If not
-  // set, Orthanc will use the default temporary folder of the
-  // operating system (such as "/tmp/" on UNIX-like systems, or
-  // "C:/Temp" on Microsoft Windows).
-  // "TemporaryDirectory" : "/tmp/Orthanc/",
-
-  // Enable the transparent compression of the DICOM instances
-  "StorageCompression" : false,
-
-  // Maximum size of the storage in MB (a value of "0" indicates no
-  // limit on the storage size)
-  "MaximumStorageSize" : 0,
-
-  // Maximum number of patients that can be stored at a given time
-  // in the storage (a value of "0" indicates no limit on the number
-  // of patients)
-  "MaximumPatientCount" : 0,
-  
-  // List of paths to the custom Lua scripts that are to be loaded
-  // into this instance of Orthanc
-  "LuaScripts" : [
-  ],
-
-  // List of paths to the plugins that are to be loaded into this
-  // 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. 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,
-
-
-  /**
-   * Configuration of the HTTP server
-   **/
-
-  // Enable the HTTP server. If this parameter is set to "false",
-  // Orthanc acts as a pure DICOM server. The REST API and Orthanc
-  // Explorer will not be available.
-  "HttpServerEnabled" : true,
-
-  // HTTP port for the REST services and for the GUI
-  "HttpPort" : 8042,
-
-  // When the following option is "true", if an error is encountered
-  // while calling the REST API, a JSON message describing the error
-  // is put in the HTTP answer. This feature can be disabled if the
-  // HTTP client does not properly handles such answers.
-  "HttpDescribeErrors" : true,
-
-  // Enable HTTP compression to improve network bandwidth utilization,
-  // at the expense of more computations on the server. Orthanc
-  // supports the "gzip" and "deflate" HTTP encodings.
-  "HttpCompressionEnabled" : true,
-
-
-
-  /**
-   * Configuration of the DICOM server
-   **/
-
-  // Enable the DICOM server. If this parameter is set to "false",
-  // Orthanc acts as a pure REST server. It will not be possible to
-  // receive files or to do query/retrieve through the DICOM protocol.
-  "DicomServerEnabled" : true,
-
-  // The DICOM Application Entity Title (cannot be longer than 16
-  // characters)
-  "DicomAet" : "ORTHANC",
-
-  // 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, 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", "Chinese", "JapaneseKanji", "Korean", and
-  // "SimplifiedChinese".
-  "DefaultEncoding" : "Latin1",
-
-  // The transfer syntaxes that are accepted by Orthanc C-Store SCP
-  "DeflatedTransferSyntaxAccepted"     : true,
-  "JpegTransferSyntaxAccepted"         : true,
-  "Jpeg2000TransferSyntaxAccepted"     : true,
-  "JpegLosslessTransferSyntaxAccepted" : true,
-  "JpipTransferSyntaxAccepted"         : true,
-  "Mpeg2TransferSyntaxAccepted"        : true,
-  "RleTransferSyntaxAccepted"          : true,
-  "Mpeg4TransferSyntaxAccepted"        : true,  // New in Orthanc 1.6.0
-
-  // 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,
-
-
-
-  /**
-   * Security-related options for the HTTP server
-   **/
-
-  // Whether remote hosts can connect to the HTTP server
-  "RemoteAccessAllowed" : false,
-
-  // Whether or not SSL is enabled
-  "SslEnabled" : false,
-
-  // 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 (using HTTP
-  // basic access authentication). Starting with Orthanc 1.5.8, if
-  // "AuthenticationEnabled" is not explicitly set, authentication is
-  // enabled iff. remote access is allowed (i.e. the default value of
-  // "AuthenticationEnabled" equals that of "RemoteAccessAllowed").
-  /**
-     "AuthenticationEnabled" : false,
-   **/
-
-  // The list of the registered users. Because Orthanc uses HTTP
-  // Basic Authentication, the passwords are stored as plain text.
-  "RegisteredUsers" : {
-    // "alice" : "alicePassword"
-  },
-
-
-
-  /**
-   * Network topology
-   **/
-
-  // The list of the known DICOM modalities
-  "DicomModalities" : {
-    /**
-     * Uncommenting the following line would enable Orthanc to
-     * connect to an instance of the "storescp" open-source DICOM
-     * store (shipped in the DCMTK distribution), as started by the
-     * command line "storescp 2000". The first parameter is the
-     * AET of the remote modality (cannot be longer than 16 
-     * characters), the second one is the remote network address,
-     * and the third one is the TCP port number corresponding
-     * to the DICOM protocol on the remote modality (usually 104).
-     **/
-    // "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),
-     * - "Vitrea",
-     * - "GE" (Enterprise Archive, MRI consoles and Advantage Workstation
-     *   from GE Healthcare).
-     *
-     * This parameter is case-sensitive.
-     **/
-    // "vitrea" : [ "VITREA", "192.168.1.1", 104, "Vitrea" ]
-
-    /**
-     * By default, the Orthanc SCP accepts all DICOM commands (C-ECHO,
-     * C-STORE, C-FIND, C-MOVE, C-GET and storage commitment) issued by the
-     * registered remote SCU modalities. Starting with Orthanc 1.5.0,
-     * it is possible to specify which DICOM commands are allowed,
-     * separately for each remote modality, using the syntax
-     * below.
-     *
-     * The "AllowEcho" (resp.  "AllowStore") option only has an effect
-     * respectively if global option "DicomAlwaysAllowEcho"
-     * (resp. "DicomAlwaysAllowStore") is set to "false".
-     *
-     * Starting with Orthanc 1.7.0, "AllowTranscoding" can be used to
-     * disable the transcoding to uncompressed transfer syntaxes if
-     * the remote modality doesn't support compressed transfer
-     * syntaxes. This option only has an effect if global option
-     * "EnableTranscoding" is set to "true".
-     **/
-    //"untrusted" : {
-    //  "AET" : "ORTHANC",
-    //  "Port" : 104,
-    //  "Host" : "127.0.0.1",
-    //  "Manufacturer" : "Generic",
-    //  "AllowEcho" : false,
-    //  "AllowFind" : false,
-    //  "AllowGet"  : false,
-    //  "AllowMove" : false,
-    //  "AllowStore" : true,
-    //  "AllowStorageCommitment" : false,  // new in 1.6.0
-    //  "AllowTranscoding" : true          // new in 1.7.0
-    //}
-  },
-
-  // Whether to store the DICOM modalities in the Orthanc database
-  // instead of in this configuration file (new in Orthanc 1.5.0)
-  "DicomModalitiesInDatabase" : false,
-
-  // 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" : {
-    /**
-     * Each line gives the base URL of an Orthanc peer, possibly
-     * followed by the username/password pair (if the password
-     * protection is enabled on the peer).
-     **/
-    // "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 one 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
-    // }
-  },
-
-  // Whether to store the Orthanc peers in the Orthanc database
-  // instead of in this configuration file (new in Orthanc 1.5.0)
-  "OrthancPeersInDatabase" : false,
-
-  // Parameters of the HTTP proxy to be used by Orthanc. If set to the
-  // empty string, no HTTP proxy is used. For instance:
-  //   "HttpProxy" : "192.168.0.1:3128"
-  //   "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" : 60,
-
-  // 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,
-
-  // Path to the CA (certification authority) certificates to validate
-  // peers in HTTPS requests. From curl documentation ("--cacert"
-  // option): "Tells curl to use the specified certificate file to
-  // verify the peers. The file may contain multiple CA
-  // certificates. The certificate(s) must be in PEM format." On
-  // Debian-based systems, this option can be set to
-  // "/etc/ssl/certs/ca-certificates.crt"
-  "HttpsCACertificates" : "",
-
-
-
-  /**
-   * Advanced options
-   **/
-
-  // Dictionary of symbolic names for the user-defined metadata. Each
-  // 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 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
-  // patient, a study or a series is considered as stable.
-  "StableAge" : 60,
-
-  // By default, Orthanc compares AET (Application Entity Titles) in a
-  // case-insensitive way. Setting this option to "true" will enable
-  // case-sensitive matching.
-  "StrictAetComparison" : false,
-
-  // When the following option is "true", the MD5 of the DICOM files
-  // will be computed and stored in the Orthanc database. This
-  // information can be used to detect disk corruption, at the price
-  // of a small performance overhead.
-  "StoreMD5ForAttachments" : true,
-
-  // The maximum number of results for a single C-FIND request at the
-  // Patient, Study or Series level. Setting this option to "0" means
-  // no limit.
-  "LimitFindResults" : 0,
-
-  // The maximum number of results for a single C-FIND request at the
-  // Instance level. Setting this option to "0" means no limit.
-  "LimitFindInstances" : 0,
-
-  // The maximum number of active jobs in the Orthanc scheduler. When
-  // this limit is reached, the addition of new jobs is blocked until
-  // some job finishes.
-  "LimitJobs" : 10,
-
-  // 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 (persistent HTTP
-  // connections). Setting this option to "true" prevents Orthanc
-  // issue #32 ("HttpServer does not support multiple HTTP requests in
-  // the same TCP stream"), but can possibly slow down HTTP clients
-  // that do not support persistent connections. The default behavior
-  // used to be "false" in Orthanc <= 1.5.1. Setting this option to
-  // "false" is also recommended if Orthanc is compiled against
-  // Mongoose.
-  "KeepAlive" : true,
-
-  // Enable or disable Nagle's algorithm. Only taken into
-  // consideration if Orthanc is compiled to use CivetWeb. Experiments
-  // show that best performance can be obtained by setting both
-  // "KeepAlive" and "TcpNoDelay" to "true". Beware however of
-  // caveats: https://eklitzke.org/the-caveats-of-tcp-nodelay
-  "TcpNoDelay" : true,
-
-  // Number of threads that are used by the embedded HTTP server.
-  "HttpThreadsCount" : 50,
-
-  // If this option is set to "false", Orthanc will run in index-only
-  // mode. The DICOM files will not be stored on the drive. Note that
-  // this option might prevent the upgrade to newer versions of Orthanc.
-  "StoreDicom" : true,
-
-  // 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. This option is only used in Lua scripts.
-  "DicomAssociationCloseDelay" : 5,
-
-  // Maximum number of query/retrieve DICOM requests that are
-  // maintained by Orthanc. The least recently used requests get
-  // deleted as new requests are issued.
-  "QueryRetrieveSize" : 100,
-
-  // When handling a C-Find SCP request, setting this flag to "true"
-  // 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,
-
-  // 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 "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" (asynchronous mode), each incoming C-Move request results
-  // in the creation of a new background job. Up to Orthanc 1.3.2, the
-  // implicit behavior was to use synchronous C-Move ("true"). Between
-  // Orthanc 1.4.0 and 1.4.2, the default behavior was set to
-  // asynchronous C-Move ("false"). Since Orthanc 1.5.0, the default
-  // behavior is back to synchronous C-Move ("true", which ensures
-  // backward compatibility with Orthanc <= 1.3.2).
-  "SynchronousCMove" : true,
-
-  // 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". Since Orthanc 1.5.0, a value of "0"
-  // indicates to keep no job in memory (i.e. jobs are removed from
-  // the history as soon as they are completed), which prevents the
-  // use of some features of Orthanc (typically, synchronous mode in
-  // REST API) and should be avoided for non-developers.
-  "JobsHistorySize" : 10,
-
-  // Whether to save the jobs into the Orthanc database. If this
-  // option is set to "true", the pending/running/completed jobs are
-  // automatically reloaded from the database if Orthanc is stopped
-  // then restarted (except if the "--no-jobs" command-line argument
-  // is specified). This option should be set to "false" if multiple
-  // Orthanc servers are using the same database (e.g. if PostgreSQL
-  // or MariaDB/MySQL is used).
-  "SaveJobs" : true,
-
-  // 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,
-
-  // Maximum number of ZIP/media archives that are maintained by
-  // Orthanc, as a response to the asynchronous creation of archives.
-  // The least recently used archives get deleted as new archives are
-  // generated. This option was introduced in Orthanc 1.5.0, and has
-  // no effect on the synchronous generation of archives.
-  "MediaArchiveSize" : 1,
-
-  // Performance setting to specify how Orthanc accesses the storage
-  // area during C-FIND. Three modes are available: (1) "Always"
-  // allows Orthanc to read the storage area as soon as it needs an
-  // information that is not present in its database (slowest mode),
-  // (2) "Never" prevents Orthanc from accessing the storage area, and
-  // makes it uses exclusively its database (fastest mode), and (3)
-  // "Answers" allows Orthanc to read the storage area to generate its
-  // answers, but not to filter the DICOM resources (balance between
-  // the two modes). By default, the mode is "Always", which
-  // corresponds to the behavior of Orthanc <= 1.5.0.
-  "StorageAccessOnFind" : "Always",
-
-  // Whether Orthanc monitors its metrics (new in Orthanc 1.5.4). If
-  // set to "true", the metrics can be retrieved at
-  // "/tools/metrics-prometheus" formetted using the Prometheus
-  // text-based exposition format.
-  "MetricsEnabled" : true,
-
-  // Whether calls to URI "/tools/execute-script" is enabled. Starting
-  // with Orthanc 1.5.8, this URI is disabled by default for security.
-  "ExecuteLuaEnabled" : false,
-
-  // Set the timeout for HTTP requests, in seconds. This corresponds
-  // to option "request_timeout_ms" of Mongoose/Civetweb. It will set
-  // the socket options "SO_RCVTIMEO" and "SO_SNDTIMEO" to the
-  // specified value.
-  "HttpRequestTimeout" : 30,
-
-  // Set the default private creator that is used by Orthanc when it
-  // looks for a private tag in its dictionary (cf. "Dictionary"
-  // option), or when it creates/modifies a DICOM file (new in Orthanc 1.6.0).
-  "DefaultPrivateCreator" : "",
-
-  // Maximum number of storage commitment reports (i.e. received from
-  // remote modalities) to be kept in memory (new in Orthanc 1.6.0).
-  "StorageCommitmentReportsSize" : 100,
-
-  // Whether Orthanc transcodes DICOM files to an uncompressed
-  // transfer syntax over the DICOM protocol, if the remote modality
-  // does not support compressed transfer syntaxes (new in Orthanc 1.7.0).
-  "TranscodeDicomProtocol" : true,
-
-  // If some plugin to decode/transcode DICOM instances is installed,
-  // this option specifies whether the built-in decoder/transcoder of
-  // Orthanc (that uses DCMTK) is applied before or after the plugins,
-  // or is not applied at all (new in Orthanc 1.7.0). The allowed
-  // values for this option are "After" (default value, corresponding
-  // to the behavior of Orthanc <= 1.6.1), "Before", or "Disabled".
-  "BuiltinDecoderTranscoderOrder" : "After",
-
-  // If this option is set, Orthanc will transparently transcode any
-  // incoming DICOM instance to the given transfer syntax before
-  // storing it into its database. Beware that this might result in
-  // high CPU usage (if transcoding to some compressed transfer
-  // syntax), or in higher disk consumption (if transcoding to an
-  // uncompressed syntax). Also, beware that transcoding to a transfer
-  // syntax with lossy compression (notably JPEG) will change the
-  // "SOPInstanceUID" DICOM tag, and thus the Orthanc identifier at
-  // the instance level, which might break external workflow.
-  /**
-     "IngestTranscoding" : "1.2.840.10008.1.2",
-  **/
-
-  // The compression level that is used when transcoding to one of the
-  // lossy/JPEG transfer syntaxes (integer between 1 and 100).
-  "DicomLossyTranscodingQuality" : 90
-}
--- a/Resources/CrossToolchain.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-#
-#  $ CROSSTOOL_NG_ARCH=mips CROSSTOOL_NG_BOARD=malta CROSSTOOL_NG_IMAGE=/tmp/mips cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/CrossToolchain.cmake -DBUILD_CONNECTIVITY_CHECKS=OFF -DUSE_SYSTEM_CIVETWEB=OFF -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON -DUSE_SYSTEM_JSONCPP=OFF -DUSE_SYSTEM_UUID=OFF -DENABLE_DCMTK_JPEG_LOSSLESS=OFF -G Ninja && ninja
-#
-
-INCLUDE(CMakeForceCompiler)
-
-SET(CROSSTOOL_NG_ROOT $ENV{CROSSTOOL_NG_ROOT} CACHE STRING "")
-SET(CROSSTOOL_NG_ARCH $ENV{CROSSTOOL_NG_ARCH} CACHE STRING "")
-SET(CROSSTOOL_NG_BOARD $ENV{CROSSTOOL_NG_BOARD} CACHE STRING "")
-SET(CROSSTOOL_NG_SUFFIX $ENV{CROSSTOOL_NG_SUFFIX} CACHE STRING "")
-SET(CROSSTOOL_NG_IMAGE $ENV{CROSSTOOL_NG_IMAGE} CACHE STRING "")
-
-IF ("${CROSSTOOL_NG_ROOT}" STREQUAL "")
-  SET(CROSSTOOL_NG_ROOT "/home/$ENV{USER}/x-tools")
-ENDIF()
-
-IF ("${CROSSTOOL_NG_SUFFIX}" STREQUAL "")
-  SET(CROSSTOOL_NG_SUFFIX "linux-gnu")
-ENDIF()
-
-SET(CROSSTOOL_NG_NAME ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_BOARD}-${CROSSTOOL_NG_SUFFIX})
-SET(CROSSTOOL_NG_BASE ${CROSSTOOL_NG_ROOT}/${CROSSTOOL_NG_NAME})
-
-# the name of the target operating system
-SET(CMAKE_SYSTEM_NAME Linux)
-SET(CMAKE_SYSTEM_VERSION CrossToolNg)
-SET(CMAKE_SYSTEM_PROCESSOR ${CROSSTOOL_NG_ARCH})
-
-# which compilers to use for C and C++
-SET(CMAKE_C_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-gcc)
-
-if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
-  CMAKE_FORCE_CXX_COMPILER(${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++ GNU)
-else()
-  SET(CMAKE_CXX_COMPILER ${CROSSTOOL_NG_BASE}/bin/${CROSSTOOL_NG_NAME}-g++)
-endif()
-
-# here is the target environment located
-SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_IMAGE})
-#SET(CMAKE_FIND_ROOT_PATH ${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot)
-
-# adjust the default behaviour of the FIND_XXX() commands:
-# 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_PACKAGE ONLY)
-
-SET(CMAKE_CROSSCOMPILING ON)
-#SET(CROSS_COMPILER_PREFIX ${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX})
-
-SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
-SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CROSSTOOL_NG_IMAGE}/usr/include -I${CROSSTOOL_NG_IMAGE}/usr/include/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
-SET(CMAKE_EXE_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
-SET(CMAKE_SHARED_LINKER_FLAGS "-Wl,--unresolved-symbols=ignore-in-shared-libs -L${CROSSTOOL_NG_BASE}/${CROSSTOOL_NG_NAME}/sysroot/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib -L${CROSSTOOL_NG_IMAGE}/usr/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX} -L${CROSSTOOL_NG_IMAGE}/lib -L${CROSSTOOL_NG_IMAGE}/lib/${CROSSTOOL_NG_ARCH}-${CROSSTOOL_NG_SUFFIX}" CACHE INTERNAL "" FORCE)
--- a/Resources/DcmtkTools/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=../../Resources/LinuxStandardBaseToolchain.cmake -G Ninja
-
-cmake_minimum_required(VERSION 2.8)
-
-project(DcmtkTools)
-
-set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../)
-include(${ORTHANC_ROOT}/Resources/CMake/Compiler.cmake)
-include(${ORTHANC_ROOT}/Resources/CMake/DownloadPackage.cmake)
-
-set(STATIC_BUILD ON CACHE BOOL "")
-set(ALLOW_DOWNLOADS ON CACHE BOOL "")
-
-set(DCMTK_STATIC_VERSION "3.6.5" CACHE STRING "")
-set(ENABLE_DCMTK_JPEG ON CACHE BOOL "")
-set(ENABLE_DCMTK_JPEG_LOSSLESS ON CACHE BOOL "")
-set(ENABLE_DCMTK_LOG ON CACHE BOOL "")
-set(ENABLE_DCMTK_NETWORKING ON CACHE BOOL "")
-set(ENABLE_DCMTK_TRANSCODING ON CACHE BOOL "")
-
-include(${ORTHANC_ROOT}/Resources/CMake/DcmtkConfiguration.cmake)
-
-add_library(dcmtk STATIC
-  ${CMAKE_SOURCE_DIR}/dummy.cpp
-  ${DCMTK_SOURCES}
-  )
-
-add_executable(getscu
-  ${DCMTK_SOURCES_DIR}/dcmnet/apps/getscu.cc
-  )
-target_link_libraries(getscu dcmtk)
--- a/Resources/DcmtkTools/dummy.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-#include <string>
-
-struct OrthancLinesIterator;
-
-OrthancLinesIterator* OrthancLinesIterator_Create(const std::string& content)
-{
-  return NULL;
-}
-
-bool OrthancLinesIterator_GetLine(std::string& target,
-                                  const OrthancLinesIterator* iterator)
-{
-  return false;
-}
-
-void OrthancLinesIterator_Next(OrthancLinesIterator* iterator)
-{
-}
-
-void OrthancLinesIterator_Free(OrthancLinesIterator* iterator)
-{
-}
--- a/Resources/DicomConformanceStatement.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 injects the UID information into the DICOM conformance
-# statement of Orthanc
-
-import re
-
-# Read the conformance statement of Orthanc
-with open('DicomConformanceStatement.txt', 'r') as f:
-    statements = f.readlines()
-
-# Create an index of all the DICOM UIDs that are known to DCMTK
-uids = {}
-with open('/usr/include/dcmtk/dcmdata/dcuid.h', 'r') as dcmtk:
-    for l in dcmtk.readlines():
-        m = re.match(r'#define UID_(.+?)\s*"(.+?)"', l)
-        if m != None:
-            uids[m.group(1)] = m.group(2)
-
-# Loop over the lines of the statement, looking for the "|" separator
-with open('/tmp/DicomConformanceStatement.txt', 'w') as f:
-    for l in statements:
-        m = re.match(r'(\s*)(.*?)(\s*)\|.*$', l)
-        if m != None:
-            name = m.group(2)
-            uid = uids[name]
-            f.write('%s%s%s| %s\n' % (m.group(1), name, m.group(3), uid))
-
-        else:
-            # No "|" in this line, just output it
-            f.write(l)
--- a/Resources/DicomConformanceStatement.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,276 +0,0 @@
-======================================
-DICOM Conformance Statement of Orthanc
-======================================
-
-
----------------------
-Echo SCP Conformance
----------------------
-
-Orthanc supports the following SOP Classes as an SCP for C-Echo:
-
-  VerificationSOPClass      | 1.2.840.10008.1.1
-
-
----------------------
-Store SCP Conformance
----------------------
-
-Orthanc supports the following SOP Classes as an SCP for C-Store:
-
-  AmbulatoryECGWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.1.3
-  ArterialPulseWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.5.1
-  AutorefractionMeasurementsStorage                        | 1.2.840.10008.5.1.4.1.1.78.2
-  BasicStructuredDisplayStorage                            | 1.2.840.10008.5.1.4.1.1.131
-  BasicTextSRStorage                                       | 1.2.840.10008.5.1.4.1.1.88.11
-  BasicVoiceAudioWaveformStorage                           | 1.2.840.10008.5.1.4.1.1.9.4.1
-  BlendingSoftcopyPresentationStateStorage                 | 1.2.840.10008.5.1.4.1.1.11.4
-  BreastTomosynthesisImageStorage                          | 1.2.840.10008.5.1.4.1.1.13.1.3
-  CardiacElectrophysiologyWaveformStorage                  | 1.2.840.10008.5.1.4.1.1.9.3.1
-  ChestCADSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.65
-  ColonCADSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.69
-  ColorSoftcopyPresentationStateStorage                    | 1.2.840.10008.5.1.4.1.1.11.2
-  ComprehensiveSRStorage                                   | 1.2.840.10008.5.1.4.1.1.88.33
-  ComputedRadiographyImageStorage                          | 1.2.840.10008.5.1.4.1.1.1
-  CTImageStorage                                           | 1.2.840.10008.5.1.4.1.1.2
-  DeformableSpatialRegistrationStorage                     | 1.2.840.10008.5.1.4.1.1.66.3
-  DigitalIntraOralXRayImageStorageForPresentation          | 1.2.840.10008.5.1.4.1.1.1.3
-  DigitalIntraOralXRayImageStorageForProcessing            | 1.2.840.10008.5.1.4.1.1.1.3.1
-  DigitalMammographyXRayImageStorageForPresentation        | 1.2.840.10008.5.1.4.1.1.1.2
-  DigitalMammographyXRayImageStorageForProcessing          | 1.2.840.10008.5.1.4.1.1.1.2.1
-  DigitalXRayImageStorageForPresentation                   | 1.2.840.10008.5.1.4.1.1.1.1
-  DigitalXRayImageStorageForProcessing                     | 1.2.840.10008.5.1.4.1.1.1.1.1
-  EncapsulatedCDAStorage                                   | 1.2.840.10008.5.1.4.1.1.104.2
-  EncapsulatedPDFStorage                                   | 1.2.840.10008.5.1.4.1.1.104.1
-  EnhancedCTImageStorage                                   | 1.2.840.10008.5.1.4.1.1.2.1
-  EnhancedMRColorImageStorage                              | 1.2.840.10008.5.1.4.1.1.4.3
-  EnhancedMRImageStorage                                   | 1.2.840.10008.5.1.4.1.1.4.1
-  EnhancedPETImageStorage                                  | 1.2.840.10008.5.1.4.1.1.130
-  EnhancedSRStorage                                        | 1.2.840.10008.5.1.4.1.1.88.22
-  EnhancedUSVolumeStorage                                  | 1.2.840.10008.5.1.4.1.1.6.2
-  EnhancedXAImageStorage                                   | 1.2.840.10008.5.1.4.1.1.12.1.1
-  EnhancedXRFImageStorage                                  | 1.2.840.10008.5.1.4.1.1.12.2.1
-  GeneralAudioWaveformStorage                              | 1.2.840.10008.5.1.4.1.1.9.4.2
-  GeneralECGWaveformStorage                                | 1.2.840.10008.5.1.4.1.1.9.1.2
-  GenericImplantTemplateStorage                            | 1.2.840.10008.5.1.4.43.1
-  GrayscaleSoftcopyPresentationStateStorage                | 1.2.840.10008.5.1.4.1.1.11.1
-  HemodynamicWaveformStorage                               | 1.2.840.10008.5.1.4.1.1.9.2.1
-  ImplantAssemblyTemplateStorage                           | 1.2.840.10008.5.1.4.44.1
-  ImplantationPlanSRDocumentStorage                        | 1.2.840.10008.5.1.4.1.1.88.70
-  ImplantTemplateGroupStorage                              | 1.2.840.10008.5.1.4.45.1
-  IntraocularLensCalculationsStorage                       | 1.2.840.10008.5.1.4.1.1.78.8
-  KeratometryMeasurementsStorage                           | 1.2.840.10008.5.1.4.1.1.78.3
-  KeyObjectSelectionDocumentStorage                        | 1.2.840.10008.5.1.4.1.1.88.59
-  LensometryMeasurementsStorage                            | 1.2.840.10008.5.1.4.1.1.78.1
-  MacularGridThicknessAndVolumeReportStorage               | 1.2.840.10008.5.1.4.1.1.79.1
-  MammographyCADSRStorage                                  | 1.2.840.10008.5.1.4.1.1.88.50
-  MRImageStorage                                           | 1.2.840.10008.5.1.4.1.1.4
-  MRSpectroscopyStorage                                    | 1.2.840.10008.5.1.4.1.1.4.2
-  MultiframeGrayscaleByteSecondaryCaptureImageStorage      | 1.2.840.10008.5.1.4.1.1.7.2
-  MultiframeGrayscaleWordSecondaryCaptureImageStorage      | 1.2.840.10008.5.1.4.1.1.7.3
-  MultiframeSingleBitSecondaryCaptureImageStorage          | 1.2.840.10008.5.1.4.1.1.7.1
-  MultiframeTrueColorSecondaryCaptureImageStorage          | 1.2.840.10008.5.1.4.1.1.7.4
-  NuclearMedicineImageStorage                              | 1.2.840.10008.5.1.4.1.1.20
-  OphthalmicAxialMeasurementsStorage                       | 1.2.840.10008.5.1.4.1.1.78.7
-  OphthalmicPhotography16BitImageStorage                   | 1.2.840.10008.5.1.4.1.1.77.1.5.2
-  OphthalmicPhotography8BitImageStorage                    | 1.2.840.10008.5.1.4.1.1.77.1.5.1
-  OphthalmicTomographyImageStorage                         | 1.2.840.10008.5.1.4.1.1.77.1.5.4
-  OphthalmicVisualFieldStaticPerimetryMeasurementsStorage  | 1.2.840.10008.5.1.4.1.1.80.1
-  PositronEmissionTomographyImageStorage                   | 1.2.840.10008.5.1.4.1.1.128
-  ProcedureLogStorage                                      | 1.2.840.10008.5.1.4.1.1.88.40
-  PseudoColorSoftcopyPresentationStateStorage              | 1.2.840.10008.5.1.4.1.1.11.3
-  RawDataStorage                                           | 1.2.840.10008.5.1.4.1.1.66
-  RealWorldValueMappingStorage                             | 1.2.840.10008.5.1.4.1.1.67
-  RespiratoryWaveformStorage                               | 1.2.840.10008.5.1.4.1.1.9.6.1
-  RTBeamsTreatmentRecordStorage                            | 1.2.840.10008.5.1.4.1.1.481.4
-  RTBrachyTreatmentRecordStorage                           | 1.2.840.10008.5.1.4.1.1.481.6
-  RTDoseStorage                                            | 1.2.840.10008.5.1.4.1.1.481.2
-  RTImageStorage                                           | 1.2.840.10008.5.1.4.1.1.481.1
-  RTIonBeamsTreatmentRecordStorage                         | 1.2.840.10008.5.1.4.1.1.481.9
-  RTIonPlanStorage                                         | 1.2.840.10008.5.1.4.1.1.481.8
-  RTPlanStorage                                            | 1.2.840.10008.5.1.4.1.1.481.5
-  RTStructureSetStorage                                    | 1.2.840.10008.5.1.4.1.1.481.3
-  RTTreatmentSummaryRecordStorage                          | 1.2.840.10008.5.1.4.1.1.481.7
-  SecondaryCaptureImageStorage                             | 1.2.840.10008.5.1.4.1.1.7
-  SegmentationStorage                                      | 1.2.840.10008.5.1.4.1.1.66.4
-  SpatialFiducialsStorage                                  | 1.2.840.10008.5.1.4.1.1.66.2
-  SpatialRegistrationStorage                               | 1.2.840.10008.5.1.4.1.1.66.1
-  SpectaclePrescriptionReportStorage                       | 1.2.840.10008.5.1.4.1.1.78.6
-  StereometricRelationshipStorage                          | 1.2.840.10008.5.1.4.1.1.77.1.5.3
-  SubjectiveRefractionMeasurementsStorage                  | 1.2.840.10008.5.1.4.1.1.78.4
-  SurfaceSegmentationStorage                               | 1.2.840.10008.5.1.4.1.1.66.5
-  TwelveLeadECGWaveformStorage                             | 1.2.840.10008.5.1.4.1.1.9.1.1
-  UltrasoundImageStorage                                   | 1.2.840.10008.5.1.4.1.1.6.1
-  UltrasoundMultiframeImageStorage                         | 1.2.840.10008.5.1.4.1.1.3.1
-  VideoEndoscopicImageStorage                              | 1.2.840.10008.5.1.4.1.1.77.1.1.1
-  VideoMicroscopicImageStorage                             | 1.2.840.10008.5.1.4.1.1.77.1.2.1
-  VideoPhotographicImageStorage                            | 1.2.840.10008.5.1.4.1.1.77.1.4.1
-  VisualAcuityMeasurementsStorage                          | 1.2.840.10008.5.1.4.1.1.78.5
-  VLEndoscopicImageStorage                                 | 1.2.840.10008.5.1.4.1.1.77.1.1
-  VLMicroscopicImageStorage                                | 1.2.840.10008.5.1.4.1.1.77.1.2
-  VLPhotographicImageStorage                               | 1.2.840.10008.5.1.4.1.1.77.1.4
-  VLSlideCoordinatesMicroscopicImageStorage                | 1.2.840.10008.5.1.4.1.1.77.1.3
-  VLWholeSlideMicroscopyImageStorage                       | 1.2.840.10008.5.1.4.1.1.77.1.6
-  XAXRFGrayscaleSoftcopyPresentationStateStorage           | 1.2.840.10008.5.1.4.1.1.11.5
-  XRay3DAngiographicImageStorage                           | 1.2.840.10008.5.1.4.1.1.13.1.1
-  XRay3DCraniofacialImageStorage                           | 1.2.840.10008.5.1.4.1.1.13.1.2
-  XRayAngiographicImageStorage                             | 1.2.840.10008.5.1.4.1.1.12.1
-  XRayRadiationDoseSRStorage                               | 1.2.840.10008.5.1.4.1.1.88.67
-  XRayRadiofluoroscopicImageStorage                        | 1.2.840.10008.5.1.4.1.1.12.2
-
-  RETIRED_HardcopyColorImageStorage                        | 1.2.840.10008.5.1.1.30
-  RETIRED_HardcopyGrayscaleImageStorage                    | 1.2.840.10008.5.1.1.29
-  RETIRED_NuclearMedicineImageStorage                      | 1.2.840.10008.5.1.4.1.1.5
-  RETIRED_StandaloneCurveStorage                           | 1.2.840.10008.5.1.4.1.1.9
-  RETIRED_StandaloneModalityLUTStorage                     | 1.2.840.10008.5.1.4.1.1.10
-  RETIRED_StandaloneOverlayStorage                         | 1.2.840.10008.5.1.4.1.1.8
-  RETIRED_StandalonePETCurveStorage                        | 1.2.840.10008.5.1.4.1.1.129
-  RETIRED_StandaloneVOILUTStorage                          | 1.2.840.10008.5.1.4.1.1.11
-  RETIRED_StoredPrintStorage                               | 1.2.840.10008.5.1.1.27
-  RETIRED_UltrasoundImageStorage                           | 1.2.840.10008.5.1.4.1.1.6
-  RETIRED_UltrasoundMultiframeImageStorage                 | 1.2.840.10008.5.1.4.1.1.3
-  RETIRED_VLImageStorage                                   | 1.2.840.10008.5.1.4.1.1.77.1
-  RETIRED_VLMultiFrameImageStorage                         | 1.2.840.10008.5.1.4.1.1.77.2
-  RETIRED_XRayAngiographicBiPlaneImageStorage              | 1.2.840.10008.5.1.4.1.1.12.3
-
-  DRAFT_SRAudioStorage                                     | 1.2.840.10008.5.1.4.1.1.88.2
-  DRAFT_SRComprehensiveStorage                             | 1.2.840.10008.5.1.4.1.1.88.4
-  DRAFT_SRDetailStorage                                    | 1.2.840.10008.5.1.4.1.1.88.3
-  DRAFT_SRTextStorage                                      | 1.2.840.10008.5.1.4.1.1.88.1
-  DRAFT_WaveformStorage                                    | 1.2.840.10008.5.1.4.1.1.9.1
-  DRAFT_RTBeamsDeliveryInstructionStorage                  | 1.2.840.10008.5.1.4.34.1
-
-
---------------------
-Find SCP Conformance
---------------------
-
-Orthanc supports the following SOP Classes as an SCP for C-Find:
-
-  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
-
-
---------------------
-Move SCP Conformance
---------------------
-
-Orthanc supports the following SOP Classes as an SCP for C-Move:
-
-  MOVEPatientRootQueryRetrieveInformationModel   | 1.2.840.10008.5.1.4.1.2.1.2
-  MOVEStudyRootQueryRetrieveInformationModel     | 1.2.840.10008.5.1.4.1.2.2.2
-
-
--------------------
-Get SCP Conformance
--------------------
-
-Orthanc supports the following SOP Classes as an SCP for C-Get:
-
-  GETPatientRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.1.3
-  GETStudyRootQueryRetrieveInformationModel      | 1.2.840.10008.5.1.4.1.2.2.3
-
-
----------------------
-Echo SCU Conformance
----------------------
-
-Orthanc supports the following SOP Classes as an SCU for C-Echo:
-
-  VerificationSOPClass      | 1.2.840.10008.1.1
-
-
----------------------
-Store SCU Conformance
----------------------
-
-All the SOP Classes that are listed in the "Store SCP Conformance"
-(see above) section are available as an SCU for C-Store.
-
-
---------------------
-Find SCU Conformance
---------------------
-
-Orthanc supports the following SOP Classes as an SCU for C-Find:
-
-  FINDPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.1
-  FINDStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.1
-
-
---------------------
-Move SCU Conformance
---------------------
-
-Orthanc supports the following SOP Classes as an SCU for C-Move:
-
-  MOVEPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.2
-  MOVEStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.2
-
-
------------------
-Transfer Syntaxes
------------------
-
-Orthanc will accept and negotiate presentation contexts for all of the
-abovementioned supported SOP Classes using any of the following
-transfer syntaxes:
-
-  LittleEndianImplicitTransferSyntax                                    | 1.2.840.10008.1.2
-  LittleEndianExplicitTransferSyntax                                    | 1.2.840.10008.1.2.1
-  BigEndianExplicitTransferSyntax                                       | 1.2.840.10008.1.2.2
-  DeflatedExplicitVRLittleEndianTransferSyntax                          | 1.2.840.10008.1.2.1.99
-  JPEGProcess1TransferSyntax                                            | 1.2.840.10008.1.2.4.50
-  JPEGProcess2_4TransferSyntax                                          | 1.2.840.10008.1.2.4.51
-  JPEGProcess3_5TransferSyntax                                          | 1.2.840.10008.1.2.4.52
-  JPEGProcess6_8TransferSyntax                                          | 1.2.840.10008.1.2.4.53
-  JPEGProcess7_9TransferSyntax                                          | 1.2.840.10008.1.2.4.54
-  JPEGProcess10_12TransferSyntax                                        | 1.2.840.10008.1.2.4.55
-  JPEGProcess11_13TransferSyntax                                        | 1.2.840.10008.1.2.4.56
-  JPEGProcess14TransferSyntax                                           | 1.2.840.10008.1.2.4.57
-  JPEGProcess15TransferSyntax                                           | 1.2.840.10008.1.2.4.58
-  JPEGProcess16_18TransferSyntax                                        | 1.2.840.10008.1.2.4.59
-  JPEGProcess17_19TransferSyntax                                        | 1.2.840.10008.1.2.4.60
-  JPEGProcess20_22TransferSyntax                                        | 1.2.840.10008.1.2.4.61
-  JPEGProcess21_23TransferSyntax                                        | 1.2.840.10008.1.2.4.62
-  JPEGProcess24_26TransferSyntax                                        | 1.2.840.10008.1.2.4.63
-  JPEGProcess25_27TransferSyntax                                        | 1.2.840.10008.1.2.4.64
-  JPEGProcess28TransferSyntax                                           | 1.2.840.10008.1.2.4.65
-  JPEGProcess29TransferSyntax                                           | 1.2.840.10008.1.2.4.66
-  JPEGProcess14SV1TransferSyntax                                        | 1.2.840.10008.1.2.4.70
-  JPEGLSLosslessTransferSyntax                                          | 1.2.840.10008.1.2.4.80
-  JPEGLSLossyTransferSyntax                                             | 1.2.840.10008.1.2.4.81
-  JPEG2000LosslessOnlyTransferSyntax                                    | 1.2.840.10008.1.2.4.90
-  JPEG2000TransferSyntax                                                | 1.2.840.10008.1.2.4.91
-  JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax | 1.2.840.10008.1.2.4.92
-  JPEG2000Part2MulticomponentImageCompressionTransferSyntax             | 1.2.840.10008.1.2.4.93
-  JPIPReferencedTransferSyntax                                          | 1.2.840.10008.1.2.4.94
-  JPIPReferencedDeflateTransferSyntax                                   | 1.2.840.10008.1.2.4.95
-  MPEG2MainProfileAtMainLevelTransferSyntax                             | 1.2.840.10008.1.2.4.100
-  MPEG2MainProfileAtHighLevelTransferSyntax                             | 1.2.840.10008.1.2.4.101
-  RLELosslessTransferSyntax                                             | 1.2.840.10008.1.2.5
-
-It is possible to disable a subset of these transfer syntaxes thanks to the
-"*TransferSyntaxAccepted" options in the Orthanc configuration file.
-
-When possible, Orthanc will prefer the
-LittleEndianImplicitTransferSyntax transfer syntax
-(1.2.840.10008.1.2).
-
-Orthanc does not support extended negotiation.
-
-
---------------------
-Implementation notes
---------------------
-
-The information above about the SCP support is readily extracted from
-the function "Orthanc::Internals::AcceptAssociation()" from file
-"Core/DicomNetworking/Internals/CommandDispatcher.cpp".
-
-The information above about the SCU support is derived from the
-classes "Orthanc::DicomControlUserConnection" and
-"Orthanc::DicomStoreUserConnection" from file
-"Core/DicomNetworking/DicomControlUserConnection.cpp" and
-"Core/DicomNetworking/DicomStoreUserConnection.cpp".
--- a/Resources/DicomTransferSyntaxes.json	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,376 +0,0 @@
-[
-  {
-    "UID" : "1.2.840.10008.1.2",
-    "Name" : "Implicit VR Little Endian",
-    "Value" : "LittleEndianImplicit",
-    "Retired" : false,
-    "DCMTK" : "EXS_LittleEndianImplicit",
-    "GDCM" : "gdcm::TransferSyntax::ImplicitVRLittleEndian"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.1",
-    "Name" : "Explicit VR Little Endian",
-    "Value" : "LittleEndianExplicit",
-    "Retired" : false,
-    "DCMTK" : "EXS_LittleEndianExplicit",
-    "GDCM" : "gdcm::TransferSyntax::ExplicitVRLittleEndian"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.1.99",
-    "Name" : "Deflated Explicit VR Little Endian",
-    "Value" : "DeflatedLittleEndianExplicit",
-    "Retired" : false,
-    "DCMTK" : "EXS_DeflatedLittleEndianExplicit"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.2",
-    "Name" : "Explicit VR Big Endian",
-    "Value" : "BigEndianExplicit",
-    "Retired" : false,
-    "DCMTK" : "EXS_BigEndianExplicit"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.50",
-    "Name" : "JPEG Baseline (process 1, lossy)",
-    "Value" : "JPEGProcess1",
-    "Retired" : false,
-    "Note" : "Default Transfer Syntax for Lossy JPEG 8-bit Image Compression",
-    "DCMTK" : "EXS_JPEGProcess1",
-    "DCMTK360" : "EXS_JPEGProcess1TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGBaselineProcess1"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.51",
-    "Name" : "JPEG Extended Sequential (processes 2 & 4)",
-    "Value" : "JPEGProcess2_4",
-    "Retired" : false,
-    "Note" : "Default Transfer Syntax for Lossy JPEG (lossy, 8/12 bit), 12-bit Image Compression (Process 4 only)",
-    "DCMTK" : "EXS_JPEGProcess2_4",
-    "DCMTK360" : "EXS_JPEGProcess2_4TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGExtendedProcess2_4"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.52",
-    "Name" : "JPEG Extended Sequential (lossy, 8/12 bit), arithmetic coding",
-    "Value" : "JPEGProcess3_5",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess3_5",
-    "DCMTK360" : "EXS_JPEGProcess3_5TransferSyntax"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.53",
-    "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit)",
-    "Value" : "JPEGProcess6_8",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess6_8",
-    "DCMTK360" : "EXS_JPEGProcess6_8TransferSyntax"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.54",
-    "Name" : "JPEG Spectral Selection, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
-    "Value" : "JPEGProcess7_9",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess7_9",
-    "DCMTK360" : "EXS_JPEGProcess7_9TransferSyntax"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.55",
-    "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit)",
-    "Value" : "JPEGProcess10_12",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess10_12",
-    "DCMTK360" : "EXS_JPEGProcess10_12TransferSyntax"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.56",
-    "Name" : "JPEG Full Progression, Nonhierarchical (lossy, 8/12 bit), arithmetic coding",
-    "Value" : "JPEGProcess11_13",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess11_13",
-    "DCMTK360" : "EXS_JPEGProcess11_13TransferSyntax"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.57",
-    "Name" : "JPEG Lossless, Nonhierarchical with any selection value (process 14)",
-    "Value" : "JPEGProcess14",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEGProcess14",
-    "DCMTK360" : "EXS_JPEGProcess14TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.58",
-    "Name" : "JPEG Lossless with any selection value, arithmetic coding",
-    "Value" : "JPEGProcess15",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess15",
-    "DCMTK360" : "EXS_JPEGProcess15TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.59",
-    "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit)",
-    "Value" : "JPEGProcess16_18",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess16_18",
-    "DCMTK360" : "EXS_JPEGProcess16_18TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.60",
-    "Name" : "JPEG Extended Sequential, Hierarchical (lossy, 8/12 bit), arithmetic coding",
-    "Value" : "JPEGProcess17_19",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess17_19",
-    "DCMTK360" : "EXS_JPEGProcess17_19TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.61",
-    "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit)",
-    "Value" : "JPEGProcess20_22",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess20_22",
-    "DCMTK360" : "EXS_JPEGProcess20_22TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.62",
-    "Name" : "JPEG Spectral Selection, Hierarchical (lossy, 8/12 bit), arithmetic coding",
-    "Value" : "JPEGProcess21_23",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess21_23",
-    "DCMTK360" : "EXS_JPEGProcess21_23TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.63",
-    "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit)",
-    "Value" : "JPEGProcess24_26",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess24_26",
-    "DCMTK360" : "EXS_JPEGProcess24_26TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.64",
-    "Name" : "JPEG Full Progression, Hierarchical (lossy, 8/12 bit), arithmetic coding",
-    "Value" : "JPEGProcess25_27",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess25_27",
-    "DCMTK360" : "EXS_JPEGProcess25_27TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.65",
-    "Name" : "JPEG Lossless, Hierarchical",
-    "Value" : "JPEGProcess28",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess28",
-    "DCMTK360" : "EXS_JPEGProcess28TransferSyntax"
-  },
-  
-  {
-    "UID" : "1.2.840.10008.1.2.4.66",
-    "Name" : "JPEG Lossless, Hierarchical, arithmetic coding",
-    "Value" : "JPEGProcess29",
-    "Retired" : true,
-    "DCMTK" : "EXS_JPEGProcess29",
-    "DCMTK360" : "EXS_JPEGProcess29TransferSyntax"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.70",
-    "Name" : "JPEG Lossless, Nonhierarchical, First-Order Prediction (Processes 14 [Selection Value 1])",
-    "Value" : "JPEGProcess14SV1",
-    "Retired" : false,
-    "Note" : "Default Transfer Syntax for Lossless JPEG Image Compression",
-    "DCMTK" : "EXS_JPEGProcess14SV1",
-    "DCMTK360" : "EXS_JPEGProcess14SV1TransferSyntax",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLosslessProcess14_1"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.80",
-    "Name" : "JPEG-LS (lossless)",
-    "Value" : "JPEGLSLossless",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEGLSLossless",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLSLossless"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.81",
-    "Name" : "JPEG-LS (lossy or near-lossless)",
-    "Value" : "JPEGLSLossy",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEGLSLossy",
-    "GDCM" : "gdcm::TransferSyntax::JPEGLSNearLossless"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.90",
-    "Name" : "JPEG 2000 (lossless)",
-    "Value" : "JPEG2000LosslessOnly",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEG2000LosslessOnly",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000Lossless"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.91",
-    "Name" : "JPEG 2000 (lossless or lossy)",
-    "Value" : "JPEG2000",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEG2000",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.92",
-    "Name" : "JPEG 2000 part 2 multicomponent extensions (lossless)",
-    "Value" : "JPEG2000MulticomponentLosslessOnly",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEG2000MulticomponentLosslessOnly",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2Lossless"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.93",
-    "Name" : "JPEG 2000 part 2 multicomponent extensions (lossless or lossy)",
-    "Value" : "JPEG2000Multicomponent",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPEG2000Multicomponent",
-    "GDCM" : "gdcm::TransferSyntax::JPEG2000Part2"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.94",
-    "Name" : "JPIP Referenced",
-    "Value" : "JPIPReferenced",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPIPReferenced"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.95",
-    "Name" : "JPIP Referenced Deflate",
-    "Value" : "JPIPReferencedDeflate",
-    "Retired" : false,
-    "DCMTK" : "EXS_JPIPReferencedDeflate"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.100",
-    "Name" : "MPEG2 Main Profile / Main Level",
-    "Value" : "MPEG2MainProfileAtMainLevel",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG2MainProfileAtMainLevel"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.101",
-    "Name" : "MPEG2 Main Profile / High Level",
-    "Value" : "MPEG2MainProfileAtHighLevel",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG2MainProfileAtHighLevel"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.102",
-    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.1",
-    "Value" : "MPEG4HighProfileLevel4_1",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG4HighProfileLevel4_1",
-    "SinceDCMTK" : "361"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.103",
-    "Name" : "MPEG4 AVC/H.264 BD-compatible High Profile / Level 4.1",
-    "Value" : "MPEG4BDcompatibleHighProfileLevel4_1",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG4BDcompatibleHighProfileLevel4_1",
-    "SinceDCMTK" : "361"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.104",
-    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 2D Video",
-    "Value" : "MPEG4HighProfileLevel4_2_For2DVideo",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For2DVideo",
-    "SinceDCMTK" : "361"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.105",
-    "Name" : "MPEG4 AVC/H.264 High Profile / Level 4.2 For 3D Video",
-    "Value" : "MPEG4HighProfileLevel4_2_For3DVideo",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG4HighProfileLevel4_2_For3DVideo",
-    "SinceDCMTK" : "361"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.106",
-    "Name" : "MPEG4 AVC/H.264 Stereo High Profile / Level 4.2",
-    "Value" : "MPEG4StereoHighProfileLevel4_2",
-    "Retired" : false,
-    "DCMTK" : "EXS_MPEG4StereoHighProfileLevel4_2",
-    "SinceDCMTK" : "361"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.107",
-    "Name" : "HEVC/H.265 Main Profile / Level 5.1",
-    "Value" : "HEVCMainProfileLevel5_1",
-    "Retired" : false,
-    "DCMTK" : "EXS_HEVCMainProfileLevel5_1",
-    "SinceDCMTK" : "362"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.4.108",
-    "Name" : "HEVC/H.265 Main 10 Profile / Level 5.1",
-    "Value" : "HEVCMain10ProfileLevel5_1",
-    "Retired" : false,
-    "DCMTK" : "EXS_HEVCMain10ProfileLevel5_1",
-    "SinceDCMTK" : "362"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.5",
-    "Name" : "RLE - Run Length Encoding (lossless)",
-    "Value" : "RLELossless",
-    "Retired" : false,
-    "DCMTK" : "EXS_RLELossless",
-    "GDCM" : "gdcm::TransferSyntax::RLELossless"
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.6.1",
-    "Name" : "RFC 2557 MIME Encapsulation",
-    "Value" : "RFC2557MimeEncapsulation",
-    "Retired" : true
-  },
-
-  {
-    "UID" : "1.2.840.10008.1.2.6.2",
-    "Name" : "XML Encoding",
-    "Value" : "XML",
-    "Retired" : true
-  }
-]
--- a/Resources/EclipseCodingStyle.xml	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<profiles version="1">
-<profile kind="CodeFormatterProfile" name="Orthanc" version="1">
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_in_empty_block" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.lineSplit" value="80"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_member_access" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_base_types" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_constructor_initializer_list" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_exception_specification" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_base_types" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_access_specifier" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_exception_specification" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_arguments" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.comment.min_distance_between_code_and_line_comment" value="1"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_expressions_in_array_initializer" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_declarator_list" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_bracket" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.tabulation.size" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_else_in_if_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_enumerator_list" value="49"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_declarator_list" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_empty_lines" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_method_declaration" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_arguments" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_base_clause" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.join_wrapped_lines" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_declarator_list" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_arguments_in_method_invocation" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.comment.never_indent_line_comments_on_first_column" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_brackets" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_bracket" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_parameters_in_method_declaration" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_type_declaration" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_expression_list" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.continuation_indentation" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_expression_list" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression" value="34"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_extra_spaces" value="0"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_access_specifier_compare_to_type_header" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_body_declarations_compare_to_namespace_header" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_compact_if" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_assignment" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_conditional_expression_chain" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_template_parameters" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_expression_list" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_exception_specification" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_identifier_in_function_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_base_clause_in_type_declaration" value="80"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_between_empty_parens_in_exception_specification" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_declaration_compare_to_template_header" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_body" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_binary_expression" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.indent_statements_compare_to_block" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_template_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_angle_bracket_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.tabulation.char" value="space"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_parameters" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_colon_in_constructor_initializer_list" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_block_in_case" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.compact_else_if" value="true"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_template_declaration" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_base_clause" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_switch" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.alignment_for_overloaded_left_shift_chain" value="18"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.indentation.size" value="2"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_namespace_declaration" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_angle_bracket_in_template_arguments" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.brace_position_for_array_initializer" value="next_line"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_brace_in_namespace_declaration" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_closing_bracket" value="do not insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_new_line_before_while_in_do_statement" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_closing_angle_bracket_in_template_parameters" value="insert"/>
-<setting id="org.eclipse.cdt.core.formatter.insert_space_after_opening_angle_bracket_in_template_arguments" value="do not insert"/>
-</profile>
-</profiles>
--- a/Resources/EmbedResources.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,455 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 sys
-import os
-import os.path
-import pprint
-import re
-
-UPCASE_CHECK = True
-USE_SYSTEM_EXCEPTION = False
-EXCEPTION_CLASS = 'OrthancException'
-OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)'
-INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)'
-NAMESPACE = 'Orthanc.EmbeddedResources'
-FRAMEWORK_PATH = None
-
-ARGS = []
-for i in range(len(sys.argv)):
-    if not sys.argv[i].startswith('--'):
-        ARGS.append(sys.argv[i])
-    elif sys.argv[i].lower() == '--no-upcase-check':
-        UPCASE_CHECK = False
-    elif sys.argv[i].lower() == '--system-exception':
-        USE_SYSTEM_EXCEPTION = True
-        EXCEPTION_CLASS = '::std::runtime_error'
-        OUT_OF_RANGE_EXCEPTION = '%s("Parameter out of range")' % EXCEPTION_CLASS
-        INEXISTENT_PATH_EXCEPTION = '%s("Unknown path in a directory resource")' % EXCEPTION_CLASS
-    elif sys.argv[i].startswith('--namespace='):
-        NAMESPACE = sys.argv[i][sys.argv[i].find('=') + 1 : ]
-    elif sys.argv[i].startswith('--framework-path='):
-        FRAMEWORK_PATH = sys.argv[i][sys.argv[i].find('=') + 1 : ]
-
-if len(ARGS) < 2 or len(ARGS) % 2 != 0:
-    print ('Usage:')
-    print ('python %s [--no-upcase-check] [--system-exception] [--namespace=<Namespace>] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
-    exit(-1)
-
-TARGET_BASE_FILENAME = ARGS[1]
-SOURCES = ARGS[2:]
-
-try:
-    # Make sure the destination directory exists
-    os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
-except:
-    pass
-
-
-#####################################################################
-## Read each resource file
-#####################################################################
-
-def CheckNoUpcase(s):
-    global UPCASE_CHECK
-    if (UPCASE_CHECK and
-        re.search('[A-Z]', s) != None):
-        raise Exception("Path in a directory with an upcase letter: %s" % s)
-
-resources = {}
-
-counter = 0
-i = 0
-while i < len(SOURCES):
-    resourceName = SOURCES[i].upper()
-    pathName = SOURCES[i + 1]
-
-    if not os.path.exists(pathName):
-        raise Exception("Non existing path: %s" % pathName)
-
-    if resourceName in resources:
-        raise Exception("Twice the same resource: " + resourceName)
-    
-    if os.path.isdir(pathName):
-        # The resource is a directory: Recursively explore its files
-        content = {}
-        for root, dirs, files in os.walk(pathName):
-            dirs.sort()
-            files.sort()
-            base = os.path.relpath(root, pathName)
-
-            # Fix issue #24 (Build fails on OSX when directory has .DS_Store files):
-            # Ignore folders whose name starts with a dot (".")
-            if base.find('/.') != -1:
-                print('Ignoring folder: %s' % root)
-                continue
-
-            for f in files:
-                if f.find('~') == -1:  # Ignore Emacs backup files
-                    if base == '.':
-                        r = f
-                    else:
-                        r = os.path.join(base, f)
-
-                    CheckNoUpcase(r)
-                    r = '/' + r.replace('\\', '/')
-                    if r in content:
-                        raise Exception("Twice the same filename (check case): " + r)
-
-                    content[r] = {
-                        'Filename' : os.path.join(root, f),
-                        'Index' : counter
-                        }
-                    counter += 1
-
-        resources[resourceName] = {
-            'Type' : 'Directory',
-            'Files' : content
-            }
-
-    elif os.path.isfile(pathName):
-        resources[resourceName] = {
-            'Type' : 'File',
-            'Index' : counter,
-            'Filename' : pathName
-            }
-        counter += 1
-
-    else:
-        raise Exception("Not a regular file, nor a directory: " + pathName)
-
-    i += 2
-
-#pprint.pprint(resources)
-
-
-#####################################################################
-## Write .h header
-#####################################################################
-
-header = open(TARGET_BASE_FILENAME + '.h', 'w')
-
-header.write("""
-#pragma once
-
-#include <string>
-#include <list>
-
-#if defined(_MSC_VER)
-#  pragma warning(disable: 4065)  // "Switch statement contains 'default' but no 'case' labels"
-#endif
-
-""")
-
-
-for ns in NAMESPACE.split('.'):
-    header.write('namespace %s {\n' % ns)
-    
-
-header.write("""
-    enum FileResourceId
-    {
-""")
-
-isFirst = True
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        if isFirst:
-            isFirst = False
-        else:    
-            header.write(',\n')
-        header.write('      %s' % name)
-
-header.write("""
-    };
-
-    enum DirectoryResourceId
-    {
-""")
-
-isFirst = True
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        if isFirst:
-            isFirst = False
-        else:    
-            header.write(',\n')
-        header.write('      %s' % name)
-
-header.write("""
-    };
-
-    const void* GetFileResourceBuffer(FileResourceId id);
-    size_t GetFileResourceSize(FileResourceId id);
-    void GetFileResource(std::string& result, FileResourceId id);
-
-    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
-    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
-    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
-
-    void ListResources(std::list<std::string>& result, DirectoryResourceId id);
-
-""")
-
-
-for ns in NAMESPACE.split('.'):
-    header.write('}\n')
-
-header.close()
-
-
-
-#####################################################################
-## Write the resource content in the .cpp source
-#####################################################################
-
-PYTHON_MAJOR_VERSION = sys.version_info[0]
-
-def WriteResource(cpp, item):
-    cpp.write('    static const uint8_t resource%dBuffer[] = {' % item['Index'])
-
-    f = open(item['Filename'], "rb")
-    content = f.read()
-    f.close()
-
-    # http://stackoverflow.com/a/1035360
-    pos = 0
-    buffer = []  # instead of appending a few bytes at a time to the cpp file, 
-                 # we first append each chunk to a list, join it and write it 
-                 # to the file.  We've measured that it was 2-3 times faster in python3.
-                 # Note that speed is important since if generation is too slow,
-                 # cmake might try to compile the EmbeddedResources.cpp file while it is
-                 # still being generated !
-    for b in content:
-        if PYTHON_MAJOR_VERSION == 2:
-            c = ord(b[0])
-        else:
-            c = b
-
-        if pos > 0:
-            buffer.append(",")
-
-        if (pos % 16) == 0:
-            buffer.append("\n")
-
-        if c < 0:
-            raise Exception("Internal error")
-
-        buffer.append("0x%02x" % c)
-        pos += 1
-
-    cpp.write("".join(buffer))
-    # Zero-size array are disallowed, so we put one single void character in it.
-    if pos == 0:
-        cpp.write('  0')
-
-    cpp.write('  };\n')
-    cpp.write('    static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
-
-
-cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
-
-cpp.write('#include "%s.h"\n' % os.path.basename(TARGET_BASE_FILENAME))
-
-if USE_SYSTEM_EXCEPTION:
-    cpp.write('#include <stdexcept>')
-elif FRAMEWORK_PATH != None:
-    cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH)
-else:
-    cpp.write('#include <OrthancException.h>')
-
-cpp.write("""
-#include <stdint.h>
-#include <string.h>
-
-""")
-
-for ns in NAMESPACE.split('.'):
-    cpp.write('namespace %s {\n' % ns)
-
-
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        WriteResource(cpp, resources[name])
-    else:
-        for f in resources[name]['Files']:
-            WriteResource(cpp, resources[name]['Files'][f])
-
-
-
-#####################################################################
-## Write the accessors to the file resources in .cpp
-#####################################################################
-
-cpp.write("""
-    const void* GetFileResourceBuffer(FileResourceId id)
-    {
-      switch (id)
-      {
-""")
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        cpp.write('      case %s:\n' % name)
-        cpp.write('        return resource%dBuffer;\n' % resources[name]['Index'])
-
-cpp.write("""
-      default:
-        throw %s;
-      }
-    }
-
-    size_t GetFileResourceSize(FileResourceId id)
-    {
-      switch (id)
-      {
-""" % OUT_OF_RANGE_EXCEPTION)
-
-for name in resources:
-    if resources[name]['Type'] == 'File':
-        cpp.write('      case %s:\n' % name)
-        cpp.write('        return resource%dSize;\n' % resources[name]['Index'])
-
-cpp.write("""
-      default:
-        throw %s;
-      }
-    }
-""" % OUT_OF_RANGE_EXCEPTION)
-
-
-
-#####################################################################
-## Write the accessors to the directory resources in .cpp
-#####################################################################
-
-cpp.write("""
-    const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
-    {
-      switch (id)
-      {
-""")
-
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        cpp.write('      case %s:\n' % name)
-        isFirst = True
-        for path in resources[name]['Files']:
-            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
-            cpp.write('          return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
-        cpp.write('        throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
-
-cpp.write("""      default:
-        throw %s;
-      }
-    }
-
-    size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
-    {
-      switch (id)
-      {
-""" % OUT_OF_RANGE_EXCEPTION)
-
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        cpp.write('      case %s:\n' % name)
-        isFirst = True
-        for path in resources[name]['Files']:
-            cpp.write('        if (!strcmp(path, "%s"))\n' % path)
-            cpp.write('          return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
-        cpp.write('        throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
-
-cpp.write("""      default:
-        throw %s;
-      }
-    }
-""" % OUT_OF_RANGE_EXCEPTION)
-
-
-
-
-#####################################################################
-## List the resources in a directory
-#####################################################################
-
-cpp.write("""
-    void ListResources(std::list<std::string>& result, DirectoryResourceId id)
-    {
-      result.clear();
-
-      switch (id)
-      {
-""")
-
-for name in resources:
-    if resources[name]['Type'] == 'Directory':
-        cpp.write('      case %s:\n' % name)
-        for path in sorted(resources[name]['Files']):
-            cpp.write('        result.push_back("%s");\n' % path)
-        cpp.write('        break;\n\n')
-
-cpp.write("""      default:
-        throw %s;
-      }
-    }
-""" % OUT_OF_RANGE_EXCEPTION)
-
-
-
-
-#####################################################################
-## Write the convenience wrappers in .cpp
-#####################################################################
-
-cpp.write("""
-    void GetFileResource(std::string& result, FileResourceId id)
-    {
-      size_t size = GetFileResourceSize(id);
-      result.resize(size);
-      if (size > 0)
-        memcpy(&result[0], GetFileResourceBuffer(id), size);
-    }
-
-    void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
-    {
-      size_t size = GetDirectoryResourceSize(id, path);
-      result.resize(size);
-      if (size > 0)
-        memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
-    }
-""")
-
-
-for ns in NAMESPACE.split('.'):
-    cpp.write('}\n')
-
-cpp.close()
--- a/Resources/EncodingTests.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-static const unsigned int testEncodingsCount = 18;
-static const ::Orthanc::Encoding testEncodings[] = {
-  ::Orthanc::Encoding_Latin5,
-  ::Orthanc::Encoding_Hebrew,
-  ::Orthanc::Encoding_Greek,
-  ::Orthanc::Encoding_Arabic,
-  ::Orthanc::Encoding_Cyrillic,
-  ::Orthanc::Encoding_Latin4,
-  ::Orthanc::Encoding_Latin3,
-  ::Orthanc::Encoding_Latin2,
-  ::Orthanc::Encoding_Latin1,
-  ::Orthanc::Encoding_Utf8,
-  ::Orthanc::Encoding_Thai,
-  ::Orthanc::Encoding_Japanese,
-  ::Orthanc::Encoding_Ascii,
-  ::Orthanc::Encoding_Windows1251,
-  ::Orthanc::Encoding_Chinese,
-  ::Orthanc::Encoding_Windows1251,
-  ::Orthanc::Encoding_Windows1251,
-  ::Orthanc::Encoding_Windows1251
-};
-static const char *testEncodingsEncoded[18] = {
-  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xdd",
-  "\x54\x65\x73\x74\xe3",
-  "\x54\x65\x73\x74\xc8",
-  "\x54\x65\x73\x74\xd5",
-  "\x54\x65\x73\x74\xb4\xfb",
-  "\x54\x65\x73\x74\xe9\xe4\xf6\xf3",
-  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2\xf8\xa9",
-  "\x54\x65\x73\x74\xe9\xe4\xf6",
-  "\x54\x65\x73\x74\xe9\xe4\xf6\xf2",
-  "\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",
-  "\x54\x65\x73\x74\xfb",
-  "\x54\x65\x73\x74\x84\x44\x83\xa6\xc8",
-  "\x54\x65\x73\x74",
-  "\x54\x65\x73\x74\xc4\x9e",
-  "\x81\x30\x89\x37\x81\x30\x89\x38\xA8\xA4\xA8\xA2\x81\x30\x89\x39\x81\x30\x8A\x30",
-  "\xd0\xe5\xed\xf2\xe3\xe5\xed\xee\xe3\xf0\xe0\xf4\xe8\xff",
-  "\xD2\xE0\xE7",
-  "\xcf\xf0\xff\xec\xe0\xff"
-};
-static const char *testEncodingsExpected[18] = {
-  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\xb0",
-  "\x54\x65\x73\x74\xd7\x93",
-  "\x54\x65\x73\x74\xce\x98",
-  "\x54\x65\x73\x74\xd8\xb5",
-  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
-  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc4\xb7",
-  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2\xc4\x9d\xc4\xb0",
-  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6",
-  "\x54\x65\x73\x74\xc3\xa9\xc3\xa4\xc3\xb6\xc3\xb2",
-  "\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",
-  "\x54\x65\x73\x74\xe0\xb9\x9b",
-  "\x54\x65\x73\x74\xd0\x94\xce\x98\xef\xbe\x88",
-  "\x54\x65\x73\x74",
-  "\x54\x65\x73\x74\xd0\x94\xd1\x9b",
-  "\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3",
-  "\xd0\xa0\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb3\xd0\xb5\xd0\xbd\xd0\xbe\xd0\xb3\xd1\x80\xd0\xb0\xd1\x84\xd0\xb8\xd1\x8f",
-  "\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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-source = u'TestéäöòДΘĝדصķћ๛ネİ'
-
-encodings = {
-    'UTF-8' : 'Utf8',
-    'ASCII' : 'Ascii',
-    'ISO-8859-1' : 'Latin1',
-    'ISO-8859-2' : 'Latin2',
-    'ISO-8859-3' : 'Latin3',
-    'ISO-8859-4' : 'Latin4',
-    'ISO-8859-9' : 'Latin5',
-    'ISO-8859-5' : 'Cyrillic',
-    'WINDOWS-1251' : 'Windows1251',
-    'ISO-8859-6' : 'Arabic',
-    'ISO-8859-7' : 'Greek',
-    'ISO-8859-8' : 'Hebrew',
-    'TIS-620' : 'Thai',
-    'SHIFT-JIS' : 'Japanese',
-    #'GB18030' : 'Chinese',  # Done manually below (*)
-}
-
-#from encodings.aliases import aliases
-#for a, b in aliases.iteritems():
-#    print '%s : %s' % (a, b)
-
-
-# "63" corresponds to "?"
-l = []
-encoded = []
-expected = []
-
-def ToArray(source):
-    result = ''
-    for byte in bytearray(source):
-        result += '\\x%02x' % byte
-    return '"%s"' % result
-    
-
-for encoding, orthancEnumeration in encodings.iteritems():
-    l.append('::Orthanc::Encoding_%s' % orthancEnumeration)
-    s = source.encode(encoding, 'ignore')
-    encoded.append(ToArray(s))
-    expected.append(ToArray(s.decode(encoding).encode('utf-8')))
-
-
-# https://en.wikipedia.org/wiki/GB_18030#Technical_details  (*)
-l.append('::Orthanc::Encoding_Chinese')
-expected.append(ToArray('Þßàáâã'))
-encoded.append('"\\x81\\x30\\x89\\x37\\x81\\x30\\x89\\x38\\xA8\\xA4\\xA8\\xA2\\x81\\x30\\x89\\x39\\x81\\x30\\x8A\\x30"')
-
-# Issue 32
-# "encoded" is the copy/paste from "dcm2xml +Ca cyrillic Issue32.dcm"
-l.append('::Orthanc::Encoding_Windows1251')
-encoded.append('"\\xd0\\xe5\\xed\\xf2\\xe3\\xe5\\xed\\xee\\xe3\\xf0\\xe0\\xf4\\xe8\\xff"')
-expected.append(ToArray('Рентгенография'))
-l.append('::Orthanc::Encoding_Windows1251')
-encoded.append('"\\xD2\\xE0\\xE7"')
-expected.append(ToArray('Таз'))
-l.append('::Orthanc::Encoding_Windows1251')
-encoded.append('"\\xcf\\xf0\\xff\\xec\\xe0\\xff"')
-expected.append(ToArray('Прямая'))
-
-
-if True:
-    print 'static const unsigned int testEncodingsCount = %d;' % len(l)
-    print 'static const ::Orthanc::Encoding testEncodings[] = {\n  %s\n};' % (',\n  '.join(l))
-    print 'static const char *testEncodingsEncoded[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(encoded))
-    print 'static const char *testEncodingsExpected[%d] = {\n  %s\n};' % (len(l), ',\n  '.join(expected))
-else:
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,576 +0,0 @@
-[
-  /** Generic error codes **/
-
-  {
-    "Code": -1, 
-    "Name": "InternalError", 
-    "Description": "Internal error"
-  }, 
-  {
-    "Code": 0, 
-    "HttpStatus": 200, 
-    "Name": "Success", 
-    "Description": "Success"
-  }, 
-  {
-    "Code": 1, 
-    "Name": "Plugin", 
-    "Description": "Error encountered within the plugin engine"
-  },
-  {
-    "Code": 2, 
-    "Name": "NotImplemented", 
-    "Description": "Not implemented yet"
-  }, 
-  {
-    "Code": 3, 
-    "HttpStatus": 400, 
-    "Name": "ParameterOutOfRange", 
-    "Description": "Parameter out of range",
-    "SQLite": true
-  }, 
-  {
-    "Code": 4, 
-    "Name": "NotEnoughMemory", 
-    "Description": "The server hosting Orthanc is running out of memory"
-  }, 
-  {
-    "Code": 5, 
-    "HttpStatus": 400, 
-    "Name": "BadParameterType", 
-    "Description": "Bad type for a parameter",
-    "SQLite": true
-  }, 
-  {
-    "Code": 6, 
-    "Name": "BadSequenceOfCalls", 
-    "Description": "Bad sequence of calls"
-  }, 
-  {
-    "Code": 7, 
-    "HttpStatus": 404, 
-    "Name": "InexistentItem", 
-    "Description": "Accessing an inexistent item"
-  }, 
-  {
-    "Code": 8, 
-    "HttpStatus": 400, 
-    "Name": "BadRequest", 
-    "Description": "Bad request"
-  }, 
-  {
-    "Code": 9, 
-    "Name": "NetworkProtocol", 
-    "Description": "Error in the network protocol"
-  }, 
-  {
-    "Code": 10, 
-    "Name": "SystemCommand", 
-    "Description": "Error while calling a system command"
-  }, 
-  {
-    "Code": 11, 
-    "Name": "Database", 
-    "Description": "Error with the database engine"
-  }, 
-  {
-    "Code": 12, 
-    "HttpStatus": 400, 
-    "Name": "UriSyntax", 
-    "Description": "Badly formatted URI"
-  }, 
-  {
-    "Code": 13, 
-    "HttpStatus": 404, 
-    "Name": "InexistentFile", 
-    "Description": "Inexistent file"
-  }, 
-  {
-    "Code": 14, 
-    "Name": "CannotWriteFile", 
-    "Description": "Cannot write to file"
-  }, 
-  {
-    "Code": 15, 
-    "HttpStatus": 400, 
-    "Name": "BadFileFormat", 
-    "Description": "Bad file format"
-  }, 
-  {
-    "Code": 16, 
-    "Name": "Timeout", 
-    "Description": "Timeout"
-  }, 
-  {
-    "Code": 17, 
-    "HttpStatus": 404, 
-    "Name": "UnknownResource", 
-    "Description": "Unknown resource"
-  }, 
-  {
-    "Code": 18, 
-    "Name": "IncompatibleDatabaseVersion", 
-    "Description": "Incompatible version of the database"
-  }, 
-  {
-    "Code": 19, 
-    "Name": "FullStorage", 
-    "Description": "The file storage is full"
-  }, 
-  {
-    "Code": 20, 
-    "Name": "CorruptedFile", 
-    "Description": "Corrupted file (e.g. inconsistent MD5 hash)"
-  }, 
-  {
-    "Code": 21, 
-    "HttpStatus": 404, 
-    "Name": "InexistentTag", 
-    "Description": "Inexistent tag"
-  }, 
-  {
-    "Code": 22, 
-    "Name": "ReadOnly", 
-    "Description": "Cannot modify a read-only data structure"
-  }, 
-  {
-    "Code": 23, 
-    "Name": "IncompatibleImageFormat", 
-    "Description": "Incompatible format of the images"
-  }, 
-  {
-    "Code": 24, 
-    "Name": "IncompatibleImageSize", 
-    "Description": "Incompatible size of the images"
-  }, 
-  {
-    "Code": 25, 
-    "Name": "SharedLibrary", 
-    "Description": "Error while using a shared library (plugin)"
-  }, 
-  {
-    "Code": 26, 
-    "Name": "UnknownPluginService", 
-    "Description": "Plugin invoking an unknown service"
-  }, 
-  {
-    "Code": 27, 
-    "Name": "UnknownDicomTag", 
-    "Description": "Unknown DICOM tag"
-  }, 
-  {
-    "Code": 28, 
-    "HttpStatus": 400, 
-    "Name": "BadJson", 
-    "Description": "Cannot parse a JSON document"
-  }, 
-  {
-    "Code": 29, 
-    "HttpStatus": 401, 
-    "Name": "Unauthorized", 
-    "Description": "Bad credentials were provided to an HTTP request"
-  }, 
-  {
-    "Code": 30, 
-    "Name": "BadFont", 
-    "Description": "Badly formatted font file"
-  },
-  {
-    "Code": 31, 
-    "Name": "DatabasePlugin", 
-    "Description": "The plugin implementing a custom database back-end does not fulfill the proper interface"
-  }, 
-  {
-    "Code": 32, 
-    "Name": "StorageAreaPlugin", 
-    "Description": "Error in the plugin implementing a custom storage area"
-  },
-  {
-    "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"
-  }, 
-  {
-    "Code": 38, 
-    "Name": "BadGeometry", 
-    "Description": "Geometry error encountered in Stone"
-  }, 
-  {
-    "Code": 39, 
-    "Name": "SslInitialization", 
-    "Description": "Cannot initialize SSL encryption, check out your certificates"
-  }, 
-
-
-
-  /** SQLite **/
-
-
-  {
-    "Code": 1000, 
-    "Name": "SQLiteNotOpened",
-    "Description": "SQLite: The database is not opened",
-    "SQLite": true
-  },
-  {
-    "Code": 1001, 
-    "Name": "SQLiteAlreadyOpened", 
-    "Description": "SQLite: Connection is already open",
-    "SQLite": true
-  },
-  {
-    "Code": 1002, 
-    "Name": "SQLiteCannotOpen", 
-    "Description": "SQLite: Unable to open the database",
-    "SQLite": true
-  },
-  {
-    "Code": 1003, 
-    "Name": "SQLiteStatementAlreadyUsed", 
-    "Description": "SQLite: This cached statement is already being referred to",
-    "SQLite": true
-  },
-  {
-    "Code": 1004, 
-    "Name": "SQLiteExecute", 
-    "Description": "SQLite: Cannot execute a command",
-    "SQLite": true
-  },
-  {
-    "Code": 1005, 
-    "Name": "SQLiteRollbackWithoutTransaction", 
-    "Description": "SQLite: Rolling back a nonexistent transaction (have you called Begin()?)",
-    "SQLite": true
-  },
-  {
-    "Code": 1006, 
-    "Name": "SQLiteCommitWithoutTransaction", 
-    "Description": "SQLite: Committing a nonexistent transaction",
-    "SQLite": true
-  },
-  {
-    "Code": 1007, 
-    "Name": "SQLiteRegisterFunction", 
-    "Description": "SQLite: Unable to register a function",
-    "SQLite": true
-  },
-  {
-    "Code": 1008, 
-    "Name": "SQLiteFlush", 
-    "Description": "SQLite: Unable to flush the database",
-    "SQLite": true
-  },
-  {
-    "Code": 1009, 
-    "Name": "SQLiteCannotRun", 
-    "Description": "SQLite: Cannot run a cached statement",
-    "SQLite": true
-  },
-  {
-    "Code": 1010, 
-    "Name": "SQLiteCannotStep", 
-    "Description": "SQLite: Cannot step over a cached statement",
-    "SQLite": true
-  },
-  {
-    "Code": 1011, 
-    "Name": "SQLiteBindOutOfRange", 
-    "Description": "SQLite: Bing a value while out of range (serious error)",
-    "SQLite": true
-  },
-  {
-    "Code": 1012, 
-    "Name": "SQLitePrepareStatement", 
-    "Description": "SQLite: Cannot prepare a cached statement",
-    "SQLite": true
-  },
-  {
-    "Code": 1013, 
-    "Name": "SQLiteTransactionAlreadyStarted", 
-    "Description": "SQLite: Beginning the same transaction twice",
-    "SQLite": true
-  },
-  {
-    "Code": 1014, 
-    "Name": "SQLiteTransactionCommit", 
-    "Description": "SQLite: Failure when committing the transaction",
-    "SQLite": true
-  },
-  {
-    "Code": 1015, 
-    "Name": "SQLiteTransactionBegin", 
-    "Description": "SQLite: Cannot start a transaction",
-    "SQLite": true
-  },
-
-
-
-  /** Specific error codes **/
-
-  
-  {
-    "Code": 2000, 
-    "Name": "DirectoryOverFile", 
-    "Description": "The directory to be created is already occupied by a regular file"
-  },
-  {
-    "Code": 2001, 
-    "Name": "FileStorageCannotWrite", 
-    "Description": "Unable to create a subdirectory or a file in the file storage"
-  },
-  {
-    "Code": 2002, 
-    "Name": "DirectoryExpected", 
-    "Description": "The specified path does not point to a directory"
-  },
-  {
-    "Code": 2003, 
-    "Name": "HttpPortInUse", 
-    "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 privileged or already in use"
-  },
-  {
-    "Code": 2005, 
-    "Name": "BadHttpStatusInRest", 
-    "Description": "This HTTP status is not allowed in a REST API"
-  },
-  {
-    "Code": 2006, 
-    "Name": "RegularFileExpected", 
-    "Description": "The specified path does not point to a regular file"
-  },
-  {
-    "Code": 2007, 
-    "Name": "PathToExecutable", 
-    "Description": "Unable to get the path to the executable"
-  },
-  {
-    "Code": 2008, 
-    "Name": "MakeDirectory", 
-    "Description": "Cannot create a directory"
-  },
-  {
-    "Code": 2009, 
-    "Name": "BadApplicationEntityTitle", 
-    "Description": "An application entity title (AET) cannot be empty or be longer than 16 characters"
-  },
-  {
-    "Code": 2010, 
-    "Name": "NoCFindHandler", 
-    "Description": "No request handler factory for DICOM C-FIND SCP"
-  },
-  {
-    "Code": 2011, 
-    "Name": "NoCMoveHandler", 
-    "Description": "No request handler factory for DICOM C-MOVE SCP"
-  },
-  {
-    "Code": 2012, 
-    "Name": "NoCStoreHandler", 
-    "Description": "No request handler factory for DICOM C-STORE SCP"
-  },
-  {
-    "Code": 2013, 
-    "Name": "NoApplicationEntityFilter", 
-    "Description": "No application entity filter"
-  },
-  {
-    "Code": 2014, 
-    "Name": "NoSopClassOrInstance", 
-    "Description": "DicomUserConnection: Unable to find the SOP class and instance"
-  },
-  {
-    "Code": 2015, 
-    "Name": "NoPresentationContext", 
-    "Description": "DicomUserConnection: No acceptable presentation context for modality"
-  },
-  {
-    "Code": 2016, 
-    "Name": "DicomFindUnavailable", 
-    "Description": "DicomUserConnection: The C-FIND command is not supported by the remote SCP"
-  },
-  {
-    "Code": 2017, 
-    "Name": "DicomMoveUnavailable", 
-    "Description": "DicomUserConnection: The C-MOVE command is not supported by the remote SCP"
-  },
-  {
-    "Code": 2018, 
-    "Name": "CannotStoreInstance", 
-    "Description": "Cannot store an instance"
-  },
-  {
-    "Code": 2019,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomNotString", 
-    "Description": "Only string values are supported when creating DICOM instances"
-  },
-  {
-    "Code": 2020,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomOverrideTag", 
-    "Description": "Trying to override a value inherited from a parent module"
-  },
-  {
-    "Code": 2021,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomUseContent", 
-    "Description": "Use \\\"Content\\\" to inject an image into a new DICOM instance"
-  },
-  {
-    "Code": 2022,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomNoPayload", 
-    "Description": "No payload is present for one instance in the series"
-  },
-  {
-    "Code": 2023,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomUseDataUriScheme", 
-    "Description": "The payload of the DICOM instance must be specified according to Data URI scheme"
-  },
-  {
-    "Code": 2024,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomBadParent", 
-    "Description": "Trying to attach a new DICOM instance to an inexistent resource"
-  },
-  {
-    "Code": 2025,
-    "HttpStatus": 400, 
-    "Name": "CreateDicomParentIsInstance", 
-    "Description": "Trying to attach a new DICOM instance to an instance (must be a series, study or patient)"
-  },
-  {
-    "Code": 2026, 
-    "Name": "CreateDicomParentEncoding", 
-    "Description": "Unable to get the encoding of the parent resource"
-  },
-  {
-    "Code": 2027, 
-    "Name": "UnknownModality", 
-    "Description": "Unknown modality"
-  },
-  {
-    "Code": 2028, 
-    "Name": "BadJobOrdering", 
-    "Description": "Bad ordering of filters in a job"
-  },
-  {
-    "Code": 2029, 
-    "Name": "JsonToLuaTable", 
-    "Description": "Cannot convert the given JSON object to a Lua table"
-  },
-  {
-    "Code": 2030, 
-    "Name": "CannotCreateLua", 
-    "Description": "Cannot create the Lua context"
-  },
-  {
-    "Code": 2031, 
-    "Name": "CannotExecuteLua", 
-    "Description": "Cannot execute a Lua command"
-  },
-  {
-    "Code": 2032, 
-    "Name": "LuaAlreadyExecuted", 
-    "Description": "Arguments cannot be pushed after the Lua function is executed"
-  },
-  {
-    "Code": 2033, 
-    "Name": "LuaBadOutput", 
-    "Description": "The Lua function does not give the expected number of outputs"
-  },
-  {
-    "Code": 2034, 
-    "Name": "NotLuaPredicate", 
-    "Description": "The Lua function is not a predicate (only true/false outputs allowed)"
-  },
-  {
-    "Code": 2035, 
-    "Name": "LuaReturnsNoString", 
-    "Description": "The Lua function does not return a string"
-  },
-  {
-    "Code": 2036,
-    "Name": "StorageAreaAlreadyRegistered",
-    "Description": "Another plugin has already registered a custom storage area"
-  },
-  {
-    "Code": 2037,
-    "Name": "DatabaseBackendAlreadyRegistered",
-    "Description": "Another plugin has already registered a custom database back-end"
-  },
-  {
-    "Code": 2038,
-    "Name": "DatabaseNotInitialized",
-    "Description": "Plugin trying to call the database during its initialization"
-  },
-  { 
-    "Code": 2039,
-    "Name": "SslDisabled",
-    "Description": "Orthanc has been built without SSL support"
-  },
-  {
-    "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"
-  },
-  {
-    "Code": 2043, 
-    "Name": "NoStorageCommitmentHandler", 
-    "Description": "No request handler factory for DICOM N-ACTION SCP (storage commitment)"
-  },
-  {
-    "Code": 2044,
-    "Name": "NoCGetHandler", 
-    "Description": "No request handler factory for DICOM C-GET SCP"
-  }
-
-
-
-  /** HTTP-related error codes **/
-
-  {
-    "Code": 3000,
-    "HttpStatus": 415, 
-    "Name": "UnsupportedMediaType",
-    "Description": "Unsupported media type"
-  }
-]
--- a/Resources/Fonts/GenerateFont.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-
-
-
-# sudo pip install freetype-py
-
-
-
-import freetype
-import json
-import os
-import sys
-import unicodedata
-
-
-if len(sys.argv) != 3:
-    print('Usage: %s <Font> <Size>\n' % sys.argv[0])
-    print('Example: %s /usr/share/fonts/truetype/ubuntu-font-family/UbuntuMono-B.ttf 16\n' % sys.argv[0])
-    sys.exit(-1)
-
-
-
-FONT = sys.argv[1]
-PIXEL_SIZE = int(sys.argv[2])
-CHARSET = 'latin-1'
-
-
-# Load the font
-face = freetype.Face(FONT)
-face.set_char_size(PIXEL_SIZE * 64)
-
-# Generate all the characters between 0 and 255
-characters = ''.join(map(chr, range(0, 256)))
-
-# Interpret the string using the required charset
-characters = characters.decode(CHARSET, 'ignore')
-
-# Keep only non-control characters
-characters = filter(lambda c: unicodedata.category(c)[0] != 'C', characters)
-
-font = {
-    'Name' : os.path.basename(FONT),
-    'Size' : PIXEL_SIZE,
-    'Characters' : {}
-}
-
-
-def PrintCharacter(c):
-    pos = 0
-    for i in range(c['Height']):
-        s = ''
-        for j in range(c['Width']):
-            if c['Bitmap'][pos] > 127:
-                s += '*'
-            else:
-                s += ' '
-            pos += 1
-        print s
-
-
-for c in characters:
-    face.load_char(c)
-
-    info = {
-        'Width' : face.glyph.bitmap.width,
-        'Height' : face.glyph.bitmap.rows,
-        'Advance' : face.glyph.metrics.horiAdvance / 64,
-        'Top' : -face.glyph.metrics.horiBearingY / 64,
-        'Bitmap' : face.glyph.bitmap.buffer,
-    }
-
-    font['Characters'][ord(c)] = info
-
-    #PrintCharacter(info)
-
-minTop = min(map(lambda (k, v): v['Top'], font['Characters'].iteritems()))
-for c in font['Characters']:
-    font['Characters'][c]['Top'] -= minTop
-
-font['MaxAdvance'] = max(map(lambda (k, v): v['Advance'], font['Characters'].iteritems()))
-font['MaxHeight'] = max(map(lambda (k, v): v['Height'], font['Characters'].iteritems()))
-
-print json.dumps(font)
--- a/Resources/Fonts/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-This file contains a precompiled version of several open-source fonts,
-that are used to draw text on images within Orthanc. These fonts are
-encoded as JSON files through the "./GenerateFont.py" script.
-
-- UbuntuMonoBold-16.json is the Ubuntu Mono Bold, size 16, licensed
-  under the Ubuntu Font Licence.
--- a/Resources/Fonts/UbuntuMonoBold-16.json	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-{"MaxAdvance": 9, "MaxHeight": 15, "Name": "UbuntuMono-B.ttf", "Characters": {"32": {"Width": 0, "Advance": 8, "Bitmap": [], "Top": 14, "Height": 0}, "33": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 254, 251, 245, 242, 224, 223, 196, 199, 0, 0, 199, 198, 199, 198], "Top": 4, "Height": 10}, "34": {"Width": 5, "Advance": 8, "Bitmap": [255, 255, 0, 255, 255, 248, 247, 0, 248, 247, 226, 226, 0, 226, 226, 197, 196, 0, 197, 196, 160, 160, 0, 160, 162], "Top": 3, "Height": 5}, "35": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 31, 255, 254, 255, 225, 0, 0, 0, 93, 255, 250, 255, 163, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 234, 255, 255, 255, 21, 0, 0, 25, 255, 253, 255, 231, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 163, 243, 161, 255, 93, 0, 0, 0, 225, 94, 105, 255, 31, 0, 0], "Top": 4, "Height": 10}, "36": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 8, 0, 33, 198, 255, 255, 248, 169, 190, 255, 255, 255, 255, 202, 246, 255, 46, 8, 44, 66, 216, 255, 231, 148, 27, 0, 71, 243, 255, 255, 239, 59, 0, 23, 115, 205, 255, 205, 82, 56, 10, 42, 255, 246, 211, 255, 255, 255, 255, 209, 120, 227, 255, 255, 245, 72, 0, 0, 255, 255, 15, 0, 0, 0, 255, 255, 0, 0], "Top": 3, "Height": 13}, "37": {"Width": 7, "Advance": 8, "Bitmap": [127, 242, 123, 0, 0, 77, 179, 240, 57, 239, 0, 8, 213, 33, 240, 55, 239, 0, 128, 127, 0, 126, 243, 123, 34, 213, 8, 0, 0, 0, 0, 179, 76, 0, 0, 0, 0, 77, 179, 0, 0, 0, 0, 8, 213, 33, 126, 59, 124, 0, 128, 127, 0, 240, 6, 239, 34, 213, 8, 0, 240, 68, 240, 179, 76, 0, 0, 128, 243, 126], "Top": 4, "Height": 10}, "38": {"Width": 9, "Advance": 8, "Bitmap": [0, 39, 190, 245, 206, 57, 0, 0, 0, 0, 201, 255, 255, 255, 219, 0, 0, 0, 0, 242, 255, 55, 255, 233, 0, 0, 0, 0, 184, 255, 116, 255, 110, 0, 0, 0, 0, 128, 255, 255, 130, 6, 188, 52, 0, 102, 255, 103, 202, 235, 70, 255, 70, 0, 225, 255, 11, 26, 231, 249, 255, 45, 0, 240, 255, 82, 17, 174, 255, 255, 17, 0, 162, 255, 255, 255, 255, 255, 255, 125, 0, 14, 152, 229, 240, 188, 114, 255, 235, 7], "Top": 4, "Height": 10}, "39": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 251, 250, 230, 230, 200, 199, 161, 163], "Top": 3, "Height": 5}, "40": {"Width": 5, "Advance": 8, "Bitmap": [0, 0, 9, 152, 142, 0, 12, 199, 253, 110, 0, 166, 255, 118, 0, 46, 255, 207, 2, 0, 146, 255, 106, 0, 0, 209, 255, 40, 0, 0, 239, 255, 12, 0, 0, 242, 255, 14, 0, 0, 213, 255, 43, 0, 0, 157, 255, 117, 0, 0, 58, 255, 219, 8, 0, 0, 180, 255, 153, 0, 0, 17, 207, 255, 160, 0, 0, 12, 157, 119], "Top": 3, "Height": 14}, "41": {"Width": 6, "Advance": 8, "Bitmap": [120, 194, 49, 0, 0, 0, 82, 230, 250, 103, 0, 0, 0, 23, 219, 255, 93, 0, 0, 0, 32, 242, 243, 24, 0, 0, 0, 129, 255, 131, 0, 0, 0, 45, 255, 208, 0, 0, 0, 8, 255, 245, 0, 0, 0, 9, 255, 247, 0, 0, 0, 50, 255, 216, 0, 0, 0, 140, 255, 146, 0, 0, 50, 247, 250, 37, 0, 47, 235, 255, 117, 0, 121, 250, 253, 123, 0, 0, 80, 201, 60, 0, 0, 0], "Top": 3, "Height": 14}, "42": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 136, 189, 68, 255, 66, 183, 136, 213, 255, 255, 255, 255, 255, 214, 0, 7, 161, 248, 162, 8, 0, 0, 174, 255, 132, 255, 176, 0, 0, 67, 187, 4, 185, 63, 0], "Top": 4, "Height": 7}, "43": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 6, "Height": 8}, "44": {"Width": 4, "Advance": 8, "Bitmap": [0, 140, 243, 125, 0, 246, 255, 240, 0, 166, 255, 234, 0, 40, 255, 177, 49, 203, 240, 42, 204, 174, 39, 0], "Top": 11, "Height": 6}, "45": {"Width": 4, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255], "Top": 9, "Height": 2}, "46": {"Width": 3, "Advance": 9, "Bitmap": [91, 243, 85, 234, 255, 212, 91, 244, 85], "Top": 11, "Height": 3}, "47": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 47, 255, 217, 0, 0, 0, 0, 124, 255, 139, 0, 0, 0, 0, 201, 255, 62, 0, 0, 0, 24, 254, 237, 3, 0, 0, 0, 99, 255, 164, 0, 0, 0, 0, 176, 255, 87, 0, 0, 0, 8, 245, 250, 15, 0, 0, 0, 75, 255, 189, 0, 0, 0, 0, 152, 255, 111, 0, 0, 0, 0, 228, 255, 34, 0, 0, 0, 50, 255, 213, 0, 0, 0, 0, 127, 255, 136, 0, 0, 0, 0, 204, 255, 59, 0, 0, 0, 26, 254, 235, 2, 0, 0, 0], "Top": 3, "Height": 14}, "48": {"Width": 6, "Advance": 8, "Bitmap": [0, 125, 237, 238, 130, 0, 85, 255, 255, 255, 255, 89, 183, 255, 112, 114, 255, 184, 231, 255, 28, 30, 255, 231, 251, 255, 205, 204, 255, 250, 251, 255, 205, 204, 255, 249, 232, 255, 28, 30, 255, 230, 185, 255, 112, 114, 255, 184, 90, 255, 255, 255, 255, 89, 0, 131, 238, 238, 131, 0], "Top": 4, "Height": 10}, "49": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 48, 216, 255, 0, 0, 49, 161, 252, 255, 255, 0, 0, 196, 255, 221, 255, 255, 0, 0, 56, 80, 3, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "50": {"Width": 7, "Advance": 8, "Bitmap": [0, 31, 166, 239, 236, 162, 19, 0, 206, 255, 255, 255, 255, 177, 0, 93, 125, 21, 76, 255, 247, 0, 0, 0, 0, 22, 255, 200, 0, 0, 0, 5, 182, 247, 54, 0, 0, 9, 184, 244, 73, 0, 0, 3, 184, 233, 49, 0, 0, 0, 117, 255, 60, 0, 0, 0, 0, 227, 255, 255, 255, 255, 255, 0, 253, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "51": {"Width": 6, "Advance": 8, "Bitmap": [78, 198, 247, 226, 148, 13, 201, 255, 255, 255, 255, 155, 66, 71, 13, 62, 255, 236, 0, 0, 12, 99, 255, 226, 0, 255, 255, 255, 250, 88, 0, 255, 255, 255, 251, 95, 0, 1, 16, 94, 255, 228, 69, 33, 7, 76, 255, 239, 215, 255, 255, 255, 255, 162, 145, 222, 251, 233, 144, 13], "Top": 4, "Height": 10}, "52": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 64, 249, 255, 0, 0, 0, 34, 238, 255, 255, 0, 0, 3, 198, 241, 255, 255, 0, 0, 119, 255, 91, 255, 255, 0, 33, 246, 179, 0, 255, 255, 0, 165, 252, 37, 0, 255, 255, 0, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0], "Top": 4, "Height": 10}, "53": {"Width": 6, "Advance": 8, "Bitmap": [0, 171, 255, 255, 255, 255, 0, 184, 255, 255, 255, 255, 0, 198, 236, 0, 0, 0, 0, 221, 255, 215, 118, 2, 0, 244, 255, 255, 255, 118, 0, 4, 34, 154, 255, 218, 0, 0, 0, 12, 255, 246, 60, 25, 8, 96, 255, 216, 213, 255, 255, 255, 255, 108, 156, 231, 251, 222, 106, 1], "Top": 4, "Height": 10}, "54": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 42, 155, 221, 227, 0, 91, 249, 255, 255, 249, 42, 249, 237, 115, 42, 11, 156, 255, 140, 100, 31, 0, 223, 255, 255, 255, 244, 68, 249, 254, 158, 193, 255, 204, 247, 252, 1, 13, 255, 246, 211, 255, 78, 70, 255, 222, 121, 255, 255, 255, 255, 126, 4, 144, 237, 244, 147, 4], "Top": 4, "Height": 10}, "55": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 77, 255, 161, 0, 0, 3, 218, 241, 18, 0, 0, 89, 255, 135, 0, 0, 0, 205, 253, 26, 0, 0, 45, 255, 193, 0, 0, 0, 132, 255, 111, 0, 0, 0, 201, 255, 52, 0, 0, 0, 242, 255, 14, 0, 0], "Top": 4, "Height": 10}, "56": {"Width": 6, "Advance": 8, "Bitmap": [7, 142, 237, 243, 186, 38, 123, 255, 255, 255, 255, 203, 184, 255, 52, 50, 255, 244, 137, 255, 104, 72, 255, 153, 16, 237, 255, 255, 213, 7, 136, 255, 86, 183, 255, 130, 231, 255, 9, 19, 255, 233, 241, 255, 68, 67, 255, 239, 174, 255, 255, 255, 255, 159, 21, 168, 238, 242, 158, 14], "Top": 4, "Height": 10}, "57": {"Width": 6, "Advance": 8, "Bitmap": [3, 141, 242, 235, 131, 1, 125, 255, 255, 255, 255, 93, 224, 255, 60, 85, 255, 198, 246, 255, 12, 2, 253, 236, 205, 255, 198, 166, 255, 249, 69, 244, 255, 255, 255, 228, 0, 29, 93, 127, 255, 174, 0, 15, 80, 227, 255, 74, 0, 253, 255, 255, 154, 0, 0, 236, 203, 101, 1, 0], "Top": 4, "Height": 10}, "58": {"Width": 3, "Advance": 9, "Bitmap": [91, 243, 85, 234, 255, 212, 91, 244, 85, 0, 0, 0, 0, 0, 0, 91, 243, 85, 234, 255, 212, 91, 244, 85], "Top": 6, "Height": 8}, "59": {"Width": 4, "Advance": 8, "Bitmap": [0, 91, 243, 85, 0, 234, 255, 212, 0, 91, 244, 85, 0, 0, 0, 0, 0, 0, 0, 0, 0, 140, 243, 125, 0, 246, 255, 240, 0, 166, 255, 234, 0, 40, 255, 177, 49, 203, 240, 42, 204, 174, 39, 0], "Top": 6, "Height": 11}, "60": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 94, 131, 0, 0, 0, 32, 185, 255, 203, 0, 2, 112, 244, 255, 255, 192, 27, 201, 255, 255, 234, 98, 1, 72, 255, 252, 148, 16, 0, 0, 72, 255, 252, 147, 16, 0, 0, 25, 195, 255, 255, 234, 97, 1, 0, 1, 108, 243, 255, 255, 192, 0, 0, 0, 30, 183, 255, 203, 0, 0, 0, 0, 0, 93, 131], "Top": 3, "Height": 10}, "61": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 8, "Height": 5}, "62": {"Width": 6, "Advance": 8, "Bitmap": [132, 83, 0, 0, 0, 0, 206, 255, 166, 18, 0, 0, 195, 255, 255, 231, 78, 0, 2, 108, 241, 255, 255, 161, 0, 0, 26, 172, 255, 255, 0, 0, 26, 171, 255, 255, 2, 107, 241, 255, 255, 155, 195, 255, 255, 229, 73, 0, 206, 255, 165, 17, 0, 0, 132, 82, 0, 0, 0, 0], "Top": 3, "Height": 10}, "63": {"Width": 6, "Advance": 8, "Bitmap": [98, 200, 241, 241, 170, 22, 200, 255, 255, 255, 255, 174, 68, 61, 11, 67, 255, 243, 0, 0, 0, 65, 255, 196, 0, 1, 119, 250, 232, 40, 0, 132, 255, 220, 35, 0, 0, 245, 255, 47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 197, 195, 0, 0, 0, 0, 198, 198, 0, 0, 0], "Top": 4, "Height": 10}, "64": {"Width": 7, "Advance": 8, "Bitmap": [0, 30, 175, 244, 229, 135, 6, 10, 211, 81, 14, 124, 255, 132, 111, 139, 0, 0, 10, 255, 224, 190, 59, 31, 209, 247, 255, 252, 232, 20, 172, 255, 247, 255, 255, 249, 5, 235, 255, 47, 255, 255, 248, 4, 251, 255, 2, 255, 255, 229, 21, 231, 255, 45, 255, 255, 179, 70, 152, 255, 253, 255, 255, 88, 176, 14, 159, 237, 229, 129, 1, 181, 163, 45, 7, 0, 0, 0, 5, 120, 209, 246, 254, 216], "Top": 4, "Height": 12}, "65": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 4, "Height": 10}, "66": {"Width": 6, "Advance": 8, "Bitmap": [212, 240, 251, 232, 160, 21, 255, 255, 255, 255, 255, 182, 255, 255, 5, 53, 255, 244, 255, 255, 1, 70, 255, 222, 255, 255, 255, 255, 247, 70, 255, 255, 255, 255, 251, 125, 255, 255, 1, 66, 255, 237, 255, 255, 2, 62, 255, 246, 255, 255, 255, 255, 255, 169, 206, 242, 251, 228, 151, 14], "Top": 4, "Height": 10}, "67": {"Width": 7, "Advance": 8, "Bitmap": [0, 15, 140, 225, 248, 209, 91, 10, 208, 255, 255, 255, 255, 223, 124, 255, 216, 58, 9, 72, 133, 210, 255, 64, 0, 0, 0, 0, 247, 255, 8, 0, 0, 0, 0, 249, 255, 15, 0, 0, 0, 0, 219, 255, 67, 0, 0, 0, 0, 148, 255, 211, 51, 8, 64, 134, 27, 235, 255, 255, 255, 255, 225, 0, 37, 171, 238, 249, 205, 87], "Top": 4, "Height": 10}, "68": {"Width": 7, "Advance": 8, "Bitmap": [214, 244, 252, 229, 159, 29, 0, 255, 255, 255, 255, 255, 229, 23, 255, 255, 5, 42, 212, 255, 143, 255, 255, 0, 0, 65, 255, 218, 255, 255, 0, 0, 12, 255, 247, 255, 255, 0, 0, 15, 255, 246, 255, 255, 0, 0, 72, 255, 214, 255, 255, 8, 61, 219, 255, 135, 255, 255, 255, 255, 255, 219, 17, 217, 248, 250, 221, 145, 19, 0], "Top": 4, "Height": 10}, "69": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "70": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 4, "Height": 10}, "71": {"Width": 7, "Advance": 8, "Bitmap": [0, 20, 151, 231, 254, 255, 234, 13, 215, 255, 255, 255, 255, 190, 130, 255, 219, 61, 11, 72, 122, 212, 255, 68, 0, 0, 0, 0, 247, 255, 13, 0, 0, 0, 0, 248, 255, 13, 0, 0, 255, 255, 219, 255, 58, 0, 0, 255, 255, 148, 255, 200, 39, 5, 255, 255, 28, 236, 255, 255, 255, 255, 255, 0, 40, 178, 242, 255, 255, 255], "Top": 4, "Height": 10}, "72": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 4, "Height": 10}, "73": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "74": {"Width": 6, "Advance": 8, "Bitmap": [0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 8, 255, 252, 115, 93, 9, 109, 255, 223, 216, 255, 255, 255, 255, 128, 72, 209, 249, 232, 131, 4], "Top": 4, "Height": 10}, "75": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 74, 255, 182, 255, 255, 0, 12, 223, 245, 35, 255, 255, 0, 163, 255, 107, 0, 255, 255, 114, 255, 172, 0, 0, 255, 255, 253, 236, 11, 0, 0, 255, 255, 216, 255, 127, 0, 0, 255, 255, 30, 235, 253, 62, 0, 255, 255, 0, 77, 255, 217, 3, 255, 255, 0, 0, 179, 255, 97, 255, 255, 0, 0, 56, 255, 205], "Top": 4, "Height": 10}, "76": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "77": {"Width": 7, "Advance": 8, "Bitmap": [119, 255, 43, 16, 255, 255, 17, 142, 255, 131, 44, 255, 255, 53, 163, 228, 217, 72, 255, 227, 88, 180, 191, 230, 149, 236, 183, 120, 195, 200, 158, 246, 164, 193, 151, 209, 210, 85, 255, 89, 204, 177, 221, 220, 0, 0, 0, 216, 199, 232, 230, 0, 0, 0, 227, 219, 242, 240, 0, 0, 0, 239, 234, 251, 250, 0, 0, 0, 250, 248], "Top": 4, "Height": 10}, "78": {"Width": 7, "Advance": 8, "Bitmap": [255, 246, 31, 0, 0, 255, 255, 255, 255, 171, 0, 0, 255, 255, 255, 255, 255, 53, 0, 255, 255, 255, 255, 229, 185, 0, 255, 255, 255, 255, 106, 255, 57, 255, 255, 255, 255, 6, 223, 178, 255, 255, 255, 255, 0, 97, 254, 255, 255, 255, 255, 0, 5, 229, 255, 255, 255, 255, 0, 0, 120, 255, 255, 255, 255, 0, 0, 17, 244, 255], "Top": 4, "Height": 10}, "79": {"Width": 7, "Advance": 8, "Bitmap": [0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 4, "Height": 10}, "80": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 255, 236, 199, 95, 1, 255, 255, 255, 255, 255, 255, 124, 255, 255, 5, 12, 120, 255, 226, 255, 255, 0, 0, 12, 255, 250, 255, 255, 0, 15, 122, 255, 223, 255, 255, 255, 255, 255, 255, 118, 255, 255, 255, 234, 195, 89, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0], "Top": 4, "Height": 10}, "81": {"Width": 7, "Advance": 8, "Bitmap": [0, 76, 212, 249, 214, 81, 0, 53, 251, 255, 255, 255, 252, 55, 166, 255, 146, 19, 145, 255, 166, 225, 255, 32, 0, 34, 255, 224, 250, 255, 5, 0, 7, 255, 248, 246, 255, 5, 0, 7, 255, 245, 223, 255, 31, 0, 36, 255, 221, 168, 255, 143, 19, 150, 255, 165, 63, 253, 255, 255, 255, 252, 60, 0, 94, 231, 255, 227, 92, 0, 0, 0, 52, 255, 181, 70, 18, 0, 0, 0, 175, 255, 255, 210, 0, 0, 0, 4, 101, 192, 149], "Top": 4, "Height": 13}, "82": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 249, 214, 121, 2, 255, 255, 255, 255, 255, 122, 255, 255, 7, 106, 255, 222, 255, 255, 0, 10, 255, 249, 255, 255, 6, 105, 255, 212, 255, 255, 255, 255, 252, 77, 255, 255, 255, 247, 51, 0, 255, 255, 142, 255, 133, 0, 255, 255, 7, 214, 252, 47, 255, 255, 0, 73, 255, 189], "Top": 4, "Height": 10}, "83": {"Width": 6, "Advance": 8, "Bitmap": [11, 142, 225, 248, 211, 92, 153, 255, 255, 255, 255, 216, 234, 255, 58, 7, 74, 128, 241, 255, 144, 31, 0, 0, 139, 255, 255, 253, 157, 10, 3, 118, 233, 255, 255, 149, 0, 0, 5, 111, 255, 240, 135, 69, 7, 61, 255, 238, 223, 255, 255, 255, 255, 167, 97, 212, 249, 231, 160, 19], "Top": 4, "Height": 10}, "84": {"Width": 8, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 4, "Height": 10}, "85": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 4, "Height": 10}, "86": {"Width": 7, "Advance": 8, "Bitmap": [232, 250, 3, 0, 3, 249, 230, 182, 255, 35, 0, 39, 255, 178, 123, 255, 79, 0, 84, 255, 123, 62, 255, 124, 0, 131, 255, 63, 7, 247, 175, 0, 182, 249, 9, 0, 188, 228, 0, 233, 190, 0, 0, 120, 255, 65, 255, 120, 0, 0, 50, 255, 194, 255, 48, 0, 0, 1, 232, 255, 225, 0, 0, 0, 0, 156, 255, 147, 0, 0], "Top": 4, "Height": 10}, "87": {"Width": 7, "Advance": 8, "Bitmap": [252, 248, 0, 0, 0, 253, 251, 245, 234, 0, 0, 0, 246, 241, 236, 221, 0, 0, 0, 237, 231, 225, 209, 32, 120, 29, 223, 221, 215, 196, 120, 255, 109, 210, 210, 201, 182, 189, 241, 177, 197, 197, 186, 182, 248, 108, 241, 191, 181, 170, 237, 227, 2, 230, 237, 165, 148, 255, 149, 0, 153, 255, 146, 124, 255, 71, 0, 72, 255, 126], "Top": 4, "Height": 10}, "88": {"Width": 8, "Advance": 8, "Bitmap": [178, 255, 160, 0, 0, 137, 255, 178, 32, 246, 252, 37, 29, 247, 246, 32, 0, 124, 255, 164, 159, 255, 124, 0, 0, 7, 218, 253, 253, 218, 7, 0, 0, 0, 82, 255, 255, 87, 0, 0, 0, 0, 152, 255, 255, 173, 0, 0, 0, 45, 253, 212, 213, 255, 68, 0, 0, 184, 255, 89, 92, 255, 206, 1, 68, 255, 221, 3, 3, 225, 255, 81, 197, 255, 109, 0, 0, 119, 255, 200], "Top": 4, "Height": 10}, "89": {"Width": 8, "Advance": 8, "Bitmap": [201, 255, 76, 0, 0, 57, 255, 203, 92, 255, 172, 0, 0, 158, 255, 98, 6, 232, 249, 22, 17, 245, 233, 8, 0, 118, 255, 130, 120, 255, 120, 0, 0, 9, 227, 236, 234, 233, 11, 0, 0, 0, 98, 255, 255, 101, 0, 0, 0, 0, 2, 255, 255, 2, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 4, "Height": 10}, "90": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 177, 0, 0, 0, 148, 245, 31, 0, 0, 39, 251, 123, 0, 0, 0, 177, 222, 8, 0, 0, 62, 255, 86, 0, 0, 0, 200, 201, 0, 0, 0, 79, 255, 67, 0, 0, 0, 204, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "91": {"Width": 4, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "92": {"Width": 6, "Advance": 8, "Bitmap": [220, 255, 44, 0, 0, 0, 147, 255, 116, 0, 0, 0, 75, 255, 189, 0, 0, 0, 10, 248, 249, 12, 0, 0, 0, 186, 255, 78, 0, 0, 0, 113, 255, 150, 0, 0, 0, 40, 255, 223, 0, 0, 0, 0, 223, 255, 40, 0, 0, 0, 151, 255, 112, 0, 0, 0, 79, 255, 185, 0, 0, 0, 12, 249, 247, 9, 0, 0, 0, 190, 255, 74, 0, 0, 0, 117, 255, 146, 0, 0, 0, 44, 255, 219], "Top": 3, "Height": 14}, "93": {"Width": 4, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "94": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 49, 250, 251, 54, 0, 0, 0, 7, 211, 255, 255, 218, 11, 0, 0, 139, 255, 171, 169, 255, 155, 0, 60, 253, 235, 22, 19, 231, 255, 80, 74, 201, 88, 0, 0, 76, 208, 90], "Top": 4, "Height": 5}, "95": {"Width": 8, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 15, "Height": 2}, "96": {"Width": 4, "Advance": 8, "Bitmap": [64, 187, 34, 0, 175, 255, 245, 113, 0, 65, 179, 136], "Top": 3, "Height": 3}, "97": {"Width": 7, "Advance": 8, "Bitmap": [0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 6, "Height": 8}, "98": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 250, 172, 10, 255, 255, 255, 255, 255, 124, 255, 255, 0, 94, 255, 210, 255, 255, 0, 10, 255, 242, 255, 255, 0, 12, 255, 238, 255, 255, 0, 95, 255, 198, 255, 255, 243, 255, 255, 87, 183, 232, 250, 226, 101, 0], "Top": 3, "Height": 11}, "99": {"Width": 6, "Advance": 8, "Bitmap": [0, 71, 198, 246, 240, 172, 60, 253, 255, 255, 255, 201, 187, 255, 160, 26, 10, 32, 236, 255, 19, 0, 0, 0, 239, 255, 20, 0, 0, 0, 197, 255, 157, 23, 7, 34, 84, 255, 255, 255, 255, 218, 0, 86, 206, 248, 240, 182], "Top": 6, "Height": 8}, "100": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 118, 226, 254, 255, 255, 255, 90, 255, 255, 255, 255, 255, 255, 199, 255, 128, 8, 0, 255, 255, 239, 255, 14, 0, 0, 255, 255, 238, 255, 21, 0, 0, 255, 255, 194, 255, 154, 17, 6, 255, 255, 70, 255, 255, 255, 255, 255, 255, 0, 86, 206, 248, 246, 226, 185], "Top": 3, "Height": 11}, "101": {"Width": 8, "Advance": 8, "Bitmap": [0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 6, "Height": 8}, "102": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 19, 159, 232, 248, 223, 147, 0, 0, 186, 255, 255, 255, 255, 208, 0, 0, 250, 255, 50, 6, 34, 54, 255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 3, "Height": 11}, "103": {"Width": 7, "Advance": 8, "Bitmap": [0, 67, 191, 243, 247, 224, 174, 72, 253, 255, 255, 255, 255, 255, 195, 255, 162, 23, 6, 255, 255, 245, 255, 23, 0, 0, 255, 255, 242, 255, 13, 0, 0, 255, 255, 206, 255, 129, 10, 34, 255, 255, 101, 255, 255, 255, 255, 255, 255, 1, 129, 230, 249, 210, 255, 244, 0, 0, 0, 6, 77, 255, 209, 27, 255, 255, 255, 255, 255, 106, 45, 198, 239, 250, 220, 122, 2], "Top": 6, "Height": 11}, "104": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 228, 246, 171, 16, 255, 255, 255, 255, 255, 153, 255, 255, 16, 82, 255, 228, 255, 255, 0, 8, 255, 251, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255], "Top": 3, "Height": 11}, "105": {"Width": 7, "Advance": 8, "Bitmap": [0, 201, 199, 0, 0, 0, 0, 0, 201, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 1, 0, 0, 0, 0, 238, 255, 63, 0, 0, 0, 0, 180, 255, 255, 255, 173, 0, 0, 33, 189, 244, 234, 136], "Top": 3, "Height": 11}, "106": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 0, 0, 201, 199, 0, 0, 0, 0, 201, 199, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 2, 255, 254, 48, 156, 25, 64, 255, 233, 122, 255, 255, 255, 255, 157, 48, 175, 234, 238, 163, 16], "Top": 3, "Height": 14}, "107": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 1, 182, 255, 141, 255, 255, 0, 129, 255, 158, 1, 255, 255, 94, 255, 161, 2, 0, 255, 255, 252, 216, 2, 0, 0, 255, 255, 179, 255, 136, 0, 0, 255, 255, 9, 205, 255, 100, 0, 255, 255, 0, 34, 241, 248, 49, 255, 255, 0, 0, 97, 196, 148], "Top": 3, "Height": 11}, "108": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 243, 255, 40, 0, 0, 0, 199, 255, 255, 223, 0, 0, 59, 220, 247, 187], "Top": 3, "Height": 11}, "109": {"Width": 7, "Advance": 8, "Bitmap": [185, 234, 244, 179, 232, 228, 76, 255, 255, 255, 255, 255, 255, 208, 255, 255, 35, 244, 37, 255, 245, 255, 255, 0, 255, 1, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 6, "Height": 8}, "110": {"Width": 7, "Advance": 8, "Bitmap": [175, 217, 238, 251, 229, 144, 8, 255, 255, 255, 255, 255, 255, 143, 255, 255, 7, 9, 115, 255, 222, 255, 255, 0, 0, 12, 255, 250, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 6, "Height": 8}, "111": {"Width": 7, "Advance": 8, "Bitmap": [0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 6, "Height": 8}, "112": {"Width": 6, "Advance": 8, "Bitmap": [178, 228, 249, 227, 118, 0, 255, 255, 255, 255, 255, 90, 255, 255, 8, 119, 255, 200, 255, 255, 0, 16, 255, 239, 255, 255, 0, 9, 255, 241, 255, 255, 20, 92, 255, 209, 255, 255, 255, 255, 255, 122, 255, 255, 210, 247, 164, 6, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 6, "Height": 11}, "113": {"Width": 6, "Advance": 8, "Bitmap": [0, 98, 225, 251, 232, 181, 85, 255, 255, 255, 255, 255, 197, 255, 117, 8, 255, 255, 239, 255, 15, 0, 255, 255, 243, 255, 9, 0, 255, 255, 212, 255, 92, 0, 255, 255, 126, 255, 255, 255, 255, 255, 7, 168, 250, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255], "Top": 6, "Height": 11}, "114": {"Width": 6, "Advance": 8, "Bitmap": [122, 195, 239, 253, 239, 187, 255, 255, 255, 255, 255, 200, 255, 255, 28, 3, 17, 34, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 6, "Height": 8}, "115": {"Width": 6, "Advance": 8, "Bitmap": [28, 163, 237, 249, 227, 158, 196, 255, 255, 255, 255, 213, 239, 255, 54, 8, 40, 65, 144, 255, 239, 158, 58, 0, 0, 79, 171, 244, 255, 125, 94, 48, 7, 49, 255, 236, 227, 255, 255, 255, 255, 209, 123, 217, 250, 241, 185, 42], "Top": 6, "Height": 8}, "116": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 250, 255, 48, 7, 48, 0, 0, 209, 255, 255, 255, 221, 0, 0, 62, 210, 249, 237, 170], "Top": 4, "Height": 10}, "117": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 6, "Height": 8}, "118": {"Width": 7, "Advance": 8, "Bitmap": [166, 255, 49, 0, 1, 236, 220, 97, 255, 110, 0, 53, 255, 149, 26, 254, 182, 0, 133, 255, 75, 0, 203, 247, 11, 218, 241, 6, 0, 123, 255, 131, 255, 162, 0, 0, 35, 255, 251, 255, 67, 0, 0, 0, 198, 255, 224, 2, 0, 0, 0, 98, 255, 123, 0, 0], "Top": 6, "Height": 8}, "119": {"Width": 8, "Advance": 8, "Bitmap": [245, 255, 0, 0, 0, 0, 255, 244, 224, 255, 0, 0, 0, 0, 255, 221, 201, 255, 64, 255, 116, 0, 255, 195, 174, 255, 113, 255, 217, 1, 255, 164, 143, 255, 166, 235, 255, 75, 255, 130, 105, 255, 227, 84, 242, 199, 255, 89, 63, 255, 245, 8, 123, 255, 255, 45, 13, 253, 179, 0, 8, 222, 246, 3], "Top": 6, "Height": 8}, "120": {"Width": 8, "Advance": 8, "Bitmap": [154, 255, 212, 9, 0, 132, 255, 168, 6, 199, 255, 153, 55, 252, 230, 18, 0, 26, 231, 255, 234, 255, 74, 0, 0, 0, 58, 253, 255, 160, 0, 0, 0, 0, 109, 255, 255, 210, 8, 0, 0, 64, 251, 238, 204, 255, 138, 0, 22, 231, 255, 83, 32, 243, 252, 46, 174, 255, 180, 0, 0, 112, 255, 188], "Top": 6, "Height": 8}, "121": {"Width": 8, "Advance": 8, "Bitmap": [0, 222, 255, 34, 0, 30, 255, 222, 0, 153, 255, 105, 0, 89, 255, 156, 0, 80, 255, 180, 0, 152, 255, 88, 0, 9, 244, 249, 16, 219, 253, 19, 0, 0, 165, 255, 141, 255, 200, 0, 0, 0, 66, 255, 254, 255, 120, 0, 0, 0, 1, 214, 255, 255, 37, 0, 0, 0, 0, 123, 255, 196, 0, 0, 0, 0, 44, 228, 255, 86, 0, 0, 174, 255, 255, 255, 185, 0, 0, 0, 198, 249, 234, 156, 13, 0, 0, 0], "Top": 6, "Height": 11}, "122": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 153, 0, 0, 29, 237, 201, 6, 0, 3, 195, 240, 31, 0, 0, 126, 255, 90, 0, 0, 49, 250, 175, 0, 0, 0, 206, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 6, "Height": 8}, "123": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 77, 220, 252, 255, 0, 0, 221, 255, 255, 255, 0, 0, 253, 255, 36, 0, 0, 0, 255, 255, 0, 0, 0, 1, 255, 255, 0, 0, 0, 59, 255, 228, 0, 0, 255, 255, 218, 76, 0, 0, 255, 255, 219, 76, 0, 0, 0, 61, 255, 228, 0, 0, 0, 1, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 253, 255, 36, 0, 0, 0, 221, 255, 255, 255, 0, 0, 77, 221, 253, 255], "Top": 3, "Height": 14}, "124": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "125": {"Width": 6, "Advance": 8, "Bitmap": [255, 252, 218, 74, 0, 0, 255, 255, 255, 221, 0, 0, 0, 38, 255, 253, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 1, 0, 0, 0, 229, 255, 59, 0, 0, 0, 78, 220, 255, 255, 0, 0, 74, 217, 255, 255, 0, 0, 228, 255, 61, 0, 0, 0, 255, 255, 1, 0, 0, 0, 255, 255, 0, 0, 0, 37, 255, 253, 0, 0, 255, 255, 255, 222, 0, 0, 255, 253, 219, 74, 0, 0], "Top": 3, "Height": 14}, "126": {"Width": 7, "Advance": 8, "Bitmap": [31, 211, 234, 134, 22, 146, 178, 174, 255, 255, 255, 255, 255, 168, 180, 143, 24, 137, 235, 212, 29], "Top": 8, "Height": 3}, "160": {"Width": 0, "Advance": 8, "Bitmap": [], "Top": 14, "Height": 0}, "161": {"Width": 2, "Advance": 8, "Bitmap": [199, 198, 199, 198, 0, 0, 202, 193, 226, 219, 245, 240, 254, 251, 255, 255, 255, 255, 255, 255], "Top": 7, "Height": 10}, "162": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 2, 0, 78, 206, 255, 255, 217, 70, 252, 255, 255, 255, 188, 191, 255, 151, 21, 0, 0, 240, 255, 18, 0, 0, 0, 240, 255, 19, 0, 0, 0, 193, 255, 154, 22, 7, 33, 73, 253, 255, 255, 255, 219, 0, 80, 207, 255, 255, 227, 0, 0, 0, 255, 255, 3, 0, 0, 0, 255, 255, 0], "Top": 4, "Height": 12}, "163": {"Width": 6, "Advance": 8, "Bitmap": [0, 24, 171, 238, 236, 148, 0, 172, 255, 255, 255, 162, 0, 240, 255, 56, 81, 54, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 0, 255, 249, 0, 0, 0, 0, 255, 224, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255], "Top": 4, "Height": 10}, "164": {"Width": 8, "Advance": 8, "Bitmap": [52, 190, 17, 0, 0, 16, 189, 54, 156, 255, 226, 242, 242, 225, 255, 153, 0, 217, 255, 255, 255, 255, 216, 0, 0, 249, 255, 60, 60, 255, 248, 0, 0, 216, 255, 255, 255, 255, 216, 0, 153, 255, 226, 245, 245, 227, 255, 155, 55, 189, 16, 0, 0, 17, 190, 52], "Top": 6, "Height": 7}, "165": {"Width": 8, "Advance": 8, "Bitmap": [185, 255, 52, 0, 0, 52, 255, 182, 46, 253, 156, 0, 0, 155, 254, 44, 0, 161, 246, 24, 24, 245, 172, 0, 0, 38, 252, 146, 147, 255, 55, 0, 0, 0, 162, 248, 248, 188, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 4, "Height": 10}, "166": {"Width": 2, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 3, "Height": 14}, "167": {"Width": 6, "Advance": 8, "Bitmap": [27, 167, 232, 247, 218, 148, 190, 255, 255, 255, 255, 154, 241, 255, 74, 1, 0, 0, 162, 255, 255, 224, 113, 2, 183, 255, 255, 255, 255, 148, 250, 255, 47, 133, 255, 241, 240, 255, 125, 44, 255, 228, 149, 255, 255, 255, 255, 121, 4, 119, 231, 255, 255, 175, 133, 91, 14, 82, 255, 243, 218, 255, 255, 255, 255, 202, 105, 207, 247, 241, 188, 41], "Top": 4, "Height": 12}, "168": {"Width": 5, "Advance": 8, "Bitmap": [199, 198, 0, 199, 199, 201, 199, 0, 201, 201], "Top": 3, "Height": 2}, "169": {"Width": 8, "Advance": 8, "Bitmap": [0, 58, 185, 244, 243, 184, 56, 0, 57, 241, 113, 23, 23, 113, 240, 55, 189, 108, 78, 227, 245, 38, 112, 186, 244, 14, 229, 58, 10, 0, 15, 244, 244, 14, 232, 61, 11, 0, 15, 244, 188, 105, 84, 230, 242, 42, 109, 184, 43, 236, 107, 21, 21, 108, 235, 39, 0, 43, 181, 244, 244, 179, 42, 0], "Top": 6, "Height": 8}, "170": {"Width": 5, "Advance": 8, "Bitmap": [0, 213, 249, 225, 83, 0, 21, 53, 255, 226, 7, 72, 90, 255, 254, 191, 255, 184, 255, 255, 240, 255, 22, 255, 255, 100, 224, 250, 238, 205], "Top": 4, "Height": 6}, "171": {"Width": 8, "Advance": 8, "Bitmap": [0, 41, 193, 16, 0, 46, 191, 14, 1, 194, 218, 1, 1, 198, 216, 1, 99, 255, 110, 0, 101, 255, 108, 0, 216, 255, 27, 0, 216, 255, 27, 0, 93, 255, 111, 0, 96, 255, 110, 0, 0, 188, 220, 2, 0, 193, 218, 1, 0, 37, 191, 15, 0, 41, 190, 14], "Top": 6, "Height": 7}, "172": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255], "Top": 8, "Height": 6}, "174": {"Width": 8, "Advance": 8, "Bitmap": [0, 58, 185, 244, 243, 184, 56, 0, 57, 241, 113, 23, 23, 113, 240, 55, 189, 108, 0, 239, 245, 142, 112, 186, 244, 14, 0, 255, 45, 234, 15, 244, 244, 14, 0, 255, 255, 82, 15, 244, 188, 105, 0, 255, 126, 198, 114, 184, 43, 236, 107, 21, 21, 108, 235, 39, 0, 43, 181, 244, 244, 179, 42, 0], "Top": 6, "Height": 8}, "175": {"Width": 5, "Advance": 8, "Bitmap": [255, 255, 255, 255, 255], "Top": 4, "Height": 1}, "176": {"Width": 4, "Advance": 8, "Bitmap": [88, 232, 230, 81, 233, 53, 51, 229, 234, 54, 52, 230, 94, 234, 231, 83], "Top": 3, "Height": 4}, "177": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 5, "Height": 9}, "178": {"Width": 4, "Advance": 8, "Bitmap": [115, 232, 239, 110, 139, 41, 255, 241, 0, 90, 255, 211, 42, 240, 238, 42, 193, 255, 83, 0, 250, 255, 255, 255], "Top": 4, "Height": 6}, "179": {"Width": 5, "Advance": 8, "Bitmap": [146, 233, 237, 128, 0, 133, 47, 255, 241, 0, 0, 255, 255, 174, 0, 0, 1, 62, 254, 16, 136, 32, 52, 252, 15, 179, 244, 230, 108, 0], "Top": 4, "Height": 6}, "180": {"Width": 4, "Advance": 8, "Bitmap": [0, 35, 187, 63, 113, 245, 255, 174, 136, 179, 64, 0], "Top": 3, "Height": 3}, "181": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 8, 0, 255, 255, 255, 255, 84, 6, 255, 255, 255, 255, 255, 255, 255, 255, 255, 251, 230, 248, 225, 172, 255, 246, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 6, "Height": 11}, "182": {"Width": 7, "Advance": 8, "Bitmap": [1, 104, 203, 240, 248, 230, 190, 129, 255, 255, 255, 254, 255, 255, 238, 255, 255, 255, 1, 255, 255, 243, 255, 255, 255, 0, 255, 255, 188, 255, 255, 255, 0, 255, 255, 36, 200, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255], "Top": 4, "Height": 13}, "183": {"Width": 3, "Advance": 9, "Bitmap": [91, 243, 85, 234, 255, 212, 91, 244, 85], "Top": 8, "Height": 3}, "184": {"Width": 3, "Advance": 8, "Bitmap": [0, 176, 122, 0, 61, 235, 207, 247, 150], "Top": 14, "Height": 3}, "185": {"Width": 5, "Advance": 8, "Bitmap": [27, 122, 236, 255, 0, 208, 224, 255, 255, 0, 26, 2, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255, 255, 255, 255], "Top": 4, "Height": 5}, "186": {"Width": 6, "Advance": 8, "Bitmap": [5, 136, 238, 239, 137, 5, 122, 255, 255, 255, 255, 122, 217, 255, 75, 75, 255, 216, 246, 255, 6, 7, 255, 246, 218, 255, 73, 73, 255, 218, 125, 255, 255, 255, 255, 126, 5, 140, 231, 231, 140, 6], "Top": 4, "Height": 7}, "187": {"Width": 7, "Advance": 8, "Bitmap": [165, 77, 0, 159, 90, 0, 0, 153, 233, 21, 145, 237, 25, 0, 32, 250, 176, 22, 243, 181, 0, 0, 187, 255, 55, 162, 255, 56, 31, 250, 179, 21, 242, 183, 0, 152, 235, 24, 144, 240, 28, 0, 164, 84, 0, 159, 97, 0, 0], "Top": 6, "Height": 7}, "188": {"Width": 8, "Advance": 8, "Bitmap": [85, 247, 0, 0, 0, 60, 191, 0, 236, 255, 0, 0, 0, 188, 63, 0, 50, 255, 0, 0, 61, 190, 0, 0, 0, 255, 0, 0, 189, 62, 0, 0, 0, 0, 0, 62, 189, 0, 0, 0, 0, 0, 0, 190, 61, 0, 0, 0, 0, 0, 63, 188, 1, 186, 255, 0, 0, 0, 191, 60, 110, 149, 255, 0, 0, 64, 188, 0, 241, 255, 255, 255, 0, 191, 60, 0, 0, 0, 255, 0], "Top": 4, "Height": 10}, "189": {"Width": 8, "Advance": 8, "Bitmap": [69, 246, 0, 0, 0, 35, 213, 6, 198, 255, 0, 0, 0, 174, 81, 0, 0, 255, 0, 0, 63, 191, 0, 0, 0, 255, 0, 2, 204, 49, 0, 0, 0, 0, 0, 97, 158, 0, 0, 0, 0, 0, 12, 218, 25, 0, 0, 0, 0, 0, 132, 123, 0, 143, 245, 176, 0, 30, 216, 9, 0, 0, 40, 234, 0, 166, 89, 0, 0, 75, 205, 65, 56, 198, 1, 0, 0, 229, 255, 255], "Top": 4, "Height": 10}, "190": {"Width": 8, "Advance": 8, "Bitmap": [156, 246, 126, 0, 0, 3, 206, 46, 11, 91, 199, 0, 0, 102, 153, 0, 25, 255, 236, 0, 14, 219, 22, 0, 209, 250, 169, 0, 136, 119, 0, 0, 0, 0, 0, 33, 215, 7, 0, 0, 0, 0, 0, 171, 85, 0, 0, 0, 0, 0, 60, 194, 0, 171, 255, 88, 0, 1, 201, 52, 94, 164, 240, 88, 0, 94, 161, 0, 225, 255, 255, 219, 10, 217, 27, 0, 0, 0, 240, 88], "Top": 4, "Height": 10}, "191": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 199, 193, 0, 0, 0, 0, 200, 196, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 184, 255, 48, 0, 0, 67, 254, 244, 10, 0, 29, 236, 255, 115, 0, 0, 170, 255, 173, 2, 0, 0, 240, 255, 19, 0, 0, 0, 233, 255, 79, 11, 103, 129, 148, 255, 255, 255, 255, 217, 13, 160, 240, 247, 196, 73], "Top": 6, "Height": 11}, "192": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 64, 187, 34, 0, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 0, "Height": 14}, "193": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 0, "Height": 14}, "194": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 0, 4, 162, 255, 255, 161, 4, 0, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 0, "Height": 14}, "195": {"Width": 8, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 1, "Height": 13}, "196": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 199, 198, 0, 199, 199, 0, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 37, 255, 255, 92, 0, 0, 0, 0, 122, 255, 255, 172, 0, 0, 0, 0, 204, 234, 207, 244, 7, 0, 0, 32, 255, 165, 135, 255, 71, 0, 0, 113, 255, 98, 72, 255, 146, 0, 0, 191, 255, 42, 18, 255, 217, 0, 16, 251, 255, 255, 255, 255, 255, 30, 85, 255, 255, 255, 255, 255, 255, 98, 154, 255, 147, 0, 0, 130, 255, 161, 222, 255, 89, 0, 0, 70, 255, 224], "Top": 1, "Height": 13}, "197": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 27, 133, 134, 30, 0, 0, 0, 0, 197, 137, 137, 209, 0, 0, 0, 0, 213, 114, 113, 221, 0, 0, 0, 0, 134, 255, 255, 149, 0, 0, 0, 0, 195, 255, 255, 211, 0, 0, 0, 16, 252, 206, 208, 255, 28, 0, 0, 85, 255, 113, 115, 255, 100, 0, 0, 158, 255, 31, 33, 255, 172, 0, 0, 227, 214, 0, 0, 218, 237, 1, 39, 255, 255, 255, 255, 255, 255, 47, 104, 255, 255, 255, 255, 255, 255, 111, 165, 255, 70, 0, 0, 76, 255, 169, 226, 255, 18, 0, 0, 20, 255, 227], "Top": 1, "Height": 13}, "198": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 45, 255, 255, 255, 255, 255, 0, 0, 135, 243, 255, 255, 255, 255, 0, 0, 222, 183, 255, 255, 0, 0, 0, 52, 255, 118, 255, 255, 0, 0, 0, 134, 255, 57, 255, 255, 255, 0, 0, 210, 247, 6, 255, 255, 255, 0, 28, 255, 255, 255, 255, 255, 0, 0, 98, 255, 255, 255, 255, 255, 0, 0, 163, 255, 104, 0, 255, 255, 255, 255, 225, 255, 48, 0, 255, 255, 255, 255], "Top": 4, "Height": 10}, "199": {"Width": 8, "Advance": 8, "Bitmap": [0, 14, 140, 225, 248, 209, 91, 0, 9, 206, 255, 255, 255, 255, 223, 0, 122, 255, 216, 58, 9, 72, 133, 0, 209, 255, 64, 0, 0, 0, 0, 0, 246, 255, 8, 0, 0, 0, 0, 0, 248, 255, 16, 0, 0, 0, 0, 0, 213, 255, 74, 0, 0, 0, 0, 0, 126, 255, 221, 65, 11, 29, 155, 2, 9, 191, 255, 255, 255, 255, 255, 1, 0, 4, 116, 205, 255, 240, 123, 0, 0, 0, 0, 10, 236, 121, 0, 0, 0, 0, 0, 16, 46, 239, 0, 0, 0, 0, 0, 209, 244, 133, 0, 0], "Top": 4, "Height": 13}, "200": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "201": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "202": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "203": {"Width": 6, "Advance": 8, "Bitmap": [199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 0, 255, 255, 255, 255, 255, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 1, "Height": 13}, "204": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "205": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "206": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 0, "Height": 14}, "207": {"Width": 6, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], "Top": 1, "Height": 13}, "208": {"Width": 8, "Advance": 8, "Bitmap": [0, 213, 243, 252, 231, 161, 31, 0, 0, 255, 255, 255, 255, 255, 231, 25, 0, 255, 255, 4, 42, 212, 255, 144, 0, 255, 255, 0, 0, 65, 255, 218, 255, 255, 255, 255, 0, 12, 255, 247, 255, 255, 255, 255, 0, 15, 255, 246, 0, 255, 255, 0, 0, 72, 255, 214, 0, 255, 255, 9, 62, 220, 255, 135, 0, 255, 255, 255, 255, 255, 219, 17, 0, 219, 247, 250, 223, 146, 19, 0], "Top": 4, "Height": 10}, "209": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 255, 246, 31, 0, 0, 255, 255, 255, 255, 171, 0, 0, 255, 255, 255, 255, 255, 53, 0, 255, 255, 255, 255, 229, 185, 0, 255, 255, 255, 255, 106, 255, 57, 255, 255, 255, 255, 6, 223, 178, 255, 255, 255, 255, 0, 97, 254, 255, 255, 255, 255, 0, 5, 229, 255, 255, 255, 255, 0, 0, 120, 255, 255, 255, 255, 0, 0, 17, 244, 255], "Top": 1, "Height": 13}, "210": {"Width": 7, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 0, "Height": 14}, "211": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 0, "Height": 14}, "212": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 0, "Height": 14}, "213": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 1, "Height": 13}, "214": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 213, 249, 215, 83, 0, 55, 252, 255, 255, 255, 252, 57, 168, 255, 146, 19, 145, 255, 169, 226, 255, 32, 0, 34, 255, 226, 250, 255, 5, 0, 7, 255, 249, 250, 255, 5, 0, 7, 255, 249, 227, 255, 31, 0, 34, 255, 225, 170, 255, 145, 19, 147, 255, 168, 59, 253, 255, 255, 255, 252, 56, 0, 84, 215, 250, 215, 82, 0], "Top": 1, "Height": 13}, "215": {"Width": 6, "Advance": 8, "Bitmap": [102, 139, 0, 0, 141, 99, 156, 255, 132, 133, 255, 154, 1, 144, 255, 255, 140, 0, 0, 129, 255, 255, 132, 0, 145, 255, 124, 131, 255, 148, 98, 123, 0, 0, 129, 92], "Top": 7, "Height": 6}, "216": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 0, 150, 18, 0, 34, 169, 237, 249, 214, 253, 52, 25, 233, 255, 255, 236, 255, 255, 0, 145, 255, 181, 20, 143, 255, 255, 0, 216, 255, 44, 57, 245, 255, 255, 0, 247, 255, 14, 216, 146, 255, 255, 0, 250, 255, 137, 247, 31, 255, 248, 0, 226, 255, 252, 143, 30, 255, 223, 0, 165, 255, 254, 46, 136, 255, 165, 0, 49, 254, 255, 255, 255, 252, 54, 0, 46, 246, 213, 249, 215, 81, 0, 0, 17, 139, 0, 0, 0, 0, 0, 0], "Top": 3, "Height": 12}, "217": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 0, "Height": 14}, "218": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 0, "Height": 14}, "219": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 0, "Height": 14}, "220": {"Width": 6, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 249, 255, 7, 8, 255, 249, 225, 255, 71, 73, 255, 225, 148, 255, 255, 255, 255, 145, 14, 168, 245, 244, 164, 13], "Top": 1, "Height": 13}, "221": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 201, 255, 76, 0, 0, 57, 255, 203, 92, 255, 172, 0, 0, 158, 255, 98, 6, 232, 249, 22, 17, 245, 233, 8, 0, 118, 255, 130, 120, 255, 120, 0, 0, 9, 227, 236, 234, 233, 11, 0, 0, 0, 98, 255, 255, 101, 0, 0, 0, 0, 2, 255, 255, 2, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0], "Top": 0, "Height": 14}, "222": {"Width": 6, "Advance": 8, "Bitmap": [255, 255, 2, 0, 0, 0, 255, 255, 254, 226, 136, 8, 255, 255, 249, 255, 255, 133, 255, 255, 0, 84, 255, 220, 255, 255, 0, 9, 255, 246, 255, 255, 3, 100, 255, 217, 255, 255, 255, 255, 255, 121, 255, 255, 252, 221, 125, 4, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0], "Top": 4, "Height": 10}, "223": {"Width": 7, "Advance": 8, "Bitmap": [9, 146, 238, 249, 193, 44, 0, 140, 255, 255, 255, 255, 206, 0, 225, 255, 88, 48, 255, 249, 0, 252, 255, 5, 44, 255, 194, 0, 255, 255, 0, 176, 255, 60, 0, 255, 255, 0, 244, 255, 20, 0, 255, 255, 0, 184, 255, 191, 17, 255, 255, 0, 7, 141, 255, 181, 255, 255, 0, 115, 40, 255, 245, 255, 255, 0, 231, 255, 255, 215, 255, 255, 0, 177, 248, 219, 70], "Top": 3, "Height": 11}, "224": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "225": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "226": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "227": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 3, "Height": 11}, "228": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 3, "Height": 11}, "229": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 141, 244, 141, 0, 0, 0, 0, 244, 72, 244, 0, 0, 0, 0, 143, 245, 143, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 181, 232, 250, 235, 164, 19, 0, 211, 255, 255, 255, 255, 163, 0, 35, 15, 6, 71, 255, 235, 22, 160, 230, 250, 234, 255, 255, 198, 255, 255, 255, 255, 255, 255, 249, 255, 76, 6, 8, 255, 255, 195, 255, 255, 255, 255, 255, 255, 27, 168, 234, 251, 241, 223, 186], "Top": 2, "Height": 12}, "230": {"Width": 9, "Advance": 8, "Bitmap": [0, 182, 243, 205, 99, 226, 207, 37, 0, 0, 218, 255, 255, 255, 255, 255, 170, 0, 0, 36, 14, 183, 255, 144, 81, 234, 0, 47, 203, 249, 247, 255, 255, 255, 255, 2, 206, 255, 255, 255, 255, 255, 255, 255, 6, 245, 255, 55, 123, 255, 150, 7, 26, 0, 189, 255, 255, 255, 255, 255, 255, 214, 0, 34, 194, 244, 196, 102, 214, 246, 186, 0], "Top": 6, "Height": 8}, "231": {"Width": 6, "Advance": 8, "Bitmap": [0, 70, 198, 246, 242, 179, 57, 253, 255, 255, 255, 190, 183, 255, 160, 26, 0, 0, 235, 255, 19, 0, 0, 0, 241, 255, 20, 0, 0, 0, 192, 255, 157, 23, 0, 0, 64, 249, 255, 255, 255, 211, 0, 59, 185, 252, 253, 206, 0, 0, 0, 211, 114, 0, 0, 0, 0, 44, 238, 0, 0, 0, 207, 245, 136, 0], "Top": 6, "Height": 11}, "232": {"Width": 8, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 2, "Height": 12}, "233": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 2, "Height": 12}, "234": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 0, 4, 162, 255, 255, 161, 4, 0, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 2, "Height": 12}, "235": {"Width": 8, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 71, 203, 249, 225, 119, 0, 0, 63, 253, 255, 255, 255, 255, 93, 0, 190, 255, 85, 8, 76, 255, 203, 0, 244, 255, 255, 255, 255, 255, 241, 0, 246, 255, 255, 255, 255, 255, 255, 4, 196, 255, 107, 20, 1, 0, 0, 0, 64, 251, 255, 255, 255, 255, 203, 0, 0, 57, 179, 236, 251, 234, 178, 0], "Top": 3, "Height": 11}, "236": {"Width": 6, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 2, "Height": 12}, "237": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 35, 187, 63, 0, 0, 113, 245, 255, 174, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 2, "Height": 12}, "238": {"Width": 6, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 4, 162, 255, 255, 161, 4, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 2, "Height": 12}, "239": {"Width": 6, "Advance": 8, "Bitmap": [199, 198, 0, 199, 199, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, 0, 255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 241, 255, 47, 103, 0, 0, 193, 255, 255, 239, 0, 0, 53, 215, 241, 155], "Top": 3, "Height": 11}, "240": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 109, 207, 59, 38, 74, 0, 0, 113, 255, 253, 255, 195, 0, 0, 196, 197, 217, 255, 53, 0, 0, 1, 0, 58, 255, 146, 0, 85, 203, 237, 247, 255, 208, 97, 255, 255, 255, 255, 255, 244, 213, 255, 116, 21, 13, 255, 247, 245, 255, 10, 0, 32, 255, 222, 212, 255, 108, 15, 149, 255, 164, 104, 255, 255, 255, 255, 254, 59, 0, 106, 223, 249, 215, 88, 0], "Top": 3, "Height": 11}, "241": {"Width": 7, "Advance": 8, "Bitmap": [75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 175, 217, 238, 251, 229, 144, 8, 255, 255, 255, 255, 255, 255, 143, 255, 255, 7, 9, 115, 255, 222, 255, 255, 0, 0, 12, 255, 250, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255], "Top": 3, "Height": 11}, "242": {"Width": 7, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 2, "Height": 12}, "243": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 2, "Height": 12}, "244": {"Width": 7, "Advance": 8, "Bitmap": [0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 2, "Height": 12}, "245": {"Width": 7, "Advance": 8, "Bitmap": [0, 75, 226, 203, 49, 86, 167, 0, 167, 85, 52, 206, 225, 75, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 3, "Height": 11}, "246": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 86, 214, 249, 215, 89, 0, 64, 255, 255, 255, 255, 255, 66, 195, 255, 130, 13, 126, 255, 194, 246, 255, 16, 0, 15, 255, 246, 245, 255, 14, 0, 17, 255, 245, 192, 255, 123, 13, 133, 255, 192, 64, 254, 255, 255, 255, 254, 63, 0, 75, 215, 250, 216, 76, 0], "Top": 3, "Height": 11}, "247": {"Width": 6, "Advance": 8, "Bitmap": [0, 0, 199, 200, 0, 0, 0, 0, 201, 202, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 199, 200, 0, 0, 0, 0, 199, 200, 0, 0], "Top": 6, "Height": 8}, "248": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 0, 0, 21, 9, 0, 86, 214, 248, 212, 206, 158, 64, 255, 255, 255, 255, 255, 86, 195, 255, 127, 45, 246, 255, 187, 246, 252, 11, 195, 131, 240, 237, 226, 239, 129, 195, 12, 253, 237, 86, 254, 245, 44, 128, 255, 196, 0, 146, 255, 255, 255, 255, 87, 0, 200, 189, 238, 235, 113, 0, 0, 29, 1, 0, 0, 0, 0], "Top": 5, "Height": 10}, "249": {"Width": 7, "Advance": 8, "Bitmap": [0, 64, 187, 34, 0, 0, 0, 0, 175, 255, 245, 113, 0, 0, 0, 0, 65, 179, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 2, "Height": 12}, "250": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 2, "Height": 12}, "251": {"Width": 7, "Advance": 8, "Bitmap": [0, 0, 1, 139, 139, 1, 0, 0, 4, 162, 255, 255, 161, 4, 0, 8, 186, 91, 89, 188, 8, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 2, "Height": 12}, "252": {"Width": 7, "Advance": 8, "Bitmap": [0, 199, 198, 0, 199, 199, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 0, 0, 0, 255, 255, 251, 255, 14, 0, 0, 255, 255, 226, 255, 120, 10, 12, 255, 255, 139, 255, 255, 255, 255, 255, 255, 10, 148, 231, 251, 238, 215, 168], "Top": 3, "Height": 11}, "253": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 0, 35, 187, 63, 0, 0, 0, 0, 113, 245, 255, 174, 0, 0, 0, 0, 136, 179, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 222, 255, 34, 0, 30, 255, 222, 0, 153, 255, 105, 0, 89, 255, 156, 0, 80, 255, 180, 0, 152, 255, 88, 0, 9, 244, 249, 16, 219, 253, 19, 0, 0, 165, 255, 141, 255, 200, 0, 0, 0, 66, 255, 254, 255, 120, 0, 0, 0, 1, 214, 255, 255, 37, 0, 0, 0, 0, 123, 255, 196, 0, 0, 0, 0, 44, 228, 255, 86, 0, 0, 174, 255, 255, 255, 185, 0, 0, 0, 198, 249, 234, 156, 13, 0, 0, 0], "Top": 2, "Height": 15}, "254": {"Width": 7, "Advance": 8, "Bitmap": [255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 240, 246, 198, 73, 0, 255, 255, 255, 255, 255, 254, 71, 255, 255, 8, 19, 163, 255, 196, 255, 255, 0, 0, 24, 255, 246, 255, 255, 0, 0, 14, 255, 238, 255, 255, 28, 10, 131, 255, 197, 255, 255, 255, 255, 255, 255, 90, 255, 255, 203, 248, 228, 106, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0], "Top": 3, "Height": 14}, "255": {"Width": 8, "Advance": 8, "Bitmap": [0, 0, 199, 198, 0, 199, 199, 0, 0, 0, 201, 199, 0, 201, 201, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 222, 255, 34, 0, 30, 255, 222, 0, 153, 255, 105, 0, 89, 255, 156, 0, 80, 255, 180, 0, 152, 255, 88, 0, 9, 244, 249, 16, 219, 253, 19, 0, 0, 165, 255, 141, 255, 200, 0, 0, 0, 66, 255, 254, 255, 120, 0, 0, 0, 1, 214, 255, 255, 37, 0, 0, 0, 0, 123, 255, 196, 0, 0, 0, 0, 44, 228, 255, 86, 0, 0, 174, 255, 255, 255, 185, 0, 0, 0, 198, 249, 234, 156, 13, 0, 0, 0], "Top": 3, "Height": 14}}, "Size": 16}
--- a/Resources/GenerateAnonymizationProfile.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 json
-import os
-import re
-import sys
-
-START_PLUGINS = 1000000
-BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
-
-
-
-## 
-## Read all the available error codes and HTTP status
-##
-
-with open(os.path.join(BASE, 'Resources', 'ErrorCodes.json'), 'r') as f:
-    ERRORS = json.loads(re.sub('/\*.*?\*/', '', f.read()))
-
-for error in ERRORS:
-    if error['Code'] >= START_PLUGINS:
-        print('ERROR: Error code must be below %d, but "%s" is set to %d' % (START_PLUGINS, error['Name'], error['Code']))
-        sys.exit(-1)
-
-with open(os.path.join(BASE, 'Core', 'Enumerations.h'), 'r') as f:
-    a = f.read()
-
-HTTP = {}
-for i in re.findall('(HttpStatus_([0-9]+)_\w+)', a):
-    HTTP[int(i[1])] = i[0]
-
-
-
-##
-## Generate the "ErrorCode" enumeration in "Enumerations.h"
-##
-
-path = os.path.join(BASE, 'Core', 'Enumerations.h')
-with open(path, 'r') as f:
-    a = f.read()
-
-s = ',\n'.join(map(lambda x: '    ErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
-
-s += ',\n    ErrorCode_START_PLUGINS = %d' % START_PLUGINS
-a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
-
-with open(path, 'w') as f:
-    f.write(a)
-
-
-
-##
-## Generate the "OrthancPluginErrorCode" enumeration in "OrthancCPlugin.h"
-##
-
-path = os.path.join(BASE, 'Plugins', 'Include', 'orthanc', 'OrthancCPlugin.h')
-with open(path, 'r') as f:
-    a = f.read()
-
-s = ',\n'.join(map(lambda x: '    OrthancPluginErrorCode_%s = %d    /*!< %s */' % (x['Name'], int(x['Code']), x['Description']), ERRORS))
-s += ',\n\n    _OrthancPluginErrorCode_INTERNAL = 0x7fffffff\n  '
-a = re.sub('(typedef enum\s*{)[^}]*?(} OrthancPluginErrorCode;)', r'\1\n%s\2' % s, a, re.DOTALL)
-
-with open(path, 'w') as f:
-    f.write(a)
-
-
-
-##
-## Generate the "EnumerationToString(ErrorCode)" and
-## "ConvertErrorCodeToHttpStatus(ErrorCode)" functions in
-## "Enumerations.cpp"
-##
-
-path = os.path.join(BASE, 'Core', 'Enumerations.cpp')
-with open(path, 'r') as f:
-    a = f.read()
-
-s = '\n\n'.join(map(lambda x: '      case ErrorCode_%s:\n        return "%s";' % (x['Name'], x['Description']), ERRORS))
-a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
-           r'\1\n%s\2' % s, a, re.DOTALL)
-
-def GetHttpStatus(x):
-    s = HTTP[x['HttpStatus']]
-    return '      case ErrorCode_%s:\n        return %s;' % (x['Name'], s)
-
-s = '\n\n'.join(map(GetHttpStatus, filter(lambda x: 'HttpStatus' in x, ERRORS)))
-a = re.sub('(ConvertErrorCodeToHttpStatus\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
-           r'\1\n%s\2' % s, a, re.DOTALL)
-
-with open(path, 'w') as f:
-    f.write(a)
-
-
-
-##
-## Generate the "ErrorCode" enumeration in "OrthancSQLiteException.h"
-##
-
-path = os.path.join(BASE, 'Core', 'SQLite', 'OrthancSQLiteException.h')
-with open(path, 'r') as f:
-    a = f.read()
-
-e = filter(lambda x: 'SQLite' in x and x['SQLite'], ERRORS)
-s = ',\n'.join(map(lambda x: '      ErrorCode_%s' % x['Name'], e))
-a = re.sub('(enum ErrorCode\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
-
-s = '\n\n'.join(map(lambda x: '          case ErrorCode_%s:\n            return "%s";' % (x['Name'], x['Description']), e))
-a = re.sub('(EnumerationToString\(ErrorCode.*?\)\s*{\s*switch \([^)]*?\)\s*{)[^}]*?(\s*default:)',
-           r'\1\n%s\2' % s, a, re.DOTALL)
-
-with open(path, 'w') as f:
-    f.write(a)
-
-
-
-##
-## Generate the "PrintErrors" function in "main.cpp"
-##
-
-path = os.path.join(BASE, 'OrthancServer', 'main.cpp')
-with open(path, 'r') as f:
-    a = f.read()
-
-s = '\n'.join(map(lambda x: '    PrintErrorCode(ErrorCode_%s, "%s");' % (x['Name'], x['Description']), ERRORS))
-a = re.sub('(static void PrintErrors[^{}]*?{[^{}]*?{)([^}]*?)}', r'\1\n%s\n  }' % s, a, re.DOTALL)
-
-with open(path, 'w') as f:
-    f.write(a)
--- a/Resources/GenerateTransferSyntaxes.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 json
-import os
-import re
-import sys
-import pystache
-
-BASE = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
-
-
-
-## https://www.dicomlibrary.com/dicom/transfer-syntax/
-## https://cedocs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=EDICOM_transfer_syntax
-
-
-with open(os.path.join(BASE, 'Resources', 'DicomTransferSyntaxes.json'), 'r') as f:
-    SYNTAXES = json.loads(f.read())
-
-
-
-##
-## Generate the "DicomTransferSyntax" enumeration in "Enumerations.h"
-##
-
-path = os.path.join(BASE, 'Core', 'Enumerations.h')
-with open(path, 'r') as f:
-    a = f.read()
-
-s = ',\n'.join(map(lambda x: '    DicomTransferSyntax_%s    /*!< %s */' % (x['Value'], x['Name']), SYNTAXES))
-
-a = re.sub('(enum DicomTransferSyntax\s*{)[^}]*?(\s*};)', r'\1\n%s\2' % s, a, re.DOTALL)
-
-with open(path, 'w') as f:
-    f.write(a)
-
-
-
-##
-## Generate the implementations
-##
-
-with open(os.path.join(BASE, 'Core', 'Enumerations_TransferSyntaxes.impl.h'), 'w') as b:
-    with open(os.path.join(BASE, 'Resources', 'GenerateTransferSyntaxesEnumerations.mustache'), 'r') as a:
-        b.write(pystache.render(a.read(), {
-            'Syntaxes' : SYNTAXES
-        }))
-
-with open(os.path.join(BASE, 'Core', 'DicomParsing', 'FromDcmtkBridge_TransferSyntaxes.impl.h'), 'w') as b:
-    with open(os.path.join(BASE, 'Resources', 'GenerateTransferSyntaxesDcmtk.mustache'), 'r') as a:
-        b.write(pystache.render(a.read(), {
-            'Syntaxes' : SYNTAXES
-        }))
--- a/Resources/GenerateTransferSyntaxesDcmtk.mustache	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
-
-namespace Orthanc
-{
-  bool FromDcmtkBridge::LookupDcmtkTransferSyntax(E_TransferSyntax& target,
-                                                  DicomTransferSyntax source)
-  {
-    switch (source)
-    {
-      {{#Syntaxes}}
-      {{#DCMTK}}
-      {{#SinceDCMTK}}
-#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
-      {{/SinceDCMTK}}
-      case DicomTransferSyntax_{{Value}}:
-        {{#DCMTK360}}
-#  if DCMTK_VERSION_NUMBER <= 360
-        target = {{DCMTK360}};
-#  else
-        target = {{DCMTK}};
-#  endif
-        {{/DCMTK360}}
-        {{^DCMTK360}}
-        target = {{DCMTK}};
-        {{/DCMTK360}}
-        return true;
-      {{#SinceDCMTK}}
-#endif
-      {{/SinceDCMTK}}
-
-      {{/DCMTK}}
-      {{/Syntaxes}}
-      default:
-        return false;
-    }
-  }
-  
-
-  bool FromDcmtkBridge::LookupOrthancTransferSyntax(DicomTransferSyntax& target,
-                                                    E_TransferSyntax source)
-  {
-    switch (source)
-    {
-      {{#Syntaxes}}
-      {{#DCMTK}}
-      {{#SinceDCMTK}}
-#if DCMTK_VERSION_NUMBER >= {{SinceDCMTK}}
-      {{/SinceDCMTK}}
-      {{#DCMTK360}}
-#  if DCMTK_VERSION_NUMBER <= 360
-      case {{DCMTK360}}:
-#  else
-      case {{DCMTK}}:
-#  endif
-      {{/DCMTK360}}
-      {{^DCMTK360}}
-      case {{DCMTK}}:
-      {{/DCMTK360}}
-        target = DicomTransferSyntax_{{Value}};
-        return true;
-      {{#SinceDCMTK}}
-#endif
-      {{/SinceDCMTK}}
-
-      {{/DCMTK}}
-      {{/Syntaxes}}
-      default:
-        return false;
-    }
-  }
-}
--- a/Resources/GenerateTransferSyntaxesEnumerations.mustache	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 autogenerated by "../Resources/GenerateTransferSyntaxes.py"
-
-namespace Orthanc
-{
-  const char* GetTransferSyntaxUid(DicomTransferSyntax syntax)
-  {
-    switch (syntax)
-    {
-      {{#Syntaxes}}
-      case DicomTransferSyntax_{{Value}}:
-        return "{{UID}}";
-
-      {{/Syntaxes}}
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool IsRetiredTransferSyntax(DicomTransferSyntax syntax)
-  {
-    switch (syntax)
-    {
-      {{#Syntaxes}}
-      case DicomTransferSyntax_{{Value}}:
-        {{#Retired}}
-        return true;
-        {{/Retired}}
-        {{^Retired}}
-        return false;
-        {{/Retired}}
-
-      {{/Syntaxes}}
-      default:
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  bool LookupTransferSyntax(DicomTransferSyntax& target,
-                            const std::string& uid)
-  {
-    {{#Syntaxes}}
-    if (uid == "{{UID}}")
-    {
-      target = DicomTransferSyntax_{{Value}};
-      return true;
-    }
-    
-    {{/Syntaxes}}
-    return false;
-  }
-}
--- a/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "LookupIdentifierQuery.h"
-
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../../Core/OrthancException.h"
-#include "../ServerToolbox.h"
-#include "SetOfResources.h"
-
-#include <cassert>
-
-
-
-namespace Orthanc
-{
-  LookupIdentifierQuery::SingleConstraint::
-  SingleConstraint(const DicomTag& tag,
-                   IdentifierConstraintType type,
-                   const std::string& value) : 
-    tag_(tag),
-    type_(type),
-    value_(ServerToolbox::NormalizeIdentifier(value))
-  {
-  }
-
-
-  LookupIdentifierQuery::RangeConstraint::
-  RangeConstraint(const DicomTag& tag,
-                  const std::string& start,
-                  const std::string& end) : 
-    tag_(tag),
-    start_(ServerToolbox::NormalizeIdentifier(start)),
-    end_(ServerToolbox::NormalizeIdentifier(end))
-  {
-  }
-
-
-  LookupIdentifierQuery::Disjunction::~Disjunction()
-  {
-    for (size_t i = 0; i < singleConstraints_.size(); i++)
-    {
-      delete singleConstraints_[i];
-    }
-
-    for (size_t i = 0; i < rangeConstraints_.size(); i++)
-    {
-      delete rangeConstraints_[i];
-    }
-  }
-
-
-  void LookupIdentifierQuery::Disjunction::Add(const DicomTag& tag,
-                                               IdentifierConstraintType type,
-                                               const std::string& 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 (Disjunctions::iterator it = disjunctions_.begin();
-         it != disjunctions_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  bool LookupIdentifierQuery::IsIdentifier(const DicomTag& tag)
-  {
-    return ServerToolbox::IsIdentifier(tag, level_);
-  }
-
-
-  void LookupIdentifierQuery::AddConstraint(DicomTag tag,
-                                            IdentifierConstraintType type,
-                                            const std::string& value)
-  {
-    assert(IsIdentifier(tag));
-    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()
-  {
-    disjunctions_.push_back(new Disjunction);
-    return *disjunctions_.back();
-  }
-
-
-  void LookupIdentifierQuery::Apply(std::list<std::string>& result,
-                                    IDatabaseWrapper& database)
-  {
-    SetOfResources resources(database, level_);
-    Apply(resources, database);
-
-    resources.Flatten(result);
-  }
-
-
-  void LookupIdentifierQuery::Apply(SetOfResources& result,
-                                    IDatabaseWrapper& database)
-  {
-    for (size_t i = 0; i < disjunctions_.size(); i++)
-    {
-      std::list<int64_t> a;
-
-      for (size_t j = 0; j < disjunctions_[i]->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& constraint = disjunctions_[i]->GetSingleConstraint(j);
-        std::list<int64_t> b;
-        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);
-      }
-
-      result.Intersect(a);
-    }
-  }
-
-
-  void LookupIdentifierQuery::Print(std::ostream& s) const
-  {
-    s << "Constraint: " << std::endl;
-    for (Disjunctions::const_iterator
-           it = disjunctions_.begin(); it != disjunctions_.end(); ++it)
-    {
-      if (it == disjunctions_.begin())
-        s << "   ";
-      else
-        s << "OR ";
-
-      for (size_t j = 0; j < (*it)->GetSingleConstraintsCount(); j++)
-      {
-        const SingleConstraint& c = (*it)->GetSingleConstraint(j);
-        s << FromDcmtkBridge::GetTagName(c.GetTag(), "");
-
-        switch (c.GetType())
-        {
-          case IdentifierConstraintType_Equal: s << " == "; break;
-          case IdentifierConstraintType_SmallerOrEqual: s << " <= "; break;
-          case IdentifierConstraintType_GreaterOrEqual: s << " >= "; break;
-          case IdentifierConstraintType_Wildcard: s << " ~= "; break;
-          default:
-            s << " ? ";
-        }
-
-        s << c.GetValue() << std::endl;
-      }
-    }
-  }
-}
--- a/Resources/Graveyard/DatabaseOptimizations/LookupIdentifierQuery.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,207 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../IDatabaseWrapper.h"
-
-#include "SetOfResources.h"
-
-#include <vector>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  /**
-   * Primitive for wildcard matching, as defined in DICOM:
-   * http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_C.2.html#sect_C.2.2.2.4
-   * 
-   * "Any occurrence of an "*" or a "?", then "*" shall match any
-   * sequence of characters (including a zero length value) and "?"
-   * shall match any single character. This matching is case
-   * sensitive, except for Attributes with an PN Value
-   * Representation (e.g., Patient Name (0010,0010))."
-   * 
-   * Pay attention to the fact that "*" (resp. "?") generally
-   * corresponds to "%" (resp. "_") in primitive LIKE of SQL. The
-   * values "%", "_", "\" should in the user request should
-   * respectively be escaped as "\%", "\_" and "\\".
-   *
-   * This matching must be case sensitive: The special case of PN VR
-   * is taken into consideration by normalizing the query string in
-   * method "NormalizeIdentifier()".
-   **/
-
-  class LookupIdentifierQuery : public boost::noncopyable
-  {
-    // This class encodes a conjunction ("AND") of disjunctions. Each
-    // disjunction represents an "OR" of several constraints.
-
-  public:
-    class SingleConstraint
-    {
-    private:
-      DicomTag                  tag_;
-      IdentifierConstraintType  type_;
-      std::string               value_;
-
-    public:
-      SingleConstraint(const DicomTag& tag,
-                       IdentifierConstraintType type,
-                       const std::string& value);
-
-      const DicomTag& GetTag() const
-      {
-        return tag_;
-      }
-
-      IdentifierConstraintType GetType() const
-      {
-        return type_;
-      }
-      
-      const std::string& GetValue() const
-      {
-        return value_;
-      }
-    };
-
-
-    class RangeConstraint
-    {
-    private:
-      DicomTag     tag_;
-      std::string  start_;
-      std::string  end_;
-
-    public:
-      RangeConstraint(const DicomTag& tag,
-                      const std::string& start,
-                      const std::string& 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<SingleConstraint*>  singleConstraints_;
-      std::vector<RangeConstraint*>   rangeConstraints_;
-
-    public:
-      ~Disjunction();
-
-      void Add(const DicomTag& tag,
-               IdentifierConstraintType type,
-               const std::string& value);
-
-      void AddRange(const DicomTag& tag,
-                    const std::string& start,
-                    const std::string& end);
-
-      size_t GetSingleConstraintsCount() const
-      {
-        return singleConstraints_.size();
-      }
-
-      const SingleConstraint&  GetSingleConstraint(size_t i) const
-      {
-        return *singleConstraints_[i];
-      }
-
-      size_t GetRangeConstraintsCount() const
-      {
-        return rangeConstraints_.size();
-      }
-
-      const RangeConstraint&  GetRangeConstraint(size_t i) const
-      {
-        return *rangeConstraints_[i];
-      }
-    };
-
-
-  private:
-    typedef std::vector<Disjunction*>  Disjunctions;
-
-    ResourceType  level_;
-    Disjunctions  disjunctions_;
-
-  public:
-    LookupIdentifierQuery(ResourceType level) : level_(level)
-    {
-    }
-
-    ~LookupIdentifierQuery();
-
-    bool IsIdentifier(const DicomTag& tag);
-
-    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
-    {
-      return level_;
-    }
-
-    // The database must be locked
-    void Apply(std::list<std::string>& result,
-               IDatabaseWrapper& database);
-
-    void Apply(SetOfResources& result,
-               IDatabaseWrapper& database);
-
-    void Print(std::ostream& s) const;
-  };
-}
--- a/Resources/Graveyard/DatabaseOptimizations/LookupResource.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,479 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "LookupResource.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/FileStorage/StorageAccessor.h"
-#include "../ServerToolbox.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-
-
-namespace Orthanc
-{
-  static bool DoesDicomMapMatch(const DicomMap& dicom,
-                                const DicomTag& tag,
-                                const IFindConstraint& constraint)
-  {
-    const DicomValue* value = dicom.TestAndGetValue(tag);
-
-    return (value != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary() &&
-            constraint.Match(value->GetContent()));
-  }
-
-  
-  LookupResource::Level::Level(ResourceType level) : level_(level)
-  {
-    const DicomTag* tags = NULL;
-    size_t size;
-    
-    ServerToolbox::LoadIdentifiers(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      identifiers_.insert(tags[i]);
-    }
-    
-    DicomMap::LoadMainDicomTags(tags, size, level);
-    
-    for (size_t i = 0; i < size; i++)
-    {
-      if (identifiers_.find(tags[i]) == identifiers_.end())
-      {
-        mainTags_.insert(tags[i]);
-      }
-    }    
-  }
-
-  LookupResource::Level::~Level()
-  {
-    for (Constraints::iterator it = mainTagsConstraints_.begin();
-         it != mainTagsConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    for (Constraints::iterator it = identifiersConstraints_.begin();
-         it != identifiersConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }
-  }
-
-  bool LookupResource::Level::Add(const DicomTag& tag,
-                                  std::auto_ptr<IFindConstraint>& constraint)
-  {
-    if (identifiers_.find(tag) != identifiers_.end())
-    {
-      if (level_ == ResourceType_Patient)
-      {
-        // The filters on the patient level must be cloned to the study level
-        identifiersConstraints_[tag] = constraint->Clone();
-      }
-      else
-      {
-        identifiersConstraints_[tag] = constraint.release();
-      }
-
-      return true;
-    }
-    else if (mainTags_.find(tag) != mainTags_.end())
-    {
-      if (level_ == ResourceType_Patient)
-      {
-        // The filters on the patient level must be cloned to the study level
-        mainTagsConstraints_[tag] = constraint->Clone();
-      }
-      else
-      {
-        mainTagsConstraints_[tag] = constraint.release();
-      }
-
-      return true;
-    }
-    else
-    {
-      // This is not a main DICOM tag
-      return false;
-    }
-  }
-
-
-  bool LookupResource::Level::IsMatch(const DicomMap& dicom) const
-  {
-    for (Constraints::const_iterator it = identifiersConstraints_.begin();
-         it != identifiersConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    for (Constraints::const_iterator it = mainTagsConstraints_.begin();
-         it != mainTagsConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-  
-
-  LookupResource::LookupResource(ResourceType level) : level_(level)
-  {
-    switch (level)
-    {
-      case ResourceType_Patient:
-        levels_[ResourceType_Patient] = new Level(ResourceType_Patient);
-        break;
-
-      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_Study:
-        levels_[ResourceType_Study] = new Level(ResourceType_Study);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  LookupResource::~LookupResource()
-  {
-    for (Levels::iterator it = levels_.begin();
-         it != levels_.end(); ++it)
-    {
-      delete it->second;
-    }
-
-    for (Constraints::iterator it = unoptimizedConstraints_.begin();
-         it != unoptimizedConstraints_.end(); ++it)
-    {
-      delete it->second;
-    }    
-  }
-
-
-
-  bool LookupResource::AddInternal(ResourceType level,
-                                   const DicomTag& tag,
-                                   std::auto_ptr<IFindConstraint>& constraint)
-  {
-    Levels::iterator it = levels_.find(level);
-    if (it != levels_.end())
-    {
-      if (it->second->Add(tag, constraint))
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-
-  void LookupResource::Add(const DicomTag& tag,
-                           IFindConstraint* constraint)
-  {
-    std::auto_ptr<IFindConstraint> c(constraint);
-
-    if (!AddInternal(ResourceType_Patient, tag, c) &&
-        !AddInternal(ResourceType_Study, tag, c) &&
-        !AddInternal(ResourceType_Series, tag, c) &&
-        !AddInternal(ResourceType_Instance, tag, c))
-    {
-      unoptimizedConstraints_[tag] = c.release();
-    }
-  }
-
-
-  static bool Match(const DicomMap& tags,
-                    const DicomTag& tag,
-                    const IFindConstraint& constraint)
-  {
-    const DicomValue* value = tags.TestAndGetValue(tag);
-
-    if (value == NULL ||
-        value->IsNull() ||
-        value->IsBinary())
-    {
-      return false;
-    }
-    else
-    {
-      return constraint.Match(value->GetContent());
-    }
-  }
-
-
-  void LookupResource::Level::Apply(SetOfResources& candidates,
-                                    IDatabaseWrapper& database) const
-  {
-    // First, use the indexed identifiers
-    LookupIdentifierQuery query(level_);
-
-    for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
-         it != identifiersConstraints_.end(); ++it)
-    {
-      it->second->Setup(query, it->first);
-    }
-
-    query.Apply(candidates, database);
-
-    /*{
-      query.Print(std::cout);
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      printf("=> %d\n", source.size());
-      }*/
-
-    // Secondly, filter using the main DICOM tags
-    if (!identifiersConstraints_.empty() ||
-        !mainTagsConstraints_.empty())
-    {
-      std::list<int64_t>  source;
-      candidates.Flatten(source);
-      candidates.Clear();
-
-      std::list<int64_t>  filtered;
-      for (std::list<int64_t>::const_iterator candidate = source.begin(); 
-           candidate != source.end(); ++candidate)
-      {
-        DicomMap tags;
-        database.GetMainDicomTags(tags, *candidate);
-
-        bool match = true;
-
-        // Re-apply the identifier constraints, as their "Setup"
-        // method is less restrictive than their "Match" method
-        for (Constraints::const_iterator it = identifiersConstraints_.begin(); 
-             match && it != identifiersConstraints_.end(); ++it)
-        {
-          if (!Match(tags, it->first, *it->second))
-          {
-            match = false;
-          }
-        }
-
-        for (Constraints::const_iterator it = mainTagsConstraints_.begin(); 
-             match && it != mainTagsConstraints_.end(); ++it)
-        {
-          if (!Match(tags, it->first, *it->second))
-          {
-            match = false;
-          }
-        }
-
-        if (match)
-        {
-          filtered.push_back(*candidate);
-        }
-      }
-      
-      candidates.Intersect(filtered);
-    }
-  }
-
-
-
-  bool LookupResource::IsMatch(const DicomMap& dicom) const
-  {
-    for (Levels::const_iterator it = levels_.begin(); it != levels_.end(); ++it)
-    {
-      if (!it->second->IsMatch(dicom))
-      {
-        return false;
-      }
-    }
-
-    for (Constraints::const_iterator it = unoptimizedConstraints_.begin(); 
-         it != unoptimizedConstraints_.end(); ++it)
-    {
-      assert(it->second != NULL);
-
-      if (!DoesDicomMapMatch(dicom, it->first, *it->second))
-      {
-        return false;
-      }
-    }
-
-    return true;
-  }
-
-
-  void LookupResource::ApplyLevel(SetOfResources& candidates,
-                                  ResourceType level,
-                                  IDatabaseWrapper& database) const
-  {
-    Levels::const_iterator it = levels_.find(level);
-    if (it != levels_.end())
-    {
-      it->second->Apply(candidates, database);
-    }
-
-    if (level == ResourceType_Study &&
-        modalitiesInStudy_.get() != NULL)
-    {
-      // There is a constraint on the "ModalitiesInStudy" DICOM
-      // extension. Check out whether one child series has one of the
-      // allowed modalities
-      std::list<int64_t> allStudies, matchingStudies;
-      candidates.Flatten(allStudies);
- 
-      for (std::list<int64_t>::const_iterator
-             study = allStudies.begin(); study != allStudies.end(); ++study)
-      {
-        std::list<int64_t> childrenSeries;
-        database.GetChildrenInternalId(childrenSeries, *study);
-
-        for (std::list<int64_t>::const_iterator
-               series = childrenSeries.begin(); series != childrenSeries.end(); ++series)
-        {
-          DicomMap tags;
-          database.GetMainDicomTags(tags, *series);
-
-          const DicomValue* value = tags.TestAndGetValue(DICOM_TAG_MODALITY);
-          if (value != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary())
-          {
-            if (modalitiesInStudy_->Match(value->GetContent()))
-            {
-              matchingStudies.push_back(*study);
-              break;
-            }
-          }
-        }
-      }
-
-      candidates.Intersect(matchingStudies);
-    }
-  }
-
-
-  void LookupResource::FindCandidates(std::list<int64_t>& result,
-                                      IDatabaseWrapper& database) const
-  {
-    ResourceType startingLevel;
-    if (level_ == ResourceType_Patient)
-    {
-      startingLevel = ResourceType_Patient;
-    }
-    else
-    {
-      startingLevel = ResourceType_Study;
-    }
-
-    SetOfResources candidates(database, startingLevel);
-
-    switch (level_)
-    {
-      case ResourceType_Patient:
-        ApplyLevel(candidates, ResourceType_Patient, database);
-        break;
-
-      case ResourceType_Study:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        break;
-
-      case ResourceType_Series:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Series, database);
-        break;
-
-      case ResourceType_Instance:
-        ApplyLevel(candidates, ResourceType_Study, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Series, database);
-        candidates.GoDown();
-        ApplyLevel(candidates, ResourceType_Instance, database);
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-    candidates.Flatten(result);
-  }
-
-
-  void LookupResource::SetModalitiesInStudy(const std::string& modalities)
-  {
-    modalitiesInStudy_.reset(new ListConstraint(true /* case sensitive */));
-    
-    std::vector<std::string> items;
-    Toolbox::TokenizeString(items, modalities, '\\');
-    
-    for (size_t i = 0; i < items.size(); i++)
-    {
-      modalitiesInStudy_->AddAllowedValue(items[i]);
-    }
-  }
-
-
-  void LookupResource::AddDicomConstraint(const DicomTag& tag,
-                                          const std::string& dicomQuery,
-                                          bool caseSensitive)
-  {
-    // 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 
-    {
-      Add(tag, IFindConstraint::ParseDicomConstraint(tag, dicomQuery, caseSensitive));
-    }
-  }
-
-}
--- a/Resources/Graveyard/DatabaseOptimizations/LookupResource.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,115 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "ListConstraint.h"
-#include "SetOfResources.h"
-
-#include <memory>
-
-namespace Orthanc
-{
-  class LookupResource : public boost::noncopyable
-  {
-  private:
-    typedef std::map<DicomTag, IFindConstraint*>  Constraints;
-    
-    class Level
-    {
-    private:
-      ResourceType        level_;
-      std::set<DicomTag>  identifiers_;
-      std::set<DicomTag>  mainTags_;
-      Constraints         identifiersConstraints_;
-      Constraints         mainTagsConstraints_;
-
-    public:
-      Level(ResourceType level);
-
-      ~Level();
-
-      bool Add(const DicomTag& tag,
-               std::auto_ptr<IFindConstraint>& constraint);
-
-      void Apply(SetOfResources& candidates,
-                 IDatabaseWrapper& database) const;
-
-      bool IsMatch(const DicomMap& dicom) const;
-    };
-
-    typedef std::map<ResourceType, Level*>  Levels;
-
-    ResourceType                    level_;
-    Levels                          levels_;
-    Constraints                     unoptimizedConstraints_;   // Constraints on non-main DICOM tags
-    std::auto_ptr<ListConstraint>   modalitiesInStudy_;
-
-    bool AddInternal(ResourceType level,
-                     const DicomTag& tag,
-                     std::auto_ptr<IFindConstraint>& constraint);
-
-    void ApplyLevel(SetOfResources& candidates,
-                    ResourceType level,
-                    IDatabaseWrapper& database) const;
-
-  public:
-    LookupResource(ResourceType level);
-
-    ~LookupResource();
-
-    ResourceType GetLevel() const
-    {
-      return level_;
-    }
-
-    void SetModalitiesInStudy(const std::string& modalities); 
-
-    void Add(const DicomTag& tag,
-             IFindConstraint* constraint);   // Takes ownership
-
-    void AddDicomConstraint(const DicomTag& tag,
-                            const std::string& dicomQuery,
-                            bool caseSensitive);
-
-    void FindCandidates(std::list<int64_t>& result,
-                        IDatabaseWrapper& database) const;
-
-    bool HasOnlyMainDicomTags() const
-    {
-      return unoptimizedConstraints_.empty();
-    }
-
-    bool IsMatch(const DicomMap& dicom) const;
-  };
-}
--- a/Resources/Graveyard/DatabasePluginSample/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +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_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
-  )
--- a/Resources/Graveyard/DatabasePluginSample/Database.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,565 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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");
-  }
-}
--- a/Resources/Graveyard/DatabasePluginSample/Database.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,757 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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));
-    }    
-  }
-}
--- a/Resources/Graveyard/DatabasePluginSample/DatabaseWrapperBase.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,210 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
-
--- a/Resources/Graveyard/DatabasePluginSample/Plugin.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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";
-  }
-}
--- a/Resources/Graveyard/FromDcmtkBridge.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-  DcmElement* FromDcmtkBridge::CreateElementForTag(const DicomTag& tag)
-  {
-    DcmTag key(tag.GetGroup(), tag.GetElement());
-
-    if (tag.IsPrivate())
-    {
-      // This raises BitBucket issue 140 (Modifying private tags with
-      // REST API changes VR from LO to UN)
-      // https://bitbucket.org/sjodogne/orthanc/issues/140
-      LOG(WARNING) << "You are using DCMTK < 3.6.1: All the private tags "
-        "are considered as having a binary value representation";
-      return new DcmOtherByteOtherWord(key);
-    }
-    else if (IsBinaryTag(key))
-    {
-      return new DcmOtherByteOtherWord(key);
-    }
-
-    switch (key.getEVR())
-    {
-      // http://support.dcmtk.org/docs/dcvr_8h-source.html
-
-      /**
-       * Binary types, handled above
-       **/
-    
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_OD:
-#endif            
-
-#if DCMTK_VERSION_NUMBER >= 362
-      case EVR_OL:
-#endif            
-
-      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);
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UC:  // unlimited characters
-        return new DcmUnlimitedCharacters(key);
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-      case EVR_UR:  // URI/URL
-        return new DcmUniversalResourceIdentifierOrLocator(key);
-#endif
-          
-        
-      /**
-       * 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);
-  }
--- a/Resources/Graveyard/Multithreading/BagOfTasks.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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();
-    }
-  };
-}
--- a/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,277 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  }
-}
--- a/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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);
-  };
-}
--- a/Resources/Graveyard/Multithreading/ICommand.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Resources/Graveyard/Multithreading/ILockable.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Resources/Graveyard/Multithreading/Locker.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Resources/Graveyard/Multithreading/Mutex.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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
-}
--- a/Resources/Graveyard/Multithreading/Mutex.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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_;
-  }
-}
--- a/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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/Resources/Graveyard/SetupAnonymization2011.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,258 +0,0 @@
-  /**
-   * 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);
-  }
--- a/Resources/Graveyard/TestTranscoding.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,967 +0,0 @@
-  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmFileFormat& dicom,
-                                           DicomTransferSyntax syntax)
-  {
-    E_TransferSyntax xfer;
-    if (!LookupDcmtkTransferSyntax(xfer, syntax))
-    {
-      return false;
-    }
-    else if (!dicom.validateMetaInfo(xfer).good())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot setup the transfer syntax to write a DICOM instance");
-    }
-    else
-    {
-      return SaveToMemoryBufferInternal(buffer, dicom, xfer);
-    }
-  }
-
-
-  bool FromDcmtkBridge::SaveToMemoryBuffer(std::string& buffer,
-                                           DcmFileFormat& dicom)
-  {
-    E_TransferSyntax xfer = dicom.getDataset()->getCurrentXfer();
-    if (xfer == EXS_Unknown)
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot write a DICOM instance with unknown transfer syntax");
-    }
-    else if (!dicom.validateMetaInfo(xfer).good())
-    {
-      throw OrthancException(ErrorCode_InternalError,
-                             "Cannot setup the transfer syntax to write a DICOM instance");
-    }
-    else
-    {
-      return SaveToMemoryBufferInternal(buffer, dicom, xfer);
-    }
-  }
-
-
-
-
-
-#include <dcmtk/dcmdata/dcostrmb.h>
-#include <dcmtk/dcmdata/dcpixel.h>
-#include <dcmtk/dcmdata/dcpxitem.h>
-
-#include "../Core/DicomParsing/Internals/DicomFrameIndex.h"
-
-namespace Orthanc
-{
-  class IParsedDicomImage : public boost::noncopyable
-  {
-  public:
-    virtual ~IParsedDicomImage()
-    {
-    }
-
-    virtual DicomTransferSyntax GetTransferSyntax() = 0;
-
-    virtual std::string GetSopClassUid() = 0;
-
-    virtual std::string GetSopInstanceUid() = 0;
-
-    virtual unsigned int GetFramesCount() = 0;
-
-    // Can return NULL, for compressed transfer syntaxes
-    virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) = 0;
-    
-    virtual void GetCompressedFrame(std::string& target,
-                                    unsigned int frame) = 0;
-    
-    virtual void WriteToMemoryBuffer(std::string& target) = 0;
-  };
-
-
-  class IDicomImageReader : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomImageReader()
-    {
-    }
-
-    virtual IParsedDicomImage* Read(const void* data,
-                                    size_t size) = 0;
-
-    virtual IParsedDicomImage* Transcode(const void* data,
-                                         size_t size,
-                                         DicomTransferSyntax syntax,
-                                         bool allowNewSopInstanceUid) = 0;
-  };
-
-
-  class DcmtkImageReader : public IDicomImageReader
-  {
-  private:
-    class Image : public IParsedDicomImage
-    {
-    private:
-      std::unique_ptr<DcmFileFormat>    dicom_;
-      std::unique_ptr<DicomFrameIndex>  index_;
-      DicomTransferSyntax               transferSyntax_;
-      std::string                       sopClassUid_;
-      std::string                       sopInstanceUid_;
-
-      static std::string GetStringTag(DcmDataset& dataset,
-                                      const DcmTagKey& tag)
-      {
-        const char* value = NULL;
-
-        if (!dataset.findAndGetString(tag, value).good() ||
-            value == NULL)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "Missing SOP class/instance UID in DICOM instance");
-        }
-        else
-        {
-          return std::string(value);
-        }
-      }
-
-    public:
-      Image(DcmFileFormat* dicom,
-            DicomTransferSyntax syntax) :
-        dicom_(dicom),
-        transferSyntax_(syntax)
-      {
-        if (dicom == NULL ||
-            dicom_->getDataset() == NULL)
-        {
-          throw OrthancException(ErrorCode_NullPointer);
-        }
-
-        DcmDataset& dataset = *dicom_->getDataset();
-        index_.reset(new DicomFrameIndex(dataset));
-
-        sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID);
-        sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID);
-      }
-
-      virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
-      {
-        return transferSyntax_;
-      }
-
-      virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
-      {
-        return sopClassUid_;
-      }
-    
-      virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
-      {
-        return sopInstanceUid_;
-      }
-
-      virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
-      {
-        return index_->GetFramesCount();
-      }
-
-      virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE
-      {
-        assert(dicom_.get() != NULL);
-        if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, transferSyntax_))
-        {
-          throw OrthancException(ErrorCode_InternalError,
-                                 "Cannot write the DICOM instance to a memory buffer");
-        }
-      }
-
-      virtual ImageAccessor* GetUncompressedFrame(unsigned int frame) ORTHANC_OVERRIDE
-      {
-        assert(dicom_.get() != NULL &&
-               dicom_->getDataset() != NULL);
-        return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
-      }
-
-      virtual void GetCompressedFrame(std::string& target,
-                                      unsigned int frame) ORTHANC_OVERRIDE
-      {
-        assert(index_.get() != NULL);
-        index_->GetRawFrame(target, frame);
-      }
-    };
-
-    unsigned int lossyQuality_;
-
-    static DicomTransferSyntax DetectTransferSyntax(DcmFileFormat& dicom)
-    {
-      if (dicom.getDataset() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-        
-      DcmDataset& dataset = *dicom.getDataset();
-
-      E_TransferSyntax xfer = dataset.getCurrentXfer();
-      if (xfer == EXS_Unknown)
-      {
-        dataset.updateOriginalXfer();
-        xfer = dataset.getCurrentXfer();
-        if (xfer == EXS_Unknown)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "Cannot determine the transfer syntax of the DICOM instance");
-        }
-      }
-
-      DicomTransferSyntax syntax;
-      if (FromDcmtkBridge::LookupOrthancTransferSyntax(syntax, xfer))
-      {
-        return syntax;
-      }
-      else
-      {
-        throw OrthancException(
-          ErrorCode_BadFileFormat,
-          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
-      }
-    }
-
-
-    static uint16_t GetBitsStored(DcmFileFormat& dicom)
-    {
-      if (dicom.getDataset() == NULL)
-      {
-        throw OrthancException(ErrorCode_InternalError);
-      }
-
-      uint16_t bitsStored;
-      if (dicom.getDataset()->findAndGetUint16(DCM_BitsStored, bitsStored).good())
-      {
-        return bitsStored;
-      }
-      else
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing \"Bits Stored\" tag in DICOM instance");
-      }      
-    }
-    
-      
-  public:
-    DcmtkImageReader() :
-      lossyQuality_(90)
-    {
-    }
-
-    void SetLossyQuality(unsigned int quality)
-    {
-      if (quality <= 0 ||
-          quality > 100)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        lossyQuality_ = quality;
-      }
-    }
-
-    unsigned int GetLossyQuality() const
-    {
-      return lossyQuality_;
-    }
-
-    virtual IParsedDicomImage* Read(const void* data,
-                                    size_t size)
-    {
-      std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size));
-      if (dicom.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      DicomTransferSyntax transferSyntax = DetectTransferSyntax(*dicom);
-
-      return new Image(dicom.release(), transferSyntax);
-    }
-
-    virtual IParsedDicomImage* Transcode(const void* data,
-                                         size_t size,
-                                         DicomTransferSyntax syntax,
-                                         bool allowNewSopInstanceUid)
-    {
-      std::unique_ptr<DcmFileFormat> dicom(FromDcmtkBridge::LoadFromMemoryBuffer(data, size));
-      if (dicom.get() == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat);
-      }
-
-      const uint16_t bitsStored = GetBitsStored(*dicom);
-
-      if (syntax == DetectTransferSyntax(*dicom))
-      {
-        // No transcoding is needed
-        return new Image(dicom.release(), syntax);
-      }
-      
-      if (syntax == DicomTransferSyntax_LittleEndianImplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianImplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-
-      if (syntax == DicomTransferSyntax_LittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_LittleEndianExplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-      
-      if (syntax == DicomTransferSyntax_BigEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_BigEndianExplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-
-      if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_DeflatedLittleEndianExplicit, NULL))
-      {
-        return new Image(dicom.release(), syntax);
-      }
-
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess1 &&
-          allowNewSopInstanceUid &&
-          bitsStored == 8)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        
-        if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess1, &rpLossy))
-        {
-          return new Image(dicom.release(), syntax);
-        }
-      }
-#endif
-      
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess2_4 &&
-          allowNewSopInstanceUid &&
-          bitsStored <= 12)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        if (FromDcmtkBridge::Transcode(*dicom, DicomTransferSyntax_JPEGProcess2_4, &rpLossy))
-        {
-          return new Image(dicom.release(), syntax);
-        }
-      }
-#endif
-
-      //LOG(INFO) << "Unable to transcode DICOM image using the built-in reader";
-      return NULL;
-    }
-  };
-  
-
-  
-  class IDicomTranscoder1 : public boost::noncopyable
-  {
-  public:
-    virtual ~IDicomTranscoder1()
-    {
-    }
-
-    virtual DcmFileFormat& GetDicom() = 0;
-
-    virtual DicomTransferSyntax GetTransferSyntax() = 0;
-
-    virtual std::string GetSopClassUid() = 0;
-
-    virtual std::string GetSopInstanceUid() = 0;
-
-    virtual unsigned int GetFramesCount() = 0;
-
-    virtual ImageAccessor* DecodeFrame(unsigned int frame) = 0;
-
-    virtual void GetCompressedFrame(std::string& target,
-                                    unsigned int frame) = 0;
-
-    // NB: Transcoding can change the value of "GetSopInstanceUid()"
-    // and "GetTransferSyntax()" if lossy compression is applied
-    virtual bool Transcode(std::string& target,
-                           DicomTransferSyntax syntax,
-                           bool allowNewSopInstanceUid) = 0;
-
-    virtual void WriteToMemoryBuffer(std::string& target) = 0;
-  };
-
-
-  class DcmtkTranscoder2 : public IDicomTranscoder1
-  {
-  private:
-    std::unique_ptr<DcmFileFormat>    dicom_;
-    std::unique_ptr<DicomFrameIndex>  index_;
-    DicomTransferSyntax               transferSyntax_;
-    std::string                       sopClassUid_;
-    std::string                       sopInstanceUid_;
-    uint16_t                          bitsStored_;
-    unsigned int                      lossyQuality_;
-
-    static std::string GetStringTag(DcmDataset& dataset,
-                                    const DcmTagKey& tag)
-    {
-      const char* value = NULL;
-
-      if (!dataset.findAndGetString(tag, value).good() ||
-          value == NULL)
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing SOP class/instance UID in DICOM instance");
-      }
-      else
-      {
-        return std::string(value);
-      }
-    }
-
-    void Setup(DcmFileFormat* dicom)
-    {
-      lossyQuality_ = 90;
-      
-      dicom_.reset(dicom);
-      
-      if (dicom == NULL ||
-          dicom_->getDataset() == NULL)
-      {
-        throw OrthancException(ErrorCode_NullPointer);
-      }
-
-      DcmDataset& dataset = *dicom_->getDataset();
-      index_.reset(new DicomFrameIndex(dataset));
-
-      E_TransferSyntax xfer = dataset.getCurrentXfer();
-      if (xfer == EXS_Unknown)
-      {
-        dataset.updateOriginalXfer();
-        xfer = dataset.getCurrentXfer();
-        if (xfer == EXS_Unknown)
-        {
-          throw OrthancException(ErrorCode_BadFileFormat,
-                                 "Cannot determine the transfer syntax of the DICOM instance");
-        }
-      }
-
-      if (!FromDcmtkBridge::LookupOrthancTransferSyntax(transferSyntax_, xfer))
-      {
-        throw OrthancException(
-          ErrorCode_BadFileFormat,
-          "Unsupported transfer syntax: " + boost::lexical_cast<std::string>(xfer));
-      }
-
-      if (!dataset.findAndGetUint16(DCM_BitsStored, bitsStored_).good())
-      {
-        throw OrthancException(ErrorCode_BadFileFormat,
-                               "Missing \"Bits Stored\" tag in DICOM instance");
-      }      
-
-      sopClassUid_ = GetStringTag(dataset, DCM_SOPClassUID);
-      sopInstanceUid_ = GetStringTag(dataset, DCM_SOPInstanceUID);
-    }
-    
-  public:
-    DcmtkTranscoder2(DcmFileFormat* dicom)  // Takes ownership
-    {
-      Setup(dicom);
-    }
-
-    DcmtkTranscoder2(const void* dicom,
-                    size_t size)
-    {
-      Setup(FromDcmtkBridge::LoadFromMemoryBuffer(dicom, size));
-    }
-
-    void SetLossyQuality(unsigned int quality)
-    {
-      if (quality <= 0 ||
-          quality > 100)
-      {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
-      }
-      else
-      {
-        lossyQuality_ = quality;
-      }
-    }
-
-    unsigned int GetLossyQuality() const
-    {
-      return lossyQuality_;
-    }
-
-    unsigned int GetBitsStored() const
-    {
-      return bitsStored_;
-    }
-
-    virtual DcmFileFormat& GetDicom()
-    {
-      assert(dicom_ != NULL);
-      return *dicom_;
-    }
-
-    virtual DicomTransferSyntax GetTransferSyntax() ORTHANC_OVERRIDE
-    {
-      return transferSyntax_;
-    }
-
-    virtual std::string GetSopClassUid() ORTHANC_OVERRIDE
-    {
-      return sopClassUid_;
-    }
-    
-    virtual std::string GetSopInstanceUid() ORTHANC_OVERRIDE
-    {
-      return sopInstanceUid_;
-    }
-
-    virtual unsigned int GetFramesCount() ORTHANC_OVERRIDE
-    {
-      return index_->GetFramesCount();
-    }
-
-    virtual void WriteToMemoryBuffer(std::string& target) ORTHANC_OVERRIDE
-    {
-      if (!FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_))
-      {
-        throw OrthancException(ErrorCode_InternalError,
-                               "Cannot write the DICOM instance to a memory buffer");
-      }
-    }
-
-    virtual ImageAccessor* DecodeFrame(unsigned int frame) ORTHANC_OVERRIDE
-    {
-      assert(dicom_->getDataset() != NULL);
-      return DicomImageDecoder::Decode(*dicom_->getDataset(), frame);
-    }
-
-    virtual void GetCompressedFrame(std::string& target,
-                                    unsigned int frame) ORTHANC_OVERRIDE
-    {
-      index_->GetRawFrame(target, frame);
-    }
-
-    virtual bool Transcode(std::string& target,
-                           DicomTransferSyntax syntax,
-                           bool allowNewSopInstanceUid) ORTHANC_OVERRIDE
-    {
-      assert(dicom_ != NULL &&
-             dicom_->getDataset() != NULL);
-      
-      if (syntax == GetTransferSyntax())
-      {
-        printf("NO TRANSCODING\n");
-        
-        // No change in the transfer syntax => simply serialize the current dataset
-        WriteToMemoryBuffer(target);
-        return true;
-      }
-      
-      printf(">> %d\n", bitsStored_);
-
-      if (syntax == DicomTransferSyntax_LittleEndianImplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_LittleEndianImplicit;
-        return true;
-      }
-
-      if (syntax == DicomTransferSyntax_LittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_LittleEndianExplicit;
-        return true;
-      }
-      
-      if (syntax == DicomTransferSyntax_BigEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_BigEndianExplicit;
-        return true;
-      }
-
-      if (syntax == DicomTransferSyntax_DeflatedLittleEndianExplicit &&
-          FromDcmtkBridge::Transcode(*dicom_, syntax, NULL) &&
-          FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-      {
-        transferSyntax_ = DicomTransferSyntax_DeflatedLittleEndianExplicit;
-        return true;
-      }
-
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess1 &&
-          allowNewSopInstanceUid &&
-          GetBitsStored() == 8)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        
-        if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) &&
-            FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-        {
-          transferSyntax_ = DicomTransferSyntax_JPEGProcess1;
-          sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID);
-          return true;
-        }
-      }
-#endif
-      
-#if ORTHANC_ENABLE_JPEG == 1
-      if (syntax == DicomTransferSyntax_JPEGProcess2_4 &&
-          allowNewSopInstanceUid &&
-          GetBitsStored() <= 12)
-      {
-        DJ_RPLossy rpLossy(lossyQuality_);
-        if (FromDcmtkBridge::Transcode(*dicom_, syntax, &rpLossy) &&
-            FromDcmtkBridge::SaveToMemoryBuffer(target, *dicom_, syntax))
-        {
-          transferSyntax_ = DicomTransferSyntax_JPEGProcess2_4;
-          sopInstanceUid_ = GetStringTag(*dicom_->getDataset(), DCM_SOPInstanceUID);
-          return true;
-        }
-      }
-#endif
-
-      return false;
-    }
-  };
-}
-
-
-
-
-#include <boost/filesystem.hpp>
-
-
-static void TestFile(const std::string& path)
-{
-  static unsigned int count = 0;
-  count++;
-  
-
-  printf("** %s\n", path.c_str());
-
-  std::string s;
-  SystemToolbox::ReadFile(s, path);
-
-  Orthanc::DcmtkTranscoder2 transcoder(s.c_str(), s.size());
-
-  /*if (transcoder.GetBitsStored() != 8)  // TODO
-    return; */
-
-  {
-    char buf[1024];
-    sprintf(buf, "/tmp/source-%06d.dcm", count);
-    printf(">> %s\n", buf);
-    Orthanc::SystemToolbox::WriteFile(s, buf);
-  }
-
-  printf("[%s] [%s] [%s] %d %d\n", GetTransferSyntaxUid(transcoder.GetTransferSyntax()),
-         transcoder.GetSopClassUid().c_str(), transcoder.GetSopInstanceUid().c_str(),
-         transcoder.GetFramesCount(), transcoder.GetTransferSyntax());
-
-  for (size_t i = 0; i < transcoder.GetFramesCount(); i++)
-  {
-    std::string f;
-    transcoder.GetCompressedFrame(f, i);
-
-    if (i == 0)
-    {
-      char buf[1024];
-      sprintf(buf, "/tmp/frame-%06d.raw", count);
-      printf(">> %s\n", buf);
-      Orthanc::SystemToolbox::WriteFile(f, buf);
-    }
-  }
-
-  {
-    std::string t;
-    transcoder.WriteToMemoryBuffer(t);
-
-    Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size());
-    printf(">> %d %d ; %lu bytes\n", transcoder.GetTransferSyntax(), transcoder2.GetTransferSyntax(), t.size());
-  }
-
-  {
-    std::string a = transcoder.GetSopInstanceUid();
-    DicomTransferSyntax b = transcoder.GetTransferSyntax();
-    
-    DicomTransferSyntax syntax = DicomTransferSyntax_JPEGProcess2_4;
-    //DicomTransferSyntax syntax = DicomTransferSyntax_LittleEndianExplicit;
-
-    std::string t;
-    bool ok = transcoder.Transcode(t, syntax, true);
-    printf("Transcoding: %d\n", ok);
-
-    if (ok)
-    {
-      printf("[%s] => [%s]\n", a.c_str(), transcoder.GetSopInstanceUid().c_str());
-      printf("[%s] => [%s]\n", GetTransferSyntaxUid(b),
-             GetTransferSyntaxUid(transcoder.GetTransferSyntax()));
-      
-      {
-        char buf[1024];
-        sprintf(buf, "/tmp/transcoded-%06d.dcm", count);
-        printf(">> %s\n", buf);
-        Orthanc::SystemToolbox::WriteFile(t, buf);
-      }
-
-      Orthanc::DcmtkTranscoder2 transcoder2(t.c_str(), t.size());
-      printf("  => transcoded transfer syntax %d ; %lu bytes\n", transcoder2.GetTransferSyntax(), t.size());
-    }
-  }
-  
-  printf("\n");
-}
-
-TEST(Toto, DISABLED_Transcode)
-{
-  //OFLog::configure(OFLogger::DEBUG_LOG_LEVEL);
-
-  if (1)
-  {
-    const char* const PATH = "/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes";
-    
-    for (boost::filesystem::directory_iterator it(PATH);
-         it != boost::filesystem::directory_iterator(); ++it)
-    {
-      if (boost::filesystem::is_regular_file(it->status()))
-      {
-        TestFile(it->path().string());
-      }
-    }
-  }
-
-  if (0)
-  {
-    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Multiframe.dcm");
-    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/Issue44/Monochrome1-Jpeg.dcm");
-  }
-
-  if (0)
-  {
-    TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm");
-  }
-}
-
-
-TEST(Toto, DISABLED_Transcode2)
-{
-  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
-  {
-    DicomTransferSyntax a = (DicomTransferSyntax) i;
-
-    std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
-                        std::string(GetTransferSyntaxUid(a)) + ".dcm");
-    if (Orthanc::SystemToolbox::IsRegularFile(path))
-    {
-      printf("\n======= %s\n", GetTransferSyntaxUid(a));
-
-      std::string source;
-      Orthanc::SystemToolbox::ReadFile(source, path);
-
-      DcmtkImageReader reader;
-
-      {
-        std::unique_ptr<IParsedDicomImage> image(
-          reader.Read(source.c_str(), source.size()));
-        ASSERT_TRUE(image.get() != NULL);
-        ASSERT_EQ(a, image->GetTransferSyntax());
-
-        std::string target;
-        image->WriteToMemoryBuffer(target);
-      }
-
-      for (int j = 0; j <= DicomTransferSyntax_XML; j++)
-      {
-        DicomTransferSyntax b = (DicomTransferSyntax) j;
-        //if (a == b) continue;
-
-        std::unique_ptr<IParsedDicomImage> image(
-          reader.Transcode(source.c_str(), source.size(), b, true));
-        if (image.get() != NULL)
-        {
-          printf("[%s] -> [%s]\n", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b));
-
-          std::string target;
-          image->WriteToMemoryBuffer(target);
-
-          char buf[1024];
-          sprintf(buf, "/tmp/%s-%s.dcm", GetTransferSyntaxUid(a), GetTransferSyntaxUid(b));
-          
-          SystemToolbox::WriteFile(target, buf);
-        }
-        else if (a != DicomTransferSyntax_JPEG2000 &&
-                 a != DicomTransferSyntax_JPEG2000LosslessOnly)
-        {
-          ASSERT_TRUE(b != DicomTransferSyntax_LittleEndianImplicit &&
-                      b != DicomTransferSyntax_LittleEndianExplicit &&
-                      b != DicomTransferSyntax_BigEndianExplicit &&
-                      b != DicomTransferSyntax_DeflatedLittleEndianExplicit);
-        }
-      }
-    }
-  }
-}
-
-
-#include "../Core/DicomNetworking/DicomAssociation.h"
-#include "../Core/DicomNetworking/DicomControlUserConnection.h"
-#include "../Core/DicomNetworking/DicomStoreUserConnection.h"
-
-TEST(Toto, DISABLED_DicomAssociation)
-{
-  DicomAssociationParameters params;
-  params.SetLocalApplicationEntityTitle("ORTHANC");
-  params.SetRemoteApplicationEntityTitle("PACS");
-  params.SetRemotePort(2001);
-
-#if 0
-  DicomAssociation assoc;
-  assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass);
-  assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass);
-  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
-                                   DicomTransferSyntax_JPEGProcess1);
-  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
-                                   DicomTransferSyntax_JPEGProcess2_4);
-  assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage,
-                                   DicomTransferSyntax_JPEG2000);
-  
-  assoc.Open(params);
-
-  int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage);
-  printf(">> %d\n", presID);
-    
-  std::map<DicomTransferSyntax, uint8_t> pc;
-  printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage));
-  
-  for (std::map<DicomTransferSyntax, uint8_t>::const_iterator
-         it = pc.begin(); it != pc.end(); ++it)
-  {
-    printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second);
-  }
-#else
-  {
-    DicomControlUserConnection assoc(params);
-
-    try
-    {
-      printf(">> %d\n", assoc.Echo());
-    }
-    catch (OrthancException&)
-    {
-    }
-  }
-    
-  params.SetRemoteApplicationEntityTitle("PACS");
-  params.SetRemotePort(2000);
-
-  {
-    DicomControlUserConnection assoc(params);
-    printf(">> %d\n", assoc.Echo());
-  }
-
-#endif
-}
-
-static void TestTranscode(DicomStoreUserConnection& scu,
-                          const std::string& sopClassUid,
-                          DicomTransferSyntax transferSyntax)
-{
-  std::set<DicomTransferSyntax> accepted;
-
-  scu.LookupTranscoding(accepted, sopClassUid, transferSyntax);
-  if (accepted.empty())
-  {
-    throw OrthancException(ErrorCode_NetworkProtocol,
-                           "The SOP class is not supported by the remote modality");
-  }
-
-  {
-    unsigned int count = 0;
-    for (std::set<DicomTransferSyntax>::const_iterator
-           it = accepted.begin(); it != accepted.end(); ++it)
-    {
-      LOG(INFO) << "available for transcoding " << (count++) << ": " << sopClassUid
-                << " / " << GetTransferSyntaxUid(*it);
-    }
-  }
-  
-  if (accepted.find(transferSyntax) != accepted.end())
-  {
-    printf("**** OK, without transcoding !! [%s]\n", GetTransferSyntaxUid(transferSyntax));
-  }
-  else
-  {
-    // Transcoding - only in Orthanc >= 1.7.0
-
-    const DicomTransferSyntax uncompressed[] = {
-      DicomTransferSyntax_LittleEndianImplicit,  // Default transfer syntax
-      DicomTransferSyntax_LittleEndianExplicit,
-      DicomTransferSyntax_BigEndianExplicit
-    };
-
-    bool found = false;
-    for (size_t i = 0; i < 3; i++)
-    {
-      if (accepted.find(uncompressed[i]) != accepted.end())
-      {
-        printf("**** TRANSCODING to %s\n", GetTransferSyntaxUid(uncompressed[i]));
-        found = true;
-        break;
-      }
-    }
-
-    if (!found)
-    {
-      printf("**** KO KO KO\n");
-    }
-  }
-}
-
-
-TEST(Toto, DISABLED_Store)
-{
-  DicomAssociationParameters params;
-  params.SetLocalApplicationEntityTitle("ORTHANC");
-  params.SetRemoteApplicationEntityTitle("STORESCP");
-  params.SetRemotePort(2000);
-
-  DicomStoreUserConnection assoc(params);
-  assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess1);
-  assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_JPEGProcess2_4);
-  //assoc.RegisterStorageClass(UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
-
-  //assoc.SetUncompressedSyntaxesProposed(false);  // Necessary for transcoding
-  assoc.SetCommonClassesProposed(false);
-  assoc.SetRetiredBigEndianProposed(true);
-  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_LittleEndianExplicit);
-  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
-  TestTranscode(assoc, UID_MRImageStorage, DicomTransferSyntax_JPEG2000);
-}
-
-
-TEST(Toto, DISABLED_Store2)
-{
-  DicomAssociationParameters params;
-  params.SetLocalApplicationEntityTitle("ORTHANC");
-  params.SetRemoteApplicationEntityTitle("STORESCP");
-  params.SetRemotePort(2000);
-
-  DicomStoreUserConnection assoc(params);
-  //assoc.SetCommonClassesProposed(false);
-  assoc.SetRetiredBigEndianProposed(true);
-
-  std::string s;
-  Orthanc::SystemToolbox::ReadFile(s, "/tmp/i/" + std::string(GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit)) +".dcm");
-
-  std::string c, i;
-  assoc.Store(c, i, s.c_str(), s.size());
-  printf("[%s] [%s]\n", c.c_str(), i.c_str());
-}
-
--- a/Resources/ImplementationNotes/JobsEngineStates.dot	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-// 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-#
-# Full build, as used on the BuildBot CIS:
-#
-#   $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../Resources/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja
-#
-# Or, more lightweight version (without libp11 and ICU):
-#
-#   $ 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 -G Ninja
-#
-
-INCLUDE(CMakeForceCompiler)
-
-SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "")
-SET(LSB_CC $ENV{LSB_CC} CACHE STRING "")
-SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
-SET(LSB_TARGET_VERSION "4.0" CACHE STRING "")
-
-IF ("${LSB_PATH}" STREQUAL "")
-  SET(LSB_PATH "/opt/lsb")
-ENDIF()
-
-IF (EXISTS ${LSB_PATH}/lib64)
-  SET(LSB_TARGET_PROCESSOR "x86_64")
-  SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION})
-ELSEIF (EXISTS ${LSB_PATH}/lib)
-  SET(LSB_TARGET_PROCESSOR "x86")
-  SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION})
-ELSE()
-  MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.")
-ENDIF()
-
-SET(LSB_CPPPATH ${LSB_PATH}/include)
-SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/)
-
-# the name of the target operating system
-SET(CMAKE_SYSTEM_NAME Linux)
-SET(CMAKE_SYSTEM_VERSION LinuxStandardBase)
-SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR})
-
-# which compilers to use for C and C++
-SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc)
-
-if (${CMAKE_VERSION} VERSION_LESS "3.6.0") 
-  CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU)
-else()
-  SET(CMAKE_CXX_COMPILER ${LSB_PATH}/bin/lsbc++)
-endif()
-
-# here is the target environment located
-SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH})
-
-# adjust the default behaviour of the FIND_XXX() commands:
-# 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 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/MinGW-W64-Toolchain32.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# the name of the target operating system
-set(CMAKE_SYSTEM_NAME Windows)
-
-# which compilers to use for C and C++
-set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
-set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
-set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
-
-# here is the target environment located
-set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
-
-# adjust the default behaviour of the FIND_XXX() commands:
-# 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)
--- a/Resources/MinGW-W64-Toolchain64.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# the name of the target operating system
-set(CMAKE_SYSTEM_NAME Windows)
-
-# which compilers to use for C and C++
-set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
-set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
-set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
-
-# here is the target environment located
-set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
-
-# adjust the default behaviour of the FIND_XXX() commands:
-# 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)
--- a/Resources/MinGWToolchain.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# the name of the target operating system
-set(CMAKE_SYSTEM_NAME Windows)
-
-# which compilers to use for C and C++
-set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
-set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
-set(CMAKE_RC_COMPILER i586-mingw32msvc-windres)
-
-# here is the target environment located
-set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc)
-
-# adjust the default behaviour of the FIND_XXX() commands:
-# 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_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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,159 +0,0 @@
-This file contains old build instructions that are not tested anymore.
-
-
-Debian Squeeze (6.x)
---------------------
-
-# sudo apt-get install build-essential unzip cmake mercurial \
-       	       	       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
-       	       	       libgoogle-glog-dev libpng-dev libgtest-dev \
-       	       	       libsqlite3-dev libssl-dev zlib1g-dev
-
-# cmake -DALLOW_DOWNLOADS=ON \
-  	-DUSE_SYSTEM_BOOST=OFF \
-	-DUSE_SYSTEM_DCMTK=OFF \
-	-DUSE_SYSTEM_MONGOOSE=OFF \
-	-DUSE_SYSTEM_JSONCPP=OFF \
-	-DUSE_SYSTEM_PUGIXML=OFF \
-        -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
-	~/Orthanc 
-
-
-Debian Wheezy (7.x)
--------------------
-
-# sudo apt-get install 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 \
-       	       	       libboost-all-dev libwrap0-dev libjsoncpp-dev
-
-# cmake -DALLOW_DOWNLOADS=ON \
-        -DUSE_SYSTEM_GOOGLE_LOG=OFF \
-	-DUSE_SYSTEM_MONGOOSE=OFF \
-        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
-	-DUSE_SYSTEM_PUGIXML=OFF \
-        -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
-	~/Orthanc
-
-
-Ubuntu 12.04.5 LTS
-------------------
-
-# sudo apt-get install build-essential unzip cmake mercurial \
-                       uuid-dev libcurl4-openssl-dev liblua5.1-0-dev \
-                       libgtest-dev libpng-dev libsqlite3-dev libssl-dev libjpeg-dev \
-                       zlib1g-dev libdcmtk2-dev libboost1.48-all-dev libwrap0-dev \
-                       libcharls-dev
-
-# cmake "-DDCMTK_LIBRARIES=boost_locale;CharLS;dcmjpls;wrap;oflog" \
-        -DALLOW_DOWNLOADS=ON \
-        -DUSE_SYSTEM_CIVETWEB=OFF \
-        -DUSE_SYSTEM_JSONCPP=OFF \
-        -DUSE_SYSTEM_PUGIXML=OFF \
-        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
-        -DCMAKE_BUILD_TYPE=Release \
-        ~/Orthanc
-
-
-Ubuntu 12.10
-------------
-
-# 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 \
-       	       	       libsqlite3-dev libssl-dev zlib1g-dev \
-       	       	       libdcmtk2-dev libboost-all-dev libwrap0-dev libcharls-dev
-
-With JPEG:
-
-# cmake "-DDCMTK_LIBRARIES=CharLS;dcmjpls;wrap;oflog" \
-        -DALLOW_DOWNLOADS=ON \
-	-DUSE_SYSTEM_MONGOOSE=OFF \
-	-DUSE_SYSTEM_JSONCPP=OFF \
-        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
-	-DUSE_SYSTEM_PUGIXML=OFF \
-	~/Orthanc
-
-
-Without JPEG:
-
-# cmake "-DDCMTK_LIBRARIES=wrap;oflog" \
-        -DALLOW_DOWNLOADS=ON \
-	-DUSE_SYSTEM_MONGOOSE=OFF \
-	-DUSE_SYSTEM_JSONCPP=OFF \
-        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
-	-DUSE_SYSTEM_PUGIXML=OFF \
-        -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
-	~/Orthanc
-
-
-Ubuntu 13.10
-------------
-
-# 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 \
-       	       	       libsqlite3-dev libssl-dev zlib1g-dev \
-       	       	       libdcmtk2-dev libboost-all-dev libwrap0-dev libjsoncpp-dev
-
-# cmake "-DDCMTK_LIBRARIES=wrap;oflog" \
-        -DALLOW_DOWNLOADS=ON \
-	-DUSE_SYSTEM_MONGOOSE=OFF \
-        -DUSE_GOOGLE_TEST_DEBIAN_PACKAGE=ON \
-	-DUSE_SYSTEM_PUGIXML=OFF \
-        -DENABLE_JPEG=OFF \
-        -DENABLE_JPEG_LOSSLESS=OFF \
-	~/Orthanc
-
-
-Fedora 19
----------
-
-# sudo yum install unzip make automake gcc gcc-c++ python cmake \
-                   boost-devel curl-devel dcmtk-devel glog-devel \
-                   gtest-devel libpng-devel libsqlite3x-devel libuuid-devel \
-                   mongoose-devel openssl-devel jsoncpp-devel lua-devel pugixml-devel
-
-# cmake  "-DDCMTK_LIBRARIES=CharLS" \
-         -DSYSTEM_MONGOOSE_USE_CALLBACKS=OFF \
-         ~/Orthanc
-       
-Note: Have also a look at the official package:
-http://pkgs.fedoraproject.org/cgit/orthanc.git/tree/?h=f18
-
-
-CentOS 6
---------
-
-# yum install unzip make automake gcc gcc-c++ python cmake curl-devel \
-              libpng-devel sqlite-devel libuuid-devel openssl-devel \
-              lua-devel mercurial patch tar
-
-Using static linking with Civetweb (tested with Orthanc 1.5.7):
-
-# cmake -DSTATIC_BUILD=ON \
-        -DSTANDALONE_BUILD=ON \
-        -DUSE_LEGACY_JSONCPP=ON \
-        -DUSE_LEGACY_LIBICU=ON \
-        -DBOOST_LOCALE_BACKEND=icu \
-        -DCMAKE_BUILD_TYPE=Debug \
-        ~/Orthanc
-
-Using Mongoose (untested):
-
-# cmake -DALLOW_DOWNLOADS=ON \
-        -DUSE_SYSTEM_JSONCPP=OFF \
-        -DUSE_SYSTEM_CIVETWEB=OFF \
-        -DUSE_SYSTEM_PUGIXML=OFF \
-        -DUSE_SYSTEM_SQLITE=OFF \
-        -DUSE_SYSTEM_BOOST=OFF \
-        -DUSE_SYSTEM_DCMTK=OFF \
-        -DUSE_SYSTEM_GOOGLE_TEST=OFF \
-        -DUSE_SYSTEM_LIBJPEG=OFF \
-        -DCMAKE_BUILD_TYPE=Release \
-        ~/Orthanc
-
--- a/Resources/Orthanc.doxygen	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2313 +0,0 @@
-# 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 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 (\" \").
-
-#---------------------------------------------------------------------------
-# 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.
-# The default value is: UTF-8.
-
-DOXYFILE_ENCODING      = UTF-8
-
-# 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.
-
-PROJECT_NUMBER         =
-
-# Using the PROJECT_BRIEF tag one can provide an optional one line description
-# for a project that appears at the top of each page and should give viewer a
-# quick idea about the purpose of the project. Keep the description short.
-
-PROJECT_BRIEF          = "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.
-
-PROJECT_LOGO           = @CMAKE_SOURCE_DIR@/Resources/OrthancLogoDocumentation.png
-
-# 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 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.
-# 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 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 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 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
-# description.
-# The default value is: NO.
-
-ALWAYS_DETAILED_SEC    = NO
-
-# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
-# inherited members of a class in the documentation of that class as if those
-# members were ordinary class members. Constructors, destructors and assignment
-# operators of the base classes will not be shown.
-# The default value is: NO.
-
-INLINE_INHERITED_MEMB  = NO
-
-# 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
-
-# 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 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 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-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 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 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 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.
-# 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.
-# Minimum value: 1, maximum value: 16, default value: 4.
-
-TAB_SIZE               = 8
-
-# 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.
-
-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.
-# The default value is: NO.
-
-OPTIMIZE_OUTPUT_FOR_C  = NO
-
-# 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. 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.
-# 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,
-# 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 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 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);
-# 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 (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 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
-
-# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
-# tag is set to YES, then doxygen will reuse the documentation of the first
-# member in the group (if any) for the other members of the group. By default
-# all members of a group must be documented explicitly.
-# The default value is: NO.
-
-DISTRIBUTE_GROUP_DOC   = NO
-
-# 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).
-#
-# 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 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 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
-# types are typedef'ed and only the typedef is referenced, never the tag name.
-# The default value is: NO.
-
-TYPEDEF_HIDES_STRUCT   = NO
-
-# 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
-
-#---------------------------------------------------------------------------
-# Build related configuration options
-#---------------------------------------------------------------------------
-
-# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
-# documentation are documented, even if no documentation was available. Private
-# class members and static file members will be hidden unless the
-# EXTRACT_PRIVATE 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.
-# 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.
-# 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.
-# 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. 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 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 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 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 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 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 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 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
-# 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 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 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 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 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 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
-# 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        = YES
-
-# 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 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 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.
-# 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.
-# 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 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 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 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 default value is: YES.
-
-GENERATE_DEPRECATEDLIST= YES
-
-# 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 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.
-# 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 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 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. 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.
-#
-# 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 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
-#---------------------------------------------------------------------------
-
-# 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 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 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 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
-
-# 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 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 standard
-# error (stderr).
-
-WARN_LOGFILE           =
-
-#---------------------------------------------------------------------------
-# Configuration options related to the input files
-#---------------------------------------------------------------------------
-
-# 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. 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 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 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.
-
-EXCLUDE                =
-
-# 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/*
-
-EXCLUDE_PATTERNS       =
-
-# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
-# (namespaces, classes, functions, etc.) that should be excluded from the
-# output. The symbol name can be a fully qualified name, a word, or if the
-# wildcard * is used, a substring. Examples: ANamespace, AClass,
-# AClass::ANamespace, ANamespace::*Test
-#
-# 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).
-
-EXAMPLE_PATH           =
-
-# If the value of the EXAMPLE_PATH tag contains directories, you can use the
-# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
-# *.h) to filter out the source-files in the directories. If left blank all
-# files are included.
-
-EXAMPLE_PATTERNS       =
-
-# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
-# searched for input files to be used with the \include or \dontinclude commands
-# irrespective of the value of the RECURSIVE tag.
-# 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 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.
-#
-# 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 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 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 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
-#---------------------------------------------------------------------------
-
-# 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,
-# classes and enums directly into the documentation.
-# The default value is: NO.
-
-INLINE_SOURCES         = NO
-
-# 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.
-# 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.
-# The default value is: NO.
-
-REFERENCES_RELATION    = NO
-
-# 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 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 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
-#---------------------------------------------------------------------------
-
-# 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
-
-# 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 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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).
-# 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 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 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 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.
-# 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 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.
-# 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.
-# 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.
-# 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.
-# 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 (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
-
-# 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"
-
-# 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
-
-# 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 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 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
-
-# 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               =
-
-# 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           =
-
-# 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
-
-# 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     =
-
-# 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 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.
-# 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.
-# 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 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 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 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_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. 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  =
-
-# 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, 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. 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
-
-# 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. 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 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.
-# 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.
-# 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. 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 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.
-# 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 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 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     =
-
-# 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 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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.
-#
-# 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.
-# 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.
-# 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 (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 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. 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.
-#
-# 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           =
-
-# 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 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.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_LATEX is set to YES.
-
-LATEX_BATCHMODE        = NO
-
-# 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 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. 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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.
-# 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 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 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. 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
-#---------------------------------------------------------------------------
-
-# 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. 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. 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
-
-# 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
-#---------------------------------------------------------------------------
-
-# 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.
-# The default directory is: xml.
-# This tag requires that the tag GENERATE_XML is set to YES.
-
-XML_OUTPUT             = xml
-
-# 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 related to the DOCBOOK output
-#---------------------------------------------------------------------------
-
-# 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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.
-# 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.
-# This tag requires that the tag GENERATE_PERLMOD is set to YES.
-
-PERLMOD_MAKEVAR_PREFIX =
-
-#---------------------------------------------------------------------------
-# Configuration options related to the preprocessor
-#---------------------------------------------------------------------------
-
-# 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 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.
-# 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 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.
-# 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.
-# 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 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.
-# 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 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 options related to external references
-#---------------------------------------------------------------------------
-
-# 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. 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. 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 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.
-# 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').
-# The default file (with absolute path) is: /usr/bin/perl.
-
-PERL_PATH              = /usr/bin/perl
-
-#---------------------------------------------------------------------------
-# Configuration options related to the dot tool
-#---------------------------------------------------------------------------
-
-# 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
-# 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            =
-
-# 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 (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 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
-
-# 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 (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 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 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 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 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 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 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 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 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 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 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 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 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.
-# 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.
-# 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 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).
-# 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).
-
-MSCFILE_DIRS           =
-
-# 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
-# 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).
-# 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.
-# 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 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 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2314 +0,0 @@
-# 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 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 (\" \").
-
-#---------------------------------------------------------------------------
-# 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.
-# The default value is: UTF-8.
-
-DOXYFILE_ENCODING      = UTF-8
-
-# 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         = @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.
-
-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.
-
-PROJECT_LOGO           = @CMAKE_SOURCE_DIR@/Resources/OrthancLogoDocumentation.png
-
-# 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 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.
-# 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 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 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 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
-# description.
-# The default value is: NO.
-
-ALWAYS_DETAILED_SEC    = NO
-
-# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
-# inherited members of a class in the documentation of that class as if those
-# members were ordinary class members. Constructors, destructors and assignment
-# operators of the base classes will not be shown.
-# The default value is: NO.
-
-INLINE_INHERITED_MEMB  = NO
-
-# 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
-
-# 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 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 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-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 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 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 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.
-# 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.
-# Minimum value: 1, maximum value: 16, default value: 4.
-
-TAB_SIZE               = 8
-
-# 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.
-
-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.
-# The default value is: NO.
-
-OPTIMIZE_OUTPUT_FOR_C  = NO
-
-# 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. 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.
-# 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,
-# 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 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 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);
-# 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 (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 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
-
-# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
-# tag is set to YES, then doxygen will reuse the documentation of the first
-# member in the group (if any) for the other members of the group. By default
-# all members of a group must be documented explicitly.
-# The default value is: NO.
-
-DISTRIBUTE_GROUP_DOC   = NO
-
-# 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).
-#
-# 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 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 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
-# types are typedef'ed and only the typedef is referenced, never the tag name.
-# The default value is: NO.
-
-TYPEDEF_HIDES_STRUCT   = NO
-
-# 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
-
-#---------------------------------------------------------------------------
-# Build related configuration options
-#---------------------------------------------------------------------------
-
-# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
-# documentation are documented, even if no documentation was available. Private
-# class members and static file members will be hidden unless the
-# EXTRACT_PRIVATE 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.
-# 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.
-# 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.
-# 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. 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 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 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 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 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 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 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 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
-# 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 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 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 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 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 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
-# 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        = YES
-
-# 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 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 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.
-# 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.
-# 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 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 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 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 default value is: YES.
-
-GENERATE_DEPRECATEDLIST= YES
-
-# 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 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.
-# 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 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 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. 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.
-#
-# 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 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
-#---------------------------------------------------------------------------
-
-# 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 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 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 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
-
-# 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 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 standard
-# error (stderr).
-
-WARN_LOGFILE           =
-
-#---------------------------------------------------------------------------
-# Configuration options related to the input files
-#---------------------------------------------------------------------------
-
-# 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
-
-# This tag can be used to specify the character encoding of the source files
-# 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 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 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.
-
-EXCLUDE                =
-
-# 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/*
-
-EXCLUDE_PATTERNS       =
-
-# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
-# (namespaces, classes, functions, etc.) that should be excluded from the
-# output. The symbol name can be a fully qualified name, a word, or if the
-# wildcard * is used, a substring. Examples: ANamespace, AClass,
-# AClass::ANamespace, ANamespace::*Test
-#
-# 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       =
-
-# 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.
-# 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 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.
-#
-# 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 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 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 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
-#---------------------------------------------------------------------------
-
-# 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,
-# classes and enums directly into the documentation.
-# The default value is: NO.
-
-INLINE_SOURCES         = NO
-
-# 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.
-# 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.
-# The default value is: NO.
-
-REFERENCES_RELATION    = NO
-
-# 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 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 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
-#---------------------------------------------------------------------------
-
-# 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
-
-# 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 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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).
-# 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 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 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 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.
-# 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 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.
-# 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.
-# 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.
-# 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.
-# 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 (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
-
-# 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"
-
-# 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
-
-# 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 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 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
-
-# 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               =
-
-# 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           =
-
-# 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
-
-# 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     =
-
-# 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 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.
-# 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.
-# 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 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 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 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_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. 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  =
-
-# 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, 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. 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
-
-# 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. 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 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.
-# 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.
-# 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. 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 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.
-# 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 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 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     =
-
-# 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 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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.
-#
-# 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.
-# 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.
-# 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 (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 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. 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.
-#
-# 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           =
-
-# 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 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.
-# The default value is: NO.
-# This tag requires that the tag GENERATE_LATEX is set to YES.
-
-LATEX_BATCHMODE        = NO
-
-# 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 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. 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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.
-# 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 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 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. 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
-#---------------------------------------------------------------------------
-
-# 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. 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. 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
-
-# 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
-#---------------------------------------------------------------------------
-
-# 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.
-# The default directory is: xml.
-# This tag requires that the tag GENERATE_XML is set to YES.
-
-XML_OUTPUT             = xml
-
-# 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 related to the DOCBOOK output
-#---------------------------------------------------------------------------
-
-# 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
-#---------------------------------------------------------------------------
-
-# 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.
-# 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.
-# 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.
-# This tag requires that the tag GENERATE_PERLMOD is set to YES.
-
-PERLMOD_MAKEVAR_PREFIX =
-
-#---------------------------------------------------------------------------
-# Configuration options related to the preprocessor
-#---------------------------------------------------------------------------
-
-# 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 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.
-# 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 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.
-# 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.
-# 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 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.
-# 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 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 options related to external references
-#---------------------------------------------------------------------------
-
-# 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. 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. 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 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.
-# 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').
-# The default file (with absolute path) is: /usr/bin/perl.
-
-PERL_PATH              = /usr/bin/perl
-
-#---------------------------------------------------------------------------
-# Configuration options related to the dot tool
-#---------------------------------------------------------------------------
-
-# 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
-# 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            =
-
-# 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 (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 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
-
-# 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 (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 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 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 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 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 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 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 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 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 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 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 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 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.
-# 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.
-# 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 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).
-# 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).
-
-MSCFILE_DIRS           =
-
-# 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
-# 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).
-# 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.
-# 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 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 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
--- a/Resources/Patches/boost-1.65.1-linux-standard-base.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-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
--- a/Resources/Patches/boost-1.66.0-linux-standard-base.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-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~
--- a/Resources/Patches/boost-1.67.0-linux-standard-base.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-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
--- a/Resources/Patches/boost-1.68.0-linux-standard-base.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-diff -urEb boost_1_68_0.orig/boost/move/adl_move_swap.hpp boost_1_68_0/boost/move/adl_move_swap.hpp
---- boost_1_68_0.orig/boost/move/adl_move_swap.hpp	2018-11-13 16:08:32.214434915 +0100
-+++ boost_1_68_0/boost/move/adl_move_swap.hpp	2018-11-13 16:09:03.558399048 +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
-diff -urEb boost_1_68_0.orig/boost/thread/detail/config.hpp boost_1_68_0/boost/thread/detail/config.hpp
---- boost_1_68_0.orig/boost/thread/detail/config.hpp	2018-11-13 16:08:32.210434920 +0100
-+++ boost_1_68_0/boost/thread/detail/config.hpp	2018-11-13 16:10:03.386329911 +0100
-@@ -417,7 +417,7 @@
-   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
- #elif defined(BOOST_THREAD_CHRONO_MAC_API)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
--#elif defined(__ANDROID__)
-+#elif defined(__LSB_VERSION__) || defined(__ANDROID__)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
-   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
-     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
-diff -urEb boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp
---- boost_1_68_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
-+++ boost_1_68_0/boost/type_traits/detail/has_postfix_operator.hpp	2018-11-13 16:11:08.374253901 +0100
-@@ -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_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp
---- boost_1_68_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:08:32.206434924 +0100
-+++ boost_1_68_0/boost/type_traits/detail/has_prefix_operator.hpp	2018-11-13 16:14:30.278012856 +0100
-@@ -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,10 @@
-    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
-Only in boost_1_68_0/boost/type_traits/detail: has_prefix_operator.hpp~
--- a/Resources/Patches/boost-1.69.0-linux-standard-base.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-diff -urEb boost_1_69_0.orig/boost/move/adl_move_swap.hpp boost_1_69_0/boost/move/adl_move_swap.hpp
---- boost_1_69_0.orig/boost/move/adl_move_swap.hpp	2019-02-22 15:05:32.682359994 +0100
-+++ boost_1_69_0/boost/move/adl_move_swap.hpp	2019-02-22 15:05:48.426358034 +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
-diff -urEb boost_1_69_0.orig/boost/system/detail/system_category_win32.hpp boost_1_69_0/boost/system/detail/system_category_win32.hpp
---- boost_1_69_0.orig/boost/system/detail/system_category_win32.hpp	2019-02-22 15:05:32.722359989 +0100
-+++ boost_1_69_0/boost/system/detail/system_category_win32.hpp	2019-02-22 15:06:31.922352713 +0100
-@@ -26,7 +26,7 @@
- namespace detail
- {
- 
--#if ( defined(_MSC_VER) && _MSC_VER < 1900 ) || ( defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) )
-+#if ( defined(_MSC_VER) && _MSC_VER < 1900 ) || ( defined(__MINGW32__) && !defined(__MINGW64_VERSION_MAJOR) ) || 1  /* std::snprintf() does not seem to exist on Visual Studio 2015 */
- 
- inline char const * unknown_message_win32( int ev, char * buffer, std::size_t len )
- {
-diff -urEb boost_1_69_0.orig/boost/thread/detail/config.hpp boost_1_69_0/boost/thread/detail/config.hpp
---- boost_1_69_0.orig/boost/thread/detail/config.hpp	2019-02-22 15:05:32.598360004 +0100
-+++ boost_1_69_0/boost/thread/detail/config.hpp	2019-02-22 15:05:48.426358034 +0100
-@@ -418,7 +418,7 @@
-   #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
- #elif defined(BOOST_THREAD_CHRONO_MAC_API)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
--#elif defined(__ANDROID__)
-+#elif defined(__ANDROID__) || defined(__LSB_VERSION__)
-   #define BOOST_THREAD_HAS_MONO_CLOCK
-   #if defined(__ANDROID_API__) && __ANDROID_API__ >= 21
-     #define BOOST_THREAD_INTERNAL_CLOCK_IS_MONO
-diff -urEb boost_1_69_0.orig/boost/type_traits/detail/has_postfix_operator.hpp boost_1_69_0/boost/type_traits/detail/has_postfix_operator.hpp
---- boost_1_69_0.orig/boost/type_traits/detail/has_postfix_operator.hpp	2019-02-22 15:05:32.650359998 +0100
-+++ boost_1_69_0/boost/type_traits/detail/has_postfix_operator.hpp	2019-02-22 15:05:48.426358034 +0100
-@@ -85,8 +85,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
-@@ -234,7 +237,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_69_0.orig/boost/type_traits/detail/has_prefix_operator.hpp boost_1_69_0/boost/type_traits/detail/has_prefix_operator.hpp
---- boost_1_69_0.orig/boost/type_traits/detail/has_prefix_operator.hpp	2019-02-22 15:05:32.650359998 +0100
-+++ boost_1_69_0/boost/type_traits/detail/has_prefix_operator.hpp	2019-02-22 15:05:48.426358034 +0100
-@@ -114,8 +114,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
-@@ -263,7 +266,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
-diff -urEb boost_1_69_0.orig/libs/filesystem/src/operations.cpp boost_1_69_0/libs/filesystem/src/operations.cpp
---- boost_1_69_0.orig/libs/filesystem/src/operations.cpp	2019-02-22 15:05:32.566360008 +0100
-+++ boost_1_69_0/libs/filesystem/src/operations.cpp	2019-02-22 18:04:17.346573047 +0100
-@@ -2111,9 +2111,16 @@
-     std::size_t path_size (0);  // initialization quiets gcc warning (ticket #3509)
-     error_code ec = path_max(path_size);
-     if (ec)return ec;
--    dirent de;
--    buffer = std::malloc((sizeof(dirent) - sizeof(de.d_name))
--      +  path_size + 1); // + 1 for "/0"
-+
-+    // Fixed possible use of uninitialized dirent::d_type in dir_iterator
-+    // https://github.com/boostorg/filesystem/commit/bbe9d1771e5d679b3f10c42a58fc81f7e8c024a9
-+    const std::size_t buffer_size = (sizeof(dirent) - sizeof(dirent().d_name))
-+      +  path_size + 1; // + 1 for "\0"
-+    buffer = std::malloc(buffer_size);
-+    if (BOOST_UNLIKELY(!buffer))
-+      return make_error_code(boost::system::errc::not_enough_memory);
-+    std::memset(buffer, 0, buffer_size);
-+    
-     return ok;
-   }  
- 
-@@ -2142,6 +2149,13 @@
-     *result = 0;
-     if ((p = ::readdir(dirp))== 0)
-       return errno;
-+
-+    // Fixed possible use of uninitialized dirent::d_type in dir_iterator
-+    // https://github.com/boostorg/filesystem/commit/bbe9d1771e5d679b3f10c42a58fc81f7e8c024a9    
-+#   ifdef BOOST_FILESYSTEM_STATUS_CACHE
-+    entry->d_type = p->d_type;
-+#   endif
-+
-     std::strcpy(entry->d_name, p->d_name);
-     *result = entry;
-     return 0;
--- a/Resources/Patches/civetweb-1.11.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-diff -urEb civetweb-1.11.orig/include/civetweb.h civetweb-1.11/include/civetweb.h
---- civetweb-1.11.orig/include/civetweb.h	2019-01-17 21:09:41.844888908 +0100
-+++ civetweb-1.11/include/civetweb.h	2019-01-21 12:05:08.138998659 +0100
-@@ -1507,6 +1507,10 @@
- #endif
- 
- 
-+// Added by SJ
-+CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
-+
-+
- #ifdef __cplusplus
- }
- #endif /* __cplusplus */
-diff -urEb civetweb-1.11.orig/src/civetweb.c civetweb-1.11/src/civetweb.c
---- civetweb-1.11.orig/src/civetweb.c	2019-01-17 21:09:41.852888857 +0100
-+++ civetweb-1.11/src/civetweb.c	2019-01-21 12:06:35.826868284 +0100
-@@ -59,6 +59,9 @@
- #if defined(__linux__) && !defined(_XOPEN_SOURCE)
- #define _XOPEN_SOURCE 600 /* For flockfile() on Linux */
- #endif
-+#if defined(__LSB_VERSION__)
-+#define NEED_TIMEGM
-+#endif
- #if !defined(_LARGEFILE_SOURCE)
- #define _LARGEFILE_SOURCE /* For fseeko(), ftello() */
- #endif
-@@ -129,6 +132,12 @@
- 
- 
- /* Alternative queue is well tested and should be the new default */
-+#if defined(__LSB_VERSION__)
-+/* Function "eventfd()" is not available in Linux Standard Base, can't
-+ * use the alternative queue */
-+#define NO_ALTERNATIVE_QUEUE
-+#endif
-+
- #if defined(NO_ALTERNATIVE_QUEUE)
- #if defined(ALTERNATIVE_QUEUE)
- #error "Define ALTERNATIVE_QUEUE or NO_ALTERNATIVE_QUEUE or none, but not both"
-@@ -536,6 +545,10 @@
- #if !defined(EWOULDBLOCK)
- #define EWOULDBLOCK WSAEWOULDBLOCK
- #endif /* !EWOULDBLOCK */
-+#if !defined(ECONNRESET)
-+/* This macro is not defined e.g. in Visual Studio 2008 */
-+#define ECONNRESET WSAECONNRESET
-+#endif /* !ECONNRESET */
- #define _POSIX_
- #define INT64_FMT "I64d"
- #define UINT64_FMT "I64u"
-@@ -2939,6 +2952,13 @@
- #endif
- 
- 
-+#if defined(__LSB_VERSION__)
-+static void
-+mg_set_thread_name(const char *threadName)
-+{
-+  /* prctl() does not seem to be available in Linux Standard Base */
-+}
-+#else
- static void
- mg_set_thread_name(const char *name)
- {
-@@ -2980,6 +3000,7 @@
- 	(void)prctl(PR_SET_NAME, threadName, 0, 0, 0);
- #endif
- }
-+#endif
- #else /* !defined(NO_THREAD_NAME) */
- void
- mg_set_thread_name(const char *threadName)
-@@ -16919,6 +16940,10 @@
- 	/* Message is a valid request */
- 
- 	/* Is there a "host" ? */
-+        /* https://github.com/civetweb/civetweb/pull/675/commits/96e3e8c50acb4b8e0c946d02b5f880a3e62986e1 */
-+	if (conn->host!=NULL) {
-+		mg_free((void *)conn->host);
-+	}
- 	conn->host = alloc_get_host(conn);
- 	if (!conn->host) {
- 		mg_snprintf(conn,
-@@ -19857,4 +19882,13 @@
- }
- 
- 
-+// Added by SJ
-+void mg_disable_keep_alive(struct mg_connection *conn)
-+{
-+  if (conn != NULL) {
-+    conn->must_close = 1;
-+  }
-+}
-+
-+
- /* End of civetweb.c */
--- a/Resources/Patches/civetweb-1.12.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-diff -urEb civetweb-1.12.orig/include/civetweb.h civetweb-1.12/include/civetweb.h
---- civetweb-1.12.orig/include/civetweb.h	2020-04-02 12:07:20.727054140 +0200
-+++ civetweb-1.12/include/civetweb.h	2020-04-02 12:07:42.734996559 +0200
-@@ -1614,6 +1614,9 @@
-                                   struct mg_error_data *error);
- #endif
- 
-+// Added by SJ
-+CIVETWEB_API void mg_disable_keep_alive(struct mg_connection *conn);
-+
- #ifdef __cplusplus
- }
- #endif /* __cplusplus */
-diff -urEb civetweb-1.12.orig/src/civetweb.c civetweb-1.12/src/civetweb.c
---- civetweb-1.12.orig/src/civetweb.c	2020-04-02 12:07:20.731054129 +0200
-+++ civetweb-1.12/src/civetweb.c	2020-04-02 12:07:52.250971600 +0200
-@@ -20704,5 +20704,12 @@
- 	return 1;
- }
- 
-+// Added by SJ
-+void mg_disable_keep_alive(struct mg_connection *conn)
-+{
-+  if (conn != NULL) {
-+    conn->must_close = 1;
-+  }
-+}
- 
- /* End of civetweb.c */
--- a/Resources/Patches/curl-7.57.0-cmake.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-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)
--- a/Resources/Patches/curl-7.64.0-cmake.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-diff -urEb curl-7.64.0.orig/CMake/Macros.cmake curl-7.64.0/CMake/Macros.cmake
---- curl-7.64.0.orig/CMake/Macros.cmake	2019-02-21 20:35:26.403471603 +0100
-+++ curl-7.64.0/CMake/Macros.cmake	2019-02-21 20:36:19.987272782 +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)
--- a/Resources/Patches/dcmtk-3.6.0-dulparse-vulnerability.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-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;
-             }
--- a/Resources/Patches/dcmtk-3.6.0-mingw64.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +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 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_);
--- a/Resources/Patches/dcmtk-3.6.0-speed.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +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	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
-         /*
--- a/Resources/Patches/dcmtk-3.6.2-linux-standard-base.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-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_);
--- a/Resources/Patches/dcmtk-3.6.2-private.dic	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3040 +0,0 @@
-#
-#  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-3.6.2.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-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	2020-01-06 17:42:52.299540389 +0100
-+++ dcmtk-3.6.2/CMake/GenerateDCMTKConfigure.cmake	2020-01-06 17:43:56.707520036 +0100
-@@ -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)
-diff -urEb dcmtk-3.6.2.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.2/dcmdata/include/dcmtk/dcmdata/dcdict.h
---- dcmtk-3.6.2.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 17:42:52.283540394 +0100
-+++ dcmtk-3.6.2/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 17:46:21.711473976 +0100
-@@ -152,6 +152,12 @@
-     /// 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
-diff -urEb dcmtk-3.6.2.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.2/dcmdata/libsrc/dcdict.cc
---- dcmtk-3.6.2.orig/dcmdata/libsrc/dcdict.cc	2020-01-06 17:42:52.287540392 +0100
-+++ dcmtk-3.6.2/dcmdata/libsrc/dcdict.cc	2020-01-06 17:47:18.335299472 +0100
-@@ -876,3 +876,6 @@
-   wrlock().clear();
-   unlock();
- }
-+
-+
-+#include "dcdict_orthanc.cc"
--- a/Resources/Patches/dcmtk-3.6.4.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-diff -urEb dcmtk-3.6.4.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcdict.h
---- dcmtk-3.6.4.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 19:55:12.887153062 +0100
-+++ dcmtk-3.6.4/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-01-06 19:55:28.156447233 +0100
-@@ -152,6 +152,12 @@
-     /// 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
-diff -urEb dcmtk-3.6.4.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.4/dcmdata/libsrc/dcdict.cc
---- dcmtk-3.6.4.orig/dcmdata/libsrc/dcdict.cc	2020-01-06 19:55:12.899154075 +0100
-+++ dcmtk-3.6.4/dcmdata/libsrc/dcdict.cc	2020-01-06 19:55:28.156447233 +0100
-@@ -899,3 +899,6 @@
-   wrlock().clear();
-   wrunlock();
- }
-+
-+
-+#include "dcdict_orthanc.cc"
-diff -urEb dcmtk-3.6.4.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.4/dcmdata/libsrc/dcpxitem.cc
---- dcmtk-3.6.4.orig/dcmdata/libsrc/dcpxitem.cc	2020-01-06 19:55:12.899154075 +0100
-+++ dcmtk-3.6.4/dcmdata/libsrc/dcpxitem.cc	2020-01-06 19:55:28.156447233 +0100
-@@ -36,6 +36,9 @@
- #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
- #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
- 
-+#undef max
-+#include "dcmtk/ofstd/oflimits.h"
-+
- 
- // ********************************
- 
-diff -urEb dcmtk-3.6.4.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.4/oflog/include/dcmtk/oflog/thread/syncpub.h
---- dcmtk-3.6.4.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-01-06 19:55:12.911155088 +0100
-+++ dcmtk-3.6.4/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-01-06 19:56:26.991372656 +0100
-@@ -63,7 +63,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- Mutex::Mutex (Mutex::Type t)
--    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
-+    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
- { }
- 
- 
-@@ -106,7 +106,7 @@
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
-     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
--    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
-+    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
- { }
- 
- 
-@@ -148,7 +148,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- FairMutex::FairMutex ()
--    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
-+    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
- { }
- 
- 
-@@ -190,7 +190,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
--    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
-+    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
- { }
- 
- 
-@@ -252,7 +252,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- SharedMutex::SharedMutex ()
--    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
-+    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
- { }
- 
- 
-diff -urEb dcmtk-3.6.4.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.4/ofstd/include/dcmtk/ofstd/offile.h
---- dcmtk-3.6.4.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-01-06 19:55:12.951158464 +0100
-+++ dcmtk-3.6.4/ofstd/include/dcmtk/ofstd/offile.h	2020-01-06 19:55:28.156447233 +0100
-@@ -575,7 +575,7 @@
-    */
-   void setlinebuf()
-   {
--#if defined(_WIN32) || defined(__hpux)
-+#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
-     this->setvbuf(NULL, _IOLBF, 0);
- #else
-     :: setlinebuf(file_);
--- a/Resources/Patches/dcmtk-3.6.5.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,133 +0,0 @@
-diff -urEb dcmtk-3.6.5.orig/CMake/GenerateDCMTKConfigure.cmake dcmtk-3.6.5/CMake/GenerateDCMTKConfigure.cmake
---- dcmtk-3.6.5.orig/CMake/GenerateDCMTKConfigure.cmake	2020-06-08 22:19:03.265799573 +0200
-+++ dcmtk-3.6.5/CMake/GenerateDCMTKConfigure.cmake	2020-06-08 22:21:22.670025141 +0200
-@@ -169,6 +169,8 @@
- endif()
- 
- # Check the sizes of various types
-+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
-+  # This doesn't work for wasm, Orthanc defines the macros manually
- include (CheckTypeSize)
- CHECK_TYPE_SIZE("char" SIZEOF_CHAR)
- CHECK_TYPE_SIZE("double" SIZEOF_DOUBLE)
-@@ -177,6 +179,7 @@
- CHECK_TYPE_SIZE("long" SIZEOF_LONG)
- CHECK_TYPE_SIZE("short" SIZEOF_SHORT)
- CHECK_TYPE_SIZE("void*" SIZEOF_VOID_P)
-+endif()
- 
- # Check for include files, libraries, and functions
- include("${DCMTK_CMAKE_INCLUDE}CMake/dcmtkTryCompile.cmake")
-diff -urEb dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h
---- dcmtk-3.6.5.orig/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-06-08 22:19:03.229799498 +0200
-+++ dcmtk-3.6.5/dcmdata/include/dcmtk/dcmdata/dcdict.h	2020-06-08 22:19:35.317862998 +0200
-@@ -152,6 +152,12 @@
-     /// 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
-diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc
---- dcmtk-3.6.5.orig/dcmdata/libsrc/dcdict.cc	2020-06-08 22:19:03.245799531 +0200
-+++ dcmtk-3.6.5/dcmdata/libsrc/dcdict.cc	2020-06-08 22:19:35.317862998 +0200
-@@ -900,3 +900,6 @@
-   wrlock().clear();
-   wrunlock();
- }
-+
-+
-+#include "dcdict_orthanc.cc"
-diff -urEb dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc
---- dcmtk-3.6.5.orig/dcmdata/libsrc/dcpxitem.cc	2020-06-08 22:19:03.245799531 +0200
-+++ dcmtk-3.6.5/dcmdata/libsrc/dcpxitem.cc	2020-06-08 22:19:35.317862998 +0200
-@@ -36,6 +36,9 @@
- #include "dcmtk/dcmdata/dcostrma.h"    /* for class DcmOutputStream */
- #include "dcmtk/dcmdata/dcwcache.h"    /* for class DcmWriteCache */
- 
-+#undef max
-+#include "dcmtk/ofstd/oflimits.h"
-+
- 
- // ********************************
- 
-diff -urEb dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h
---- dcmtk-3.6.5.orig/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-06-08 22:19:03.261799565 +0200
-+++ dcmtk-3.6.5/oflog/include/dcmtk/oflog/thread/syncpub.h	2020-06-08 22:19:35.317862998 +0200
-@@ -63,7 +63,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- Mutex::Mutex (Mutex::Type t)
--    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)) + 0)
-+    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::Mutex (t)))
- { }
- 
- 
-@@ -106,7 +106,7 @@
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- Semaphore::Semaphore (unsigned DCMTK_LOG4CPLUS_THREADED (max),
-     unsigned DCMTK_LOG4CPLUS_THREADED (initial))
--    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)) + 0)
-+    : sem (DCMTK_LOG4CPLUS_THREADED (new impl::Semaphore (max, initial)))
- { }
- 
- 
-@@ -148,7 +148,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- FairMutex::FairMutex ()
--    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex) + 0)
-+    : mtx (DCMTK_LOG4CPLUS_THREADED (new impl::FairMutex))
- { }
- 
- 
-@@ -190,7 +190,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- ManualResetEvent::ManualResetEvent (bool DCMTK_LOG4CPLUS_THREADED (sig))
--    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)) + 0)
-+    : ev (DCMTK_LOG4CPLUS_THREADED (new impl::ManualResetEvent (sig)))
- { }
- 
- 
-@@ -252,7 +252,7 @@
- 
- DCMTK_LOG4CPLUS_INLINE_EXPORT
- SharedMutex::SharedMutex ()
--    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex) + 0)
-+    : sm (DCMTK_LOG4CPLUS_THREADED (new impl::SharedMutex))
- { }
- 
- 
-diff -urEb dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc dcmtk-3.6.5/oflog/libsrc/oflog.cc
---- dcmtk-3.6.5.orig/oflog/libsrc/oflog.cc	2020-06-08 22:19:03.261799565 +0200
-+++ dcmtk-3.6.5/oflog/libsrc/oflog.cc	2020-06-08 22:19:35.317862998 +0200
-@@ -19,6 +19,10 @@
-  *
-  */
- 
-+#if defined(_WIN32)
-+#  include <winsock2.h>
-+#endif
-+
- #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
- #include "dcmtk/oflog/oflog.h"
- 
-diff -urEb dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h
---- dcmtk-3.6.5.orig/ofstd/include/dcmtk/ofstd/offile.h	2020-06-08 22:19:03.293799632 +0200
-+++ dcmtk-3.6.5/ofstd/include/dcmtk/ofstd/offile.h	2020-06-08 22:19:35.317862998 +0200
-@@ -575,7 +575,7 @@
-    */
-   void setlinebuf()
-   {
--#if defined(_WIN32) || defined(__hpux)
-+#if defined(_WIN32) || defined(__hpux) || defined(__LSB_VERSION__)
-     this->setvbuf(NULL, _IOLBF, 0);
- #else
-     :: setlinebuf(file_);
--- a/Resources/Patches/dcmtk-dcdict_orthanc.cc	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,205 +0,0 @@
-// 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 <string>
-#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;
-  }
-}
--- a/Resources/Patches/dcmtk.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-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
-diff -urEb dcmtk-3.6.4.orig/ dcmtk-3.6.4
-diff -urEb dcmtk-3.6.5.orig/ dcmtk-3.6.5
-
-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/e2fsprogs-1.43.8-apple.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-diff -urEb e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in e2fsprogs-1.43.8/lib/uuid/uuid.h.in
---- e2fsprogs-1.43.8.orig/lib/uuid/uuid.h.in	2018-01-02 05:52:58.000000000 +0100
-+++ e2fsprogs-1.43.8/lib/uuid/uuid.h.in	2018-11-05 12:18:29.962235770 +0100
-@@ -35,6 +35,20 @@
- #ifndef _UUID_UUID_H
- #define _UUID_UUID_H
- 
-+
-+#if defined(__APPLE__)
-+// This patch defines the "uuid_string_t" type on OS X, which is
-+// required if linking against Cocoa (this occurs in Stone of Orthanc)
-+#include <sys/_types.h>
-+#include <sys/_types/_uuid_t.h>
-+
-+#ifndef _UUID_STRING_T
-+#define _UUID_STRING_T
-+typedef __darwin_uuid_string_t  uuid_string_t;
-+#endif /* _UUID_STRING_T */
-+#endif
-+
-+
- #include <sys/types.h>
- #ifndef _WIN32
- #include <sys/time.h>
--- a/Resources/Patches/e2fsprogs-1.44.5-apple.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-diff -urEb e2fsprogs-1.44.5.orig/lib/uuid/uuid.h.in e2fsprogs-1.44.5/lib/uuid/uuid.h.in
---- e2fsprogs-1.44.5.orig/lib/uuid/uuid.h.in	2019-02-21 20:17:23.461402522 +0100
-+++ e2fsprogs-1.44.5/lib/uuid/uuid.h.in	2019-02-21 20:25:05.664540445 +0100
-@@ -35,6 +35,20 @@
- #ifndef _UUID_UUID_H
- #define _UUID_UUID_H
- 
-+
-+#if defined(__APPLE__)
-+// This patch defines the "uuid_string_t" type on OS X, which is
-+// required if linking against Cocoa (this occurs in Stone of Orthanc)
-+#include <sys/_types.h>
-+#include <sys/_types/_uuid_t.h>
-+
-+#ifndef _UUID_STRING_T
-+#define _UUID_STRING_T
-+typedef __darwin_uuid_string_t  uuid_string_t;
-+#endif /* _UUID_STRING_T */
-+#endif
-+
-+
- #include <sys/types.h>
- #ifndef _WIN32
- #include <sys/time.h>
--- a/Resources/Patches/libp11-0.4.0.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-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	2020-04-02 17:03:55.340634019 +0200
-+++ libp11-0.4.0/src/atfork.c	2020-04-02 17:04:10.152619121 +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	2020-04-02 17:03:55.340634019 +0200
-+++ libp11-0.4.0/src/engine.h	2020-04-02 17:04:10.152619121 +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	2020-04-02 17:03:55.340634019 +0200
-+++ libp11-0.4.0/src/libp11-int.h	2020-04-02 17:04:10.152619121 +0200
-@@ -20,7 +20,7 @@
- #define _LIBP11_INT_H
- 
- #ifndef _WIN32
--#include "config.h"
-+//#include "config.h"
- #endif
- 
- #include "libp11.h"
-diff -urEb libp11-0.4.0.orig/src/p11_key.c libp11-0.4.0/src/p11_key.c
---- libp11-0.4.0.orig/src/p11_key.c	2020-04-02 17:03:55.340634019 +0200
-+++ libp11-0.4.0/src/p11_key.c	2020-04-02 17:05:39.892516032 +0200
-@@ -21,6 +21,12 @@
- #include <string.h>
- #include <openssl/bn.h>
- 
-+#if OPENSSL_VERSION_NUMBER >= 0x10100105L // File renamed in OpenSSL 1.1.1e
-+#  include <crypto/rsa/rsa_local.h>
-+#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.0.2
-+#  include <crypto/rsa/rsa_locl.h>
-+#endif
-+
- #ifdef _WIN32
- #define strncasecmp strnicmp
- #endif
-diff -urEb libp11-0.4.0.orig/src/p11_rsa.c libp11-0.4.0/src/p11_rsa.c
---- libp11-0.4.0.orig/src/p11_rsa.c	2020-04-02 17:03:55.340634019 +0200
-+++ libp11-0.4.0/src/p11_rsa.c	2020-04-02 17:05:49.176504198 +0200
-@@ -27,6 +27,12 @@
- #include <openssl/evp.h>
- #include <openssl/rsa.h>
- 
-+#if OPENSSL_VERSION_NUMBER >= 0x10100105L // File renamed in OpenSSL 1.1.1e
-+#  include <crypto/rsa/rsa_local.h>
-+#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // OpenSSL 1.0.2
-+#  include <crypto/rsa/rsa_locl.h>
-+#endif
-+
- static int rsa_ex_index = 0;
- 
- #if OPENSSL_VERSION_NUMBER < 0x10100003L
--- a/Resources/Patches/mongoose-3.1-patch.diff	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
---- /home/jodogne/Subversion/Orthanc/ThirdPartyDownloads/mongoose/mongoose.c	2012-03-11 23:41:35.000000000 +0100
-+++ mongoose.c	2013-03-07 10:07:00.566266153 +0100
-@@ -92,8 +92,9 @@
- #define strtoll(x, y, z) strtol(x, y, z)
- #else
- #define __func__  __FUNCTION__
--#define strtoull(x, y, z) _strtoui64(x, y, z)
--#define strtoll(x, y, z) _strtoi64(x, y, z)
-+#include <stdlib.h>
-+//#define strtoull(x, y, z) _strtoui64(x, y, z)
-+//#define strtoll(x, y, z) _strtoi64(x, y, z)
- #endif // _MSC_VER
- 
- #define ERRNO   GetLastError()
-@@ -253,6 +254,14 @@
- #define MSG_NOSIGNAL 0
- #endif
- 
-+#if __gnu_hurd__ == 1
-+/**
-+ * There is no limit on the length on a path under GNU Hurd, so we set
-+ * it to an arbitrary constant.
-+ **/
-+#define PATH_MAX 4096
-+#endif
-+
- typedef void * (*mg_thread_func_t)(void *);
- 
- static const char *http_500_error = "Internal Server Error";
-@@ -3844,10 +3853,8 @@
- }
- 
- static void discard_current_request_from_buffer(struct mg_connection *conn) {
--  char *buffered;
-   int buffered_len, body_len;
- 
--  buffered = conn->buf + conn->request_len;
-   buffered_len = conn->data_len - conn->request_len;
-   assert(buffered_len >= 0);
- 
-@@ -4148,7 +4155,13 @@
- 
-   // Wait until mg_fini() stops
-   while (ctx->stop_flag != 2) {
-+#if defined(__linux__)
-+    usleep(100000);
-+#elif defined(_WIN32)
-+    Sleep(100);
-+#else
-     (void) sleep(0);
-+#endif
-   }
-   free_context(ctx);
- 
--- a/Resources/Patches/mongoose-3.8-patch.diff	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
---- mongoose.c.orig	2019-07-31 14:06:36.043726677 +0200
-+++ mongoose.c	2019-07-31 14:10:06.767727652 +0200
-@@ -50,6 +50,14 @@
- #define PATH_MAX FILENAME_MAX
- #endif // __SYMBIAN32__
- 
-+#if __gnu_hurd__ == 1
-+/**
-+ * There is no limit on the length on a path under GNU Hurd, so we set
-+ * it to an arbitrary constant.
-+ **/
-+#define PATH_MAX 4096
-+#endif
-+
- #ifndef _WIN32_WCE // Some ANSI #includes are not available on Windows CE
- #include <sys/types.h>
- #include <sys/stat.h>
-@@ -108,8 +116,9 @@
- #define strtoll(x, y, z) _atoi64(x)
- #else
- #define __func__  __FUNCTION__
--#define strtoull(x, y, z) _strtoui64(x, y, z)
--#define strtoll(x, y, z) _strtoi64(x, y, z)
-+#include <stdlib.h>
-+//#define strtoull(x, y, z) _strtoui64(x, y, z)
-+//#define strtoll(x, y, z) _strtoi64(x, y, z)
- #endif // _MSC_VER
- 
- #define ERRNO   GetLastError()
-@@ -2997,19 +3006,19 @@
-   }
- }
- 
--static int is_valid_http_method(const char *method) {
--  return !strcmp(method, "GET") || !strcmp(method, "POST") ||
-+static int is_valid_http_method(const char *method, int *isValidHttpMethod) {
-+  *isValidHttpMethod = !strcmp(method, "GET") || !strcmp(method, "POST") ||
-     !strcmp(method, "HEAD") || !strcmp(method, "CONNECT") ||
-     !strcmp(method, "PUT") || !strcmp(method, "DELETE") ||
-     !strcmp(method, "OPTIONS") || !strcmp(method, "PROPFIND")
--    || !strcmp(method, "MKCOL")
--          ;
-+    || !strcmp(method, "MKCOL");
-+  return *isValidHttpMethod;
- }
- 
- // Parse HTTP request, fill in mg_request_info structure.
- // This function modifies the buffer by NUL-terminating
- // HTTP request components, header names and header values.
--static int parse_http_message(char *buf, int len, struct mg_request_info *ri) {
-+static int parse_http_message(char *buf, int len, struct mg_request_info *ri, int *isValidHttpMethod) {
-   int is_request, request_length = get_request_len(buf, len);
-   if (request_length > 0) {
-     // Reset attributes. DO NOT TOUCH is_ssl, remote_ip, remote_port
-@@ -3025,7 +3034,7 @@
-     ri->request_method = skip(&buf, " ");
-     ri->uri = skip(&buf, " ");
-     ri->http_version = skip(&buf, "\r\n");
--    if (((is_request = is_valid_http_method(ri->request_method)) &&
-+    if (((is_request = is_valid_http_method(ri->request_method, isValidHttpMethod)) &&
-          memcmp(ri->http_version, "HTTP/", 5) != 0) ||
-         (!is_request && memcmp(ri->request_method, "HTTP/", 5)) != 0) {
-       request_length = -1;
-@@ -4930,7 +4939,7 @@
-   return uri[0] == '/' || (uri[0] == '*' && uri[1] == '\0');
- }
- 
--static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len) {
-+static int getreq(struct mg_connection *conn, char *ebuf, size_t ebuf_len, int *isValidHttpMethod) {
-   const char *cl;
- 
-   ebuf[0] = '\0';
-@@ -4944,7 +4953,7 @@
-   } else if (conn->request_len <= 0) {
-     snprintf(ebuf, ebuf_len, "%s", "Client closed connection");
-   } else if (parse_http_message(conn->buf, conn->buf_size,
--                                &conn->request_info) <= 0) {
-+                                &conn->request_info, isValidHttpMethod) <= 0) {
-     snprintf(ebuf, ebuf_len, "Bad request: [%.*s]", conn->data_len, conn->buf);
-   } else {
-     // Request is valid
-@@ -4973,7 +4982,8 @@
-   } else if (mg_vprintf(conn, fmt, ap) <= 0) {
-     snprintf(ebuf, ebuf_len, "%s", "Error sending request");
-   } else {
--    getreq(conn, ebuf, ebuf_len);
-+    int isValidHttpMethod = 1; /* unused in this case */
-+    getreq(conn, ebuf, ebuf_len, &isValidHttpMethod);
-   }
-   if (ebuf[0] != '\0' && conn != NULL) {
-     mg_close_connection(conn);
-@@ -4995,8 +5005,13 @@
-   // to crule42.
-   conn->data_len = 0;
-   do {
--    if (!getreq(conn, ebuf, sizeof(ebuf))) {
-+    int isValidHttpMethod = 1;
-+    if (!getreq(conn, ebuf, sizeof(ebuf), &isValidHttpMethod)) {
-+      if (isValidHttpMethod) {
-       send_http_error(conn, 500, "Server Error", "%s", ebuf);
-+      } else {
-+        send_http_error(conn, 400, "Bad Request", "%s", ebuf);
-+      }
-       conn->must_close = 1;
-     } else if (!is_valid_uri(conn->request_info.uri)) {
-       snprintf(ebuf, sizeof(ebuf), "Invalid URI: [%s]", ri->uri);
--- a/Resources/Patches/openssl-1.1.1-conf.h.in	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/*
- * {- join("\n * ", @autowarntext) -}
- *
- * Copyright 2016-2018 The OpenSSL Project Authors. All Rights Reserved.
- *
- * Licensed under the OpenSSL license (the "License").  You may not use
- * this file except in compliance with the License.  You can obtain a copy
- * in the file LICENSE in the source distribution or at
- * https://www.openssl.org/source/license.html
- */
-
-#include <openssl/opensslv.h>
-
-#ifdef  __cplusplus
-extern "C" {
-#endif
-
-#ifdef OPENSSL_ALGORITHM_DEFINES
-# error OPENSSL_ALGORITHM_DEFINES no longer supported
-#endif
-
-
-/*
- * Sometimes OPENSSSL_NO_xxx ends up with an empty file and some compilers
- * don't like that.  This will hopefully silence them.
- */
-#define NON_EMPTY_TRANSLATION_UNIT static void *dummy = &dummy;
-
-/*
- * Applications should use -DOPENSSL_API_COMPAT=<version> to suppress the
- * declarations of functions deprecated in or before <version>. Otherwise, they
- * still won't see them if the library has been built to disable deprecated
- * functions.
- */
-#ifndef DECLARE_DEPRECATED
-# define DECLARE_DEPRECATED(f)   f;
-# ifdef __GNUC__
-#  if __GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ > 0)
-#   undef DECLARE_DEPRECATED
-#   define DECLARE_DEPRECATED(f)    f __attribute__ ((deprecated));
-#  endif
-# endif
-#endif
-
-#ifndef OPENSSL_FILE
-# ifdef OPENSSL_NO_FILENAMES
-#  define OPENSSL_FILE ""
-#  define OPENSSL_LINE 0
-# else
-#  define OPENSSL_FILE __FILE__
-#  define OPENSSL_LINE __LINE__
-# endif
-#endif
-
-#ifndef OPENSSL_MIN_API
-# define OPENSSL_MIN_API 0
-#endif
-
-#if !defined(OPENSSL_API_COMPAT) || OPENSSL_API_COMPAT < OPENSSL_MIN_API
-# undef OPENSSL_API_COMPAT
-# define OPENSSL_API_COMPAT OPENSSL_MIN_API
-#endif
-
-/*
- * Do not deprecate things to be deprecated in version 1.2.0 before the
- * OpenSSL version number matches.
- */
-#if OPENSSL_VERSION_NUMBER < 0x10200000L
-# define DEPRECATEDIN_1_2_0(f)   f;
-#elif OPENSSL_API_COMPAT < 0x10200000L
-# define DEPRECATEDIN_1_2_0(f)   DECLARE_DEPRECATED(f)
-#else
-# define DEPRECATEDIN_1_2_0(f)
-#endif
-
-#if OPENSSL_API_COMPAT < 0x10100000L
-# define DEPRECATEDIN_1_1_0(f)   DECLARE_DEPRECATED(f)
-#else
-# define DEPRECATEDIN_1_1_0(f)
-#endif
-
-#if OPENSSL_API_COMPAT < 0x10000000L
-# define DEPRECATEDIN_1_0_0(f)   DECLARE_DEPRECATED(f)
-#else
-# define DEPRECATEDIN_1_0_0(f)
-#endif
-
-#if OPENSSL_API_COMPAT < 0x00908000L
-# define DEPRECATEDIN_0_9_8(f)   DECLARE_DEPRECATED(f)
-#else
-# define DEPRECATEDIN_0_9_8(f)
-#endif
-
-
-#define OPENSSL_UNISTD <unistd.h>
-
-#if 0
-/* Generate 80386 code? */
-{- ${processor} eq "386" ? "#define" : "#undef" -} I386_ONLY
-
-#undef OPENSSL_UNISTD
-#define OPENSSL_UNISTD {- ${unistd} -}
-
-{- ${export_var_as_fn} ? "#define" : "#undef" -} OPENSSL_EXPORT_VAR_AS_FUNCTION
-
-/*
- * The following are cipher-specific, but are part of the public API.
- */
-#if !defined(OPENSSL_SYS_UEFI)
-{- ${bn_ll} ? "# define" : "# undef" -} BN_LLONG
-/* Only one for the following should be defined */
-{- ${b64l} ? "# define" : "# undef" -} SIXTY_FOUR_BIT_LONG
-{- ${b64}  ? "# define" : "# undef" -} SIXTY_FOUR_BIT
-{- ${b32}  ? "# define" : "# undef" -} THIRTY_TWO_BIT
-#endif
-
-#define RC4_INT {- ${rc4_int} -}
-#endif
-
-#ifdef  __cplusplus
-}
-#endif
--- a/Resources/Patches/openssl-1.1.1g.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-diff -urEb openssl-1.1.1g.orig/crypto/rand/rand_unix.c openssl-1.1.1g/crypto/rand/rand_unix.c
---- openssl-1.1.1g.orig/crypto/rand/rand_unix.c	2020-05-05 17:58:08.785998440 +0200
-+++ openssl-1.1.1g/crypto/rand/rand_unix.c	2020-05-05 17:58:55.201881117 +0200
-@@ -445,6 +445,7 @@
-              * system call and this should always succeed which renders
-              * this alternative but essentially identical source moot.
-              */
-+#if !defined(__LSB_VERSION__)  // "syscall()" is not available in LSB
-             if (uname(&un) == 0) {
-                 kernel[0] = atoi(un.release);
-                 p = strchr(un.release, '.');
-@@ -455,6 +456,7 @@
-                     return 0;
-                 }
-             }
-+#endif
-             /* Open /dev/random and wait for it to be readable */
-             if ((fd = open(DEVRANDOM_WAIT, O_RDONLY)) != -1) {
-                 if (DEVRANDM_WAIT_USE_SELECT && fd < FD_SETSIZE) {
--- a/Resources/RetrieveCACertificates.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 subprocess
-import urllib2
-
-
-if len(sys.argv) <= 2:
-    print('Download a set of CA certificates, convert them to PEM, then format them as a C macro')
-    print('Usage: %s [Macro] [Certificate1] <Certificate2>...' % sys.argv[0])
-    print('')
-    print('Example: %s BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt' % sys.argv[0])
-    print('')
-    sys.exit(-1)
-
-MACRO = sys.argv[1]
-
-sys.stdout.write('#define %s ' % MACRO)
-
-for url in sys.argv[2:]:
-    # Download the certificate from the CA authority, in the DES format
-    des = urllib2.urlopen(url).read()
-
-    # Convert DES to PEM
-    p = subprocess.Popen([ 'openssl', 'x509', '-inform', 'DES', '-outform', 'PEM' ],
-                         stdin = subprocess.PIPE,
-                         stdout = subprocess.PIPE)
-    pem = p.communicate(input = des)[0]
-    pem = re.sub(r'\r', '', pem)       # Remove any carriage return
-    pem = re.sub(r'\\', r'\\\\', pem)  # Escape any backslash
-    pem = re.sub(r'"', r'\\"', pem)    # Escape any quote
-
-    # Write the PEM data into the macro
-    for line in pem.split('\n'):
-        sys.stdout.write(' \\\n')
-        sys.stdout.write('"%s\\n" ' % line)
-
-sys.stdout.write('\n')
-sys.stderr.write('Done!\n')
--- a/Resources/Samples/CppHelpers/Logging/ILogger.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,168 +0,0 @@
-#pragma once
-
-#include <string>
-#include <vector>
-#include <boost/algorithm/string.hpp>
-#include <boost/thread.hpp>
-
-namespace OrthancHelpers
-{
-
-
-  inline std::string ShortenId(const std::string& orthancUuid)
-  {
-    size_t firstHyphenPos = orthancUuid.find_first_of('-');
-    if (firstHyphenPos == std::string::npos)
-    {
-      return orthancUuid;
-    }
-    else
-    {
-      return orthancUuid.substr(0, firstHyphenPos);
-    }
-  }
-
-
-  // Interface for loggers providing the same interface
-  // in Orthanc framework or in an Orthanc plugins.
-  // Furthermore, compared to the LOG and VLOG macros,
-  // these loggers will provide "contexts".
-  class ILogger
-  {
-  public:
-    virtual ~ILogger() {}
-    virtual void Trace(const char* message) = 0;
-    virtual void Trace(const std::string& message) = 0;
-    virtual void Info(const char* message) = 0;
-    virtual void Info(const std::string& message) = 0;
-    virtual void Warning(const char* message) = 0;
-    virtual void Warning(const std::string& message) = 0;
-    virtual void Error(const char* message) = 0;
-    virtual void Error(const std::string& message) = 0;
-
-    virtual void EnterContext(const char* message, bool forceLogContextChange = false) = 0;
-    virtual void EnterContext(const std::string& message, bool forceLogContextChange = false) = 0;
-    virtual void LeaveContext(bool forceLogContextChange = false) = 0;
-  };
-
-
-  // Implements ILogger by providing contexts.  Contexts defines
-  // the "call-stack" of the logs and are prepended to the log.
-  // check LogContext class for more details
-  class BaseLogger : public ILogger
-  {
-#if ORTHANC_ENABLE_THREADS == 1
-    boost::thread_specific_ptr<std::vector<std::string>> contexts_;
-#else
-    std::auto_ptr<std::vector<std::string>> contexts_;
-#endif
-    bool logContextChanges_;
-
-  public:
-
-    BaseLogger()
-      : logContextChanges_(false)
-    {
-    }
-
-    void EnableLogContextChanges(bool enable)
-    {
-      logContextChanges_ = enable;
-    }
-
-    virtual void EnterContext(const char* message, bool forceLogContextChange = false)
-    {
-      EnterContext(std::string(message), forceLogContextChange);
-    }
-
-    virtual void EnterContext(const std::string& message, bool forceLogContextChange = false)
-    {
-      if (!contexts_.get())
-      {
-        contexts_.reset(new std::vector<std::string>());
-      }
-      contexts_->push_back(message);
-
-      if (logContextChanges_ || forceLogContextChange)
-      {
-        Info(".. entering");
-      }
-    }
-
-    virtual void LeaveContext(bool forceLogContextChange = false)
-    {
-      if (logContextChanges_ || forceLogContextChange)
-      {
-        Info(".. leaving");
-      }
-
-      contexts_->pop_back();
-      if (contexts_->size() == 0)
-      {
-        contexts_.reset(NULL);
-      }
-    }
-
-  protected:
-
-    virtual std::string GetContext()
-    {
-      if (contexts_.get() != NULL && contexts_->size() > 0)
-      {
-        return "|" + boost::algorithm::join(*contexts_, " | ") + "|";
-      }
-      else
-      {
-        return std::string("|");
-      }
-    }
-  };
-
-
-  /* RAII to set a Log context.
-  * Example:
-  * ILogger* logger = new OrthancPluginLogger(..);
-  * {
-  *   LogContext logContext(logger, "A");
-  *   {
-  *     LogContext nestedLogContext(logger, "B");
-  *     logger->Error("out of memory");
-  *   }
-  * }
-  * will produce:
-  * |A | B| out of memory
-  *
-  * furthermore, if LogContextChanges are enabled in the BaseLogger,
-  * you'll get;
-  * |A| .. entering
-  * |A | B| .. entering
-  * |A | B| out of memory
-  * |A | B| .. leaving
-  * |A| .. leaving
-  */
-  class LogContext
-  {
-    ILogger* logger_;
-    bool     forceLogContextChange_;
-  public:
-    LogContext(ILogger* logger, const char* context, bool forceLogContextChange = false) :
-      logger_(logger),
-      forceLogContextChange_(forceLogContextChange)
-    {
-      logger_->EnterContext(context, forceLogContextChange_);
-    }
-
-    LogContext(ILogger* logger, const std::string& context, bool forceLogContextChange = false) :
-      logger_(logger),
-      forceLogContextChange_(forceLogContextChange)
-    {
-      logger_->EnterContext(context, forceLogContextChange_);
-    }
-
-    ~LogContext()
-    {
-      logger_->LeaveContext(forceLogContextChange_);
-    }
-
-  };
-}
--- a/Resources/Samples/CppHelpers/Logging/NullLogger.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-#pragma once
-
-#include "ILogger.h"
-
-namespace OrthancHelpers
-{
-  // a logger ... that does not log.
-  // Instead of writing:
-  // if (logger != NULL)
-  // {
-  //   logger->Info("hello")   ;
-  // }
-  // you should create a NullLogger:
-  // logger = new NullLogger();
-  // ...
-  // logger->Info("hello");
-  class NullLogger : public BaseLogger {
-  public:
-    NullLogger() {}
-
-    virtual void Trace(const char* message) {}
-    virtual void Trace(const std::string& message) {}
-    virtual void Info(const char* message) {}
-    virtual void Info(const std::string& message) {}
-    virtual void Warning(const char* message) {}
-    virtual void Warning(const std::string& message) {}
-    virtual void Error(const char* message) {}
-    virtual void Error(const std::string& message) {}
-  };
-}
--- a/Resources/Samples/CppHelpers/Logging/OrthancLogger.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-#include "OrthancLogger.h"
-#include "Logging.h"
-
-namespace OrthancHelpers
-{
-
-  void OrthancLogger::Trace(const char *message)
-  {
-    VLOG(1) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Trace(const std::string& message)
-  {
-    VLOG(1) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Info(const char *message)
-  {
-    LOG(INFO) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Info(const std::string& message)
-  {
-    LOG(INFO) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Warning(const char *message)
-  {
-    LOG(WARNING) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Warning(const std::string& message)
-  {
-    LOG(WARNING) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Error(const char *message)
-  {
-    LOG(ERROR) << GetContext() << " " << message;
-  }
-
-  void OrthancLogger::Error(const std::string& message)
-  {
-    LOG(ERROR) << GetContext() << " " << message;
-  }
-}
--- a/Resources/Samples/CppHelpers/Logging/OrthancLogger.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-#pragma once
-
-#include "ILogger.h"
-
-namespace OrthancHelpers
-{
-
-  class OrthancLogger : public BaseLogger
-  {
-  public:
-    virtual void Trace(const char *message);
-    virtual void Trace(const std::string &message);
-    virtual void Info(const char *message);
-    virtual void Info(const std::string &message);
-    virtual void Warning(const char *message);
-    virtual void Warning(const std::string &message);
-    virtual void Error(const char *message);
-    virtual void Error(const std::string &message);
-  };
-} // namespace OrthancHelpers
--- a/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-#include "OrthancPluginLogger.h"
-
-namespace OrthancHelpers
-{
-
-  OrthancPluginLogger::OrthancPluginLogger(OrthancPluginContext *context)
-    : pluginContext_(context),
-      hasAlreadyLoggedTraceWarning_(false)
-  {
-  }
-
-  void OrthancPluginLogger::Trace(const char *message)
-  {
-    Trace(std::string(message));
-  }
-
-  void OrthancPluginLogger::Trace(const std::string &message)
-  {
-    if (!hasAlreadyLoggedTraceWarning_)
-    {
-      Warning("Trying to log 'TRACE' level information in a plugin is not possible.  These logs won't appear.");
-      hasAlreadyLoggedTraceWarning_ = true;
-    }
-  }
-
-  void OrthancPluginLogger::Info(const char *message)
-  {
-    Info(std::string(message));
-  }
-
-  void OrthancPluginLogger::Info(const std::string &message)
-  {
-    OrthancPluginLogInfo(pluginContext_, (GetContext() + " " + message).c_str());
-  }
-
-  void OrthancPluginLogger::Warning(const char *message)
-  {
-    Warning(std::string(message));
-  }
-
-  void OrthancPluginLogger::Warning(const std::string &message)
-  {
-    OrthancPluginLogWarning(pluginContext_, (GetContext() + " " + message).c_str());
-  }
-
-  void OrthancPluginLogger::Error(const char *message)
-  {
-    Error(std::string(message));
-  }
-
-  void OrthancPluginLogger::Error(const std::string &message)
-  {
-    OrthancPluginLogError(pluginContext_, (GetContext() + " " + message).c_str());
-  }
-} // namespace OrthancHelpers
--- a/Resources/Samples/CppHelpers/Logging/OrthancPluginLogger.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-#pragma once
-
-#include "ILogger.h"
-#include <orthanc/OrthancCPlugin.h>
-
-namespace OrthancHelpers
-{
-
-  class OrthancPluginLogger : public BaseLogger
-  {
-    OrthancPluginContext *pluginContext_;
-    bool hasAlreadyLoggedTraceWarning_;
-
-  public:
-    OrthancPluginLogger(OrthancPluginContext *context);
-
-    virtual void Trace(const char *message);
-    virtual void Trace(const std::string &message);
-    virtual void Info(const char *message);
-    virtual void Info(const std::string &message);
-    virtual void Warning(const char *message);
-    virtual void Warning(const std::string &message);
-    virtual void Error(const char *message);
-    virtual void Error(const std::string &message);
-  };
-} // namespace OrthancHelpers
--- a/Resources/Samples/CppHelpers/README.md	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-This folder contains a bunch of helpers that are used in multiple Orthanc side projects (either plugins, standalone executables using the Orthanc framework or orthanc-stone based applications)
-
-When writing plugins, you may also want to check the Plugins/Samples/Common folder for other helpers.
\ No newline at end of file
--- a/Resources/Samples/ImportDicomFiles/ImportDicomFiles.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 sys
-import os.path
-import httplib2
-import base64
-
-if len(sys.argv) != 4 and len(sys.argv) != 6:
-    print("""
-Sample script to recursively import in Orthanc all the DICOM files
-that are stored in some path. Please make sure that Orthanc is running
-before starting this script. The files are uploaded through the REST
-API.
-
-Usage: %s [hostname] [HTTP port] [path]
-Usage: %s [hostname] [HTTP port] [path] [username] [password]
-For instance: %s 127.0.0.1 8042 .
-""" % (sys.argv[0], sys.argv[0], sys.argv[0]))
-    exit(-1)
-
-URL = 'http://%s:%d/instances' % (sys.argv[1], int(sys.argv[2]))
-
-success_count = 0
-total_file_count = 0
-
-
-# This function will upload a single file to Orthanc through the REST API
-def UploadFile(path):
-    global success_count
-    global total_file_count
-
-    f = open(path, "rb")
-    content = f.read()
-    f.close()
-    total_file_count += 1
-
-    try:
-        sys.stdout.write("Importing %s" % path)
-
-        h = httplib2.Http()
-
-        headers = { 'content-type' : 'application/dicom' }
-
-        if len(sys.argv) == 6:
-            username = sys.argv[4]
-            password = sys.argv[5]
-
-            # h.add_credentials(username, password)
-
-            # This is a custom reimplementation of the
-            # "Http.add_credentials()" method for Basic HTTP Access
-            # Authentication (for some weird reason, this method does
-            # not always work)
-            # http://en.wikipedia.org/wiki/Basic_access_authentication
-            creds_str = username + ':' + password
-            creds_str_bytes = creds_str.encode("ascii")
-            creds_str_bytes_b64 = b'Basic ' + base64.b64encode(creds_str_bytes)
-            headers['authorization'] = creds_str_bytes_b64.decode("ascii")
-
-        resp, content = h.request(URL, 'POST', 
-                                  body = content,
-                                  headers = headers)
-
-        if resp.status == 200:
-            sys.stdout.write(" => success\n")
-            success_count += 1
-        else:
-            sys.stdout.write(" => failure (Is it a DICOM file? Is there a password?)\n")
-
-    except:
-        type, value, traceback = sys.exc_info()
-        sys.stderr.write(str(value))
-        sys.stdout.write(" => unable to connect (Is Orthanc running? Is there a password?)\n")
-
-
-if os.path.isfile(sys.argv[3]):
-    # Upload a single file
-    UploadFile(sys.argv[3])
-else:
-    # Recursively upload a directory
-    for root, dirs, files in os.walk(sys.argv[3]):
-        for f in files:
-            UploadFile(os.path.join(root, f))
-
-if success_count == total_file_count:
-    print("\nSummary: all %d DICOM file(s) have been imported successfully" % success_count)
-else:
-    print("\nSummary: %d out of %d files have been imported successfully as DICOM instances" % (success_count, total_file_count))
--- a/Resources/Samples/Lua/AutomatedJpeg2kCompression.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
--- This sample shows how to use Orthanc to compress on-the-fly any
--- incoming DICOM file, as a JPEG2k file.
-
-function OnStoredInstance(instanceId, tags, metadata, origin)
-   -- Do not compress twice the same file
-   if origin['RequestOrigin'] ~= 'Lua' then
-
-      -- Retrieve the incoming DICOM instance from Orthanc
-      local dicom = RestApiGet('/instances/' .. instanceId .. '/file')
-
-      -- Write the DICOM content to some temporary file
-      local uncompressed = instanceId .. '-uncompressed.dcm'
-      local target = assert(io.open(uncompressed, 'wb'))
-      target:write(dicom)
-      target:close()
-
-      -- Compress to JPEG2000 using gdcm
-      local compressed = instanceId .. '-compressed.dcm'
-      os.execute('gdcmconv -U --j2k ' .. uncompressed .. ' ' .. compressed)
-
-      -- Generate a new SOPInstanceUID for the JPEG2000 file, as
-      -- gdcmconv does not do this by itself
-      os.execute('dcmodify --no-backup -gin ' .. compressed)
-
-      -- Read the JPEG2000 file
-      local source = assert(io.open(compressed, 'rb'))
-      local jpeg2k = source:read("*all")
-      source:close()
-
-      -- Upload the JPEG2000 file and remove the uncompressed file
-      RestApiPost('/instances', jpeg2k)
-      RestApiDelete('/instances/' .. instanceId)
-
-      -- Remove the temporary DICOM files
-      os.remove(uncompressed)
-      os.remove(compressed)
-   end
-end
--- a/Resources/Samples/Lua/Autorouting.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-function OnStoredInstance(instanceId, tags, metadata)
-   Delete(SendToModality(instanceId, 'sample'))
-end
--- a/Resources/Samples/Lua/AutoroutingConditional.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-function OnStoredInstance(instanceId, tags, metadata, origin)
-   -- The "origin" is only available since Orthanc 0.9.4
-   PrintRecursive(origin)
-
-   -- Extract the value of the "PatientName" DICOM tag
-   local patientName = string.lower(tags['PatientName'])
-
-   if string.find(patientName, 'david') ~= nil then
-      -- Only send patients whose name contains "David"
-      Delete(SendToModality(instanceId, 'sample'))
-
-   else
-      -- Delete the patients that are not called "David"
-      Delete(instanceId)
-   end
-end
--- a/Resources/Samples/Lua/AutoroutingModification.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-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 = {}
-      replace['StationName'] = 'My Medical Device'
-      replace['0031-1020'] = 'Some private tag'
-
-      -- The tags to be removed
-      local remove = { 'MilitaryRank' }
-
-      -- Modify the instance
-      local command = {}
-      command['Replace'] = replace
-      command['Remove'] = remove
-      local modifiedFile = RestApiPost('/instances/' .. instanceId .. '/modify', DumpJson(command, true))
-
-      -- 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/CallDcm2Xml.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-function OnStoredInstance(instanceId, tags, metadata)
-   -- Assume Latin1 encoding in dcm2xml
-   local args = {}
-   table.insert(args, '+Ca')
-   table.insert(args, 'latin-1')
-
-   Delete(CallSystem(instanceId, 'dcm2xml', args))
-end
--- a/Resources/Samples/Lua/CallImageJ.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
--- This sample shows how to invoke an ImageJ script on every DICOM
--- image received by Orthanc. The ImageJ script is generated by the
--- "Initialize()" function at the startup of Orthanc. Whenever a new
--- instance is received, its DICOM file is stored into a temporary
--- file, and a system call to ImageJ is triggered.
-
-SCRIPT = 'ImageJScript.txt'
-
-function Initialize()
-   local target = assert(io.open(SCRIPT, 'w'))
-
-   -- This is a sample ImageJ script that display the size of the DICOM image
-   target:write('if (getArgument=="") exit ("No argument!");\n')
-   target:write('open(getArgument);\n')
-   target:write('print(getTitle + ": " + getWidth + "x" + getHeight);\n')
-
-   target:close()
-end
-
-function OnStoredInstance(instanceId)
-   -- Retrieve the DICOM instance from Orthanc
-   local dicom = RestApiGet('/instances/' .. instanceId .. '/file')
-
-   -- Write the DICOM content to some temporary file
-   local path = instanceId .. '.dcm'
-   local target = assert(io.open(path, 'wb'))
-   target:write(dicom)
-   target:close()
-
-   -- Call ImageJ
-   os.execute('imagej -b ' .. SCRIPT .. ' ' .. path)
-
-   -- Remove the temporary DICOM file
-   os.remove(path)
-end
--- a/Resources/Samples/Lua/CallWebService.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,74 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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 file is a simple echo Web service implemented using
- * "node.js". Whenever it receives a POST HTTP query, it echoes its
- * body both to stdout and to the client. Credentials are checked.
- **/
-
-
-// Parameters of the ECHO server 
-var port = 8000;
-var username = 'alice';
-var password = 'alicePassword';
-
-
-var http = require('http');
-var authorization = 'Basic ' + new Buffer(username + ':' + password).toString('base64')
-
-var server = http.createServer(function(req, response) {
-  // Check the credentials
-  if (req.headers.authorization != authorization)
-  {
-    console.log('Bad credentials, access not allowed');
-    response.writeHead(401);
-    response.end();
-    return;
-  }
-
-  switch (req.method)
-  {
-  case 'POST':
-    {
-      var body = '';
-
-      req.on('data', function (data) {
-        response.write(data);
-        body += data;
-      });
-
-      req.on('end', function () {
-        console.log('Message received: ' + body);
-        response.end();
-      });
-
-      break;
-    }
-
-  default:
-    console.log('Method ' + req.method + ' is not supported by this ECHO Web service');
-    response.writeHead(405, {'Allow': 'POST'});
-    response.end();
-  }
-});
-
-server.listen(port);
--- a/Resources/Samples/Lua/CallWebService.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
--- This sample shows how to call a remote Web service whenever an
--- instance is received by Orthanc. For this sample to work, you have
--- to start the "CallWebService.js" script next to this file using
--- NodeJs.
-
--- Download and install the JSON module for Lua by Jeffrey Friedl
--- http://regex.info/blog/lua/json
-
--- NOTE : Replace "load" by "loadstring" for Lua <= 5.1
-JSON = (load(HttpGet('http://regex.info/code/JSON.lua'))) ()
-
-SetHttpCredentials('alice', 'alicePassword')
-
-function OnStoredInstance(instanceId, tags, metadata)
-   -- Build the POST body
-   local info = {}
-   info['InstanceID'] = instanceId
-   info['PatientName'] = tags['PatientName']
-   info['PatientID'] = tags['PatientID']
-
-   -- Send the POST request
-   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)
-end
--- a/Resources/Samples/Lua/IncomingFindRequestFilter.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
--- 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
--- Answer to:
--- https://groups.google.com/d/msg/orthanc-users/0ymHe1cDBCQ/YfD0NoOTn0wJ
--- Applicable starting with Orthanc 0.9.5
-
-function OnStoredInstance(instanceId, tags, metadata, origin)
-   -- Do not modify twice the same file
-   if origin['RequestOrigin'] ~= 'Lua' then
-      local replace = {}
-      replace['0010,1002'] = {}
-      replace['0010,1002'][1] = {}
-      replace['0010,1002'][1]['PatientID'] = 'Hello'
-      replace['0010,1002'][2] = {}
-      replace['0010,1002'][2]['PatientID'] = 'World'
-
-      local request = {}
-      request['Replace'] = replace
-
-      -- Create the modified instance
-      local modified = RestApiPost('/instances/' .. instanceId .. '/modify',
-                                   DumpJson(request, true))
-
-      -- Upload the modified instance to the Orthanc store
-      RestApiPost('/instances/', modified)
-
-      -- Delete the original instance
-      RestApiDelete('/instances/' .. instanceId)
-   end
-end
--- a/Resources/Samples/Lua/OnStableStudy.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-function Initialize()
-   print('Number of stored studies at initialization: ' ..
-            table.getn(ParseJson(RestApiGet('/studies'))))
-end
-
-
-function Finalize()
-   print('Number of stored studies at finalization: ' ..
-            table.getn(ParseJson(RestApiGet('/studies'))))
-end
-
-
-function OnStoredInstance(instanceId, tags, metadata)
-   patient = ParseJson(RestApiGet('/instances/' .. instanceId .. '/patient'))
-   print('Received an instance for patient: ' .. 
-            patient['MainDicomTags']['PatientID'] .. ' - ' .. 
-            patient['MainDicomTags']['PatientName'])
-end
-
-
-function OnStableStudy(studyId, tags, metadata)
-   if (metadata['ModifiedFrom'] == nil and
-       metadata['AnonymizedFrom'] == nil) then
-
-      print('This study is now stable: ' .. studyId)
-      
-      -- The tags to be replaced
-      local replace = {}
-      replace['StudyDescription'] = 'Modified study'
-      replace['StationName'] = 'My Medical Device'
-      replace['0031-1020'] = 'Some private tag'
-
-      -- The tags to be removed
-      local remove = { 'MilitaryRank' }
-
-      -- The modification command
-      local command = {}
-      command['Remove'] = remove
-      command['Replace'] = replace
-
-      -- Modify the entire study in one single call
-      local m = RestApiPost('/studies/' .. studyId .. '/modify',
-                            DumpJson(command, true))
-      print('Modified study: ' .. m)
-   end
-end
--- a/Resources/Samples/Lua/ParseDoseReport.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
--- Sample Lua script that demonstrates how to extract DICOM tags
--- related to dose reports. In this example, the value of the DLP
--- (Dose Length Product), the value of the mean CTDI volume (Computed
--- Tomography Dose Index), and the number of "IntervalsRejected" are
--- extracted and printed. Furthermore, these values are saved as
--- metadata that is attached to their parent DICOM instance for
--- further processing by external software.
-
-
-function ExploreContentSequence(instanceId, tags)
-   if tags then
-      for key, value in pairs(tags) do
-         -- Recursive exploration
-         ExploreContentSequence(instanceId, value['ContentSequence'])
-
-         local concept = value['ConceptNameCodeSequence'] 
-         local measure = value['MeasuredValueSequence']
-         if concept and measure then
-
-            local value = measure[1]['NumericValue']
-            local code = concept[1]['CodeValue']
-
-            if type(value) == 'string' and type(code) == 'string' then
-               -- If the field contains the DLP, stores it as a metadata.
-               -- "DLP" is associated with CodeValue 113838.
-               -- ftp://medical.nema.org/medical/dicom/final/sup127_ft.pdf
-               if code == '113838' then
-                  print('DLP = ' .. value)
-                  RestApiPut('/instances/' .. instanceId .. '/metadata/2001', value)
-               end
-
-               -- Extract the mean CTDI volume
-               if code == '113830' then
-                  print('CTDI = ' .. value)
-                  RestApiPut('/instances/' .. instanceId .. '/metadata/2002', value)
-               end
-
-               -- Other values can be extracted here
-            end
-         end
-      end
-   end
-end
-
-
-function StoreTagToMetadata(instanceId, tags, name, metadata)
-   if tags then
-      for key, value in pairs(tags) do
-         if type(value) ~= 'string' then
-            -- Recursive exploration
-            StoreTagToMetadata(instanceId, value, name, metadata)
-         elseif key == name then
-            print(name .. ' = ' .. value)
-            if metadata then
-               RestApiPut('/instances/' .. instanceId .. '/metadata/' .. metadata, value)
-            end
-         end
-      end
-   end
-end
-
-
-function OnStoredInstance(instanceId, tags)
-   StoreTagToMetadata(instanceId, tags, 'IntervalsRejected', 2000)
-   ExploreContentSequence(instanceId, tags['ContentSequence'])
-end
--- a/Resources/Samples/Lua/TransferSyntaxDisable.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-function IsDeflatedTransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsJpegTransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsJpeg2000TransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsJpipTransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsMpeg2TransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsMpeg4TransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsRleTransferSyntaxAccepted(aet, ip)
-   return false
-end
-
-function IsUnknownSopClassAccepted(aet, ip)
-   return false
-end
-
-print('All special transfer syntaxes are now disallowed')
--- a/Resources/Samples/Lua/TransferSyntaxEnable.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-function IsDeflatedTransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsJpegTransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsJpeg2000TransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsJpegLosslessTransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsJpipTransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsMpeg2TransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsMpeg4TransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsRleTransferSyntaxAccepted(aet, ip)
-   return true
-end
-
-function IsUnknownSopClassAccepted(aet, ip)
-   return true
-end
-
-print('All special transfer syntaxes are now accepted')
--- a/Resources/Samples/Lua/WriteToDisk.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-TARGET = '/tmp/lua'
-
-function ToAscii(s)
-   -- http://www.lua.org/manual/5.1/manual.html#pdf-string.gsub
-   -- https://groups.google.com/d/msg/orthanc-users/qMLgkEmwwPI/6jRpCrlgBwAJ
-   return s:gsub('[^a-zA-Z0-9-/-: ]', '_')
-end
-
-function OnStableSeries(seriesId, tags, metadata)
-   print('This series is now stable, writing its instances on the disk: ' .. seriesId)
-
-   local instances = ParseJson(RestApiGet('/series/' .. seriesId)) ['Instances']
-   local patient = ParseJson(RestApiGet('/series/' .. seriesId .. '/patient')) ['MainDicomTags']
-   local study = ParseJson(RestApiGet('/series/' .. seriesId .. '/study')) ['MainDicomTags']
-   local series = ParseJson(RestApiGet('/series/' .. seriesId)) ['MainDicomTags']
-
-   for i, instance in pairs(instances) do
-      local path = ToAscii(TARGET .. '/' .. 
-                              patient['PatientID'] .. ' - ' .. patient['PatientName'] .. '/' ..
-                              study['StudyDate'] .. ' - ' .. study['StudyDescription'] .. '/' ..
-                              series['SeriesDescription'])
-
-      -- Retrieve the DICOM file from Orthanc
-      local dicom = RestApiGet('/instances/' .. instance .. '/file')
-
-      -- Create the subdirectory (CAUTION: For Linux demo only, this is insecure!)
-      -- http://stackoverflow.com/a/16029744/881731
-      os.execute('mkdir -p "' .. path .. '"')
-
-      -- Write to the file
-      local target = assert(io.open(path .. '/' .. instance .. '.dcm', 'wb'))
-      target:write(dicom)
-      target:close()
-   end
-end
--- a/Resources/Samples/OrthancFramework/MicroService/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,16 +0,0 @@
-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)
--- a/Resources/Samples/OrthancFramework/MicroService/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-This file shows how to create a simple Web service in C++ (similar to
-Python's Flask) using the Orthanc standalone framework.
--- a/Resources/Samples/OrthancFramework/MicroService/Sample.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-#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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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/>.
-
-
-
-URL = 'http://127.0.0.1:8042'
-
-#
-# This sample code will anonymize all the patients that are stored in
-# Orthanc.
-#
-
-import sys
-import RestToolbox
-
-# Loop over the patients
-for patient in RestToolbox.DoGet('%s/patients' % URL):
-
-    # Ignore patients whose name starts with "Anonymized", as it is
-    # the result of a previous anonymization
-    infos = RestToolbox.DoGet('%s/patients/%s' % (URL, patient))
-    name = infos['MainDicomTags']['PatientName'].lower()
-    if not name.startswith('anonymized'):
-
-        # Trigger the anonymization
-        RestToolbox.DoPost('%s/patients/%s/anonymize' % (URL, patient),
-                           { 'Keep' : [ 'SeriesDescription',
-                                        'StudyDescription' ] })
--- a/Resources/Samples/Python/ArchiveAllPatients.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 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://127.0.0.1:8042/ 20150101 20151231 /tmp/\n' % sys.argv[0])
-    exit(-1)
-
-def CheckIsDate(date):
-    if len(date) != 8 or not date.isdigit():
-        print '"%s" is not a valid date!\n' % date
-        exit(-1)
-
-
-if len(sys.argv) != 5:
-    PrintHelp()
-
-URL = sys.argv[1]
-START = sys.argv[2]
-END = sys.argv[3]
-TARGET = sys.argv[4]
-
-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
-    study = RestToolbox.DoGet('%s/studies/%s' % (URL, studyId))['MainDicomTags']
-
-    # Retrieve the DICOM tags of the parent patient of this study
-    patient = RestToolbox.DoGet('%s/studies/%s/patient' % (URL, studyId))['MainDicomTags']
-
-    # Check that the StudyDate tag lies within the given range
-    studyDate = study['StudyDate'][:8]
-    if studyDate >= START and studyDate <= END:
-        # Create a filename
-        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').translate(None, r"'\/:*?\"<>|!=").strip()
-
-        # Download the ZIP archive of the study
-        print('Downloading %s' % filename)
-        zipContent = RestToolbox.DoGet('%s/studies/%s/archive' % (URL, studyId))
-
-        # Write the ZIP archive at the proper location
-        with open(os.path.join(TARGET, filename), 'wb') as f:
-            f.write(zipContent)
--- a/Resources/Samples/Python/AutoClassify.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,129 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 argparse
-import time
-import os
-import os.path
-import sys
-import RestToolbox
-
-parser = argparse.ArgumentParser(
-    description = 'Automated classification of DICOM files from Orthanc.',
-    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
-
-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')
-parser.add_argument('--target', default = 'OrthancFiles',
-                    help = 'The target directory where to store the DICOM files')
-parser.add_argument('--all', action = 'store_true',
-                    help = 'Replay the entire history on startup (disabled by default)')
-parser.set_defaults(all = False)
-parser.add_argument('--remove', action = 'store_true',
-                    help = 'Remove DICOM files from Orthanc once classified (disabled by default)')
-parser.set_defaults(remove = False)
-
-
-def FixPath(p):
-    return p.encode('ascii', errors = 'replace').translate(None, r"'\/:*?\"<>|!=").strip()
-
-def GetTag(resource, tag):
-    if ('MainDicomTags' in resource and
-        tag in resource['MainDicomTags']):
-        return resource['MainDicomTags'][tag]
-    else:
-        return 'No' + tag
-
-def ClassifyInstance(instanceId):
-    # Extract the patient, study, series and instance information
-    instance = RestToolbox.DoGet('%s/instances/%s' % (URL, instanceId))
-    series = RestToolbox.DoGet('%s/series/%s' % (URL, instance['ParentSeries']))
-    study = RestToolbox.DoGet('%s/studies/%s' % (URL, series['ParentStudy']))
-    patient = RestToolbox.DoGet('%s/patients/%s' % (URL, study['ParentPatient']))
-
-    # Construct a target path
-    a = '%s - %s' % (GetTag(patient, 'PatientID'),
-                     GetTag(patient, 'PatientName'))
-    b = GetTag(study, 'StudyDescription')
-    c = '%s - %s' % (GetTag(series, 'Modality'),
-                     GetTag(series, 'SeriesDescription'))
-    d = '%s.dcm' % GetTag(instance, 'SOPInstanceUID')
-    
-    p = os.path.join(args.target, FixPath(a), FixPath(b), FixPath(c))
-    f = os.path.join(p, FixPath(d))
-
-    # Copy the DICOM file to the target path
-    print('Writing new DICOM file: %s' % f)
-    
-    try:
-        os.makedirs(p)
-    except:
-        # Already existing directory, ignore the error
-        pass
-    
-    dcm = RestToolbox.DoGet('%s/instances/%s/file' % (URL, instanceId))
-    with open(f, 'wb') as g:
-        g.write(dcm)
-
-
-# Parse the arguments
-args = parser.parse_args()
-URL = 'http://%s:%d' % (args.host, args.port)
-print('Connecting to Orthanc on address: %s' % URL)
-
-# Compute the starting point for the changes loop
-if args.all:
-    current = 0
-else:
-    current = RestToolbox.DoGet(URL + '/changes?last')['Last']
-
-# Polling loop using the 'changes' API of Orthanc, waiting for the
-# incoming of new DICOM files
-while True:
-    r = RestToolbox.DoGet(URL + '/changes', {
-            'since' : current,
-            'limit' : 4   # Retrieve at most 4 changes at once
-            })
-
-    for change in r['Changes']:
-        # We are only interested in the arrival of new instances
-        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'])
-
-    current = r['Last']
-
-    if r['Done']:
-        print('Everything has been processed: Waiting...')
-        time.sleep(1)
--- a/Resources/Samples/Python/ChangesLoop.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 time
-import sys
-import RestToolbox
-
-
-##
-## Print help message
-##
-
-if len(sys.argv) != 3:
-    print("""
-Sample script that continuously monitors the arrival of new DICOM
-images into Orthanc (through the Changes API).
-
-Usage: %s [hostname] [HTTP port]
-For instance: %s 127.0.0.1 8042
-""" % (sys.argv[0], sys.argv[0]))
-    exit(-1)
-
-URL = 'http://%s:%d' % (sys.argv[1], int(sys.argv[2]))
-
-
-
-##
-## The following function is called each time a new instance is
-## received.
-##
-
-def NewInstanceReceived(path):
-    global URL
-    patientName = RestToolbox.DoGet(URL + path + '/content/PatientName')
-    
-    # Remove the possible trailing characters due to DICOM padding
-    patientName = patientName.strip()
-
-    print('New instance received for patient "%s": "%s"' % (patientName, path))
-
-
-
-##
-## Main loop that listens to the changes API.
-## 
-
-current = 0
-while True:
-    r = RestToolbox.DoGet(URL + '/changes', {
-            'since' : current,
-            'limit' : 4   # Retrieve at most 4 changes at once
-            })
-
-    for change in r['Changes']:
-        # We are only interested in the arrival of new instances
-        if change['ChangeType'] == 'NewInstance':
-            # Call the callback function
-            path = change['Path']
-            NewInstanceReceived(path)
-
-            # Delete the instance once it has been discovered
-            RestToolbox.DoDelete(URL + path)
-
-    current = r['Last']
-
-    if r['Done']:
-        print('Everything has been processed: Waiting...')
-        time.sleep(1)
--- a/Resources/Samples/Python/ContinuousPatientAnonymization.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 time
-import sys
-import RestToolbox
-import md5
-
-
-##
-## Print help message
-##
-
-if len(sys.argv) != 3:
-    print("""
-Sample script that anonymizes patients in real-time. A patient gets
-anonymized as soon as she gets stable (i.e. when no DICOM instance has
-been received for this patient for a sufficient amount of time - cf.
-the configuration option "StableAge").
-
-Usage: %s [hostname] [HTTP port]
-For instance: %s 127.0.0.1 8042
-""" % (sys.argv[0], sys.argv[0]))
-    exit(-1)
-
-URL = 'http://%s:%d' % (sys.argv[1], int(sys.argv[2]))
-
-
-
-##
-## The following function is called whenever a patient gets stable
-##
-
-COUNT = 1
-
-def AnonymizePatient(path):
-    global URL
-    global COUNT
-
-    patient = RestToolbox.DoGet(URL + path)
-    patientID = patient['MainDicomTags']['PatientID']
-
-    # Ignore anonymized patients
-    if not 'AnonymizedFrom' in patient:
-        print('Patient with ID "%s" is stabilized: anonymizing it...' % (patientID))
-        
-        # The PatientID after anonymization is taken as the 8 first
-        # characters from the MD5 hash of the original PatientID
-        anonymizedID = md5.new(patientID).hexdigest()[:8]
-        anonymizedName = 'Anonymized patient %d' % COUNT
-        COUNT += 1
-
-        RestToolbox.DoPost(URL + path + '/anonymize',
-                           { 'Replace' : { 'PatientID' : anonymizedID,
-                                           'PatientName' : anonymizedName } })
-
-        # Delete the source patient after the anonymization
-        RestToolbox.DoDelete(URL + change['Path'])
-
-
-
-##
-## Main loop that listens to the changes API.
-## 
-
-current = 0
-while True:
-    r = RestToolbox.DoGet(URL + '/changes', {
-            'since' : current,
-            'limit' : 4   # Retrieve at most 4 changes at once
-            })
-
-    for change in r['Changes']:
-        if change['ChangeType'] == 'StablePatient':
-            AnonymizePatient(change['Path'])
-
-    current = r['Last']
-
-    if r['Done']:
-        print('Everything has been processed: Waiting...')
-        time.sleep(1)
--- a/Resources/Samples/Python/DeleteAllStudies.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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('Delete all the imaging studies that are stored in Orthanc\n')
-    print('Usage: %s <URL>\n' % sys.argv[0])
-    print('Example: %s http://127.0.0.1:8042/\n' % sys.argv[0])
-    exit(-1)
-
-if len(sys.argv) != 2:
-    PrintHelp()
-
-URL = sys.argv[1]
-
-for study in RestToolbox.DoGet('%s/studies' % URL):
-    print('Removing study: %s' % study)
-    RestToolbox.DoDelete('%s/studies/%s' % (URL, study))
--- a/Resources/Samples/Python/DownloadAnonymized.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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/>.
-
-
-
-URL = 'http://127.0.0.1:8042'
-
-#
-# This sample code will download a ZIP file for each patient that has
-# been anonymized in Orthanc.
-#
-
-import os
-import os.path
-import sys
-import RestToolbox
-
-# Loop over the patients
-for patient in RestToolbox.DoGet('%s/patients' % URL):
-
-    # Ignore patients whose name starts with "Anonymized", as it is
-    # the result of a previous anonymization
-    infos = RestToolbox.DoGet('%s/patients/%s' % (URL, patient))
-    name = infos['MainDicomTags']['PatientName'].lower()
-    if name.startswith('anonymized'):
-
-        # Trigger the download
-        print('Downloading %s' % name)
-        zipContent = RestToolbox.DoGet('%s/patients/%s/archive' % (URL, patient))
-        with open(os.path.join('/tmp', name + '.zip'), 'wb') as f:
-            f.write(zipContent)
--- a/Resources/Samples/Python/HighPerformanceAutoRouting.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,163 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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/>.
-
-
-
-URL = 'http://127.0.0.1:8042'
-TARGET = 'sample'
-
-
-#
-# This sample code shows how to setup a simple, high-performance DICOM
-# auto-routing. All the DICOM instances that arrive inside Orthanc
-# will be sent to a remote modality. A producer-consumer pattern is
-# used. The target modality is specified by the TARGET variable above:
-# It must match an entry in the Orthanc configuration file inside the
-# "DicomModalities" section.
-#
-# NOTE: This sample only works with Orthanc >= 0.5.2. Make sure that
-# Orthanc was built with "-DCMAKE_BUILD_TYPE=Release" to get the best
-# performance.
-#
-
-import Queue
-import sys
-import time
-import threading
-
-import RestToolbox
-
-
-#
-# Queue that is shared between the producer and the consumer
-# threads. It holds the instances that are still to be sent.
-#
-
-queue = Queue.Queue()
-
-
-#
-# The producer thread. It monitors the arrival of new instances into
-# Orthanc, and pushes their ID into the shared queue. This code is
-# based upon the "ChangesLoop.py" sample code.
-#
-
-def Producer(queue):
-    current = 0
-
-    while True:
-        r = RestToolbox.DoGet(URL + '/changes', {
-            'since' : current,
-            'limit' : 4   # Retrieve at most 4 changes at once
-            })
-
-        for change in r['Changes']:
-            # We are only interested in the arrival of new instances
-            if change['ChangeType'] == 'NewInstance':
-                queue.put(change['ID'])
-
-        current = r['Last']
-
-        if r['Done']:
-            time.sleep(1)
-
-
-#
-# The consumer thread. It continuously reads the instances from the
-# queue, and send them to the remote modality. Each time a packet of
-# instances is sent, a single DICOM connexion is used, hence improving
-# the performance.
-#
-
-def Consumer(queue):
-    TIMEOUT = 0.1
-    
-    while True:
-        instances = []
-
-        while True:
-            try:
-                # Block for a while, waiting for the arrival of a new
-                # instance
-                instance = queue.get(True, TIMEOUT)
-
-                # A new instance has arrived: Record its ID
-                instances.append(instance)
-                queue.task_done()
-
-            except Queue.Empty:
-                # Timeout: No more data was received
-                break
-
-        if len(instances) > 0:
-            print('Sending a packet of %d instances' % len(instances))
-            start = time.time()
-
-            # Send all the instances with a single DICOM connexion
-            RestToolbox.DoPost('%s/modalities/sample/store' % URL, instances)
-
-            # Remove all the instances from Orthanc
-            for instance in instances:
-                RestToolbox.DoDelete('%s/instances/%s' % (URL, instance))
-
-            # Clear the log of the exported instances (to prevent the
-            # SQLite database from growing indefinitely). More simply,
-            # you could also set the "LogExportedResources" option to
-            # "false" in the configuration file since Orthanc 0.8.3.
-            RestToolbox.DoDelete('%s/exports' % URL)
-
-            end = time.time()
-            print('The packet of %d instances has been sent in %d seconds' % (len(instances), end - start))
-
-
-#
-# Thread to display the progress
-#
-
-def PrintProgress(queue):
-    while True:
-        print('Current queue size: %d' % (queue.qsize()))
-        time.sleep(1)
-
-
-#
-# Start the various threads
-#
-
-progress = threading.Thread(None, PrintProgress, None, (queue, ))
-progress.daemon = True
-progress.start()
-
-producer = threading.Thread(None, Producer, None, (queue, ))
-producer.daemon = True
-producer.start()
-
-consumer = threading.Thread(None, Consumer, None, (queue, ))
-consumer.daemon = True
-consumer.start()
-
-
-#
-# Active waiting for Ctrl-C
-#
-
-while True:
-    time.sleep(0.1)
--- a/Resources/Samples/Python/ManualModification.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 base64
-import httplib2
-import json
-import re
-import sys
-
-URL_REGEX = re.compile('(http|https)://((.+?):(.+?)@|)(.*)')
-
-
-if len(sys.argv) != 3:
-    print("""
-Script to copy the content of one Orthanc server to another Orthanc
-server through their REST API.
-
-Usage: %s [SourceURI] [TargetURI]
-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)
-
-
-
-def CreateHeaders(parsedUrl):
-    headers = { }
-    username = parsedUrl.group(3)
-    password = parsedUrl.group(4)
-
-    if username != None and password != None:
-        # This is a custom reimplementation of the
-        # "Http.add_credentials()" method for Basic HTTP Access
-        # Authentication (for some weird reason, this method does not
-        # always work)
-        # http://en.wikipedia.org/wiki/Basic_access_authentication
-        headers['authorization'] = 'Basic ' + base64.b64encode(username + ':' + password)
-
-    return headers
-
-
-def GetBaseUrl(parsedUrl):
-    return '%s://%s' % (parsedUrl.group(1), parsedUrl.group(5))
-
-
-def DoGetString(url):
-    global URL_REGEX
-    parsedUrl = URL_REGEX.match(url)
-    headers = CreateHeaders(parsedUrl)
-
-    h = httplib2.Http()
-    resp, content = h.request(GetBaseUrl(parsedUrl), 'GET', headers = headers)
-
-    if resp.status == 200:
-        return content
-    else:
-        raise Exception('Unable to contact Orthanc at: ' + url)
-    
-
-def DoPostDicom(url, body):
-    global URL_REGEX
-    parsedUrl = URL_REGEX.match(url)
-    headers = CreateHeaders(parsedUrl)
-    headers['content-type'] = 'application/dicom'
-
-    h = httplib2.Http()
-    resp, content = h.request(GetBaseUrl(parsedUrl), 'POST',
-                              body = body,
-                              headers = headers)
-
-    if resp.status != 200:
-        raise Exception('Unable to contact Orthanc at: ' + url)
-    
-
-def _DecodeJson(s):
-    if (sys.version_info >= (3, 0)):
-        return json.loads(s.decode())
-    else:
-        return json.loads(s)
-
-
-def DoGetJson(url):
-    return _DecodeJson(DoGetString(url))
-
-
-SOURCE = sys.argv[1]
-TARGET = sys.argv[2]
-
-for study in DoGetJson('%s/studies' % SOURCE):
-    print('Sending study %s...' % study)
-    for instance in DoGetJson('%s/studies/%s/instances' % (SOURCE, study)):
-        dicom = DoGetString('%s/instances/%s/file' % (SOURCE, instance['ID']))
-        DoPostDicom('%s/instances' % TARGET, dicom)
--- a/Resources/Samples/Python/RestToolbox.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-# Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
-# Department, University Hospital of Liege, Belgium
-# Copyright (C) 2017-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the 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 httplib2
-import json
-import sys
-
-if (sys.version_info >= (3, 0)):
-    from urllib.parse import urlencode
-else:
-    from urllib import urlencode
-
-
-_credentials = None
-
-
-def _DecodeJson(s):
-    try:
-        if (sys.version_info >= (3, 0)):
-            return json.loads(s.decode())
-        else:
-            return json.loads(s)
-    except:
-        return s
-
-
-def SetCredentials(username, password):
-    global _credentials
-    _credentials = (username, password)
-
-def _SetupCredentials(h):
-    global _credentials
-    if _credentials != None:
-        h.add_credentials(_credentials[0], _credentials[1])
-
-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(_ComputeGetUri(uri, data), 'GET')
-    if not (resp.status in [ 200 ]):
-        raise Exception(resp.status)
-    elif not interpretAsJson:
-        return content.decode()
-    else:
-        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)
-
-    if isinstance(data, str):
-        body = data
-        if len(contentType) != 0:
-            headers = { 'content-type' : contentType }
-        else:
-            headers = { 'content-type' : 'text/plain' }
-    else:
-        body = json.dumps(data)
-        headers = { 'content-type' : 'application/json' }
-    
-    resp, content = h.request(
-        uri, method,
-        body = body,
-        headers = headers)
-
-    if not (resp.status in [ 200, 302 ]):
-        raise Exception(resp.status)
-    else:
-        return _DecodeJson(content)
-
-
-def DoDelete(uri):
-    h = httplib2.Http()
-    _SetupCredentials(h)
-    resp, content = h.request(uri, 'DELETE')
-
-    if not (resp.status in [ 200 ]):
-        raise Exception(resp.status)
-    else:
-        return _DecodeJson(content)
-
-
-def DoPut(uri, data = {}, contentType = ''):
-    return _DoPutOrPost(uri, 'PUT', data, contentType)
-
-
-def DoPost(uri, data = {}, contentType = ''):
-    return _DoPutOrPost(uri, 'POST', data, contentType)
--- a/Resources/Samples/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-More contributed samples and documentation can be found and added in
-the "OrthancContributed" repository on GitHub:
-https://github.com/jodogne/OrthancContributed
-
-The integration tests of Orthanc provide many samples about the
-features of the REST API of Orthanc:
-https://hg.orthanc-server.com/orthanc-tests/file/tip/Tests/Tests.py
--- a/Resources/Samples/Tools/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(OrthancTools)
-
-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(pthread dl)
-endif()
-
-include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkParameters.cmake)
-
-set(STATIC_BUILD ON)
-set(ALLOW_DOWNLOADS ON)
-
-include(${CMAKE_SOURCE_DIR}/../../CMake/OrthancFrameworkConfiguration.cmake)
-
-add_library(CommonLibraries
-  ${BOOST_SOURCES}
-  ${JSONCPP_SOURCES}
-  ${ORTHANC_ROOT}/Core/Enumerations.cpp
-  ${ORTHANC_ROOT}/Core/Logging.cpp
-  ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
-  ${ORTHANC_ROOT}/Core/Toolbox.cpp
-  ${ORTHANC_ROOT}/Resources/ThirdParty/md5/md5.c
-  ${ORTHANC_ROOT}/Resources/ThirdParty/base64/base64.cpp
-  )
-
-add_executable(RecoverCompressedFile
-  RecoverCompressedFile.cpp
-  ${ORTHANC_ROOT}/Core/Compression/DeflateBaseCompressor.cpp
-  ${ORTHANC_ROOT}/Core/Compression/ZlibCompressor.cpp
-  ${ZLIB_SOURCES}
-  )
-
-target_link_libraries(RecoverCompressedFile CommonLibraries)
--- a/Resources/Samples/Tools/RecoverCompressedFile.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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 "../../../Core/Compression/ZlibCompressor.h"
-#include "../../../Core/SystemToolbox.h"
-#include "../../../Core/OrthancException.h"
-
-#include <stdio.h>
-
-int main(int argc, const char* argv[])
-{
-  if (argc != 2 && argc != 3)
-  {
-    fprintf(stderr, "Maintenance tool to recover a DICOM file that was compressed by Orthanc.\n\n");
-    fprintf(stderr, "Usage: %s <input> [output]\n", argv[0]);
-    fprintf(stderr, "If \"output\" is not given, the data will be output to stdout\n");
-    return -1;
-  }
-
-  try
-  {
-    fprintf(stderr, "Reading the file into memory...\n");
-    fflush(stderr);
-
-    std::string content;
-    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.empty() ? NULL : content.c_str(), 
-                          content.size());
-
-    fprintf(stderr, "Writing the uncompressed data...\n");
-    fflush(stderr);
-
-    if (argc == 3)
-    {
-      Orthanc::SystemToolbox::WriteFile(uncompressed, argv[2]);
-    }
-    else
-    {
-      if (uncompressed.size() > 0)
-      {
-        fwrite(&uncompressed[0], uncompressed.size(), 1, stdout);
-      }
-    }
-
-    fprintf(stderr, "Done!\n");
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    fprintf(stderr, "Error: %s\n", e.What());
-    return -1;
-  }
-
-  return 0;
-}
--- a/Resources/Samples/WebApplications/DrawingDicomizer.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,102 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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/>.
- **/
-
-
-
-/**
- * Parameters of the HTTP server.
- **/
-
-var orthanc = { 
-  host: 'localhost',
-  port: 8042 
-};
-
-var port = 8000;
-
-
-
-/**
- * The Web application.
- **/
-
-var http = require('http');
-var querystring = require('querystring');
-var toolbox = require('./NodeToolbox.js');
-
-var server = http.createServer(function(req, response) {
-  switch (req.method)
-  {
-  case 'GET':
-    {
-      if (req.url == '/') {
-        toolbox.Redirect('/index.html', response);
-      }
-      else if (req.url == '/index.html') {
-        toolbox.ServeFile('DrawingDicomizer/index.html', response);
-      }
-      else if (req.url == '/drawing.js') {
-        toolbox.ServeFile('DrawingDicomizer/drawing.js', response);
-      }
-      else if (req.url == '/orthanc.js') {
-        toolbox.ServeFile('DrawingDicomizer/orthanc.js', response);
-      }
-      else if (req.url == '/jquery.js') {
-        toolbox.ServeFile('../../../OrthancExplorer/libs/jquery.min.js', response);
-      }
-      else if (req.url.startsWith('/orthanc')) {
-        toolbox.ForwardGetRequest(orthanc, req.url.substr(8), response);
-      }
-      else {
-        toolbox.NotFound(response);
-      }
-
-      break;
-    }
-
-  case 'POST':
-    {
-      var body = '';
-
-      req.on('data', function (data) {
-        body += data;
-      });
-
-      req.on('end', function () {
-        if (req.url == '/orthanc/tools/create-dicom') {
-          body = JSON.stringify(querystring.parse(body));
-          toolbox.ForwardPostRequest(orthanc, '/tools/create-dicom', body, response);
-        }
-        else {
-          toolbox.NotFound(response);
-        }
-      });
-
-      break;
-    }
-
-  default:
-    toolbox.NotFound(response);
-  }
-});
-
-
-console.log('The demo is running at http://localhost:' + port + '/');
-server.listen(port);
--- a/Resources/Samples/WebApplications/DrawingDicomizer/drawing.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-/**
- * Copyright 2010 William Malone (www.williammalone.com)
- * 
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * 
- *   http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- **/
-
-
-/**
- * This code comes from the blog entry "Create a Drawing App with
- * HTML5 Canvas and JavaScript" by William Malone. It is the "simple
- * demo" of a pure HTML5 drawing application.
- *
- * http://www.williammalone.com/articles/create-html5-canvas-javascript-drawing-app/
- *
- * To keep this sample code as simple as possible, we do not implement
- * hacks for the canvas of Microsoft Internet Explorer.
- **/
-
-
-if ($.browser.msie) {
-  alert('Please use Mozilla Firefox or Google Chrome. Microsoft Internet Explorer is not supported.');
-}
-
-
-var context;
-var clickX = new Array();
-var clickY = new Array();
-var clickDrag = new Array();
-var paint;
-
-
-function addClick(x, y, dragging)
-{
-  clickX.push(x);
-  clickY.push(y);
-  clickDrag.push(dragging);
-}
-
-
-function Redraw() 
-{
-  context.fillStyle = '#ffffff';
-  context.fillRect(0, 0, context.canvas.width, context.canvas.height); // Clears the canvas
-  
-  context.strokeStyle = '#df4b26';
-  context.lineJoin = 'round';
-  context.lineWidth = 5;
-  
-  for (var i=0; i < clickX.length; i++) {		
-    context.beginPath();
-    if (clickDrag[i] && i) {
-      context.moveTo(clickX[i - 1], clickY[i - 1]);
-    } else {
-      context.moveTo(clickX[i] - 1, clickY[i]);
-    }
-    context.lineTo(clickX[i], clickY[i]);
-    context.closePath();
-    context.stroke();
-  }
-}
-
-
-$(document).ready(function() {
-  context = document.getElementById('canvas').getContext('2d');
-  Redraw();
-
-  $('#canvas').mousedown(function(e) {
-    var mouseX = e.pageX - this.offsetLeft;
-    var mouseY = e.pageY - this.offsetTop;
-    
-    paint = true;
-    addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop);
-    Redraw();
-  });
-
-  $('#canvas').mousemove(function(e) {
-    if(paint) {
-      addClick(e.pageX - this.offsetLeft, e.pageY - this.offsetTop, true);
-      Redraw();
-    }
-  });
-
-  $('#canvas').mouseup(function(e) {
-    paint = false;
-  });
-
-  $('#canvas').mouseleave(function(e) {
-    paint = false;
-  });
-});
--- a/Resources/Samples/WebApplications/DrawingDicomizer/index.html	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>HTML5 Drawing Dicomizer</title>
-    <style media="screen" type="text/css">
-      canvas {
-        border: 1px inset brown;
-      }
-    </style>
-    <script src="jquery.js"></script>
-    <script src="drawing.js"></script>
-    <script src="orthanc.js"></script>
-  </head>
-  <body>
-    <canvas id="canvas" width="490" height="220"></canvas>
-    <p>
-      Patient ID: <input type="text" id="patientID"></input>
-    </p>
-    <p>
-      Patient Name: <input type="text" id="patientName" value="HELLO^WORLD"></input>
-    </p>
-    <p>
-      Study Description: <input type="text" id="studyDescription" value="My Study"></input>
-    </p>
-    <p>
-      Series Description: <input type="text" id="seriesDescription" value="My Series"></input>
-    </p>
-    <p>
-      <button id="submit">Submit</button>
-    </p>
-  </body>
-</html>
--- a/Resources/Samples/WebApplications/DrawingDicomizer/orthanc.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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/>.
- **/
-
-
-function guid4Block() {
-  return Math.floor((1 + Math.random()) * 0x10000)
-    .toString(16)
-    .substring(1);
-}
- 
-function guid() {
-  return (guid4Block() + guid4Block() + '-' + guid4Block() + '-' + guid4Block() + '-' +
-          guid4Block() + '-' + guid4Block() + guid4Block() + guid4Block());
-}
-
-
-$(document).ready(function() {
-  $('#patientID').val(guid());
-
-  $('#submit').click(function(event) {
-    var png = context.canvas.toDataURL();
-
-    $.ajax({
-      type: 'POST',
-      url: '/orthanc/tools/create-dicom',
-      dataType: 'text',
-      data: { 
-        PatientID: $('#patientID').val(),
-        PatientName: $('#patientName').val(),
-        StudyDescription: $('#studyDescription').val(),
-        SeriesDescription: $('#seriesDescription').val(),
-        PixelData: png,
-        Modality: 'RX'
-      },
-      success : function(msg) {
-        alert('Your drawing has been DICOM-ized!\n\n' + msg);
-      },
-      error : function() {
-        alert('Error while DICOM-izing the drawing');
-      }
-    });
-
-    return false;
-  });
-});
--- a/Resources/Samples/WebApplications/NodeToolbox.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,119 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the 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/>.
- **/
-
-
-var fs = require('fs');
-var http = require('http');
-
-
-function ForwardGetRequest(orthanc, path, res) {
-  var opts = orthanc;
-  opts.path = path;
-  opts.method = 'GET';
-
-  http.get(opts, function(response) {
-    if (response.statusCode == 200) {
-      response.setEncoding('utf-8');
-      response.on('data', function(chunk) {
-        res.write(chunk);
-      });
-      response.on('end', function() {
-        res.end();
-      });
-    } else {
-      console.log('Got error on GET forwarding: ' + 
-                  response.statusCode + ' (' + path + ')');
-      res.writeHead(response.statusCode);
-      res.end();
-    }
-  }).on('error', function(e) {
-    console.log('Unable to contact Orthanc: ' + e.message);
-    res.writeHead(503);  // Service Unavailable
-    res.end();
-  });
-}
-
-
-function ForwardPostRequest(orthanc, path, body, res) {
-  var opts = orthanc;
-  opts.path = path;
-  opts.method = 'POST';
-  opts.headers = {
-    'Content-Length': body.length
-  }
-
-  var req = http.request(opts, function(response) {
-    if (response.statusCode == 200) {
-      response.setEncoding('utf-8');
-      response.on('data', function(chunk) {
-        res.write(chunk);
-      });
-      response.on('end', function() {
-        res.end();
-      });
-    } else {
-      console.log('Got error on POST forwarding: ' + 
-                  response.statusCode + ' (' + path + ')');
-      res.writeHead(response.statusCode);
-      res.end();
-    }
-  }).on('error', function(e) {
-    console.log('Unable to contact Orthanc: ' + e.message);
-    res.writeHead(503);  // Service Unavailable
-    res.end();
-  });
-
-  req.write(body);
-  req.end();
-}
-
-
-function ServeFile(filename, res) {
-  fs.readFile(filename, function(r, c) {
-    res.end(c.toString());
-  });
-}
-
-
-function NotFound(res) {
-  res.writeHead(404, {'Content-Type': 'text/plain'});
-  res.end();
-}
-
-
-function Redirect(path, res) {
-  res.writeHead(301, {
-    'Content-Type': 'text/plain',
-    'Location': path
-  });
-  res.end();
-}
-
-
-String.prototype.startsWith = function(prefix) {
-  return this.indexOf(prefix) === 0;
-}
-
-
-module.exports.ForwardGetRequest = ForwardGetRequest;
-module.exports.ForwardPostRequest = ForwardPostRequest;
-module.exports.NotFound = NotFound;
-module.exports.Redirect = Redirect;
-module.exports.ServeFile = ServeFile;
--- a/Resources/Samples/WebApplications/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-===================
-GENERAL INFORMATION
-===================
-
-This folder contains sample Web applications.
-
-These Web applications make use of NodeJs (http://nodejs.org/). To run
-the applications, you therefore need to install NodeJs on your
-computer. NodeJs acts here as a lightweight, cross-platform Web server
-that statically serves the HTML/JavaScript files and that dynamically
-serves the Orthanc REST API as a reverse proxy (to avoid cross-domain
-problems with AJAX).
-
-Once NodeJs is installed, start Orthanc with default parameters
-(i.e. HTTP port set to 8042), start NodeJs with the sample application
-you are interested in (e.g. "node DrawingDicomizer.js"). Then, open
-http://localhost:8000/ with a standard Web browser to try the sample
-application.
-
-
-
-=======================================
-DRAWING DICOMIZER (DrawingDicomizer.js)
-=======================================
-
-This sample shows how to convert the content of a HTML5 canvas as a
-DICOM file, using a single AJAX request to Orthanc.
-
-Internally, the content of the HTML5 canvas is serialized through the
-standard "toDataURL()" method of the canvas object. This returns a
-string containing the PNG image encoded using the Data URI Scheme
-(http://en.wikipedia.org/wiki/Data_URI_scheme). Such a string is then
-sent to Orthanc using the '/tools/create-dicom' REST call, that
-transparently decompresses the PNG image into a DICOM image.
--- a/Resources/Testing/Issue32/Cpp/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-
-project(Orthanc)
-
-set(ORTHANC_ROOT ${CMAKE_SOURCE_DIR}/../../../../Resources/CMake)
-
-include(${ORTHANC_ROOT}/OrthancFrameworkParameters.cmake)
-set(ENABLE_WEB_CLIENT ON)
-include(${ORTHANC_ROOT}/OrthancFrameworkConfiguration.cmake)
-
-include_directories(${ORTHANC_ROOT})
-
-add_executable(Sample
-  main.cpp
-  ${ORTHANC_CORE_SOURCES}
-  ${AUTOGENERATED_SOURCES}
-  )
--- a/Resources/Testing/Issue32/Cpp/main.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-#include <Core/HttpClient.h>
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-#include <Core/SystemToolbox.h>
-
-#include <iostream>
-#include <boost/thread.hpp>
-
-static void Worker(bool *done)
-{
-  LOG(WARNING) << "One thread has started";
-
-  Orthanc::HttpClient client;
-  //client.SetUrl("http://localhost:8042/studies");
-  //client.SetUrl("http://localhost:8042/tools/default-encoding");
-  client.SetUrl("http://localhost:8042/system");
-  //client.SetUrl("http://localhost:8042/");
-  //client.SetCredentials("orthanc", "orthanc");
-  client.SetRedirectionFollowed(false);
-  
-  while (!(*done))
-  {
-    try
-    {
-#if 0
-      Json::Value v;
-      if (!client.Apply(v) ||
-          v.type() != Json::objectValue)
-      {
-        printf("ERROR\n");
-      }
-#else
-      std::string s;
-      if (!client.Apply(s) ||
-          s.empty())
-      {
-        printf("ERROR\n");
-      }
-#endif
-    }
-    catch (Orthanc::OrthancException& e)
-    {
-      printf("EXCEPTION: %s", e.What());
-    }
-  }
-
-  LOG(WARNING) << "One thread has stopped";
-}
-
-int main()
-{
-  Orthanc::Logging::Initialize();
-  //Orthanc::Logging::EnableInfoLevel(true);
-  Orthanc::HttpClient::GlobalInitialize();
-
-  {
-    bool done = false;
-
-    std::vector<boost::thread*> threads;
-
-    for (size_t i = 0; i < 100; i++)
-    {
-      threads.push_back(new boost::thread(Worker, &done));
-    }
-
-    LOG(WARNING) << "STARTED";
-    Orthanc::SystemToolbox::ServerBarrier();
-    LOG(WARNING) << "STOPPING";
-
-    done = true;
-
-    for (size_t i = 0; i < threads.size(); i++)
-    {
-      if (threads[i]->joinable())
-      {
-        threads[i]->join();
-      }
-
-      delete threads[i];
-    }
-  }
-  
-  Orthanc::HttpClient::GlobalFinalize();
-  printf("OK\n");
-  return 0;
-}
--- a/Resources/Testing/Issue32/Java/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-$ sudo apt-get install maven
-$ mvn test
--- a/Resources/Testing/Issue32/Java/pom.xml	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>io.osimis</groupId>
-  <artifactId>issue32</artifactId>
-  <version>1.0-SNAPSHOT</version>
-
-  <name>issue32</name>
-  <!-- FIXME change it to the project's website -->
-  <url>http://www.example.com</url>
-
-  <properties>
-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    <maven.compiler.source>1.8</maven.compiler.source>
-    <maven.compiler.target>1.8</maven.compiler.target>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>org.apache.httpcomponents</groupId>
-      <artifactId>httpclient</artifactId>
-      <version>4.5.3</version>
-    </dependency>
-
-    <dependency>
-      <groupId>junit</groupId>
-      <artifactId>junit</artifactId>
-      <version>4.11</version>
-      <scope>test</scope>
-    </dependency>
-  </dependencies>
-</project>
--- a/Resources/Testing/Issue32/Java/src/test/java/io/osimis/AppTest.java	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-package io.osimis;
-
-import java.io.IOException;
-import java.util.Base64;
-import org.apache.http.HttpEntity;
-import org.apache.http.client.HttpRequestRetryHandler;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
-import org.apache.http.util.EntityUtils;
-
-import static org.junit.Assert.assertTrue;
-
-import org.junit.Test;
-
-public class AppTest 
-{
-  @Test
-  public void testKeepAlive()
-  {
-    PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
-
-    CloseableHttpClient client = HttpClients
-      .custom()
-      .setConnectionManager(cm)
-      .setRetryHandler((HttpRequestRetryHandler) (exception, executionCount, context) -> {
-          System.out.println("ERROR");
-          assertTrue(false);
-          return false;
-        }).build();
-
-    HttpRequestBase request = new HttpGet("http://localhost:8042/system");
-
-    // Low-level handling of HTTP basic authentication (for integration tests)
-    request.addHeader("Authorization", "Basic " +
-                      Base64.getEncoder().encodeToString("alice:orthanctest".getBytes()));
-
-    // The following call works
-    //HttpRequestBase request = new HttpGet("https://api.ipify.org?format=json");
-    
-    for (int i = 0; i < 5; i++) {
-      System.out.println("================================");
-      try (CloseableHttpResponse httpResponse = client.execute(request)) {
-        String responseContent = null;
-
-        HttpEntity entity = httpResponse.getEntity();
-        if (entity != null) {
-          responseContent = EntityUtils.toString(entity);
-        }
-
-        System.out.println(httpResponse.getStatusLine().getStatusCode());
-        System.out.println(responseContent);
-
-        EntityUtils.consume(entity);
-        httpResponse.close();
-      } catch (IOException e) {
-        System.out.println("Request error " + e);
-      }
-    }
-      
-    assertTrue(true);
-  }
-}
--- a/Resources/ThirdParty/VisualStudio/stdint.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,259 +0,0 @@
-// 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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,179 +0,0 @@
-/* 
-   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
-
-   ------------------------------
-   This version has been modified (changed the interface + use another decoding algorithm
-   inspired from https://stackoverflow.com/a/34571089 which was faster)
-*/
-
-#include "base64.h"
-#include <string.h>
-#include <vector>
-
-static const std::string base64_chars = 
-    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-    "abcdefghijklmnopqrstuvwxyz"
-    "0123456789+/";
-
-static inline bool is_base64(unsigned char c) {
-  return (isalnum(c) || (c == '+') || (c == '/'));
-}
-
-void base64_encode(std::string& result, const std::string& stringToEncode)
-{
-  const unsigned char* bytes_to_encode = reinterpret_cast<const unsigned char*>
-      (stringToEncode.size() > 0 ? &stringToEncode[0] : NULL);
-  size_t in_len = stringToEncode.size();
-  
-  result.reserve(result.size() + in_len * 4 / 3 + 10);
-
-  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++)
-        result += 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++)
-      result += base64_chars[char_array_4[j]];
-
-    while((i++ < 3))
-      result += '=';
-
-  }
-}
-
-// old code from Ren Nyffenegger.  This code is slower
-void base64_decode_old(std::string& result, const std::string& encoded_string) {
-  size_t in_len = encoded_string.size();
-  int i = 0;
-  int j = 0;
-  int in_ = 0;
-  unsigned char char_array_4[4], char_array_3[3];
-
-  result.reserve(result.size() + in_len * 3 / 4 + 10);
-
-  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++)
-        result += 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++)
-      result += char_array_3[j];
-  }
-}
-
-
-// new code from https://stackoverflow.com/a/34571089
-// note that the encoding algorithm from this page was slower (and bugged !)
-// this code is not using std::vector::find
-
-// static init equivalent to:
-// decode_indexes.assign(256, -1);
-// for (int i=0; i<64; ++i)
-//   decode_indexes[base64_chars[i]] = i;
-
-static const int decode_indexes[] = {
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
-  52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,
-  -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
-  15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
-  -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
-  41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-  -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
-};
-
-
-void base64_decode(std::string& result, const std::string &stringToDecode) {
-
-  result.reserve(result.size() + stringToDecode.size() * 3 / 4 + 10);
-
-  int val=0, valb=-8;
-  for (std::string::const_iterator c = stringToDecode.begin(); c != stringToDecode.end(); ++c)
-  {
-    size_t index = static_cast<size_t>(*c);
-    if (decode_indexes[index] == -1)
-      break;
-    val = (val<<6) + decode_indexes[index];
-    valb += 6;
-    if (valb>=0) {
-      result.push_back(char((val>>valb)&0xFF));
-      valb-=8;
-    }
-  }
-}
--- a/Resources/ThirdParty/base64/base64.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-#include <string>
-
-void base64_encode(std::string& result, const std::string& stringToEncode);
-void base64_decode(std::string& result, const std::string& s);
--- a/Resources/ThirdParty/icu/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-cmake_minimum_required(VERSION 2.8)
-project(IcuCodeGeneration)
-
-set(USE_LEGACY_LIBICU OFF CACHE BOOL "Use icu icu4c-58_2, latest version not requiring a C++11 compiler (for LSB and old versions of Visual Studio)")
-
-if (NOT USE_LEGACY_LIBICU)
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
-endif()
-
-include(${CMAKE_SOURCE_DIR}/../../CMake/Compiler.cmake)
-include(${CMAKE_SOURCE_DIR}/../../CMake/DownloadPackage.cmake)
-include(Version.cmake)
-
-set(SOURCE_DATA
-  "${LIBICU_SOURCES_DIR}/source/data/in/${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat")
-
-set(ALLOW_DOWNLOADS ON)
-DownloadPackage(${LIBICU_MD5} ${LIBICU_URL} "${LIBICU_SOURCES_DIR}")
-
-include_directories(
-  ${LIBICU_SOURCES_DIR}/source/common
-  ${LIBICU_SOURCES_DIR}/source/i18n
-  ${LIBICU_SOURCES_DIR}/source/tools/toolutil/
-  )
-
-aux_source_directory(${LIBICU_SOURCES_DIR}/source/common         LIBICU_SOURCES)
-aux_source_directory(${LIBICU_SOURCES_DIR}/source/i18n           LIBICU_SOURCES)
-aux_source_directory(${LIBICU_SOURCES_DIR}/source/tools/toolutil LIBICU_SOURCES)
-
-if (USE_LEGACY_LIBICU)
-  list(APPEND LIBICU_SOURCES
-    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.c
-    )
-else()
-  list(APPEND LIBICU_SOURCES
-    ${LIBICU_SOURCES_DIR}/source/stubdata/stubdata.cpp
-    )
-  set_source_files_properties(
-    ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
-    PROPERTIES COMPILE_DEFINITIONS "char16_t=uint16_t"
-    )
-endif()
-
-
-
-add_executable(IcuCodeGeneration
-  ${LIBICU_SOURCES_DIR}/source/tools/genccode/genccode.c
-  ${LIBICU_SOURCES}
-  )
-
-configure_file(${SOURCE_DATA}
-  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
-  COPYONLY)
-
-add_custom_command(
-  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}
-  COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat
-  DEPENDS  IcuCodeGeneration
-  )
-
-# "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
-add_custom_command(
-  OUTPUT   ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
-  COMMAND  gzip ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.c --no-name -c > ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
-  DEPENDS  ${LIBICU_DATA}
-  )
-
-
-if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
-  # Generate a precompiled version for Visual Studio 64bit
-  set(TMP_ASSEMBLER      ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat.S)
-  set(TMP_OBJECT         ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.o)
-  set(TMP_LIBRARY        ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.lib)
-  set(PRECOMPILED_WIN64  ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}_dat-x86_64-mingw32.lib.gz)
-  
-  add_custom_command(
-    OUTPUT   ${TMP_ASSEMBLER}
-    COMMAND  IcuCodeGeneration ${CMAKE_BINARY_DIR}/${LIBICU_DATA_VERSION}.dat --assembly gcc-mingw64
-    DEPENDS  IcuCodeGeneration
-    )
-
-  add_custom_command(
-    OUTPUT   ${TMP_OBJECT}
-    COMMAND  x86_64-w64-mingw32-gcc -c ${TMP_ASSEMBLER} -o ${TMP_OBJECT}
-    DEPENDS  ${TMP_ASSEMBLER}
-    )
-
-  add_custom_command(
-    OUTPUT   ${TMP_LIBRARY}
-    COMMAND  x86_64-w64-mingw32-ar qc ${TMP_LIBRARY} ${TMP_OBJECT}
-    COMMAND  x86_64-w64-mingw32-ranlib ${TMP_LIBRARY}
-    DEPENDS  ${TMP_OBJECT}
-    )
-
-  # "--no-name" is necessary for 7-zip on Windows to behave similarly to gunzip
-  add_custom_command(
-    OUTPUT   ${PRECOMPILED_WIN64}
-    COMMAND  gzip ${TMP_LIBRARY} --no-name -c > ${PRECOMPILED_WIN64}
-    DEPENDS  ${TMP_LIBRARY}
-    )
-endif()
-
-
-add_custom_target(Final ALL DEPENDS
-  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
-  ${PRECOMPILED_WIN64}
-  )
-
-install(
-  FILES
-  ${CMAKE_BINARY_DIR}/${LIBICU_DATA}.gz
-  ${PRECOMPILED_WIN64}
-  DESTINATION ${CMAKE_SOURCE_DIR}/../../../ThirdPartyDownloads
-  )
-
-add_definitions(
-  #-DU_COMBINED_IMPLEMENTATION
-  -DUCONFIG_NO_SERVICE=1
-  -DU_COMMON_IMPLEMENTATION
-  -DU_ENABLE_DYLOAD=0
-  -DU_HAVE_STD_STRING=1
-  -DU_I18N_IMPLEMENTATION
-  -DU_IO_IMPLEMENTATION
-  -DU_STATIC_IMPLEMENTATION=1
-  -DU_TOOLUTIL_IMPLEMENTATION
-  )
--- a/Resources/ThirdParty/icu/README.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-Generating ICU data file
-========================
-
-This folder generates the "icudtXXX_dat.c" file that contains the
-resources internal to ICU.
-
-IMPORTANT: Since ICU 59, C++11 is mandatory, making it incompatible
-with Linux Standard Base (LSB) SDK. The option
-"-DUSE_LEGACY_LIBICU=ON" will use the latest version of ICU that does
-not use C++11 (58-2).
-
-
-Usage
------
-
-Newest release of icu:
-
-$ cmake .. -G Ninja && ninja install
-
-Legacy version suitable for LSB:
-
-$ cmake .. -G Ninja -DUSE_LEGACY_LIBICU=ON && ninja install
-
-Legacy version, compiled using LSB:
-
-$ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake .. -G Ninja \
-  -DCMAKE_TOOLCHAIN_FILE=../../../LinuxStandardBaseToolchain.cmake \
-  -DUSE_LEGACY_LIBICU=ON
-$ ninja install
-
-
-Result
-------
-
-The resulting files are placed in the "ThirdPartyDownloads" folder at
-the root of the Orthanc repository (next to the main "CMakeLists.txt").
--- a/Resources/ThirdParty/icu/Version.cmake	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-# NB: Orthanc assume that the platform is of ASCII-family, not of
-# EBCDIC-family. A "e" suffix would be needed on EBCDIC. Look for
-# macro "U_ICUDATA_TYPE_LETTER" in the source code of icu for more
-# information.
-
-include(TestBigEndian)
-TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
-if(IS_BIG_ENDIAN)
-  set(LIBICU_SUFFIX "b")
-else()
-  set(LIBICU_SUFFIX "l")
-endif()
-
-set(LIBICU_BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads")
-
-if (USE_LEGACY_LIBICU)
-  # This is the latest version of icu that compiles without C++11
-  # support. It is used for Linux Standard Base and Visual Studio 2008.
-  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-58_2-src.tgz")
-  set(LIBICU_MD5 "fac212b32b7ec7ab007a12dff1f3aea1")
-  set(LIBICU_DATA_VERSION "icudt58")
-  set(LIBICU_DATA_COMPRESSED_MD5 "a39b07b38195158c6c3070332cef2173")
-  set(LIBICU_DATA_UNCOMPRESSED_MD5 "54d2593cec5c6a4469373231658153ce")
-else()
-  set(LIBICU_URL "${LIBICU_BASE_URL}/icu4c-63_1-src.tgz")
-  set(LIBICU_MD5 "9e40f6055294284df958200e308bce50")
-  set(LIBICU_DATA_VERSION "icudt63")
-  set(LIBICU_DATA_COMPRESSED_MD5 "be495c0830de5f377fdfa8301a5faf3d")
-  set(LIBICU_DATA_UNCOMPRESSED_MD5 "99613c3f2ca9426c45dc554ad28cfb79")
-endif()
-
-set(LIBICU_SOURCES_DIR ${CMAKE_BINARY_DIR}/icu)
-set(LIBICU_DATA "${LIBICU_DATA_VERSION}${LIBICU_SUFFIX}.dat.gz")
-set(LIBICU_DATA_URL "${LIBICU_BASE_URL}/${LIBICU_DATA}")
--- a/Resources/ThirdParty/md5/md5.c	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,381 +0,0 @@
-/*
-  Copyright (C) 1999, 2000, 2002 Aladdin Enterprises.  All rights reserved.
-
-  This software is provided 'as-is', without any express or implied
-  warranty.  In no event will the authors be held liable for any damages
-  arising from the use of this software.
-
-  Permission is granted to anyone to use this software for any purpose,
-  including commercial applications, and to alter it and redistribute it
-  freely, subject to the following restrictions:
-
-  1. The origin of this software must not be misrepresented; you must not
-     claim that you wrote the original software. If you use this software
-     in a product, an acknowledgment in the product documentation would be
-     appreciated but is not required.
-  2. Altered source versions must be plainly marked as such, and must not be
-     misrepresented as being the original software.
-  3. This notice may not be removed or altered from any source distribution.
-
-  L. Peter Deutsch
-  ghost@aladdin.com
-
- */
-/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */
-/*
-  Independent implementation of MD5 (RFC 1321).
-
-  This code implements the MD5 Algorithm defined in RFC 1321, whose
-  text is available at
-	http://www.ietf.org/rfc/rfc1321.txt
-  The code is derived from the text of the RFC, including the test suite
-  (section A.5) but excluding the rest of Appendix A.  It does not include
-  any code or documentation that is identified in the RFC as being
-  copyrighted.
-
-  The original and principal author of md5.c is L. Peter Deutsch
-  <ghost@aladdin.com>.  Other authors are noted in the change history
-  that follows (in reverse chronological order):
-
-  2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order
-	either statically or dynamically; added missing #include <string.h>
-	in library.
-  2002-03-11 lpd Corrected argument list for main(), and added int return
-	type, in test program and T value program.
-  2002-02-21 lpd Added missing #include <stdio.h> in test program.
-  2000-07-03 lpd Patched to eliminate warnings about "constant is
-	unsigned in ANSI C, signed in traditional"; made test program
-	self-checking.
-  1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
-  1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5).
-  1999-05-03 lpd Original version.
- */
-
-#include "md5.h"
-#include <string.h>
-
-#undef BYTE_ORDER	/* 1 = big-endian, -1 = little-endian, 0 = unknown */
-#ifdef ARCH_IS_BIG_ENDIAN
-#  define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1)
-#else
-#  define BYTE_ORDER 0
-#endif
-
-#define T_MASK ((md5_word_t)~0)
-#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87)
-#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9)
-#define T3    0x242070db
-#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111)
-#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050)
-#define T6    0x4787c62a
-#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec)
-#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe)
-#define T9    0x698098d8
-#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850)
-#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e)
-#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841)
-#define T13    0x6b901122
-#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c)
-#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71)
-#define T16    0x49b40821
-#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d)
-#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf)
-#define T19    0x265e5a51
-#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855)
-#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2)
-#define T22    0x02441453
-#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e)
-#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437)
-#define T25    0x21e1cde6
-#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829)
-#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278)
-#define T28    0x455a14ed
-#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa)
-#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07)
-#define T31    0x676f02d9
-#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375)
-#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd)
-#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e)
-#define T35    0x6d9d6122
-#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3)
-#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb)
-#define T38    0x4bdecfa9
-#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f)
-#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f)
-#define T41    0x289b7ec6
-#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805)
-#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a)
-#define T44    0x04881d05
-#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6)
-#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a)
-#define T47    0x1fa27cf8
-#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a)
-#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb)
-#define T50    0x432aff97
-#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58)
-#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6)
-#define T53    0x655b59c3
-#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d)
-#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82)
-#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e)
-#define T57    0x6fa87e4f
-#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f)
-#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb)
-#define T60    0x4e0811a1
-#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d)
-#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca)
-#define T63    0x2ad7d2bb
-#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e)
-
-
-static void
-md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/)
-{
-    md5_word_t
-	a = pms->abcd[0], b = pms->abcd[1],
-	c = pms->abcd[2], d = pms->abcd[3];
-    md5_word_t t;
-#if BYTE_ORDER > 0
-    /* Define storage only for big-endian CPUs. */
-    md5_word_t X[16];
-#else
-    /* Define storage for little-endian or both types of CPUs. */
-    md5_word_t xbuf[16];
-    const md5_word_t *X;
-#endif
-
-    {
-#if BYTE_ORDER == 0
-	/*
-	 * Determine dynamically whether this is a big-endian or
-	 * little-endian machine, since we can use a more efficient
-	 * algorithm on the latter.
-	 */
-	static const int w = 1;
-
-	if (*((const md5_byte_t *)&w)) /* dynamic little-endian */
-#endif
-#if BYTE_ORDER <= 0		/* little-endian */
-	{
-	    /*
-	     * On little-endian machines, we can process properly aligned
-	     * data without copying it.
-	     */
-	    if (!((data - (const md5_byte_t *)0) & 3)) {
-		/* data are properly aligned */
-		X = (const md5_word_t *)data;
-	    } else {
-		/* not aligned */
-		memcpy(xbuf, data, 64);
-		X = xbuf;
-	    }
-	}
-#endif
-#if BYTE_ORDER == 0
-	else			/* dynamic big-endian */
-#endif
-#if BYTE_ORDER >= 0		/* big-endian */
-	{
-	    /*
-	     * On big-endian machines, we must arrange the bytes in the
-	     * right order.
-	     */
-	    const md5_byte_t *xp = data;
-	    int i;
-
-#  if BYTE_ORDER == 0
-	    X = xbuf;		/* (dynamic only) */
-#  else
-#    define xbuf X		/* (static only) */
-#  endif
-	    for (i = 0; i < 16; ++i, xp += 4)
-		xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24);
-	}
-#endif
-    }
-
-#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
-
-    /* Round 1. */
-    /* Let [abcd k s i] denote the operation
-       a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */
-#define F(x, y, z) (((x) & (y)) | (~(x) & (z)))
-#define SET(a, b, c, d, k, s, Ti)\
-  t = a + F(b,c,d) + X[k] + Ti;\
-  a = ROTATE_LEFT(t, s) + b
-    /* Do the following 16 operations. */
-    SET(a, b, c, d,  0,  7,  T1);
-    SET(d, a, b, c,  1, 12,  T2);
-    SET(c, d, a, b,  2, 17,  T3);
-    SET(b, c, d, a,  3, 22,  T4);
-    SET(a, b, c, d,  4,  7,  T5);
-    SET(d, a, b, c,  5, 12,  T6);
-    SET(c, d, a, b,  6, 17,  T7);
-    SET(b, c, d, a,  7, 22,  T8);
-    SET(a, b, c, d,  8,  7,  T9);
-    SET(d, a, b, c,  9, 12, T10);
-    SET(c, d, a, b, 10, 17, T11);
-    SET(b, c, d, a, 11, 22, T12);
-    SET(a, b, c, d, 12,  7, T13);
-    SET(d, a, b, c, 13, 12, T14);
-    SET(c, d, a, b, 14, 17, T15);
-    SET(b, c, d, a, 15, 22, T16);
-#undef SET
-
-     /* Round 2. */
-     /* Let [abcd k s i] denote the operation
-          a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */
-#define G(x, y, z) (((x) & (z)) | ((y) & ~(z)))
-#define SET(a, b, c, d, k, s, Ti)\
-  t = a + G(b,c,d) + X[k] + Ti;\
-  a = ROTATE_LEFT(t, s) + b
-     /* Do the following 16 operations. */
-    SET(a, b, c, d,  1,  5, T17);
-    SET(d, a, b, c,  6,  9, T18);
-    SET(c, d, a, b, 11, 14, T19);
-    SET(b, c, d, a,  0, 20, T20);
-    SET(a, b, c, d,  5,  5, T21);
-    SET(d, a, b, c, 10,  9, T22);
-    SET(c, d, a, b, 15, 14, T23);
-    SET(b, c, d, a,  4, 20, T24);
-    SET(a, b, c, d,  9,  5, T25);
-    SET(d, a, b, c, 14,  9, T26);
-    SET(c, d, a, b,  3, 14, T27);
-    SET(b, c, d, a,  8, 20, T28);
-    SET(a, b, c, d, 13,  5, T29);
-    SET(d, a, b, c,  2,  9, T30);
-    SET(c, d, a, b,  7, 14, T31);
-    SET(b, c, d, a, 12, 20, T32);
-#undef SET
-
-     /* Round 3. */
-     /* Let [abcd k s t] denote the operation
-          a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */
-#define H(x, y, z) ((x) ^ (y) ^ (z))
-#define SET(a, b, c, d, k, s, Ti)\
-  t = a + H(b,c,d) + X[k] + Ti;\
-  a = ROTATE_LEFT(t, s) + b
-     /* Do the following 16 operations. */
-    SET(a, b, c, d,  5,  4, T33);
-    SET(d, a, b, c,  8, 11, T34);
-    SET(c, d, a, b, 11, 16, T35);
-    SET(b, c, d, a, 14, 23, T36);
-    SET(a, b, c, d,  1,  4, T37);
-    SET(d, a, b, c,  4, 11, T38);
-    SET(c, d, a, b,  7, 16, T39);
-    SET(b, c, d, a, 10, 23, T40);
-    SET(a, b, c, d, 13,  4, T41);
-    SET(d, a, b, c,  0, 11, T42);
-    SET(c, d, a, b,  3, 16, T43);
-    SET(b, c, d, a,  6, 23, T44);
-    SET(a, b, c, d,  9,  4, T45);
-    SET(d, a, b, c, 12, 11, T46);
-    SET(c, d, a, b, 15, 16, T47);
-    SET(b, c, d, a,  2, 23, T48);
-#undef SET
-
-     /* Round 4. */
-     /* Let [abcd k s t] denote the operation
-          a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */
-#define I(x, y, z) ((y) ^ ((x) | ~(z)))
-#define SET(a, b, c, d, k, s, Ti)\
-  t = a + I(b,c,d) + X[k] + Ti;\
-  a = ROTATE_LEFT(t, s) + b
-     /* Do the following 16 operations. */
-    SET(a, b, c, d,  0,  6, T49);
-    SET(d, a, b, c,  7, 10, T50);
-    SET(c, d, a, b, 14, 15, T51);
-    SET(b, c, d, a,  5, 21, T52);
-    SET(a, b, c, d, 12,  6, T53);
-    SET(d, a, b, c,  3, 10, T54);
-    SET(c, d, a, b, 10, 15, T55);
-    SET(b, c, d, a,  1, 21, T56);
-    SET(a, b, c, d,  8,  6, T57);
-    SET(d, a, b, c, 15, 10, T58);
-    SET(c, d, a, b,  6, 15, T59);
-    SET(b, c, d, a, 13, 21, T60);
-    SET(a, b, c, d,  4,  6, T61);
-    SET(d, a, b, c, 11, 10, T62);
-    SET(c, d, a, b,  2, 15, T63);
-    SET(b, c, d, a,  9, 21, T64);
-#undef SET
-
-     /* Then perform the following additions. (That is increment each
-        of the four registers by the value it had before this block
-        was started.) */
-    pms->abcd[0] += a;
-    pms->abcd[1] += b;
-    pms->abcd[2] += c;
-    pms->abcd[3] += d;
-}
-
-void
-md5_init(md5_state_t *pms)
-{
-    pms->count[0] = pms->count[1] = 0;
-    pms->abcd[0] = 0x67452301;
-    pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476;
-    pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301;
-    pms->abcd[3] = 0x10325476;
-}
-
-void
-md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes)
-{
-    const md5_byte_t *p = data;
-    int left = nbytes;
-    int offset = (pms->count[0] >> 3) & 63;
-    md5_word_t nbits = (md5_word_t)(nbytes << 3);
-
-    if (nbytes <= 0)
-	return;
-
-    /* Update the message length. */
-    pms->count[1] += nbytes >> 29;
-    pms->count[0] += nbits;
-    if (pms->count[0] < nbits)
-	pms->count[1]++;
-
-    /* Process an initial partial block. */
-    if (offset) {
-	int copy = (offset + nbytes > 64 ? 64 - offset : nbytes);
-
-	memcpy(pms->buf + offset, p, copy);
-	if (offset + copy < 64)
-	    return;
-	p += copy;
-	left -= copy;
-	md5_process(pms, pms->buf);
-    }
-
-    /* Process full blocks. */
-    for (; left >= 64; p += 64, left -= 64)
-	md5_process(pms, p);
-
-    /* Process a final partial block. */
-    if (left)
-	memcpy(pms->buf, p, left);
-}
-
-void
-md5_finish(md5_state_t *pms, md5_byte_t digest[16])
-{
-    static const md5_byte_t pad[64] = {
-	0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
-    };
-    md5_byte_t data[8];
-    int i;
-
-    /* Save the length before padding. */
-    for (i = 0; i < 8; ++i)
-	data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3));
-    /* Pad to 56 bytes mod 64. */
-    md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1);
-    /* Append the length. */
-    md5_append(pms, data, 8);
-    for (i = 0; i < 16; ++i)
-	digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3));
-}
--- a/Resources/ThirdParty/md5/md5.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-/*
-  Copyright (C) 1999, 2002 Aladdin Enterprises.  All rights reserved.
-
-  This software is provided 'as-is', without any express or implied
-  warranty.  In no event will the authors be held liable for any damages
-  arising from the use of this software.
-
-  Permission is granted to anyone to use this software for any purpose,
-  including commercial applications, and to alter it and redistribute it
-  freely, subject to the following restrictions:
-
-  1. The origin of this software must not be misrepresented; you must not
-     claim that you wrote the original software. If you use this software
-     in a product, an acknowledgment in the product documentation would be
-     appreciated but is not required.
-  2. Altered source versions must be plainly marked as such, and must not be
-     misrepresented as being the original software.
-  3. This notice may not be removed or altered from any source distribution.
-
-  L. Peter Deutsch
-  ghost@aladdin.com
-
- */
-/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */
-/*
-  Independent implementation of MD5 (RFC 1321).
-
-  This code implements the MD5 Algorithm defined in RFC 1321, whose
-  text is available at
-	http://www.ietf.org/rfc/rfc1321.txt
-  The code is derived from the text of the RFC, including the test suite
-  (section A.5) but excluding the rest of Appendix A.  It does not include
-  any code or documentation that is identified in the RFC as being
-  copyrighted.
-
-  The original and principal author of md5.h is L. Peter Deutsch
-  <ghost@aladdin.com>.  Other authors are noted in the change history
-  that follows (in reverse chronological order):
-
-  2002-04-13 lpd Removed support for non-ANSI compilers; removed
-	references to Ghostscript; clarified derivation from RFC 1321;
-	now handles byte order either statically or dynamically.
-  1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
-  1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5);
-	added conditionalization for C++ compilation from Martin
-	Purschke <purschke@bnl.gov>.
-  1999-05-03 lpd Original version.
- */
-
-#ifndef md5_INCLUDED
-#  define md5_INCLUDED
-
-/*
- * This package supports both compile-time and run-time determination of CPU
- * byte order.  If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be
- * compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is
- * defined as non-zero, the code will be compiled to run only on big-endian
- * CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to
- * run on either big- or little-endian CPUs, but will run slightly less
- * efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined.
- */
-
-typedef unsigned char md5_byte_t; /* 8-bit byte */
-typedef unsigned int md5_word_t; /* 32-bit word */
-
-/* Define the state of the MD5 Algorithm. */
-typedef struct md5_state_s {
-    md5_word_t count[2];	/* message length in bits, lsw first */
-    md5_word_t abcd[4];		/* digest buffer */
-    md5_byte_t buf[64];		/* accumulate block */
-} md5_state_t;
-
-#ifdef __cplusplus
-extern "C" 
-{
-#endif
-
-/* Initialize the algorithm. */
-void md5_init(md5_state_t *pms);
-
-/* Append a string to the message. */
-void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes);
-
-/* Finish the message and return the digest. */
-void md5_finish(md5_state_t *pms, md5_byte_t digest[16]);
-
-#ifdef __cplusplus
-}  /* end extern "C" */
-#endif
-
-#endif /* md5_INCLUDED */
--- a/Resources/ThirdParty/minizip/NOTES	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-These files come from the "contrib/minizip" directory in zlib 1.2.11.
--- a/Resources/ThirdParty/minizip/crypt.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,131 +0,0 @@
-/* crypt.h -- base code for crypt/uncrypt ZIPfile
-
-
-   Version 1.01e, February 12th, 2005
-
-   Copyright (C) 1998-2005 Gilles Vollant
-
-   This code is a modified version of crypting code in Infozip distribution
-
-   The encryption/decryption parts of this source code (as opposed to the
-   non-echoing password parts) were originally written in Europe.  The
-   whole source package can be freely distributed, including from the USA.
-   (Prior to January 2000, re-export from the US was a violation of US law.)
-
-   This encryption code is a direct transcription of the algorithm from
-   Roger Schlafly, described by Phil Katz in the file appnote.txt.  This
-   file (appnote.txt) is distributed with the PKZIP program (even in the
-   version without encryption capabilities).
-
-   If you don't need crypting in your application, just define symbols
-   NOCRYPT and NOUNCRYPT.
-
-   This code support the "Traditional PKWARE Encryption".
-
-   The new AES encryption added on Zip format by Winzip (see the page
-   http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong
-   Encryption is not supported.
-*/
-
-#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8))
-
-/***********************************************************************
- * Return the next byte in the pseudo-random sequence
- */
-static int decrypt_byte(unsigned long* pkeys, const z_crc_t* pcrc_32_tab)
-{
-    unsigned temp;  /* POTENTIAL BUG:  temp*(temp^1) may overflow in an
-                     * unpredictable manner on 16-bit systems; not a problem
-                     * with any known compiler so far, though */
-
-    temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2;
-    return (int)(((temp * (temp ^ 1)) >> 8) & 0xff);
-}
-
-/***********************************************************************
- * Update the encryption keys with the next byte of plain text
- */
-static int update_keys(unsigned long* pkeys,const z_crc_t* pcrc_32_tab,int c)
-{
-    (*(pkeys+0)) = CRC32((*(pkeys+0)), c);
-    (*(pkeys+1)) += (*(pkeys+0)) & 0xff;
-    (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1;
-    {
-      register int keyshift = (int)((*(pkeys+1)) >> 24);
-      (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift);
-    }
-    return c;
-}
-
-
-/***********************************************************************
- * Initialize the encryption keys and the random header according to
- * the given password.
- */
-static void init_keys(const char* passwd,unsigned long* pkeys,const z_crc_t* pcrc_32_tab)
-{
-    *(pkeys+0) = 305419896L;
-    *(pkeys+1) = 591751049L;
-    *(pkeys+2) = 878082192L;
-    while (*passwd != '\0') {
-        update_keys(pkeys,pcrc_32_tab,(int)*passwd);
-        passwd++;
-    }
-}
-
-#define zdecode(pkeys,pcrc_32_tab,c) \
-    (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab)))
-
-#define zencode(pkeys,pcrc_32_tab,c,t) \
-    (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c))
-
-#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED
-
-#define RAND_HEAD_LEN  12
-   /* "last resort" source for second part of crypt seed pattern */
-#  ifndef ZCR_SEED2
-#    define ZCR_SEED2 3141592654UL     /* use PI as default pattern */
-#  endif
-
-static int crypthead(const char* passwd,      /* password string */
-                     unsigned char* buf,      /* where to write header */
-                     int bufSize,
-                     unsigned long* pkeys,
-                     const z_crc_t* pcrc_32_tab,
-                     unsigned long crcForCrypting)
-{
-    int n;                       /* index in random header */
-    int t;                       /* temporary */
-    int c;                       /* random byte */
-    unsigned char header[RAND_HEAD_LEN-2]; /* random header */
-    static unsigned calls = 0;   /* ensure different random header each time */
-
-    if (bufSize<RAND_HEAD_LEN)
-      return 0;
-
-    /* First generate RAND_HEAD_LEN-2 random bytes. We encrypt the
-     * output of rand() to get less predictability, since rand() is
-     * often poorly implemented.
-     */
-    if (++calls == 1)
-    {
-        srand((unsigned)(time(NULL) ^ ZCR_SEED2));
-    }
-    init_keys(passwd, pkeys, pcrc_32_tab);
-    for (n = 0; n < RAND_HEAD_LEN-2; n++)
-    {
-        c = (rand() >> 7) & 0xff;
-        header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t);
-    }
-    /* Encrypt random header (last two bytes is high word of crc) */
-    init_keys(passwd, pkeys, pcrc_32_tab);
-    for (n = 0; n < RAND_HEAD_LEN-2; n++)
-    {
-        buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t);
-    }
-    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t);
-    buf[n++] = (unsigned char)zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t);
-    return n;
-}
-
-#endif
--- a/Resources/ThirdParty/minizip/ioapi.c	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
-/* ioapi.h -- IO base function header for compress/uncompress .zip
-   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Modifications for Zip64 support
-         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
-
-         For more info read MiniZip_info.txt
-
-*/
-
-#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS)))
-        #define _CRT_SECURE_NO_WARNINGS
-#endif
-
-#if defined(__APPLE__) || defined(IOAPI_NO_64)
-// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions
-#define FOPEN_FUNC(filename, mode) fopen(filename, mode)
-#define FTELLO_FUNC(stream) ftello(stream)
-#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin)
-#else
-#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)
-#define FTELLO_FUNC(stream) ftello64(stream)
-#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)
-#endif
-
-
-#include "ioapi.h"
-
-voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)
-{
-    if (pfilefunc->zfile_func64.zopen64_file != NULL)
-        return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);
-    else
-    {
-        return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode);
-    }
-}
-
-long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)
-{
-    if (pfilefunc->zfile_func64.zseek64_file != NULL)
-        return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);
-    else
-    {
-        uLong offsetTruncated = (uLong)offset;
-        if (offsetTruncated != offset)
-            return -1;
-        else
-            return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin);
-    }
-}
-
-ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)
-{
-    if (pfilefunc->zfile_func64.zseek64_file != NULL)
-        return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);
-    else
-    {
-        uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
-        if ((tell_uLong) == MAXU32)
-            return (ZPOS64_T)-1;
-        else
-            return tell_uLong;
-    }
-}
-
-void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32)
-{
-    p_filefunc64_32->zfile_func64.zopen64_file = NULL;
-    p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;
-    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
-    p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;
-    p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;
-    p_filefunc64_32->zfile_func64.ztell64_file = NULL;
-    p_filefunc64_32->zfile_func64.zseek64_file = NULL;
-    p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file;
-    p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
-    p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque;
-    p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file;
-    p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file;
-}
-
-
-
-static voidpf  ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode));
-static uLong   ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size));
-static uLong   ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size));
-static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream));
-static long    ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
-static int     ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream));
-static int     ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream));
-
-static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode)
-{
-    FILE* file = NULL;
-    const char* mode_fopen = NULL;
-    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
-        mode_fopen = "rb";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
-        mode_fopen = "r+b";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
-        mode_fopen = "wb";
-
-    if ((filename!=NULL) && (mode_fopen != NULL))
-        file = fopen(filename, mode_fopen);
-    return file;
-}
-
-static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode)
-{
-    FILE* file = NULL;
-    const char* mode_fopen = NULL;
-    if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
-        mode_fopen = "rb";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
-        mode_fopen = "r+b";
-    else
-    if (mode & ZLIB_FILEFUNC_MODE_CREATE)
-        mode_fopen = "wb";
-
-    if ((filename!=NULL) && (mode_fopen != NULL))
-        file = FOPEN_FUNC((const char*)filename, mode_fopen);
-    return file;
-}
-
-
-static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size)
-{
-    uLong ret;
-    ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);
-    return ret;
-}
-
-static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size)
-{
-    uLong ret;
-    ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);
-    return ret;
-}
-
-static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream)
-{
-    long ret;
-    ret = ftell((FILE *)stream);
-    return ret;
-}
-
-
-static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream)
-{
-    ZPOS64_T ret;
-    ret = FTELLO_FUNC((FILE *)stream);
-    return ret;
-}
-
-static long ZCALLBACK fseek_file_func (voidpf  opaque, voidpf stream, uLong offset, int origin)
-{
-    int fseek_origin=0;
-    long ret;
-    switch (origin)
-    {
-    case ZLIB_FILEFUNC_SEEK_CUR :
-        fseek_origin = SEEK_CUR;
-        break;
-    case ZLIB_FILEFUNC_SEEK_END :
-        fseek_origin = SEEK_END;
-        break;
-    case ZLIB_FILEFUNC_SEEK_SET :
-        fseek_origin = SEEK_SET;
-        break;
-    default: return -1;
-    }
-    ret = 0;
-    if (fseek((FILE *)stream, offset, fseek_origin) != 0)
-        ret = -1;
-    return ret;
-}
-
-static long ZCALLBACK fseek64_file_func (voidpf  opaque, voidpf stream, ZPOS64_T offset, int origin)
-{
-    int fseek_origin=0;
-    long ret;
-    switch (origin)
-    {
-    case ZLIB_FILEFUNC_SEEK_CUR :
-        fseek_origin = SEEK_CUR;
-        break;
-    case ZLIB_FILEFUNC_SEEK_END :
-        fseek_origin = SEEK_END;
-        break;
-    case ZLIB_FILEFUNC_SEEK_SET :
-        fseek_origin = SEEK_SET;
-        break;
-    default: return -1;
-    }
-    ret = 0;
-
-    if(FSEEKO_FUNC((FILE *)stream, offset, fseek_origin) != 0)
-                        ret = -1;
-
-    return ret;
-}
-
-
-static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream)
-{
-    int ret;
-    ret = fclose((FILE *)stream);
-    return ret;
-}
-
-static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream)
-{
-    int ret;
-    ret = ferror((FILE *)stream);
-    return ret;
-}
-
-void fill_fopen_filefunc (pzlib_filefunc_def)
-  zlib_filefunc_def* pzlib_filefunc_def;
-{
-    pzlib_filefunc_def->zopen_file = fopen_file_func;
-    pzlib_filefunc_def->zread_file = fread_file_func;
-    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
-    pzlib_filefunc_def->ztell_file = ftell_file_func;
-    pzlib_filefunc_def->zseek_file = fseek_file_func;
-    pzlib_filefunc_def->zclose_file = fclose_file_func;
-    pzlib_filefunc_def->zerror_file = ferror_file_func;
-    pzlib_filefunc_def->opaque = NULL;
-}
-
-void fill_fopen64_filefunc (zlib_filefunc64_def*  pzlib_filefunc_def)
-{
-    pzlib_filefunc_def->zopen64_file = fopen64_file_func;
-    pzlib_filefunc_def->zread_file = fread_file_func;
-    pzlib_filefunc_def->zwrite_file = fwrite_file_func;
-    pzlib_filefunc_def->ztell64_file = ftell64_file_func;
-    pzlib_filefunc_def->zseek64_file = fseek64_file_func;
-    pzlib_filefunc_def->zclose_file = fclose_file_func;
-    pzlib_filefunc_def->zerror_file = ferror_file_func;
-    pzlib_filefunc_def->opaque = NULL;
-}
--- a/Resources/ThirdParty/minizip/ioapi.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,208 +0,0 @@
-/* ioapi.h -- IO base function header for compress/uncompress .zip
-   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Modifications for Zip64 support
-         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
-
-         For more info read MiniZip_info.txt
-
-         Changes
-
-    Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this)
-    Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux.
-               More if/def section may be needed to support other platforms
-    Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows.
-                          (but you should use iowin32.c for windows instead)
-
-*/
-
-#ifndef _ZLIBIOAPI64_H
-#define _ZLIBIOAPI64_H
-
-#if (!defined(_WIN32)) && (!defined(WIN32)) && (!defined(__APPLE__))
-
-  // Linux needs this to support file operation on files larger then 4+GB
-  // But might need better if/def to select just the platforms that needs them.
-
-        #ifndef __USE_FILE_OFFSET64
-                #define __USE_FILE_OFFSET64
-        #endif
-        #ifndef __USE_LARGEFILE64
-                #define __USE_LARGEFILE64
-        #endif
-        #ifndef _LARGEFILE64_SOURCE
-                #define _LARGEFILE64_SOURCE
-        #endif
-        #ifndef _FILE_OFFSET_BIT
-                #define _FILE_OFFSET_BIT 64
-        #endif
-
-#endif
-
-#include <stdio.h>
-#include <stdlib.h>
-#include "zlib.h"
-
-#if defined(USE_FILE32API)
-#define fopen64 fopen
-#define ftello64 ftell
-#define fseeko64 fseek
-#else
-#ifdef __FreeBSD__
-#define fopen64 fopen
-#define ftello64 ftello
-#define fseeko64 fseeko
-#endif
-#ifdef _MSC_VER
- #define fopen64 fopen
- #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC)))
-  #define ftello64 _ftelli64
-  #define fseeko64 _fseeki64
- #else // old MSC
-  #define ftello64 ftell
-  #define fseeko64 fseek
- #endif
-#endif
-#endif
-
-/*
-#ifndef ZPOS64_T
-  #ifdef _WIN32
-                #define ZPOS64_T fpos_t
-  #else
-    #include <stdint.h>
-    #define ZPOS64_T uint64_t
-  #endif
-#endif
-*/
-
-#ifdef HAVE_MINIZIP64_CONF_H
-#include "mz64conf.h"
-#endif
-
-/* a type choosen by DEFINE */
-#ifdef HAVE_64BIT_INT_CUSTOM
-typedef  64BIT_INT_CUSTOM_TYPE ZPOS64_T;
-#else
-#ifdef HAS_STDINT_H
-#include "stdint.h"
-typedef uint64_t ZPOS64_T;
-#else
-
-/* Maximum unsigned 32-bit value used as placeholder for zip64 */
-#define MAXU32 0xffffffff
-
-#if defined(_MSC_VER) || defined(__BORLANDC__)
-typedef unsigned __int64 ZPOS64_T;
-#else
-typedef unsigned long long int ZPOS64_T;
-#endif
-#endif
-#endif
-
-
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-#define ZLIB_FILEFUNC_SEEK_CUR (1)
-#define ZLIB_FILEFUNC_SEEK_END (2)
-#define ZLIB_FILEFUNC_SEEK_SET (0)
-
-#define ZLIB_FILEFUNC_MODE_READ      (1)
-#define ZLIB_FILEFUNC_MODE_WRITE     (2)
-#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3)
-
-#define ZLIB_FILEFUNC_MODE_EXISTING (4)
-#define ZLIB_FILEFUNC_MODE_CREATE   (8)
-
-
-#ifndef ZCALLBACK
- #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK)
-   #define ZCALLBACK CALLBACK
- #else
-   #define ZCALLBACK
- #endif
-#endif
-
-
-
-
-typedef voidpf   (ZCALLBACK *open_file_func)      OF((voidpf opaque, const char* filename, int mode));
-typedef uLong    (ZCALLBACK *read_file_func)      OF((voidpf opaque, voidpf stream, void* buf, uLong size));
-typedef uLong    (ZCALLBACK *write_file_func)     OF((voidpf opaque, voidpf stream, const void* buf, uLong size));
-typedef int      (ZCALLBACK *close_file_func)     OF((voidpf opaque, voidpf stream));
-typedef int      (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream));
-
-typedef long     (ZCALLBACK *tell_file_func)      OF((voidpf opaque, voidpf stream));
-typedef long     (ZCALLBACK *seek_file_func)      OF((voidpf opaque, voidpf stream, uLong offset, int origin));
-
-
-/* here is the "old" 32 bits structure structure */
-typedef struct zlib_filefunc_def_s
-{
-    open_file_func      zopen_file;
-    read_file_func      zread_file;
-    write_file_func     zwrite_file;
-    tell_file_func      ztell_file;
-    seek_file_func      zseek_file;
-    close_file_func     zclose_file;
-    testerror_file_func zerror_file;
-    voidpf              opaque;
-} zlib_filefunc_def;
-
-typedef ZPOS64_T (ZCALLBACK *tell64_file_func)    OF((voidpf opaque, voidpf stream));
-typedef long     (ZCALLBACK *seek64_file_func)    OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
-typedef voidpf   (ZCALLBACK *open64_file_func)    OF((voidpf opaque, const void* filename, int mode));
-
-typedef struct zlib_filefunc64_def_s
-{
-    open64_file_func    zopen64_file;
-    read_file_func      zread_file;
-    write_file_func     zwrite_file;
-    tell64_file_func    ztell64_file;
-    seek64_file_func    zseek64_file;
-    close_file_func     zclose_file;
-    testerror_file_func zerror_file;
-    voidpf              opaque;
-} zlib_filefunc64_def;
-
-void fill_fopen64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def));
-void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def));
-
-/* now internal definition, only for zip.c and unzip.h */
-typedef struct zlib_filefunc64_32_def_s
-{
-    zlib_filefunc64_def zfile_func64;
-    open_file_func      zopen32_file;
-    tell_file_func      ztell32_file;
-    seek_file_func      zseek32_file;
-} zlib_filefunc64_32_def;
-
-
-#define ZREAD64(filefunc,filestream,buf,size)     ((*((filefunc).zfile_func64.zread_file))   ((filefunc).zfile_func64.opaque,filestream,buf,size))
-#define ZWRITE64(filefunc,filestream,buf,size)    ((*((filefunc).zfile_func64.zwrite_file))  ((filefunc).zfile_func64.opaque,filestream,buf,size))
-//#define ZTELL64(filefunc,filestream)            ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream))
-//#define ZSEEK64(filefunc,filestream,pos,mode)   ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode))
-#define ZCLOSE64(filefunc,filestream)             ((*((filefunc).zfile_func64.zclose_file))  ((filefunc).zfile_func64.opaque,filestream))
-#define ZERROR64(filefunc,filestream)             ((*((filefunc).zfile_func64.zerror_file))  ((filefunc).zfile_func64.opaque,filestream))
-
-voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode));
-long    call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin));
-ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream));
-
-void    fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);
-
-#define ZOPEN64(filefunc,filename,mode)         (call_zopen64((&(filefunc)),(filename),(mode)))
-#define ZTELL64(filefunc,filestream)            (call_ztell64((&(filefunc)),(filestream)))
-#define ZSEEK64(filefunc,filestream,pos,mode)   (call_zseek64((&(filefunc)),(filestream),(pos),(mode)))
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif
--- a/Resources/ThirdParty/minizip/zip.c	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2007 +0,0 @@
-/* zip.c -- IO on .zip files using zlib
-   Version 1.1, February 14h, 2010
-   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Modifications for Zip64 support
-         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
-
-         For more info read MiniZip_info.txt
-
-         Changes
-   Oct-2009 - Mathias Svensson - Remove old C style function prototypes
-   Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives
-   Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions.
-   Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data
-                                 It is used when recreting zip archive with RAW when deleting items from a zip.
-                                 ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed.
-   Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required)
-   Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
-
-*/
-
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <time.h>
-#include "zlib.h"
-#include "zip.h"
-
-#ifdef STDC
-#  include <stddef.h>
-#  include <string.h>
-#  include <stdlib.h>
-#endif
-#ifdef NO_ERRNO_H
-    extern int errno;
-#else
-#   include <errno.h>
-#endif
-
-
-#ifndef local
-#  define local static
-#endif
-/* compile with -Dlocal if your debugger can't find static symbols */
-
-#ifndef VERSIONMADEBY
-# define VERSIONMADEBY   (0x0) /* platform depedent */
-#endif
-
-#ifndef Z_BUFSIZE
-#define Z_BUFSIZE (64*1024) //(16384)
-#endif
-
-#ifndef Z_MAXFILENAMEINZIP
-#define Z_MAXFILENAMEINZIP (256)
-#endif
-
-#ifndef ALLOC
-# define ALLOC(size) (malloc(size))
-#endif
-#ifndef TRYFREE
-# define TRYFREE(p) {if (p) free(p);}
-#endif
-
-/*
-#define SIZECENTRALDIRITEM (0x2e)
-#define SIZEZIPLOCALHEADER (0x1e)
-*/
-
-/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */
-
-
-// NOT sure that this work on ALL platform
-#define MAKEULONG64(a, b) ((ZPOS64_T)(((unsigned long)(a)) | ((ZPOS64_T)((unsigned long)(b))) << 32))
-
-#ifndef SEEK_CUR
-#define SEEK_CUR    1
-#endif
-
-#ifndef SEEK_END
-#define SEEK_END    2
-#endif
-
-#ifndef SEEK_SET
-#define SEEK_SET    0
-#endif
-
-#ifndef DEF_MEM_LEVEL
-#if MAX_MEM_LEVEL >= 8
-#  define DEF_MEM_LEVEL 8
-#else
-#  define DEF_MEM_LEVEL  MAX_MEM_LEVEL
-#endif
-#endif
-const char zip_copyright[] =" zip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll";
-
-
-#define SIZEDATA_INDATABLOCK (4096-(4*4))
-
-#define LOCALHEADERMAGIC    (0x04034b50)
-#define CENTRALHEADERMAGIC  (0x02014b50)
-#define ENDHEADERMAGIC      (0x06054b50)
-#define ZIP64ENDHEADERMAGIC      (0x6064b50)
-#define ZIP64ENDLOCHEADERMAGIC   (0x7064b50)
-
-#define FLAG_LOCALHEADER_OFFSET (0x06)
-#define CRC_LOCALHEADER_OFFSET  (0x0e)
-
-#define SIZECENTRALHEADER (0x2e) /* 46 */
-
-typedef struct linkedlist_datablock_internal_s
-{
-  struct linkedlist_datablock_internal_s* next_datablock;
-  uLong  avail_in_this_block;
-  uLong  filled_in_this_block;
-  uLong  unused; /* for future use and alignment */
-  unsigned char data[SIZEDATA_INDATABLOCK];
-} linkedlist_datablock_internal;
-
-typedef struct linkedlist_data_s
-{
-    linkedlist_datablock_internal* first_block;
-    linkedlist_datablock_internal* last_block;
-} linkedlist_data;
-
-
-typedef struct
-{
-    z_stream stream;            /* zLib stream structure for inflate */
-#ifdef HAVE_BZIP2
-    bz_stream bstream;          /* bzLib stream structure for bziped */
-#endif
-
-    int  stream_initialised;    /* 1 is stream is initialised */
-    uInt pos_in_buffered_data;  /* last written byte in buffered_data */
-
-    ZPOS64_T pos_local_header;     /* offset of the local header of the file
-                                     currenty writing */
-    char* central_header;       /* central header data for the current file */
-    uLong size_centralExtra;
-    uLong size_centralheader;   /* size of the central header for cur file */
-    uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */
-    uLong flag;                 /* flag of the file currently writing */
-
-    int  method;                /* compression method of file currenty wr.*/
-    int  raw;                   /* 1 for directly writing raw data */
-    Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/
-    uLong dosDate;
-    uLong crc32;
-    int  encrypt;
-    int  zip64;               /* Add ZIP64 extened information in the extra field */
-    ZPOS64_T pos_zip64extrainfo;
-    ZPOS64_T totalCompressedData;
-    ZPOS64_T totalUncompressedData;
-#ifndef NOCRYPT
-    unsigned long keys[3];     /* keys defining the pseudo-random sequence */
-    const z_crc_t* pcrc_32_tab;
-    int crypt_header_size;
-#endif
-} curfile64_info;
-
-typedef struct
-{
-    zlib_filefunc64_32_def z_filefunc;
-    voidpf filestream;        /* io structore of the zipfile */
-    linkedlist_data central_dir;/* datablock with central dir in construction*/
-    int  in_opened_file_inzip;  /* 1 if a file in the zip is currently writ.*/
-    curfile64_info ci;            /* info on the file curretly writing */
-
-    ZPOS64_T begin_pos;            /* position of the beginning of the zipfile */
-    ZPOS64_T add_position_when_writing_offset;
-    ZPOS64_T number_entry;
-
-#ifndef NO_ADDFILEINEXISTINGZIP
-    char *globalcomment;
-#endif
-
-} zip64_internal;
-
-
-#ifndef NOCRYPT
-#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED
-#include "crypt.h"
-#endif
-
-local linkedlist_datablock_internal* allocate_new_datablock()
-{
-    linkedlist_datablock_internal* ldi;
-    ldi = (linkedlist_datablock_internal*)
-                 ALLOC(sizeof(linkedlist_datablock_internal));
-    if (ldi!=NULL)
-    {
-        ldi->next_datablock = NULL ;
-        ldi->filled_in_this_block = 0 ;
-        ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ;
-    }
-    return ldi;
-}
-
-local void free_datablock(linkedlist_datablock_internal* ldi)
-{
-    while (ldi!=NULL)
-    {
-        linkedlist_datablock_internal* ldinext = ldi->next_datablock;
-        TRYFREE(ldi);
-        ldi = ldinext;
-    }
-}
-
-local void init_linkedlist(linkedlist_data* ll)
-{
-    ll->first_block = ll->last_block = NULL;
-}
-
-local void free_linkedlist(linkedlist_data* ll)
-{
-    free_datablock(ll->first_block);
-    ll->first_block = ll->last_block = NULL;
-}
-
-
-local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len)
-{
-    linkedlist_datablock_internal* ldi;
-    const unsigned char* from_copy;
-
-    if (ll==NULL)
-        return ZIP_INTERNALERROR;
-
-    if (ll->last_block == NULL)
-    {
-        ll->first_block = ll->last_block = allocate_new_datablock();
-        if (ll->first_block == NULL)
-            return ZIP_INTERNALERROR;
-    }
-
-    ldi = ll->last_block;
-    from_copy = (unsigned char*)buf;
-
-    while (len>0)
-    {
-        uInt copy_this;
-        uInt i;
-        unsigned char* to_copy;
-
-        if (ldi->avail_in_this_block==0)
-        {
-            ldi->next_datablock = allocate_new_datablock();
-            if (ldi->next_datablock == NULL)
-                return ZIP_INTERNALERROR;
-            ldi = ldi->next_datablock ;
-            ll->last_block = ldi;
-        }
-
-        if (ldi->avail_in_this_block < len)
-            copy_this = (uInt)ldi->avail_in_this_block;
-        else
-            copy_this = (uInt)len;
-
-        to_copy = &(ldi->data[ldi->filled_in_this_block]);
-
-        for (i=0;i<copy_this;i++)
-            *(to_copy+i)=*(from_copy+i);
-
-        ldi->filled_in_this_block += copy_this;
-        ldi->avail_in_this_block -= copy_this;
-        from_copy += copy_this ;
-        len -= copy_this;
-    }
-    return ZIP_OK;
-}
-
-
-
-/****************************************************************************/
-
-#ifndef NO_ADDFILEINEXISTINGZIP
-/* ===========================================================================
-   Inputs a long in LSB order to the given file
-   nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T)
-*/
-
-local int zip64local_putValue OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte));
-local int zip64local_putValue (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte)
-{
-    unsigned char buf[8];
-    int n;
-    for (n = 0; n < nbByte; n++)
-    {
-        buf[n] = (unsigned char)(x & 0xff);
-        x >>= 8;
-    }
-    if (x != 0)
-      {     /* data overflow - hack for ZIP64 (X Roche) */
-      for (n = 0; n < nbByte; n++)
-        {
-          buf[n] = 0xff;
-        }
-      }
-
-    if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,nbByte)!=(uLong)nbByte)
-        return ZIP_ERRNO;
-    else
-        return ZIP_OK;
-}
-
-local void zip64local_putValue_inmemory OF((void* dest, ZPOS64_T x, int nbByte));
-local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte)
-{
-    unsigned char* buf=(unsigned char*)dest;
-    int n;
-    for (n = 0; n < nbByte; n++) {
-        buf[n] = (unsigned char)(x & 0xff);
-        x >>= 8;
-    }
-
-    if (x != 0)
-    {     /* data overflow - hack for ZIP64 */
-       for (n = 0; n < nbByte; n++)
-       {
-          buf[n] = 0xff;
-       }
-    }
-}
-
-/****************************************************************************/
-
-
-local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm)
-{
-    uLong year = (uLong)ptm->tm_year;
-    if (year>=1980)
-        year-=1980;
-    else if (year>=80)
-        year-=80;
-    return
-      (uLong) (((ptm->tm_mday) + (32 * (ptm->tm_mon+1)) + (512 * year)) << 16) |
-        ((ptm->tm_sec/2) + (32* ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));
-}
-
-
-/****************************************************************************/
-
-local int zip64local_getByte OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi));
-
-local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def,voidpf filestream,int* pi)
-{
-    unsigned char c;
-    int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
-    if (err==1)
-    {
-        *pi = (int)c;
-        return ZIP_OK;
-    }
-    else
-    {
-        if (ZERROR64(*pzlib_filefunc_def,filestream))
-            return ZIP_ERRNO;
-        else
-            return ZIP_EOF;
-    }
-}
-
-
-/* ===========================================================================
-   Reads a long in LSB order from the given gz_stream. Sets
-*/
-local int zip64local_getShort OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
-
-local int zip64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
-{
-    uLong x ;
-    int i = 0;
-    int err;
-
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x = (uLong)i;
-
-    if (err==ZIP_OK)
-        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x += ((uLong)i)<<8;
-
-    if (err==ZIP_OK)
-        *pX = x;
-    else
-        *pX = 0;
-    return err;
-}
-
-local int zip64local_getLong OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
-
-local int zip64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
-{
-    uLong x ;
-    int i = 0;
-    int err;
-
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x = (uLong)i;
-
-    if (err==ZIP_OK)
-        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x += ((uLong)i)<<8;
-
-    if (err==ZIP_OK)
-        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x += ((uLong)i)<<16;
-
-    if (err==ZIP_OK)
-        err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-    x += ((uLong)i)<<24;
-
-    if (err==ZIP_OK)
-        *pX = x;
-    else
-        *pX = 0;
-    return err;
-}
-
-local int zip64local_getLong64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX));
-
-
-local int zip64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX)
-{
-  ZPOS64_T x;
-  int i = 0;
-  int err;
-
-  err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x = (ZPOS64_T)i;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<8;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<16;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<24;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<32;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<40;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<48;
-
-  if (err==ZIP_OK)
-    err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
-  x += ((ZPOS64_T)i)<<56;
-
-  if (err==ZIP_OK)
-    *pX = x;
-  else
-    *pX = 0;
-
-  return err;
-}
-
-#ifndef BUFREADCOMMENT
-#define BUFREADCOMMENT (0x400)
-#endif
-/*
-  Locate the Central directory of a zipfile (at the end, just before
-    the global comment)
-*/
-local ZPOS64_T zip64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
-
-local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
-{
-  unsigned char* buf;
-  ZPOS64_T uSizeFile;
-  ZPOS64_T uBackRead;
-  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
-  ZPOS64_T uPosFound=0;
-
-  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
-    return 0;
-
-
-  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
-
-  if (uMaxBack>uSizeFile)
-    uMaxBack = uSizeFile;
-
-  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
-  if (buf==NULL)
-    return 0;
-
-  uBackRead = 4;
-  while (uBackRead<uMaxBack)
-  {
-    uLong uReadSize;
-    ZPOS64_T uReadPos ;
-    int i;
-    if (uBackRead+BUFREADCOMMENT>uMaxBack)
-      uBackRead = uMaxBack;
-    else
-      uBackRead+=BUFREADCOMMENT;
-    uReadPos = uSizeFile-uBackRead ;
-
-    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
-      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
-    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
-      break;
-
-    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
-      break;
-
-    for (i=(int)uReadSize-3; (i--)>0;)
-      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
-        ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
-      {
-        uPosFound = uReadPos+i;
-        break;
-      }
-
-    if (uPosFound!=0)
-      break;
-  }
-  TRYFREE(buf);
-  return uPosFound;
-}
-
-/*
-Locate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before
-the global comment)
-*/
-local ZPOS64_T zip64local_SearchCentralDir64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
-
-local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
-{
-  unsigned char* buf;
-  ZPOS64_T uSizeFile;
-  ZPOS64_T uBackRead;
-  ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
-  ZPOS64_T uPosFound=0;
-  uLong uL;
-  ZPOS64_T relativeOffset;
-
-  if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
-    return 0;
-
-  uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
-
-  if (uMaxBack>uSizeFile)
-    uMaxBack = uSizeFile;
-
-  buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
-  if (buf==NULL)
-    return 0;
-
-  uBackRead = 4;
-  while (uBackRead<uMaxBack)
-  {
-    uLong uReadSize;
-    ZPOS64_T uReadPos;
-    int i;
-    if (uBackRead+BUFREADCOMMENT>uMaxBack)
-      uBackRead = uMaxBack;
-    else
-      uBackRead+=BUFREADCOMMENT;
-    uReadPos = uSizeFile-uBackRead ;
-
-    uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
-      (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
-    if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
-      break;
-
-    if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
-      break;
-
-    for (i=(int)uReadSize-3; (i--)>0;)
-    {
-      // Signature "0x07064b50" Zip64 end of central directory locater
-      if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
-      {
-        uPosFound = uReadPos+i;
-        break;
-      }
-    }
-
-      if (uPosFound!=0)
-        break;
-  }
-
-  TRYFREE(buf);
-  if (uPosFound == 0)
-    return 0;
-
-  /* Zip64 end of central directory locator */
-  if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
-    return 0;
-
-  /* the signature, already checked */
-  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
-    return 0;
-
-  /* number of the disk with the start of the zip64 end of  central directory */
-  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
-    return 0;
-  if (uL != 0)
-    return 0;
-
-  /* relative offset of the zip64 end of central directory record */
-  if (zip64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=ZIP_OK)
-    return 0;
-
-  /* total number of disks */
-  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
-    return 0;
-  if (uL != 1)
-    return 0;
-
-  /* Goto Zip64 end of central directory record */
-  if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
-    return 0;
-
-  /* the signature */
-  if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
-    return 0;
-
-  if (uL != 0x06064b50) // signature of 'Zip64 end of central directory'
-    return 0;
-
-  return relativeOffset;
-}
-
-int LoadCentralDirectoryRecord(zip64_internal* pziinit)
-{
-  int err=ZIP_OK;
-  ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
-
-  ZPOS64_T size_central_dir;     /* size of the central directory  */
-  ZPOS64_T offset_central_dir;   /* offset of start of central directory */
-  ZPOS64_T central_pos;
-  uLong uL;
-
-  uLong number_disk;          /* number of the current dist, used for
-                              spaning ZIP, unsupported, always 0*/
-  uLong number_disk_with_CD;  /* number the the disk with central dir, used
-                              for spaning ZIP, unsupported, always 0*/
-  ZPOS64_T number_entry;
-  ZPOS64_T number_entry_CD;      /* total number of entries in
-                                the central dir
-                                (same than number_entry on nospan) */
-  uLong VersionMadeBy;
-  uLong VersionNeeded;
-  uLong size_comment;
-
-  int hasZIP64Record = 0;
-
-  // check first if we find a ZIP64 record
-  central_pos = zip64local_SearchCentralDir64(&pziinit->z_filefunc,pziinit->filestream);
-  if(central_pos > 0)
-  {
-    hasZIP64Record = 1;
-  }
-  else if(central_pos == 0)
-  {
-    central_pos = zip64local_SearchCentralDir(&pziinit->z_filefunc,pziinit->filestream);
-  }
-
-/* disable to allow appending to empty ZIP archive
-        if (central_pos==0)
-            err=ZIP_ERRNO;
-*/
-
-  if(hasZIP64Record)
-  {
-    ZPOS64_T sizeEndOfCentralDirectory;
-    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos, ZLIB_FILEFUNC_SEEK_SET) != 0)
-      err=ZIP_ERRNO;
-
-    /* the signature, already checked */
-    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* size of zip64 end of central directory record */
-    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &sizeEndOfCentralDirectory)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* version made by */
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionMadeBy)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* version needed to extract */
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionNeeded)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* number of this disk */
-    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* number of the disk with the start of the central directory */
-    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* total number of entries in the central directory on this disk */
-    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &number_entry)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* total number of entries in the central directory */
-    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&number_entry_CD)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
-      err=ZIP_BADZIPFILE;
-
-    /* size of the central directory */
-    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&size_central_dir)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* offset of start of central directory with respect to the
-    starting disk number */
-    if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&offset_central_dir)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    // TODO..
-    // read the comment from the standard central header.
-    size_comment = 0;
-  }
-  else
-  {
-    // Read End of central Directory info
-    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
-      err=ZIP_ERRNO;
-
-    /* the signature, already checked */
-    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* number of this disk */
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* number of the disk with the start of the central directory */
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
-      err=ZIP_ERRNO;
-
-    /* total number of entries in the central dir on this disk */
-    number_entry = 0;
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
-      err=ZIP_ERRNO;
-    else
-      number_entry = uL;
-
-    /* total number of entries in the central dir */
-    number_entry_CD = 0;
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
-      err=ZIP_ERRNO;
-    else
-      number_entry_CD = uL;
-
-    if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
-      err=ZIP_BADZIPFILE;
-
-    /* size of the central directory */
-    size_central_dir = 0;
-    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
-      err=ZIP_ERRNO;
-    else
-      size_central_dir = uL;
-
-    /* offset of start of central directory with respect to the starting disk number */
-    offset_central_dir = 0;
-    if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
-      err=ZIP_ERRNO;
-    else
-      offset_central_dir = uL;
-
-
-    /* zipfile global comment length */
-    if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &size_comment)!=ZIP_OK)
-      err=ZIP_ERRNO;
-  }
-
-  if ((central_pos<offset_central_dir+size_central_dir) &&
-    (err==ZIP_OK))
-    err=ZIP_BADZIPFILE;
-
-  if (err!=ZIP_OK)
-  {
-    ZCLOSE64(pziinit->z_filefunc, pziinit->filestream);
-    return ZIP_ERRNO;
-  }
-
-  if (size_comment>0)
-  {
-    pziinit->globalcomment = (char*)ALLOC(size_comment+1);
-    if (pziinit->globalcomment)
-    {
-      size_comment = ZREAD64(pziinit->z_filefunc, pziinit->filestream, pziinit->globalcomment,size_comment);
-      pziinit->globalcomment[size_comment]=0;
-    }
-  }
-
-  byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir);
-  pziinit->add_position_when_writing_offset = byte_before_the_zipfile;
-
-  {
-    ZPOS64_T size_central_dir_to_read = size_central_dir;
-    size_t buf_size = SIZEDATA_INDATABLOCK;
-    void* buf_read = (void*)ALLOC(buf_size);
-    if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir + byte_before_the_zipfile, ZLIB_FILEFUNC_SEEK_SET) != 0)
-      err=ZIP_ERRNO;
-
-    while ((size_central_dir_to_read>0) && (err==ZIP_OK))
-    {
-      ZPOS64_T read_this = SIZEDATA_INDATABLOCK;
-      if (read_this > size_central_dir_to_read)
-        read_this = size_central_dir_to_read;
-
-      if (ZREAD64(pziinit->z_filefunc, pziinit->filestream,buf_read,(uLong)read_this) != read_this)
-        err=ZIP_ERRNO;
-
-      if (err==ZIP_OK)
-        err = add_data_in_datablock(&pziinit->central_dir,buf_read, (uLong)read_this);
-
-      size_central_dir_to_read-=read_this;
-    }
-    TRYFREE(buf_read);
-  }
-  pziinit->begin_pos = byte_before_the_zipfile;
-  pziinit->number_entry = number_entry_CD;
-
-  if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET) != 0)
-    err=ZIP_ERRNO;
-
-  return err;
-}
-
-
-#endif /* !NO_ADDFILEINEXISTINGZIP*/
-
-
-/************************************************************/
-extern zipFile ZEXPORT zipOpen3 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def)
-{
-    zip64_internal ziinit;
-    zip64_internal* zi;
-    int err=ZIP_OK;
-
-    ziinit.z_filefunc.zseek32_file = NULL;
-    ziinit.z_filefunc.ztell32_file = NULL;
-    if (pzlib_filefunc64_32_def==NULL)
-        fill_fopen64_filefunc(&ziinit.z_filefunc.zfile_func64);
-    else
-        ziinit.z_filefunc = *pzlib_filefunc64_32_def;
-
-    ziinit.filestream = ZOPEN64(ziinit.z_filefunc,
-                  pathname,
-                  (append == APPEND_STATUS_CREATE) ?
-                  (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) :
-                    (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING));
-
-    if (ziinit.filestream == NULL)
-        return NULL;
-
-    if (append == APPEND_STATUS_CREATEAFTER)
-        ZSEEK64(ziinit.z_filefunc,ziinit.filestream,0,SEEK_END);
-
-    ziinit.begin_pos = ZTELL64(ziinit.z_filefunc,ziinit.filestream);
-    ziinit.in_opened_file_inzip = 0;
-    ziinit.ci.stream_initialised = 0;
-    ziinit.number_entry = 0;
-    ziinit.add_position_when_writing_offset = 0;
-    init_linkedlist(&(ziinit.central_dir));
-
-
-
-    zi = (zip64_internal*)ALLOC(sizeof(zip64_internal));
-    if (zi==NULL)
-    {
-        ZCLOSE64(ziinit.z_filefunc,ziinit.filestream);
-        return NULL;
-    }
-
-    /* now we add file in a zipfile */
-#    ifndef NO_ADDFILEINEXISTINGZIP
-    ziinit.globalcomment = NULL;
-    if (append == APPEND_STATUS_ADDINZIP)
-    {
-      // Read and Cache Central Directory Records
-      err = LoadCentralDirectoryRecord(&ziinit);
-    }
-
-    if (globalcomment)
-    {
-      *globalcomment = ziinit.globalcomment;
-    }
-#    endif /* !NO_ADDFILEINEXISTINGZIP*/
-
-    if (err != ZIP_OK)
-    {
-#    ifndef NO_ADDFILEINEXISTINGZIP
-        TRYFREE(ziinit.globalcomment);
-#    endif /* !NO_ADDFILEINEXISTINGZIP*/
-        TRYFREE(zi);
-        return NULL;
-    }
-    else
-    {
-        *zi = ziinit;
-        return (zipFile)zi;
-    }
-}
-
-extern zipFile ZEXPORT zipOpen2 (const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def)
-{
-    if (pzlib_filefunc32_def != NULL)
-    {
-        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
-        fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);
-        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
-    }
-    else
-        return zipOpen3(pathname, append, globalcomment, NULL);
-}
-
-extern zipFile ZEXPORT zipOpen2_64 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def)
-{
-    if (pzlib_filefunc_def != NULL)
-    {
-        zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
-        zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
-        zlib_filefunc64_32_def_fill.ztell32_file = NULL;
-        zlib_filefunc64_32_def_fill.zseek32_file = NULL;
-        return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
-    }
-    else
-        return zipOpen3(pathname, append, globalcomment, NULL);
-}
-
-
-
-extern zipFile ZEXPORT zipOpen (const char* pathname, int append)
-{
-    return zipOpen3((const void*)pathname,append,NULL,NULL);
-}
-
-extern zipFile ZEXPORT zipOpen64 (const void* pathname, int append)
-{
-    return zipOpen3(pathname,append,NULL,NULL);
-}
-
-int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local)
-{
-  /* write the local header */
-  int err;
-  uInt size_filename = (uInt)strlen(filename);
-  uInt size_extrafield = size_extrafield_local;
-
-  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC, 4);
-
-  if (err==ZIP_OK)
-  {
-    if(zi->ci.zip64)
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);/* version needed to extract */
-    else
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */
-  }
-
-  if (err==ZIP_OK)
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2);
-
-  if (err==ZIP_OK)
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2);
-
-  if (err==ZIP_OK)
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4);
-
-  // CRC / Compressed size / Uncompressed size will be filled in later and rewritten later
-  if (err==ZIP_OK)
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */
-  if (err==ZIP_OK)
-  {
-    if(zi->ci.zip64)
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* compressed size, unknown */
-    else
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */
-  }
-  if (err==ZIP_OK)
-  {
-    if(zi->ci.zip64)
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* uncompressed size, unknown */
-    else
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */
-  }
-
-  if (err==ZIP_OK)
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2);
-
-  if(zi->ci.zip64)
-  {
-    size_extrafield += 20;
-  }
-
-  if (err==ZIP_OK)
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield,2);
-
-  if ((err==ZIP_OK) && (size_filename > 0))
-  {
-    if (ZWRITE64(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename)
-      err = ZIP_ERRNO;
-  }
-
-  if ((err==ZIP_OK) && (size_extrafield_local > 0))
-  {
-    if (ZWRITE64(zi->z_filefunc, zi->filestream, extrafield_local, size_extrafield_local) != size_extrafield_local)
-      err = ZIP_ERRNO;
-  }
-
-
-  if ((err==ZIP_OK) && (zi->ci.zip64))
-  {
-      // write the Zip64 extended info
-      short HeaderID = 1;
-      short DataSize = 16;
-      ZPOS64_T CompressedSize = 0;
-      ZPOS64_T UncompressedSize = 0;
-
-      // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file)
-      zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream);
-
-      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)HeaderID,2);
-      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)DataSize,2);
-
-      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8);
-      err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8);
-  }
-
-  return err;
-}
-
-/*
- NOTE.
- When writing RAW the ZIP64 extended information in extrafield_local and extrafield_global needs to be stripped
- before calling this function it can be done with zipRemoveExtraInfoBlock
-
- It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize
- unnecessary allocations.
- */
-extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting,
-                                         uLong versionMadeBy, uLong flagBase, int zip64)
-{
-    zip64_internal* zi;
-    uInt size_filename;
-    uInt size_comment;
-    uInt i;
-    int err = ZIP_OK;
-
-#    ifdef NOCRYPT
-    (crcForCrypting);
-    if (password != NULL)
-        return ZIP_PARAMERROR;
-#    endif
-
-    if (file == NULL)
-        return ZIP_PARAMERROR;
-
-#ifdef HAVE_BZIP2
-    if ((method!=0) && (method!=Z_DEFLATED) && (method!=Z_BZIP2ED))
-      return ZIP_PARAMERROR;
-#else
-    if ((method!=0) && (method!=Z_DEFLATED))
-      return ZIP_PARAMERROR;
-#endif
-
-    zi = (zip64_internal*)file;
-
-    if (zi->in_opened_file_inzip == 1)
-    {
-        err = zipCloseFileInZip (file);
-        if (err != ZIP_OK)
-            return err;
-    }
-
-    if (filename==NULL)
-        filename="-";
-
-    if (comment==NULL)
-        size_comment = 0;
-    else
-        size_comment = (uInt)strlen(comment);
-
-    size_filename = (uInt)strlen(filename);
-
-    if (zipfi == NULL)
-        zi->ci.dosDate = 0;
-    else
-    {
-        if (zipfi->dosDate != 0)
-            zi->ci.dosDate = zipfi->dosDate;
-        else
-          zi->ci.dosDate = zip64local_TmzDateToDosDate(&zipfi->tmz_date);
-    }
-
-    zi->ci.flag = flagBase;
-    if ((level==8) || (level==9))
-      zi->ci.flag |= 2;
-    if (level==2)
-      zi->ci.flag |= 4;
-    if (level==1)
-      zi->ci.flag |= 6;
-    if (password != NULL)
-      zi->ci.flag |= 1;
-
-    zi->ci.crc32 = 0;
-    zi->ci.method = method;
-    zi->ci.encrypt = 0;
-    zi->ci.stream_initialised = 0;
-    zi->ci.pos_in_buffered_data = 0;
-    zi->ci.raw = raw;
-    zi->ci.pos_local_header = ZTELL64(zi->z_filefunc,zi->filestream);
-
-    zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + size_extrafield_global + size_comment;
-    zi->ci.size_centralExtraFree = 32; // Extra space we have reserved in case we need to add ZIP64 extra info data
-
-    zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader + zi->ci.size_centralExtraFree);
-
-    zi->ci.size_centralExtra = size_extrafield_global;
-    zip64local_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4);
-    /* version info */
-    zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)versionMadeBy,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4);
-    zip64local_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/
-    zip64local_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/
-    zip64local_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/
-    zip64local_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2);
-    zip64local_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/
-
-    if (zipfi==NULL)
-        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2);
-    else
-        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2);
-
-    if (zipfi==NULL)
-        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4);
-    else
-        zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4);
-
-    if(zi->ci.pos_local_header >= 0xffffffff)
-      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4);
-    else
-      zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writing_offset,4);
-
-    for (i=0;i<size_filename;i++)
-        *(zi->ci.central_header+SIZECENTRALHEADER+i) = *(filename+i);
-
-    for (i=0;i<size_extrafield_global;i++)
-        *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+i) =
-              *(((const char*)extrafield_global)+i);
-
-    for (i=0;i<size_comment;i++)
-        *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+
-              size_extrafield_global+i) = *(comment+i);
-    if (zi->ci.central_header == NULL)
-        return ZIP_INTERNALERROR;
-
-    zi->ci.zip64 = zip64;
-    zi->ci.totalCompressedData = 0;
-    zi->ci.totalUncompressedData = 0;
-    zi->ci.pos_zip64extrainfo = 0;
-
-    err = Write_LocalFileHeader(zi, filename, size_extrafield_local, extrafield_local);
-
-#ifdef HAVE_BZIP2
-    zi->ci.bstream.avail_in = (uInt)0;
-    zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
-    zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
-    zi->ci.bstream.total_in_hi32 = 0;
-    zi->ci.bstream.total_in_lo32 = 0;
-    zi->ci.bstream.total_out_hi32 = 0;
-    zi->ci.bstream.total_out_lo32 = 0;
-#endif
-
-    zi->ci.stream.avail_in = (uInt)0;
-    zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
-    zi->ci.stream.next_out = zi->ci.buffered_data;
-    zi->ci.stream.total_in = 0;
-    zi->ci.stream.total_out = 0;
-    zi->ci.stream.data_type = Z_BINARY;
-
-#ifdef HAVE_BZIP2
-    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED || zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
-#else
-    if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
-#endif
-    {
-        if(zi->ci.method == Z_DEFLATED)
-        {
-          zi->ci.stream.zalloc = (alloc_func)0;
-          zi->ci.stream.zfree = (free_func)0;
-          zi->ci.stream.opaque = (voidpf)0;
-
-          if (windowBits>0)
-              windowBits = -windowBits;
-
-          err = deflateInit2(&zi->ci.stream, level, Z_DEFLATED, windowBits, memLevel, strategy);
-
-          if (err==Z_OK)
-              zi->ci.stream_initialised = Z_DEFLATED;
-        }
-        else if(zi->ci.method == Z_BZIP2ED)
-        {
-#ifdef HAVE_BZIP2
-            // Init BZip stuff here
-          zi->ci.bstream.bzalloc = 0;
-          zi->ci.bstream.bzfree = 0;
-          zi->ci.bstream.opaque = (voidpf)0;
-
-          err = BZ2_bzCompressInit(&zi->ci.bstream, level, 0,35);
-          if(err == BZ_OK)
-            zi->ci.stream_initialised = Z_BZIP2ED;
-#endif
-        }
-
-    }
-
-#    ifndef NOCRYPT
-    zi->ci.crypt_header_size = 0;
-    if ((err==Z_OK) && (password != NULL))
-    {
-        unsigned char bufHead[RAND_HEAD_LEN];
-        unsigned int sizeHead;
-        zi->ci.encrypt = 1;
-        zi->ci.pcrc_32_tab = get_crc_table();
-        /*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/
-
-        sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);
-        zi->ci.crypt_header_size = sizeHead;
-
-        if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)
-                err = ZIP_ERRNO;
-    }
-#    endif
-
-    if (err==Z_OK)
-        zi->in_opened_file_inzip = 1;
-    return err;
-}
-
-extern int ZEXPORT zipOpenNewFileInZip4 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting,
-                                         uLong versionMadeBy, uLong flagBase)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 windowBits, memLevel, strategy,
-                                 password, crcForCrypting, versionMadeBy, flagBase, 0);
-}
-
-extern int ZEXPORT zipOpenNewFileInZip3 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 windowBits, memLevel, strategy,
-                                 password, crcForCrypting, VERSIONMADEBY, 0, 0);
-}
-
-extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                         const void* extrafield_local, uInt size_extrafield_local,
-                                         const void* extrafield_global, uInt size_extrafield_global,
-                                         const char* comment, int method, int level, int raw,
-                                         int windowBits,int memLevel, int strategy,
-                                         const char* password, uLong crcForCrypting, int zip64)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 windowBits, memLevel, strategy,
-                                 password, crcForCrypting, VERSIONMADEBY, 0, zip64);
-}
-
-extern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void* extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level, int raw)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, 0);
-}
-
-extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void* extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level, int raw, int zip64)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, raw,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, zip64);
-}
-
-extern int ZEXPORT zipOpenNewFileInZip64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void*extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level, int zip64)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, 0,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, zip64);
-}
-
-extern int ZEXPORT zipOpenNewFileInZip (zipFile file, const char* filename, const zip_fileinfo* zipfi,
-                                        const void* extrafield_local, uInt size_extrafield_local,
-                                        const void*extrafield_global, uInt size_extrafield_global,
-                                        const char* comment, int method, int level)
-{
-    return zipOpenNewFileInZip4_64 (file, filename, zipfi,
-                                 extrafield_local, size_extrafield_local,
-                                 extrafield_global, size_extrafield_global,
-                                 comment, method, level, 0,
-                                 -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
-                                 NULL, 0, VERSIONMADEBY, 0, 0);
-}
-
-local int zip64FlushWriteBuffer(zip64_internal* zi)
-{
-    int err=ZIP_OK;
-
-    if (zi->ci.encrypt != 0)
-    {
-#ifndef NOCRYPT
-        uInt i;
-        int t;
-        for (i=0;i<zi->ci.pos_in_buffered_data;i++)
-            zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);
-#endif
-    }
-
-    if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
-      err = ZIP_ERRNO;
-
-    zi->ci.totalCompressedData += zi->ci.pos_in_buffered_data;
-
-#ifdef HAVE_BZIP2
-    if(zi->ci.method == Z_BZIP2ED)
-    {
-      zi->ci.totalUncompressedData += zi->ci.bstream.total_in_lo32;
-      zi->ci.bstream.total_in_lo32 = 0;
-      zi->ci.bstream.total_in_hi32 = 0;
-    }
-    else
-#endif
-    {
-      zi->ci.totalUncompressedData += zi->ci.stream.total_in;
-      zi->ci.stream.total_in = 0;
-    }
-
-
-    zi->ci.pos_in_buffered_data = 0;
-
-    return err;
-}
-
-extern int ZEXPORT zipWriteInFileInZip (zipFile file,const void* buf,unsigned int len)
-{
-    zip64_internal* zi;
-    int err=ZIP_OK;
-
-    if (file == NULL)
-        return ZIP_PARAMERROR;
-    zi = (zip64_internal*)file;
-
-    if (zi->in_opened_file_inzip == 0)
-        return ZIP_PARAMERROR;
-
-    zi->ci.crc32 = crc32(zi->ci.crc32,buf,(uInt)len);
-
-#ifdef HAVE_BZIP2
-    if(zi->ci.method == Z_BZIP2ED && (!zi->ci.raw))
-    {
-      zi->ci.bstream.next_in = (void*)buf;
-      zi->ci.bstream.avail_in = len;
-      err = BZ_RUN_OK;
-
-      while ((err==BZ_RUN_OK) && (zi->ci.bstream.avail_in>0))
-      {
-        if (zi->ci.bstream.avail_out == 0)
-        {
-          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
-            err = ZIP_ERRNO;
-          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
-          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
-        }
-
-
-        if(err != BZ_RUN_OK)
-          break;
-
-        if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
-        {
-          uLong uTotalOutBefore_lo = zi->ci.bstream.total_out_lo32;
-//          uLong uTotalOutBefore_hi = zi->ci.bstream.total_out_hi32;
-          err=BZ2_bzCompress(&zi->ci.bstream,  BZ_RUN);
-
-          zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore_lo) ;
-        }
-      }
-
-      if(err == BZ_RUN_OK)
-        err = ZIP_OK;
-    }
-    else
-#endif
-    {
-      zi->ci.stream.next_in = (Bytef*)buf;
-      zi->ci.stream.avail_in = len;
-
-      while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0))
-      {
-          if (zi->ci.stream.avail_out == 0)
-          {
-              if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
-                  err = ZIP_ERRNO;
-              zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
-              zi->ci.stream.next_out = zi->ci.buffered_data;
-          }
-
-
-          if(err != ZIP_OK)
-              break;
-
-          if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
-          {
-              uLong uTotalOutBefore = zi->ci.stream.total_out;
-              err=deflate(&zi->ci.stream,  Z_NO_FLUSH);
-              if(uTotalOutBefore > zi->ci.stream.total_out)
-              {
-                int bBreak = 0;
-                bBreak++;
-              }
-
-              zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
-          }
-          else
-          {
-              uInt copy_this,i;
-              if (zi->ci.stream.avail_in < zi->ci.stream.avail_out)
-                  copy_this = zi->ci.stream.avail_in;
-              else
-                  copy_this = zi->ci.stream.avail_out;
-
-              for (i = 0; i < copy_this; i++)
-                  *(((char*)zi->ci.stream.next_out)+i) =
-                      *(((const char*)zi->ci.stream.next_in)+i);
-              {
-                  zi->ci.stream.avail_in -= copy_this;
-                  zi->ci.stream.avail_out-= copy_this;
-                  zi->ci.stream.next_in+= copy_this;
-                  zi->ci.stream.next_out+= copy_this;
-                  zi->ci.stream.total_in+= copy_this;
-                  zi->ci.stream.total_out+= copy_this;
-                  zi->ci.pos_in_buffered_data += copy_this;
-              }
-          }
-      }// while(...)
-    }
-
-    return err;
-}
-
-extern int ZEXPORT zipCloseFileInZipRaw (zipFile file, uLong uncompressed_size, uLong crc32)
-{
-    return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32);
-}
-
-extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_size, uLong crc32)
-{
-    zip64_internal* zi;
-    ZPOS64_T compressed_size;
-    uLong invalidValue = 0xffffffff;
-    short datasize = 0;
-    int err=ZIP_OK;
-
-    if (file == NULL)
-        return ZIP_PARAMERROR;
-    zi = (zip64_internal*)file;
-
-    if (zi->in_opened_file_inzip == 0)
-        return ZIP_PARAMERROR;
-    zi->ci.stream.avail_in = 0;
-
-    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
-                {
-                        while (err==ZIP_OK)
-                        {
-                                uLong uTotalOutBefore;
-                                if (zi->ci.stream.avail_out == 0)
-                                {
-                                        if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
-                                                err = ZIP_ERRNO;
-                                        zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
-                                        zi->ci.stream.next_out = zi->ci.buffered_data;
-                                }
-                                uTotalOutBefore = zi->ci.stream.total_out;
-                                err=deflate(&zi->ci.stream,  Z_FINISH);
-                                zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
-                        }
-                }
-    else if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
-    {
-#ifdef HAVE_BZIP2
-      err = BZ_FINISH_OK;
-      while (err==BZ_FINISH_OK)
-      {
-        uLong uTotalOutBefore;
-        if (zi->ci.bstream.avail_out == 0)
-        {
-          if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
-            err = ZIP_ERRNO;
-          zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
-          zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
-        }
-        uTotalOutBefore = zi->ci.bstream.total_out_lo32;
-        err=BZ2_bzCompress(&zi->ci.bstream,  BZ_FINISH);
-        if(err == BZ_STREAM_END)
-          err = Z_STREAM_END;
-
-        zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore);
-      }
-
-      if(err == BZ_FINISH_OK)
-        err = ZIP_OK;
-#endif
-    }
-
-    if (err==Z_STREAM_END)
-        err=ZIP_OK; /* this is normal */
-
-    if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK))
-                {
-        if (zip64FlushWriteBuffer(zi)==ZIP_ERRNO)
-            err = ZIP_ERRNO;
-                }
-
-    if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
-    {
-        int tmp_err = deflateEnd(&zi->ci.stream);
-        if (err == ZIP_OK)
-            err = tmp_err;
-        zi->ci.stream_initialised = 0;
-    }
-#ifdef HAVE_BZIP2
-    else if((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
-    {
-      int tmperr = BZ2_bzCompressEnd(&zi->ci.bstream);
-                        if (err==ZIP_OK)
-                                err = tmperr;
-                        zi->ci.stream_initialised = 0;
-    }
-#endif
-
-    if (!zi->ci.raw)
-    {
-        crc32 = (uLong)zi->ci.crc32;
-        uncompressed_size = zi->ci.totalUncompressedData;
-    }
-    compressed_size = zi->ci.totalCompressedData;
-
-#    ifndef NOCRYPT
-    compressed_size += zi->ci.crypt_header_size;
-#    endif
-
-    // update Current Item crc and sizes,
-    if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
-    {
-      /*version Made by*/
-      zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)45,2);
-      /*version needed*/
-      zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)45,2);
-
-    }
-
-    zip64local_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/
-
-
-    if(compressed_size >= 0xffffffff)
-      zip64local_putValue_inmemory(zi->ci.central_header+20, invalidValue,4); /*compr size*/
-    else
-      zip64local_putValue_inmemory(zi->ci.central_header+20, compressed_size,4); /*compr size*/
-
-    /// set internal file attributes field
-    if (zi->ci.stream.data_type == Z_ASCII)
-        zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2);
-
-    if(uncompressed_size >= 0xffffffff)
-      zip64local_putValue_inmemory(zi->ci.central_header+24, invalidValue,4); /*uncompr size*/
-    else
-      zip64local_putValue_inmemory(zi->ci.central_header+24, uncompressed_size,4); /*uncompr size*/
-
-    // Add ZIP64 extra info field for uncompressed size
-    if(uncompressed_size >= 0xffffffff)
-      datasize += 8;
-
-    // Add ZIP64 extra info field for compressed size
-    if(compressed_size >= 0xffffffff)
-      datasize += 8;
-
-    // Add ZIP64 extra info field for relative offset to local file header of current file
-    if(zi->ci.pos_local_header >= 0xffffffff)
-      datasize += 8;
-
-    if(datasize > 0)
-    {
-      char* p = NULL;
-
-      if((uLong)(datasize + 4) > zi->ci.size_centralExtraFree)
-      {
-        // we can not write more data to the buffer that we have room for.
-        return ZIP_BADZIPFILE;
-      }
-
-      p = zi->ci.central_header + zi->ci.size_centralheader;
-
-      // Add Extra Information Header for 'ZIP64 information'
-      zip64local_putValue_inmemory(p, 0x0001, 2); // HeaderID
-      p += 2;
-      zip64local_putValue_inmemory(p, datasize, 2); // DataSize
-      p += 2;
-
-      if(uncompressed_size >= 0xffffffff)
-      {
-        zip64local_putValue_inmemory(p, uncompressed_size, 8);
-        p += 8;
-      }
-
-      if(compressed_size >= 0xffffffff)
-      {
-        zip64local_putValue_inmemory(p, compressed_size, 8);
-        p += 8;
-      }
-
-      if(zi->ci.pos_local_header >= 0xffffffff)
-      {
-        zip64local_putValue_inmemory(p, zi->ci.pos_local_header, 8);
-        p += 8;
-      }
-
-      // Update how much extra free space we got in the memory buffer
-      // and increase the centralheader size so the new ZIP64 fields are included
-      // ( 4 below is the size of HeaderID and DataSize field )
-      zi->ci.size_centralExtraFree -= datasize + 4;
-      zi->ci.size_centralheader += datasize + 4;
-
-      // Update the extra info size field
-      zi->ci.size_centralExtra += datasize + 4;
-      zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)zi->ci.size_centralExtra,2);
-    }
-
-    if (err==ZIP_OK)
-        err = add_data_in_datablock(&zi->central_dir, zi->ci.central_header, (uLong)zi->ci.size_centralheader);
-
-    free(zi->ci.central_header);
-
-    if (err==ZIP_OK)
-    {
-        // Update the LocalFileHeader with the new values.
-
-        ZPOS64_T cur_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
-
-        if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0)
-            err = ZIP_ERRNO;
-
-        if (err==ZIP_OK)
-            err = zip64local_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */
-
-        if(uncompressed_size >= 0xffffffff || compressed_size >= 0xffffffff )
-        {
-          if(zi->ci.pos_zip64extrainfo > 0)
-          {
-            // Update the size in the ZIP64 extended field.
-            if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_zip64extrainfo + 4,ZLIB_FILEFUNC_SEEK_SET)!=0)
-              err = ZIP_ERRNO;
-
-            if (err==ZIP_OK) /* compressed size, unknown */
-              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, uncompressed_size, 8);
-
-            if (err==ZIP_OK) /* uncompressed size, unknown */
-              err = zip64local_putValue(&zi->z_filefunc, zi->filestream, compressed_size, 8);
-          }
-          else
-              err = ZIP_BADZIPFILE; // Caller passed zip64 = 0, so no room for zip64 info -> fatal
-        }
-        else
-        {
-          if (err==ZIP_OK) /* compressed size, unknown */
-              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4);
-
-          if (err==ZIP_OK) /* uncompressed size, unknown */
-              err = zip64local_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4);
-        }
-
-        if (ZSEEK64(zi->z_filefunc,zi->filestream, cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0)
-            err = ZIP_ERRNO;
-    }
-
-    zi->number_entry ++;
-    zi->in_opened_file_inzip = 0;
-
-    return err;
-}
-
-extern int ZEXPORT zipCloseFileInZip (zipFile file)
-{
-    return zipCloseFileInZipRaw (file,0,0);
-}
-
-int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip)
-{
-  int err = ZIP_OK;
-  ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset;
-
-  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4);
-
-  /*num disks*/
-    if (err==ZIP_OK) /* number of the disk with the start of the central directory */
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
-
-  /*relative offset*/
-    if (err==ZIP_OK) /* Relative offset to the Zip64EndOfCentralDirectory */
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, pos,8);
-
-  /*total disks*/ /* Do not support spawning of disk so always say 1 here*/
-    if (err==ZIP_OK) /* number of the disk with the start of the central directory */
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)1,4);
-
-    return err;
-}
-
-int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
-{
-  int err = ZIP_OK;
-
-  uLong Zip64DataSize = 44;
-
-  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDHEADERMAGIC,4);
-
-  if (err==ZIP_OK) /* size of this 'zip64 end of central directory' */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)Zip64DataSize,8); // why ZPOS64_T of this ?
-
-  if (err==ZIP_OK) /* version made by */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
-
-  if (err==ZIP_OK) /* version needed */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
-
-  if (err==ZIP_OK) /* number of this disk */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
-
-  if (err==ZIP_OK) /* number of the disk with the start of the central directory */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
-
-  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
-    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
-
-  if (err==ZIP_OK) /* total number of entries in the central dir */
-    err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
-
-  if (err==ZIP_OK) /* size of the central directory */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)size_centraldir,8);
-
-  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
-  {
-    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8);
-  }
-  return err;
-}
-int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
-{
-  int err = ZIP_OK;
-
-  /*signature*/
-  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4);
-
-  if (err==ZIP_OK) /* number of this disk */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
-
-  if (err==ZIP_OK) /* number of the disk with the start of the central directory */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
-
-  if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
-  {
-    {
-      if(zi->number_entry >= 0xFFFF)
-        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
-      else
-        err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
-    }
-  }
-
-  if (err==ZIP_OK) /* total number of entries in the central dir */
-  {
-    if(zi->number_entry >= 0xFFFF)
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
-    else
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
-  }
-
-  if (err==ZIP_OK) /* size of the central directory */
-    err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4);
-
-  if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
-  {
-    ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
-    if(pos >= 0xffffffff)
-    {
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4);
-    }
-    else
-      err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writing_offset),4);
-  }
-
-   return err;
-}
-
-int Write_GlobalComment(zip64_internal* zi, const char* global_comment)
-{
-  int err = ZIP_OK;
-  uInt size_global_comment = 0;
-
-  if(global_comment != NULL)
-    size_global_comment = (uInt)strlen(global_comment);
-
-  err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2);
-
-  if (err == ZIP_OK && size_global_comment > 0)
-  {
-    if (ZWRITE64(zi->z_filefunc,zi->filestream, global_comment, size_global_comment) != size_global_comment)
-      err = ZIP_ERRNO;
-  }
-  return err;
-}
-
-extern int ZEXPORT zipClose (zipFile file, const char* global_comment)
-{
-    zip64_internal* zi;
-    int err = 0;
-    uLong size_centraldir = 0;
-    ZPOS64_T centraldir_pos_inzip;
-    ZPOS64_T pos;
-
-    if (file == NULL)
-        return ZIP_PARAMERROR;
-
-    zi = (zip64_internal*)file;
-
-    if (zi->in_opened_file_inzip == 1)
-    {
-        err = zipCloseFileInZip (file);
-    }
-
-#ifndef NO_ADDFILEINEXISTINGZIP
-    if (global_comment==NULL)
-        global_comment = zi->globalcomment;
-#endif
-
-    centraldir_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
-
-    if (err==ZIP_OK)
-    {
-        linkedlist_datablock_internal* ldi = zi->central_dir.first_block;
-        while (ldi!=NULL)
-        {
-            if ((err==ZIP_OK) && (ldi->filled_in_this_block>0))
-            {
-                if (ZWRITE64(zi->z_filefunc,zi->filestream, ldi->data, ldi->filled_in_this_block) != ldi->filled_in_this_block)
-                    err = ZIP_ERRNO;
-            }
-
-            size_centraldir += ldi->filled_in_this_block;
-            ldi = ldi->next_datablock;
-        }
-    }
-    free_linkedlist(&(zi->central_dir));
-
-    pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
-    if(pos >= 0xffffffff || zi->number_entry > 0xFFFF)
-    {
-      ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream);
-      Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
-
-      Write_Zip64EndOfCentralDirectoryLocator(zi, Zip64EOCDpos);
-    }
-
-    if (err==ZIP_OK)
-      err = Write_EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
-
-    if(err == ZIP_OK)
-      err = Write_GlobalComment(zi, global_comment);
-
-    if (ZCLOSE64(zi->z_filefunc,zi->filestream) != 0)
-        if (err == ZIP_OK)
-            err = ZIP_ERRNO;
-
-#ifndef NO_ADDFILEINEXISTINGZIP
-    TRYFREE(zi->globalcomment);
-#endif
-    TRYFREE(zi);
-
-    return err;
-}
-
-extern int ZEXPORT zipRemoveExtraInfoBlock (char* pData, int* dataLen, short sHeader)
-{
-  char* p = pData;
-  int size = 0;
-  char* pNewHeader;
-  char* pTmp;
-  short header;
-  short dataSize;
-
-  int retVal = ZIP_OK;
-
-  if(pData == NULL || *dataLen < 4)
-    return ZIP_PARAMERROR;
-
-  pNewHeader = (char*)ALLOC(*dataLen);
-  pTmp = pNewHeader;
-
-  while(p < (pData + *dataLen))
-  {
-    header = *(short*)p;
-    dataSize = *(((short*)p)+1);
-
-    if( header == sHeader ) // Header found.
-    {
-      p += dataSize + 4; // skip it. do not copy to temp buffer
-    }
-    else
-    {
-      // Extra Info block should not be removed, So copy it to the temp buffer.
-      memcpy(pTmp, p, dataSize + 4);
-      p += dataSize + 4;
-      size += dataSize + 4;
-    }
-
-  }
-
-  if(size < *dataLen)
-  {
-    // clean old extra info block.
-    memset(pData,0, *dataLen);
-
-    // copy the new extra info block over the old
-    if(size > 0)
-      memcpy(pData, pNewHeader, size);
-
-    // set the new extra info size
-    *dataLen = size;
-
-    retVal = ZIP_OK;
-  }
-  else
-    retVal = ZIP_ERRNO;
-
-  TRYFREE(pNewHeader);
-
-  return retVal;
-}
--- a/Resources/ThirdParty/minizip/zip.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,362 +0,0 @@
-/* zip.h -- IO on .zip files using zlib
-   Version 1.1, February 14h, 2010
-   part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
-
-         Modifications for Zip64 support
-         Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
-
-         For more info read MiniZip_info.txt
-
-         ---------------------------------------------------------------------------
-
-   Condition of use and distribution are the same than zlib :
-
-  This software is provided 'as-is', without any express or implied
-  warranty.  In no event will the authors be held liable for any damages
-  arising from the use of this software.
-
-  Permission is granted to anyone to use this software for any purpose,
-  including commercial applications, and to alter it and redistribute it
-  freely, subject to the following restrictions:
-
-  1. The origin of this software must not be misrepresented; you must not
-     claim that you wrote the original software. If you use this software
-     in a product, an acknowledgment in the product documentation would be
-     appreciated but is not required.
-  2. Altered source versions must be plainly marked as such, and must not be
-     misrepresented as being the original software.
-  3. This notice may not be removed or altered from any source distribution.
-
-        ---------------------------------------------------------------------------
-
-        Changes
-
-        See header of zip.h
-
-*/
-
-#ifndef _zip12_H
-#define _zip12_H
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-//#define HAVE_BZIP2
-
-#ifndef _ZLIB_H
-#include "zlib.h"
-#endif
-
-#ifndef _ZLIBIOAPI_H
-#include "ioapi.h"
-#endif
-
-#ifdef HAVE_BZIP2
-#include "bzlib.h"
-#endif
-
-#define Z_BZIP2ED 12
-
-#if defined(STRICTZIP) || defined(STRICTZIPUNZIP)
-/* like the STRICT of WIN32, we define a pointer that cannot be converted
-    from (void*) without cast */
-typedef struct TagzipFile__ { int unused; } zipFile__;
-typedef zipFile__ *zipFile;
-#else
-typedef voidp zipFile;
-#endif
-
-#define ZIP_OK                          (0)
-#define ZIP_EOF                         (0)
-#define ZIP_ERRNO                       (Z_ERRNO)
-#define ZIP_PARAMERROR                  (-102)
-#define ZIP_BADZIPFILE                  (-103)
-#define ZIP_INTERNALERROR               (-104)
-
-#ifndef DEF_MEM_LEVEL
-#  if MAX_MEM_LEVEL >= 8
-#    define DEF_MEM_LEVEL 8
-#  else
-#    define DEF_MEM_LEVEL  MAX_MEM_LEVEL
-#  endif
-#endif
-/* default memLevel */
-
-/* tm_zip contain date/time info */
-typedef struct tm_zip_s
-{
-    uInt tm_sec;            /* seconds after the minute - [0,59] */
-    uInt tm_min;            /* minutes after the hour - [0,59] */
-    uInt tm_hour;           /* hours since midnight - [0,23] */
-    uInt tm_mday;           /* day of the month - [1,31] */
-    uInt tm_mon;            /* months since January - [0,11] */
-    uInt tm_year;           /* years - [1980..2044] */
-} tm_zip;
-
-typedef struct
-{
-    tm_zip      tmz_date;       /* date in understandable format           */
-    uLong       dosDate;       /* if dos_date == 0, tmu_date is used      */
-/*    uLong       flag;        */   /* general purpose bit flag        2 bytes */
-
-    uLong       internal_fa;    /* internal file attributes        2 bytes */
-    uLong       external_fa;    /* external file attributes        4 bytes */
-} zip_fileinfo;
-
-typedef const char* zipcharpc;
-
-
-#define APPEND_STATUS_CREATE        (0)
-#define APPEND_STATUS_CREATEAFTER   (1)
-#define APPEND_STATUS_ADDINZIP      (2)
-
-extern zipFile ZEXPORT zipOpen OF((const char *pathname, int append));
-extern zipFile ZEXPORT zipOpen64 OF((const void *pathname, int append));
-/*
-  Create a zipfile.
-     pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on
-       an Unix computer "zlib/zlib113.zip".
-     if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip
-       will be created at the end of the file.
-         (useful if the file contain a self extractor code)
-     if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will
-       add files in existing zip (be sure you don't add file that doesn't exist)
-     If the zipfile cannot be opened, the return value is NULL.
-     Else, the return value is a zipFile Handle, usable with other function
-       of this zip package.
-*/
-
-/* Note : there is no delete function into a zipfile.
-   If you want delete file into a zipfile, you must open a zipfile, and create another
-   Of couse, you can use RAW reading and writing to copy the file you did not want delte
-*/
-
-extern zipFile ZEXPORT zipOpen2 OF((const char *pathname,
-                                   int append,
-                                   zipcharpc* globalcomment,
-                                   zlib_filefunc_def* pzlib_filefunc_def));
-
-extern zipFile ZEXPORT zipOpen2_64 OF((const void *pathname,
-                                   int append,
-                                   zipcharpc* globalcomment,
-                                   zlib_filefunc64_def* pzlib_filefunc_def));
-
-extern int ZEXPORT zipOpenNewFileInZip OF((zipFile file,
-                       const char* filename,
-                       const zip_fileinfo* zipfi,
-                       const void* extrafield_local,
-                       uInt size_extrafield_local,
-                       const void* extrafield_global,
-                       uInt size_extrafield_global,
-                       const char* comment,
-                       int method,
-                       int level));
-
-extern int ZEXPORT zipOpenNewFileInZip64 OF((zipFile file,
-                       const char* filename,
-                       const zip_fileinfo* zipfi,
-                       const void* extrafield_local,
-                       uInt size_extrafield_local,
-                       const void* extrafield_global,
-                       uInt size_extrafield_global,
-                       const char* comment,
-                       int method,
-                       int level,
-                       int zip64));
-
-/*
-  Open a file in the ZIP for writing.
-  filename : the filename in zip (if NULL, '-' without quote will be used
-  *zipfi contain supplemental information
-  if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local
-    contains the extrafield data the the local header
-  if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global
-    contains the extrafield data the the local header
-  if comment != NULL, comment contain the comment string
-  method contain the compression method (0 for store, Z_DEFLATED for deflate)
-  level contain the level of compression (can be Z_DEFAULT_COMPRESSION)
-  zip64 is set to 1 if a zip64 extended information block should be added to the local file header.
-                    this MUST be '1' if the uncompressed size is >= 0xffffffff.
-
-*/
-
-
-extern int ZEXPORT zipOpenNewFileInZip2 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw));
-
-
-extern int ZEXPORT zipOpenNewFileInZip2_64 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int zip64));
-/*
-  Same than zipOpenNewFileInZip, except if raw=1, we write raw file
- */
-
-extern int ZEXPORT zipOpenNewFileInZip3 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting));
-
-extern int ZEXPORT zipOpenNewFileInZip3_64 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting,
-                                            int zip64
-                                            ));
-
-/*
-  Same than zipOpenNewFileInZip2, except
-    windowBits,memLevel,,strategy : see parameter strategy in deflateInit2
-    password : crypting password (NULL for no crypting)
-    crcForCrypting : crc of file to compress (needed for crypting)
- */
-
-extern int ZEXPORT zipOpenNewFileInZip4 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting,
-                                            uLong versionMadeBy,
-                                            uLong flagBase
-                                            ));
-
-
-extern int ZEXPORT zipOpenNewFileInZip4_64 OF((zipFile file,
-                                            const char* filename,
-                                            const zip_fileinfo* zipfi,
-                                            const void* extrafield_local,
-                                            uInt size_extrafield_local,
-                                            const void* extrafield_global,
-                                            uInt size_extrafield_global,
-                                            const char* comment,
-                                            int method,
-                                            int level,
-                                            int raw,
-                                            int windowBits,
-                                            int memLevel,
-                                            int strategy,
-                                            const char* password,
-                                            uLong crcForCrypting,
-                                            uLong versionMadeBy,
-                                            uLong flagBase,
-                                            int zip64
-                                            ));
-/*
-  Same than zipOpenNewFileInZip4, except
-    versionMadeBy : value for Version made by field
-    flag : value for flag field (compression level info will be added)
- */
-
-
-extern int ZEXPORT zipWriteInFileInZip OF((zipFile file,
-                       const void* buf,
-                       unsigned len));
-/*
-  Write data in the zipfile
-*/
-
-extern int ZEXPORT zipCloseFileInZip OF((zipFile file));
-/*
-  Close the current file in the zipfile
-*/
-
-extern int ZEXPORT zipCloseFileInZipRaw OF((zipFile file,
-                                            uLong uncompressed_size,
-                                            uLong crc32));
-
-extern int ZEXPORT zipCloseFileInZipRaw64 OF((zipFile file,
-                                            ZPOS64_T uncompressed_size,
-                                            uLong crc32));
-
-/*
-  Close the current file in the zipfile, for file opened with
-    parameter raw=1 in zipOpenNewFileInZip2
-  uncompressed_size and crc32 are value for the uncompressed size
-*/
-
-extern int ZEXPORT zipClose OF((zipFile file,
-                const char* global_comment));
-/*
-  Close the zipfile
-*/
-
-
-extern int ZEXPORT zipRemoveExtraInfoBlock OF((char* pData, int* dataLen, short sHeader));
-/*
-  zipRemoveExtraInfoBlock -  Added by Mathias Svensson
-
-  Remove extra information block from a extra information data for the local file header or central directory header
-
-  It is needed to remove ZIP64 extra information blocks when before data is written if using RAW mode.
-
-  0x0001 is the signature header for the ZIP64 extra information blocks
-
-  usage.
-                        Remove ZIP64 Extra information from a central director extra field data
-              zipRemoveExtraInfoBlock(pCenDirExtraFieldData, &nCenDirExtraFieldDataLen, 0x0001);
-
-                        Remove ZIP64 Extra information from a Local File Header extra field data
-        zipRemoveExtraInfoBlock(pLocalHeaderExtraFieldData, &nLocalHeaderExtraFieldDataLen, 0x0001);
-*/
-
-#ifdef __cplusplus
-}
-#endif
-
-#endif /* _zip64_H */
--- a/Resources/ThirdParty/patch/NOTES.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-===========
-INFORMATION
-===========
-
-This is a precompiled version of the "patch" standard tool for
-Windows. It was compiled using the MSYS framework.
-
-The binaries originate from the "Git for Windows 1.9.5" package
-(https://msysgit.github.io/). The build instructions have been
-provided on the discussion group of Git for Windows [1]. They are
-copied/pasted below for reference.
-
-
-
-================
-UPSTREAM PROJECT
-================
-
-URL to the upstream project:
-http://savannah.gnu.org/projects/patch/
-
-License of patch: GPLv2 (GNU General Public License v2)
-
-Copyright (C) 1988 Larry Wall "with lots o' patches by Paul Eggert"
-Copyright (C) 1997 Free Software Foundation, Inc.
-
-
-
-======================
-BUILD INSTRUCTIONS [1]
-======================
-
-The easiest way to find out about this is to install the Git SDK, then 
-run 
-
-     pacman -Qu $(which patch.exe) 
-
-to find out which package contains the `patch.exe` binary. It so happens 
-to be patch.2.7.5-1 at the moment. Since this is an MSys2 package (not a 
-MinGW one, otherwise the patch utility would be in /mingw64/bin/, not 
-/usr/bin/), this package is built from the recipes in 
-
-     https://github.com/msys2/MSYS2-packages 
-
-The `patch` package is obviously built from the subdirectory 
-
-     https://github.com/Alexpux/MSYS2-packages/tree/master/patch 
-
-and the PKGBUILD file specifies that the source is fetched from 
-ftp://ftp.gnu.org/gnu/patch/patch-2.7.5.tar.xz: 
-
-https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/PKGBUILD#L14 
-
-and then these two patches are applied before building: 
-     
-https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/msys2-patch-2.7.1.patch 
-
-and 
-      
-https://github.com/Alexpux/MSYS2-packages/blob/900744becd072f687029b0f830ab6fe95cf533d6/patch/msys2-patch-manifest.patch 
-
-As you can see, some light changes are applied, i.e. `patch.exe` will 
-always write in binary mode with MSys2, and the executable will have a 
-manifest embedded that allows it to run as non-administrator. 
-
-Ciao, 
-Johannes Schindelin
-
-
-[1] https://groups.google.com/d/msg/git-for-windows/xWyVr4z6Ri0/6RKeV028EAAJ
Binary file Resources/ThirdParty/patch/msys-1.0.dll has changed
Binary file Resources/ThirdParty/patch/patch.exe has changed
--- a/Resources/ThirdParty/patch/patch.exe.manifest	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
-  <assemblyIdentity version="7.95.0.0"
-     processorArchitecture="X86"
-     name="patch.exe"
-     type="win32"/>
-
-  <!-- Identify the application security requirements. -->
-  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
-    <security>
-      <requestedPrivileges>
-        <requestedExecutionLevel
-          level="asInvoker"
-          uiAccess="false"/>
-        </requestedPrivileges>
-       </security>
-  </trustInfo>
-</assembly>
-
--- a/Resources/Toolbox.lua	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
---[[ PrintRecursive(struct, [limit], [indent])   Recursively print arbitrary data. 
-Set limit (default 100) to stanch infinite loops.
-Indents tables as [KEY] VALUE, nested tables as [KEY] [KEY]...[KEY] VALUE
-Set indent ("") to prefix each line:    Mytable [KEY] [KEY]...[KEY] VALUE
-Source: https://gist.github.com/stuby/5445834#file-rprint-lua
---]]
-
-function PrintRecursive(s, l, i) -- recursive Print (structure, limit, indent)
-   l = (l) or 100;  -- default item limit
-   i = i or "";     -- indent string
-   if (l<1) then print "ERROR: Item limit reached."; return l-1 end;
-   local ts = type(s);
-   if (ts ~= "table") then print (i,ts,s); return l-1 end
-   print (i,ts);           -- print "table"
-   for k,v in pairs(s) do  -- print "[KEY] VALUE"
-      l = PrintRecursive(v, l, i.."\t["..tostring(k).."]");
-      if (l < 0) then break end
-   end
-   return l
-end	
-
-
-
-
-function _InitializeJob()
-   _job = {}
-end
-
-
-function _AccessJob()
-   return _job
-end
-
-
-function SendToModality(resourceId, modality, localAet)
-   if resourceId == nil then
-      error('Cannot send a nonexistent resource')
-   end
-
-   table.insert(_job, { 
-                   Operation = 'store-scu', 
-                   Resource = resourceId,
-                   Modality = modality,
-                   LocalAet = localAet
-                })
-   return resourceId
-end
-
-
-function SendToPeer(resourceId, peer)
-   if resourceId == nil then
-      error('Cannot send a nonexistent resource')
-   end
-
-   table.insert(_job, { 
-                   Operation = 'store-peer', 
-                   Resource = resourceId,
-                   Peer = peer
-                })
-   return resourceId
-end
-
-
-function Delete(resourceId)
-   if resourceId == nil then
-      error('Cannot delete a nonexistent resource')
-   end
-
-   table.insert(_job, { 
-                   Operation = 'delete', 
-                   Resource = resourceId
-                })
-   return nil  -- Forbid chaining
-end
-
-
-function ModifyResource(resourceId, replacements, removals, removePrivateTags)
-   if resourceId == nil then
-      error('Cannot modify a nonexistent resource')
-   end
-
-   if resourceId == '' then
-      error('Cannot modify twice an resource');
-   end
-
-   table.insert(_job, { 
-                   Operation = 'modify', 
-                   Resource = resourceId,
-                   Replace = replacements, 
-                   Remove = removals,
-                   RemovePrivateTags = removePrivateTags 
-                })
-
-   return ''  -- Chain with another operation
-end
-
-
-function ModifyInstance(resourceId, replacements, removals, removePrivateTags)
-   return ModifyResource(resourceId, replacements, removals, removePrivateTags)
-end
-
-
--- This function is only applicable to individual instances
-function CallSystem(resourceId, command, args)
-   if resourceId == nil then
-      error('Cannot execute a system call on a nonexistent resource')
-   end
-
-   if command == nil then
-      error('No command was specified for system call')
-   end
-
-   table.insert(_job, { 
-                   Operation = 'call-system', 
-                   Resource = resourceId,
-                   Command = command,
-                   Arguments = args
-                })
-
-   return resourceId
-end
-
-
-print('Lua toolbox installed')
--- a/Resources/WebAssembly/ArithmeticTests/CMakeLists.txt	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,101 +0,0 @@
-# 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://orthanc.osimis.io/ThirdPartyDownloads/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 .
-  )
--- a/Resources/WebAssembly/ArithmeticTests/Run2.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-#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;
-  }
-}
--- a/Resources/WebAssembly/ArithmeticTests/app.js	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-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 {
-}
--- a/Resources/WebAssembly/ArithmeticTests/arith.patch	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
---- /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;
- }
-+}
--- a/Resources/WebAssembly/ArithmeticTests/index.html	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,13 +0,0 @@
-<!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>
--- a/Resources/WebAssembly/arith.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-/**
-#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
--- a/Resources/WindowsResources.py	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-#!/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-2020 Osimis S.A., Belgium
-#
-# This program is free software: you can redistribute it and/or
-# modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# In addition, as a special exception, the copyright holders of this
-# program give permission to link the code of its release with the
-# OpenSSL project's "OpenSSL" library (or with modified versions of it
-# that use the same license as the "OpenSSL" library), and distribute
-# the linked executables. You must obey the GNU General Public License
-# in all respects for all of the code used other than "OpenSSL". If you
-# modify file(s) with this exception, you may extend this exception to
-# your version of the file(s), but you are not obligated to do so. If
-# you do not wish to do so, delete this exception statement from your
-# version. If you delete this exception statement from all source files
-# in the program, then also delete it here.
-# 
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# 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 sys
-import datetime
-
-if len(sys.argv) != 5:
-    sys.stderr.write('Usage: %s <Version> <ProductName> <Filename> <Description>\n\n' % sys.argv[0])
-    sys.stderr.write('Example: %s 0.9.1 Orthanc Orthanc.exe "Lightweight, RESTful DICOM server for medical imaging"\n' % sys.argv[0])
-    sys.exit(-1)
-
-SOURCE = os.path.join(os.path.dirname(__file__), 'WindowsResources.rc')
-
-VERSION = sys.argv[1]
-PRODUCT = sys.argv[2]
-FILENAME = sys.argv[3]
-DESCRIPTION = sys.argv[4]
-
-if VERSION == 'mainline':
-    VERSION = '999.999.999'
-    RELEASE = 'This is a mainline build, not an official release'
-else:
-    RELEASE = 'Release %s' % VERSION
-
-v = VERSION.split('.')
-if len(v) != 2 and len(v) != 3:
-    sys.stderr.write('Bad version number: %s\n' % VERSION)
-    sys.exit(-1)
-
-if len(v) == 2:
-    v.append('0')
-
-extension = os.path.splitext(FILENAME)[1]
-if extension.lower() == '.dll':
-    BLOCK = '040904E4'
-    TYPE = 'VFT_DLL'
-elif extension.lower() == '.exe':
-    #BLOCK = '040904B0'   # LANG_ENGLISH/SUBLANG_ENGLISH_US,
-    BLOCK = '040904E4'   # Lang=US English, CharSet=Windows Multilingual
-    TYPE = 'VFT_APP'
-else:
-    sys.stderr.write('Unsupported extension (.EXE or .DLL only): %s\n' % extension)
-    sys.exit(-1)
-
-
-with open(SOURCE, 'r') as source:
-    content = source.read()
-    content = content.replace('${VERSION_MAJOR}', v[0])
-    content = content.replace('${VERSION_MINOR}', v[1])
-    content = content.replace('${VERSION_PATCH}', v[2])
-    content = content.replace('${RELEASE}', RELEASE)
-    content = content.replace('${DESCRIPTION}', DESCRIPTION)
-    content = content.replace('${PRODUCT}', PRODUCT)   
-    content = content.replace('${FILENAME}', FILENAME)   
-    content = content.replace('${YEAR}', str(datetime.datetime.now().year))
-    content = content.replace('${BLOCK}', BLOCK)
-    content = content.replace('${TYPE}', TYPE)
-
-    sys.stdout.write(content)
--- a/Resources/WindowsResources.rc	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-#include <winver.h>
-
-VS_VERSION_INFO VERSIONINFO
-   FILEVERSION ${VERSION_MAJOR},${VERSION_MINOR},0,${VERSION_PATCH}
-   PRODUCTVERSION ${VERSION_MAJOR},${VERSION_MINOR},0,0
-   FILEOS VOS_NT_WINDOWS32
-   FILETYPE ${TYPE}
-   BEGIN
-      BLOCK "StringFileInfo"
-      BEGIN
-         BLOCK "${BLOCK}"
-         BEGIN
-            VALUE "Comments", "${RELEASE}"
-            VALUE "CompanyName", "Osimis SA, Belgium"
-            VALUE "FileDescription", "${DESCRIPTION}"
-            VALUE "FileVersion", "${VERSION_MAJOR}.${VERSION_MINOR}.0.${VERSION_PATCH}"
-            VALUE "InternalName", "${PRODUCT}"
-            VALUE "LegalCopyright", "(c) 2012-${YEAR}, Sebastien Jodogne, University Hospital of Liege, and Osimis SA, Belgium"
-            VALUE "LegalTrademarks", "Licensing information is available at http://www.orthanc-server.com/"
-            VALUE "OriginalFilename", "${FILENAME}"
-            VALUE "ProductName", "${PRODUCT}"
-            VALUE "ProductVersion", "${VERSION_MAJOR}.${VERSION_MINOR}"
-         END
-      END
-
-      BLOCK "VarFileInfo"
-      BEGIN
-        VALUE "Translation", 0x409, 1252  // U.S. English
-      END
-   END
--- a/UnitTestsSources/BitbucketCACertificates.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-#define BITBUCKET_CERTIFICATES  \
-"-----BEGIN CERTIFICATE-----\n"  \
-"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n"  \
-"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"  \
-"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n"  \
-"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n"  \
-"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n"  \
-"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n"  \
-"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n"  \
-"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n"  \
-"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n"  \
-"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n"  \
-"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n"  \
-"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n"  \
-"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n"  \
-"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n"  \
-"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n"  \
-"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n"  \
-"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n"  \
-"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n"  \
-"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n"  \
-"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n"  \
-"+OkuE6N36B9K\n"  \
-"-----END CERTIFICATE-----\n"  \
-"\n" 
--- a/UnitTestsSources/DatabaseLookupTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,279 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../OrthancServer/Search/DatabaseLookup.h"
-#include "../Core/OrthancException.h"
-
-using namespace Orthanc;
-
-
-TEST(DatabaseLookup, SingleConstraint)
-{
-  {
-    ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, 
-                                        "HEL*LO", true, true), OrthancException);
-    ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal,
-                                        "HEL?LO", true, true), OrthancException);
-    ASSERT_THROW(DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal,
-                                        true, true), OrthancException);
-
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELLO", true, true);
-    ASSERT_TRUE(tag.IsMatch("HELLO"));
-    ASSERT_FALSE(tag.IsMatch("hello"));
-
-    ASSERT_TRUE(tag.IsCaseSensitive());
-    ASSERT_EQ(ConstraintType_Equal, tag.GetConstraintType());
-
-    DicomMap m;
-    ASSERT_FALSE(tag.IsMatch(m));
-    m.SetNullValue(DICOM_TAG_PATIENT_NAME);
-    ASSERT_FALSE(tag.IsMatch(m));
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", true /* binary */);
-    ASSERT_FALSE(tag.IsMatch(m));
-    m.SetValue(DICOM_TAG_PATIENT_NAME, "HELLO", false /* string */);
-    ASSERT_TRUE(tag.IsMatch(m));
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Equal, "HELlo", false, true);
-    ASSERT_TRUE(tag.IsMatch("HELLO"));
-    ASSERT_TRUE(tag.IsMatch("hello"));
-
-    ASSERT_EQ("HELlo", tag.GetValue());
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*L?O", true, true);
-    ASSERT_TRUE(tag.IsMatch("HELLO"));
-    ASSERT_TRUE(tag.IsMatch("HELLLLLO"));
-    ASSERT_TRUE(tag.IsMatch("HELxO"));
-    ASSERT_FALSE(tag.IsMatch("hello"));
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_Wildcard, "HE*l?o", false, true);
-    ASSERT_TRUE(tag.IsMatch("HELLO"));
-    ASSERT_TRUE(tag.IsMatch("HELLLLLO"));
-    ASSERT_TRUE(tag.IsMatch("HELxO"));
-    ASSERT_TRUE(tag.IsMatch("hello"));
-
-    ASSERT_FALSE(tag.IsCaseSensitive());
-    ASSERT_EQ(ConstraintType_Wildcard, tag.GetConstraintType());
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_SmallerOrEqual, "123", true, true);
-    ASSERT_TRUE(tag.IsMatch("120"));
-    ASSERT_TRUE(tag.IsMatch("123"));
-    ASSERT_FALSE(tag.IsMatch("124"));
-    ASSERT_TRUE(tag.IsMandatory());
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_GreaterOrEqual, "123", true, false);
-    ASSERT_FALSE(tag.IsMatch("122"));
-    ASSERT_TRUE(tag.IsMatch("123"));
-    ASSERT_TRUE(tag.IsMatch("124"));
-    ASSERT_FALSE(tag.IsMandatory());
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, true, true);
-    ASSERT_FALSE(tag.IsMatch("CT"));
-    ASSERT_FALSE(tag.IsMatch("MR"));
-
-    tag.AddValue("CT");
-    ASSERT_TRUE(tag.IsMatch("CT"));
-    ASSERT_FALSE(tag.IsMatch("MR"));
-
-    tag.AddValue("MR");
-    ASSERT_TRUE(tag.IsMatch("CT"));
-    ASSERT_TRUE(tag.IsMatch("MR"));
-    ASSERT_FALSE(tag.IsMatch("ct"));
-    ASSERT_FALSE(tag.IsMatch("mr"));
-
-    ASSERT_THROW(tag.GetValue(), OrthancException);
-    ASSERT_EQ(2u, tag.GetValues().size());
-  }
-
-  {
-    DicomTagConstraint tag(DICOM_TAG_PATIENT_NAME, ConstraintType_List, false, true);
-
-    tag.AddValue("ct");
-    tag.AddValue("mr");
-
-    ASSERT_TRUE(tag.IsMatch("CT"));
-    ASSERT_TRUE(tag.IsMatch("MR"));
-    ASSERT_TRUE(tag.IsMatch("ct"));
-    ASSERT_TRUE(tag.IsMatch("mr"));
-  }
-}
-
-
-
-TEST(DatabaseLookup, FromDicom)
-{
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", true, true);
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
-    ASSERT_EQ("HELLO", lookup.GetConstraint(0).GetValue());
-    ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_ID, "HELLO", false, true);
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-
-    // This is *not* a PN VR => "false" above is *not* used
-    ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", true, true);
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_TRUE(lookup.GetConstraint(0).IsCaseSensitive());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "HELLO", false, true);
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-
-    // This is a PN VR => "false" above is used
-    ASSERT_FALSE(lookup.GetConstraint(0).IsCaseSensitive());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_SERIES_DESCRIPTION, "2012-2016", false, true);
-
-    // This is not a data VR
-    ASSERT_EQ(ConstraintType_Equal, lookup.GetConstraint(0).GetConstraintType());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-2016", false, true);
-
-    // This is a data VR => range is effective
-    ASSERT_EQ(2u, lookup.GetConstraintsCount());
-
-    ASSERT_TRUE(lookup.GetConstraint(0).GetConstraintType() != lookup.GetConstraint(1).GetConstraintType());
-
-    for (size_t i = 0; i < 2; i++)
-    {
-      ASSERT_TRUE(lookup.GetConstraint(i).GetConstraintType() == ConstraintType_SmallerOrEqual ||
-                  lookup.GetConstraint(i).GetConstraintType() == ConstraintType_GreaterOrEqual);
-    }
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "2012-", false, true);
-
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_GreaterOrEqual, lookup.GetConstraint(0).GetConstraintType());
-    ASSERT_EQ("2012", lookup.GetConstraint(0).GetValue());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_BIRTH_DATE, "-2016", false, true);
-
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(DICOM_TAG_PATIENT_BIRTH_DATE,  lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ConstraintType_SmallerOrEqual, lookup.GetConstraint(0).GetConstraintType());
-    ASSERT_EQ("2016", lookup.GetConstraint(0).GetValue());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_MODALITIES_IN_STUDY, "CT\\MR", false, true);
-
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(DICOM_TAG_MODALITY,  lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
-
-    const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
-    ASSERT_EQ(2u, values.size());
-    ASSERT_TRUE(values.find("CT") != values.end());
-    ASSERT_TRUE(values.find("MR") != values.end());
-    ASSERT_TRUE(values.find("nope") == values.end());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "CT\\MR", false, true);
-
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(DICOM_TAG_STUDY_DESCRIPTION, lookup.GetConstraint(0).GetTag());
-    ASSERT_EQ(ConstraintType_List, lookup.GetConstraint(0).GetConstraintType());
-
-    const std::set<std::string>& values = lookup.GetConstraint(0).GetValues();
-    ASSERT_EQ(2u, values.size());
-    ASSERT_TRUE(values.find("CT") != values.end());
-    ASSERT_TRUE(values.find("MR") != values.end());
-    ASSERT_TRUE(values.find("nope") == values.end());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE*O", false, true);
-
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_STUDY_DESCRIPTION, "HE?O", false, true);
-
-    ASSERT_EQ(1u, lookup.GetConstraintsCount());
-    ASSERT_EQ(ConstraintType_Wildcard, lookup.GetConstraint(0).GetConstraintType());
-  }
-
-  {
-    DatabaseLookup lookup;
-    lookup.AddDicomConstraint(DICOM_TAG_RELATED_FRAME_OF_REFERENCE_UID, "TEST", false, true);
-    lookup.AddDicomConstraint(DICOM_TAG_PATIENT_NAME, "TEST2", false, false);
-    ASSERT_TRUE(lookup.GetConstraint(0).IsMandatory());
-    ASSERT_FALSE(lookup.GetConstraint(1).IsMandatory());
-  }
-}
--- a/UnitTestsSources/DicomMapTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1048 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Compatibility.h"
-#include "../Core/OrthancException.h"
-#include "../Core/DicomFormat/DicomMap.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomParsing/ToDcmtkBridge.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/DicomParsing/DicomWebJsonVisitor.h"
-
-#include "../OrthancServer/DicomInstanceToStore.h"
-
-#include <memory>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <dcmtk/dcmdata/dcvrat.h>
-
-using namespace Orthanc;
-
-TEST(DicomMap, MainTags)
-{
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Patient));
-  ASSERT_FALSE(DicomMap::IsMainDicomTag(DICOM_TAG_PATIENT_ID, ResourceType_Study));
-
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(DicomMap::IsMainDicomTag(DICOM_TAG_SOP_INSTANCE_UID));
-
-  std::set<DicomTag> s;
-  DicomMap::GetMainDicomTags(s);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Patient);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_PATIENT_ID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Study);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Series);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SERIES_INSTANCE_UID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
-
-  DicomMap::GetMainDicomTags(s, ResourceType_Instance);
-  ASSERT_TRUE(s.end() != s.find(DICOM_TAG_SOP_INSTANCE_UID));
-  ASSERT_TRUE(s.end() == s.find(DICOM_TAG_PATIENT_ID));
-}
-
-
-TEST(DicomMap, Tags)
-{
-  std::set<DicomTag> s;
-
-  DicomMap m;
-  m.GetTags(s);
-  ASSERT_EQ(0u, s.size());
-
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_NAME));
-  ASSERT_FALSE(m.HasTag(0x0010, 0x0010));
-  m.SetValue(0x0010, 0x0010, "PatientName", false);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_NAME));
-  ASSERT_TRUE(m.HasTag(0x0010, 0x0010));
-
-  m.GetTags(s);
-  ASSERT_EQ(1u, s.size());
-  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
-
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PATIENT_ID));
-  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID", false);
-  ASSERT_TRUE(m.HasTag(0x0010, 0x0020));
-  m.SetValue(DICOM_TAG_PATIENT_ID, "PatientID2", false);
-  ASSERT_EQ("PatientID2", m.GetValue(0x0010, 0x0020).GetContent());
-
-  m.GetTags(s);
-  ASSERT_EQ(2u, s.size());
-
-  m.Remove(DICOM_TAG_PATIENT_ID);
-  ASSERT_THROW(m.GetValue(0x0010, 0x0020), OrthancException);
-
-  m.GetTags(s);
-  ASSERT_EQ(1u, s.size());
-  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, *s.begin());
-
-  std::unique_ptr<DicomMap> mm(m.Clone());
-  ASSERT_EQ("PatientName", mm->GetValue(DICOM_TAG_PATIENT_NAME).GetContent());  
-
-  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());  
-
-  DicomValue v;
-  ASSERT_TRUE(v.IsNull());
-}
-
-
-TEST(DicomMap, FindTemplates)
-{
-  DicomMap m;
-
-  DicomMap::SetupFindPatientTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_PATIENT_ID));
-
-  DicomMap::SetupFindStudyTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_STUDY_INSTANCE_UID));
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_ACCESSION_NUMBER));
-
-  DicomMap::SetupFindSeriesTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_INSTANCE_UID));
-
-  DicomMap::SetupFindInstanceTemplate(m);
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SOP_INSTANCE_UID));
-}
-
-
-
-
-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);
-  
-  // The main dicom tags are a subset of the module
-  for (std::set<DicomTag>::const_iterator it = main.begin(); it != main.end(); ++it)
-  {
-    bool ok = moduleTags.find(*it) != moduleTags.end();
-
-    // 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) ||  /* 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::GetTagName(*it, "")
-                << " not expected at level " << EnumerationToString(level) << std::endl;
-    }
-
-    EXPECT_TRUE(ok);
-  }
-}
-
-
-TEST(DicomMap, Modules)
-{
-  TestModule(ResourceType_Patient, DicomModule_Patient);
-  TestModule(ResourceType_Study, DicomModule_Study);
-  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;
-  unsigned int ui;
-  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.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-  ASSERT_TRUE(m.LookupStringValue(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.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-  ASSERT_EQ("42", s);
-  ASSERT_TRUE(m.LookupStringValue(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);
-
-
-  // "800\0" in US COLMUNS tag
-  m.SetValue(DICOM_TAG_COLUMNS, "800\0", false);
-  ASSERT_TRUE(m.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(ui));
-  ASSERT_EQ(800u, ui);
-  m.SetValue(DICOM_TAG_COLUMNS, "800", false);
-  ASSERT_TRUE(m.GetValue(DICOM_TAG_COLUMNS).ParseFirstUnsignedInteger(ui));
-  ASSERT_EQ(800u, ui);
-}
-
-
-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);
-  }
-}
-
-
-
-TEST(DicomMap, DicomAsJson)
-{
-  // This is a Latin-1 test string: "crane" with a circumflex accent
-  const unsigned char raw[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
-  std::string latin1((char*) &raw[0], sizeof(raw) / sizeof(char));
-
-  std::string utf8 = Toolbox::ConvertToUtf8(latin1, Encoding_Latin1, false);
-
-  ParsedDicomFile dicom(false);
-  dicom.SetEncoding(Encoding_Latin1);
-  dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "Hello");
-  dicom.ReplacePlainString(DICOM_TAG_STUDY_DESCRIPTION, utf8);
-  dicom.ReplacePlainString(DICOM_TAG_SERIES_DESCRIPTION, std::string(ORTHANC_MAXIMUM_TAG_LENGTH, 'a'));
-  dicom.ReplacePlainString(DICOM_TAG_MANUFACTURER, std::string(ORTHANC_MAXIMUM_TAG_LENGTH + 1, 'a'));
-  dicom.ReplacePlainString(DICOM_TAG_PIXEL_DATA, "binary");
-  dicom.ReplacePlainString(DICOM_TAG_ROWS, "512");
-
-  DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
-  dataset.insertEmptyElement(DCM_StudyID, OFFalse);
-
-  {
-    std::unique_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence));
-
-    {
-      std::unique_ptr<DcmItem> item(new DcmItem);
-      item->putAndInsertString(DCM_ReferencedSOPInstanceUID, "nope", OFFalse);
-      ASSERT_TRUE(sequence->insert(item.release(), false, false).good());
-    }
-
-    ASSERT_TRUE(dataset.insert(sequence.release(), false, false).good());
-  }
-  
-                          
-  // Check re-encoding
-  DcmElement* element = NULL;
-  ASSERT_TRUE(dataset.findAndGetElement(DCM_StudyDescription, element).good() &&
-              element != NULL);
-
-  char* c = NULL;
-  ASSERT_TRUE(element != NULL &&
-              element->isLeaf() &&
-              element->isaString() &&
-              element->getString(c).good());
-  ASSERT_EQ(0, memcmp(c, raw, latin1.length()));
-
-  ASSERT_TRUE(dataset.findAndGetElement(DCM_Rows, element).good() &&
-              element != NULL &&
-              element->getTag().getEVR() == EVR_US);
-
-  DicomInstanceToStore toStore;
-  toStore.SetParsedDicomFile(dicom);
-
-  DicomMap m;
-  m.FromDicomAsJson(toStore.GetJson());
-
-  ASSERT_EQ("ISO_IR 100", m.GetValue(DICOM_TAG_SPECIFIC_CHARACTER_SET).GetContent());
-  
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_PATIENT_NAME).IsBinary());
-  ASSERT_EQ("Hello", m.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
-  
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).IsBinary());
-  ASSERT_EQ(utf8, m.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
-
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_MANUFACTURER));                // Too long
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_PIXEL_DATA));                  // Pixel data
-  ASSERT_FALSE(m.HasTag(DICOM_TAG_REFERENCED_SERIES_SEQUENCE));  // Sequence
-  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetGroup(), DCM_ReferencedSeriesSequence.getGroup());
-  ASSERT_EQ(DICOM_TAG_REFERENCED_SERIES_SEQUENCE.GetElement(), DCM_ReferencedSeriesSequence.getElement());
-
-  ASSERT_TRUE(m.HasTag(DICOM_TAG_SERIES_DESCRIPTION));  // Maximum length
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).IsBinary());
-  ASSERT_EQ(ORTHANC_MAXIMUM_TAG_LENGTH,
-            static_cast<int>(m.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent().length()));
-
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_ROWS).IsBinary());
-  ASSERT_EQ("512", m.GetValue(DICOM_TAG_ROWS).GetContent());
-
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsNull());
-  ASSERT_FALSE(m.GetValue(DICOM_TAG_STUDY_ID).IsBinary());
-  ASSERT_EQ("", m.GetValue(DICOM_TAG_STUDY_ID).GetContent());
-
-  DicomArray a(m);
-  ASSERT_EQ(6u, a.GetSize());
-
-  
-  //dicom.SaveToFile("/tmp/test.dcm"); 
-  //std::cout << toStore.GetJson() << std::endl;
-  //a.Print(stdout);
-}
-
-
-
-TEST(DicomMap, ExtractMainDicomTags)
-{
-  DicomMap b;
-  b.SetValue(DICOM_TAG_PATIENT_NAME, "E", false);
-  ASSERT_TRUE(b.HasOnlyMainDicomTags());
-
-  {
-    DicomMap a;
-    a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
-    a.SetValue(DICOM_TAG_STUDY_DESCRIPTION, "B", false);
-    a.SetValue(DICOM_TAG_SERIES_DESCRIPTION, "C", false);
-    a.SetValue(DICOM_TAG_NUMBER_OF_FRAMES, "D", false);
-    a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false);
-    ASSERT_FALSE(a.HasOnlyMainDicomTags());
-    b.ExtractMainDicomTags(a);
-  }
-
-  ASSERT_EQ(4u, b.GetSize());
-  ASSERT_EQ("A", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
-  ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
-  ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent());
-  ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
-  ASSERT_FALSE(b.HasTag(DICOM_TAG_SLICE_THICKNESS));
-  ASSERT_TRUE(b.HasOnlyMainDicomTags());
-
-  b.SetValue(DICOM_TAG_PATIENT_NAME, "G", false);
-
-  {
-    DicomMap a;
-    a.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
-    a.SetValue(DICOM_TAG_SLICE_THICKNESS, "F", false);
-    ASSERT_FALSE(a.HasOnlyMainDicomTags());
-    b.Merge(a);
-  }
-
-  ASSERT_EQ(5u, b.GetSize());
-  ASSERT_EQ("G", b.GetValue(DICOM_TAG_PATIENT_NAME).GetContent());
-  ASSERT_EQ("B", b.GetValue(DICOM_TAG_STUDY_DESCRIPTION).GetContent());
-  ASSERT_EQ("C", b.GetValue(DICOM_TAG_SERIES_DESCRIPTION).GetContent());
-  ASSERT_EQ("D", b.GetValue(DICOM_TAG_NUMBER_OF_FRAMES).GetContent());
-  ASSERT_EQ("F", b.GetValue(DICOM_TAG_SLICE_THICKNESS).GetContent());
-  ASSERT_FALSE(b.HasOnlyMainDicomTags());
-}
-
-
-TEST(DicomMap, RemoveBinary)
-{
-  DicomMap b;
-  b.SetValue(DICOM_TAG_PATIENT_NAME, "A", false);
-  b.SetValue(DICOM_TAG_PATIENT_ID, "B", true);
-  b.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, DicomValue());  // NULL
-  b.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, DicomValue("C", false));
-  b.SetValue(DICOM_TAG_SOP_INSTANCE_UID, DicomValue("D", true));
-
-  b.RemoveBinaryTags();
-
-  std::string s;
-  ASSERT_EQ(2u, b.GetSize());
-  ASSERT_TRUE(b.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false)); ASSERT_EQ("A", s);
-  ASSERT_TRUE(b.LookupStringValue(s, DICOM_TAG_SERIES_INSTANCE_UID, false)); ASSERT_EQ("C", s);
-}
-
-
-
-TEST(DicomWebJson, Multiplicity)
-{
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.4.html
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "SB1^SB2^SB3^SB4^SB5");
-  dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1\\2.3\\4");
-  dicom.ReplacePlainString(DICOM_TAG_IMAGE_POSITION_PATIENT, "");
-
-  DicomWebJsonVisitor visitor;
-  dicom.Apply(visitor);
-
-  {
-    const Json::Value& tag = visitor.GetResult() ["00200037"];  // ImageOrientationPatient
-    const Json::Value& value = tag["Value"];
-  
-    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
-    ASSERT_EQ(2u, tag.getMemberNames().size());
-    ASSERT_EQ(3u, value.size());
-    ASSERT_EQ(Json::realValue, value[1].type());
-    ASSERT_FLOAT_EQ(1.0f, value[0].asFloat());
-    ASSERT_FLOAT_EQ(2.3f, value[1].asFloat());
-    ASSERT_FLOAT_EQ(4.0f, value[2].asFloat());
-  }
-
-  {
-    const Json::Value& tag = visitor.GetResult() ["00200032"];  // ImagePositionPatient
-    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
-    ASSERT_EQ(1u, tag.getMemberNames().size());
-  }
-
-  std::string xml;
-  visitor.FormatXml(xml);
-
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(3u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-    ASSERT_EQ("SB1^SB2^SB3^SB4^SB5", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_POSITION_PATIENT, false));
-    ASSERT_TRUE(s.empty());
-
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false));
-
-    std::vector<std::string> v;
-    Toolbox::TokenizeString(v, s, '\\');
-    ASSERT_FLOAT_EQ(1.0f, boost::lexical_cast<float>(v[0]));
-    ASSERT_FLOAT_EQ(2.3f, boost::lexical_cast<float>(v[1]));
-    ASSERT_FLOAT_EQ(4.0f, boost::lexical_cast<float>(v[2]));
-  }
-}
-
-
-TEST(DicomWebJson, NullValue)
-{
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.5.html
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_IMAGE_ORIENTATION_PATIENT, "1.5\\\\\\2.5");
-
-  DicomWebJsonVisitor visitor;
-  dicom.Apply(visitor);
-
-  {
-    const Json::Value& tag = visitor.GetResult() ["00200037"];
-    const Json::Value& value = tag["Value"];
-  
-    ASSERT_EQ(EnumerationToString(ValueRepresentation_DecimalString), tag["vr"].asString());
-    ASSERT_EQ(2u, tag.getMemberNames().size());
-    ASSERT_EQ(4u, value.size());
-    ASSERT_EQ(Json::realValue, value[0].type());
-    ASSERT_EQ(Json::nullValue, value[1].type());
-    ASSERT_EQ(Json::nullValue, value[2].type());
-    ASSERT_EQ(Json::realValue, value[3].type());
-    ASSERT_FLOAT_EQ(1.5f, value[0].asFloat());
-    ASSERT_FLOAT_EQ(2.5f, value[3].asFloat());
-  }
-
-  std::string xml;
-  visitor.FormatXml(xml);
-  
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(1u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_IMAGE_ORIENTATION_PATIENT, false));
-
-    std::vector<std::string> v;
-    Toolbox::TokenizeString(v, s, '\\');
-    ASSERT_FLOAT_EQ(1.5f, boost::lexical_cast<float>(v[0]));
-    ASSERT_TRUE(v[1].empty());
-    ASSERT_TRUE(v[2].empty());
-    ASSERT_FLOAT_EQ(2.5f, boost::lexical_cast<float>(v[3]));
-  }
-}
-
-
-static void SetTagKey(ParsedDicomFile& dicom,
-                      const DicomTag& tag,
-                      const DicomTag& value)
-{
-  // This function emulates a call to function
-  // "dicom.GetDcmtkObject().getDataset()->putAndInsertTagKey(tag,
-  // value)" that was not available in DCMTK 3.6.0
-
-  std::unique_ptr<DcmAttributeTag> element(new DcmAttributeTag(ToDcmtkBridge::Convert(tag)));
-
-  DcmTagKey v = ToDcmtkBridge::Convert(value);
-  if (!element->putTagVal(v).good())
-  {
-    throw OrthancException(ErrorCode_InternalError);
-  }
-
-  dicom.GetDcmtkObject().getDataset()->insert(element.release());
-}
-
-
-TEST(DicomWebJson, ValueRepresentation)
-{
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.2.3.html
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DicomTag(0x0040, 0x0241), "AE");
-  dicom.ReplacePlainString(DicomTag(0x0010, 0x1010), "AS");
-  SetTagKey(dicom, DicomTag(0x0020, 0x9165), DicomTag(0x0010, 0x0020));
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0052), "CS");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0012), "DA");
-  dicom.ReplacePlainString(DicomTag(0x0010, 0x1020), "42");  // DS
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x002a), "DT");
-  dicom.ReplacePlainString(DicomTag(0x0010, 0x9431), "43");  // FL
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x1163), "44");  // FD
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x1160), "45");  // IS
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0070), "LO");
-  dicom.ReplacePlainString(DicomTag(0x0010, 0x4000), "LT");
-  dicom.ReplacePlainString(DicomTag(0x0028, 0x2000), "OB");
-  dicom.ReplacePlainString(DicomTag(0x7fe0, 0x0009), "3.14159");  // OD (other double)
-  dicom.ReplacePlainString(DicomTag(0x0064, 0x0009), "2.71828");  // OF (other float)
-  dicom.ReplacePlainString(DicomTag(0x0066, 0x0040), "46");  // OL (other long)
-  ASSERT_THROW(dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "O"), OrthancException);
-  dicom.ReplacePlainString(DicomTag(0x0028, 0x1201), "OWOW");
-  dicom.ReplacePlainString(DicomTag(0x0010, 0x0010), "PN");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0050), "SH");
-  dicom.ReplacePlainString(DicomTag(0x0018, 0x6020), "-15");  // SL
-  dicom.ReplacePlainString(DicomTag(0x0018, 0x9219), "-16");  // SS
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0081), "ST");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0013), "TM");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0119), "UC");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0016), "UI");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x1161), "128");  // UL
-  dicom.ReplacePlainString(DicomTag(0x4342, 0x1234), "UN");   // Inexistent tag
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0120), "UR");
-  dicom.ReplacePlainString(DicomTag(0x0008, 0x0301), "17");   // US
-  dicom.ReplacePlainString(DicomTag(0x0040, 0x0031), "UT");  
-
-  DicomWebJsonVisitor visitor;
-  dicom.Apply(visitor);
-
-  std::string s;
-
-  // The tag (0002,0002) is "Media Storage SOP Class UID" and is
-  // automatically copied by DCMTK from tag (0008,0016)
-  ASSERT_EQ("UI", visitor.GetResult() ["00020002"]["vr"].asString());
-  ASSERT_EQ("UI", visitor.GetResult() ["00020002"]["Value"][0].asString());
-  ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["vr"].asString());
-  ASSERT_EQ("AE", visitor.GetResult() ["00400241"]["Value"][0].asString());
-  ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["vr"].asString());
-  ASSERT_EQ("AS", visitor.GetResult() ["00101010"]["Value"][0].asString());
-  ASSERT_EQ("AT", visitor.GetResult() ["00209165"]["vr"].asString());
-  ASSERT_EQ("00100020", visitor.GetResult() ["00209165"]["Value"][0].asString());
-  ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["vr"].asString());
-  ASSERT_EQ("CS", visitor.GetResult() ["00080052"]["Value"][0].asString());
-  ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["vr"].asString());
-  ASSERT_EQ("DA", visitor.GetResult() ["00080012"]["Value"][0].asString());
-  ASSERT_EQ("DS", visitor.GetResult() ["00101020"]["vr"].asString());
-  ASSERT_FLOAT_EQ(42.0f, visitor.GetResult() ["00101020"]["Value"][0].asFloat());
-  ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["vr"].asString());
-  ASSERT_EQ("DT", visitor.GetResult() ["0008002A"]["Value"][0].asString());
-  ASSERT_EQ("FL", visitor.GetResult() ["00109431"]["vr"].asString());
-  ASSERT_FLOAT_EQ(43.0f, visitor.GetResult() ["00109431"]["Value"][0].asFloat());
-  ASSERT_EQ("FD", visitor.GetResult() ["00081163"]["vr"].asString());
-  ASSERT_FLOAT_EQ(44.0f, visitor.GetResult() ["00081163"]["Value"][0].asFloat());
-  ASSERT_EQ("IS", visitor.GetResult() ["00081160"]["vr"].asString());
-  ASSERT_FLOAT_EQ(45.0f, visitor.GetResult() ["00081160"]["Value"][0].asFloat());
-  ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["vr"].asString());
-  ASSERT_EQ("LO", visitor.GetResult() ["00080070"]["Value"][0].asString());
-  ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["vr"].asString());
-  ASSERT_EQ("LT", visitor.GetResult() ["00104000"]["Value"][0].asString());
-
-  ASSERT_EQ("OB", visitor.GetResult() ["00282000"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00282000"]["InlineBinary"].asString());
-  ASSERT_EQ("OB", s);
-
-#if DCMTK_VERSION_NUMBER >= 361
-  ASSERT_EQ("OD", visitor.GetResult() ["7FE00009"]["vr"].asString());
-  ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast<float>(visitor.GetResult() ["7FE00009"]["Value"][0].asString()));
-#else
-  ASSERT_EQ("UN", visitor.GetResult() ["7FE00009"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["7FE00009"]["InlineBinary"].asString());
-  ASSERT_EQ(8u, s.size()); // Because of padding
-  ASSERT_EQ(0, s[7]);
-  ASSERT_EQ("3.14159", s.substr(0, 7));
-#endif
-
-  ASSERT_EQ("OF", visitor.GetResult() ["00640009"]["vr"].asString());
-  ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(visitor.GetResult() ["00640009"]["Value"][0].asString()));
-
-#if DCMTK_VERSION_NUMBER < 361
-  ASSERT_EQ("UN", visitor.GetResult() ["00660040"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00660040"]["InlineBinary"].asString());
-  ASSERT_EQ("46", s);
-#elif DCMTK_VERSION_NUMBER == 361
-  ASSERT_EQ("UL", visitor.GetResult() ["00660040"]["vr"].asString());
-  ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
-#else
-  ASSERT_EQ("OL", visitor.GetResult() ["00660040"]["vr"].asString());
-  ASSERT_EQ(46, visitor.GetResult() ["00660040"]["Value"][0].asInt());
-#endif
-
-  ASSERT_EQ("OW", visitor.GetResult() ["00281201"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00281201"]["InlineBinary"].asString());
-  ASSERT_EQ("OWOW", s);
-
-  ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["vr"].asString());
-  ASSERT_EQ("PN", visitor.GetResult() ["00100010"]["Value"][0]["Alphabetic"].asString());
-
-  ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["vr"].asString());
-  ASSERT_EQ("SH", visitor.GetResult() ["00080050"]["Value"][0].asString());
-
-  ASSERT_EQ("SL", visitor.GetResult() ["00186020"]["vr"].asString());
-  ASSERT_EQ(-15, visitor.GetResult() ["00186020"]["Value"][0].asInt());
-
-  ASSERT_EQ("SS", visitor.GetResult() ["00189219"]["vr"].asString());
-  ASSERT_EQ(-16, visitor.GetResult() ["00189219"]["Value"][0].asInt());
-
-  ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["vr"].asString());
-  ASSERT_EQ("ST", visitor.GetResult() ["00080081"]["Value"][0].asString());
-
-  ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["vr"].asString());
-  ASSERT_EQ("TM", visitor.GetResult() ["00080013"]["Value"][0].asString());
-
-#if DCMTK_VERSION_NUMBER >= 361
-  ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["vr"].asString());
-  ASSERT_EQ("UC", visitor.GetResult() ["00080119"]["Value"][0].asString());
-#else
-  ASSERT_EQ("UN", visitor.GetResult() ["00080119"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080119"]["InlineBinary"].asString());
-  ASSERT_EQ("UC", s);
-#endif
-
-  ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["vr"].asString());
-  ASSERT_EQ("UI", visitor.GetResult() ["00080016"]["Value"][0].asString());
-
-  ASSERT_EQ("UL", visitor.GetResult() ["00081161"]["vr"].asString());
-  ASSERT_EQ(128u, visitor.GetResult() ["00081161"]["Value"][0].asUInt());
-
-  ASSERT_EQ("UN", visitor.GetResult() ["43421234"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["43421234"]["InlineBinary"].asString());
-  ASSERT_EQ("UN", s);
-
-#if DCMTK_VERSION_NUMBER >= 361
-  ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["vr"].asString());
-  ASSERT_EQ("UR", visitor.GetResult() ["00080120"]["Value"][0].asString());
-#else
-  ASSERT_EQ("UN", visitor.GetResult() ["00080120"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080120"]["InlineBinary"].asString());
-  ASSERT_EQ("UR", s);
-#endif
-
-#if DCMTK_VERSION_NUMBER >= 361
-  ASSERT_EQ("US", visitor.GetResult() ["00080301"]["vr"].asString());
-  ASSERT_EQ(17u, visitor.GetResult() ["00080301"]["Value"][0].asUInt());
-#else
-  ASSERT_EQ("UN", visitor.GetResult() ["00080301"]["vr"].asString());
-  Toolbox::DecodeBase64(s, visitor.GetResult() ["00080301"]["InlineBinary"].asString());
-  ASSERT_EQ("17", s);
-#endif
-
-  ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["vr"].asString());
-  ASSERT_EQ("UT", visitor.GetResult() ["00400031"]["Value"][0].asString());
-
-  std::string xml;
-  visitor.FormatXml(xml);
-  
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(31u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0002, 0x0002), false));  ASSERT_EQ("UI", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0241), false));  ASSERT_EQ("AE", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x1010), false));  ASSERT_EQ("AS", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0020, 0x9165), false));  ASSERT_EQ("00100020", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0052), false));  ASSERT_EQ("CS", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0012), false));  ASSERT_EQ("DA", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x1020), false));  ASSERT_EQ("42", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x002a), false));  ASSERT_EQ("DT", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x9431), false));  ASSERT_EQ("43", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1163), false));  ASSERT_EQ("44", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1160), false));  ASSERT_EQ("45", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0070), false));  ASSERT_EQ("LO", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x4000), false));  ASSERT_EQ("LT", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x2000), true));   ASSERT_EQ("OB", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x7fe0, 0x0009), true));
-
-#if DCMTK_VERSION_NUMBER >= 361
-    ASSERT_FLOAT_EQ(3.14159f, boost::lexical_cast<float>(s));
-#else
-    ASSERT_EQ(8u, s.size()); // Because of padding
-    ASSERT_EQ(0, s[7]);
-    ASSERT_EQ("3.14159", s.substr(0, 7));
-#endif
-
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0064, 0x0009), true));
-    ASSERT_FLOAT_EQ(2.71828f, boost::lexical_cast<float>(s));
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0028, 0x1201), true));   ASSERT_EQ("OWOW", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0010, 0x0010), false));  ASSERT_EQ("PN", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0050), false));  ASSERT_EQ("SH", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x6020), false));  ASSERT_EQ("-15", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0018, 0x9219), false));  ASSERT_EQ("-16", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0081), false));  ASSERT_EQ("ST", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0013), false));  ASSERT_EQ("TM", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0016), false));  ASSERT_EQ("UI", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x1161), false));  ASSERT_EQ("128", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x4342, 0x1234), true));   ASSERT_EQ("UN", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0040, 0x0031), false));  ASSERT_EQ("UT", s);
-
-#if DCMTK_VERSION_NUMBER >= 361
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), false));  ASSERT_EQ("46", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), false));  ASSERT_EQ("UC", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), false));  ASSERT_EQ("UR", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), false));  ASSERT_EQ("17", s);
-#else
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0066, 0x0040), true));  ASSERT_EQ("46", s);  // OL
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0119), true));  ASSERT_EQ("UC", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0120), true));  ASSERT_EQ("UR", s);
-    ASSERT_TRUE(m.LookupStringValue(s, DicomTag(0x0008, 0x0301), true));  ASSERT_EQ("17", s);  // US (but tag unknown to DCMTK 3.6.0)
-#endif
-    
-  }
-}
-
-
-TEST(DicomWebJson, Sequence)
-{
-  ParsedDicomFile dicom(false);
-  
-  {
-    std::unique_ptr<DcmSequenceOfItems> sequence(new DcmSequenceOfItems(DCM_ReferencedSeriesSequence));
-
-    for (unsigned int i = 0; i < 3; i++)
-    {
-      std::unique_ptr<DcmItem> item(new DcmItem);
-      std::string s = "item" + boost::lexical_cast<std::string>(i);
-      item->putAndInsertString(DCM_ReferencedSOPInstanceUID, s.c_str(), OFFalse);
-      ASSERT_TRUE(sequence->insert(item.release(), false, false).good());
-    }
-
-    ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->insert(sequence.release(), false, false).good());
-  }
-
-  DicomWebJsonVisitor visitor;
-  dicom.Apply(visitor);
-
-  ASSERT_EQ("SQ", visitor.GetResult() ["00081115"]["vr"].asString());
-  ASSERT_EQ(3u, visitor.GetResult() ["00081115"]["Value"].size());
-
-  std::set<std::string> items;
-  
-  for (Json::Value::ArrayIndex i = 0; i < 3; i++)
-  {
-    ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i].size());
-    ASSERT_EQ(1u, visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"].size());
-    ASSERT_EQ("UI", visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["vr"].asString());
-    items.insert(visitor.GetResult() ["00081115"]["Value"][i]["00081155"]["Value"][0].asString());
-  }
-
-  ASSERT_EQ(3u, items.size());
-  ASSERT_TRUE(items.find("item0") != items.end());
-  ASSERT_TRUE(items.find("item1") != items.end());
-  ASSERT_TRUE(items.find("item2") != items.end());
-
-  std::string xml;
-  visitor.FormatXml(xml);
-
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(0u, m.GetSize());  // Sequences are not handled by DicomMap
-  }
-}
-
-
-TEST(DicomWebJson, PixelSpacing)
-{
-  // Test related to locales: Make sure that decimal separator is
-  // correctly handled (dot "." vs comma ",")
-  ParsedDicomFile source(false);
-  source.ReplacePlainString(DICOM_TAG_PIXEL_SPACING, "1.5\\1.3");
-
-  DicomWebJsonVisitor visitor;
-  source.Apply(visitor);
-
-  DicomMap target;
-  target.FromDicomWeb(visitor.GetResult());
-
-  ASSERT_EQ("DS", visitor.GetResult() ["00280030"]["vr"].asString());
-  ASSERT_FLOAT_EQ(1.5f, visitor.GetResult() ["00280030"]["Value"][0].asFloat());
-  ASSERT_FLOAT_EQ(1.3f, visitor.GetResult() ["00280030"]["Value"][1].asFloat());
-
-  std::string s;
-  ASSERT_TRUE(target.LookupStringValue(s, DICOM_TAG_PIXEL_SPACING, false));
-  ASSERT_EQ(s, "1.5\\1.3");
-}
-
-
-TEST(DicomMap, MainTagNames)
-{
-  ASSERT_EQ(3, ResourceType_Instance - ResourceType_Patient);
-  
-  for (int i = ResourceType_Patient; i <= ResourceType_Instance; i++)
-  {
-    ResourceType level = static_cast<ResourceType>(i);
-
-    std::set<DicomTag> tags;
-    DicomMap::GetMainDicomTags(tags, level);
-
-    for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it)
-    {
-      DicomMap a;
-      a.SetValue(*it, "TEST", false);
-
-      Json::Value json;
-      a.DumpMainDicomTags(json, level);
-
-      ASSERT_EQ(Json::objectValue, json.type());
-      ASSERT_EQ(1u, json.getMemberNames().size());
-
-      std::string name = json.getMemberNames() [0];
-      EXPECT_EQ(name, FromDcmtkBridge::GetTagName(*it, ""));
-
-      DicomMap b;
-      b.ParseMainDicomTags(json, level);
-
-      ASSERT_EQ(1u, b.GetSize());
-      ASSERT_EQ("TEST", b.GetStringValue(*it, "", false));
-
-      std::string main = it->GetMainTagsName();
-      if (!main.empty())
-      {
-        ASSERT_EQ(main, name);
-      }
-    }
-  }
-}
--- a/UnitTestsSources/FileStorageTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,190 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <ctype.h>
-
-#include "../Core/FileStorage/FilesystemStorage.h"
-#include "../Core/FileStorage/StorageAccessor.h"
-#include "../Core/HttpServer/BufferHttpSender.h"
-#include "../Core/HttpServer/FilesystemHttpSender.h"
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Toolbox.h"
-#include "../OrthancServer/ServerIndex.h"
-
-using namespace Orthanc;
-
-
-static void StringToVector(std::vector<uint8_t>& v,
-                           const std::string& s)
-{
-  v.resize(s.size());
-  for (size_t i = 0; i < s.size(); i++)
-    v[i] = s[i];
-}
-
-
-TEST(FilesystemStorage, Basic)
-{
-  FilesystemStorage s("UnitTestsStorage");
-
-  std::string data = Toolbox::GenerateUuid();
-  std::string uid = Toolbox::GenerateUuid();
-  s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
-  std::string d;
-  s.Read(d, uid, FileContentType_Unknown);
-  ASSERT_EQ(d.size(), data.size());
-  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
-  ASSERT_EQ(s.GetSize(uid), data.size());
-}
-
-TEST(FilesystemStorage, Basic2)
-{
-  FilesystemStorage s("UnitTestsStorage");
-
-  std::vector<uint8_t> data;
-  StringToVector(data, Toolbox::GenerateUuid());
-  std::string uid = Toolbox::GenerateUuid();
-  s.Create(uid.c_str(), &data[0], data.size(), FileContentType_Unknown);
-  std::string d;
-  s.Read(d, uid, FileContentType_Unknown);
-  ASSERT_EQ(d.size(), data.size());
-  ASSERT_FALSE(memcmp(&d[0], &data[0], data.size()));
-  ASSERT_EQ(s.GetSize(uid), data.size());
-}
-
-TEST(FilesystemStorage, EndToEnd)
-{
-  FilesystemStorage s("UnitTestsStorage");
-  s.Clear();
-
-  std::list<std::string> u;
-  for (unsigned int i = 0; i < 10; i++)
-  {
-    std::string t = Toolbox::GenerateUuid();
-    std::string uid = Toolbox::GenerateUuid();
-    s.Create(uid.c_str(), &t[0], t.size(), FileContentType_Unknown);
-    u.push_back(uid);
-  }
-
-  std::set<std::string> ss;
-  s.ListAllFiles(ss);
-  ASSERT_EQ(10u, ss.size());
-  
-  unsigned int c = 0;
-  for (std::list<std::string>::iterator
-         i = u.begin(); i != u.end(); i++, c++)
-  {
-    ASSERT_TRUE(ss.find(*i) != ss.end());
-    if (c < 5)
-      s.Remove(*i, FileContentType_Unknown);
-  }
-
-  s.ListAllFiles(ss);
-  ASSERT_EQ(5u, ss.size());
-
-  s.Clear();
-  s.ListAllFiles(ss);
-  ASSERT_EQ(0u, ss.size());
-}
-
-
-TEST(StorageAccessor, NoCompression)
-{
-  FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
-
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_Dicom, CompressionType_None, true);
-  
-  std::string r;
-  accessor.Read(r, info);
-
-  ASSERT_EQ(data, r);
-  ASSERT_EQ(CompressionType_None, info.GetCompressionType());
-  ASSERT_EQ(11u, info.GetUncompressedSize());
-  ASSERT_EQ(11u, info.GetCompressedSize());
-  ASSERT_EQ(FileContentType_Dicom, info.GetContentType());
-  ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5());
-  ASSERT_EQ(info.GetUncompressedMD5(), info.GetCompressedMD5());
-}
-
-
-TEST(StorageAccessor, Compression)
-{
-  FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
-
-  std::string data = "Hello world";
-  FileInfo info = accessor.Write(data, FileContentType_DicomAsJson, CompressionType_ZlibWithSize, true);
-  
-  std::string r;
-  accessor.Read(r, info);
-
-  ASSERT_EQ(data, r);
-  ASSERT_EQ(CompressionType_ZlibWithSize, info.GetCompressionType());
-  ASSERT_EQ(11u, info.GetUncompressedSize());
-  ASSERT_EQ(FileContentType_DicomAsJson, info.GetContentType());
-  ASSERT_EQ("3e25960a79dbc69b674cd4ec67a72c62", info.GetUncompressedMD5());
-  ASSERT_NE(info.GetUncompressedMD5(), info.GetCompressedMD5());
-}
-
-
-TEST(StorageAccessor, Mix)
-{
-  FilesystemStorage s("UnitTestsStorage");
-  StorageAccessor accessor(s);
-
-  std::string r;
-  std::string compressedData = "Hello";
-  std::string uncompressedData = "HelloWorld";
-
-  FileInfo compressedInfo = accessor.Write(compressedData, FileContentType_Dicom, CompressionType_ZlibWithSize, false);  
-  FileInfo uncompressedInfo = accessor.Write(uncompressedData, FileContentType_Dicom, CompressionType_None, false);
-  
-  accessor.Read(r, compressedInfo);
-  ASSERT_EQ(compressedData, r);
-
-  accessor.Read(r, uncompressedInfo);
-  ASSERT_EQ(uncompressedData, r);
-  ASSERT_NE(compressedData, r);
-
-  /*
-  // This test is too slow on Windows
-  accessor.SetCompressionForNextOperations(CompressionType_ZlibWithSize);
-  ASSERT_THROW(accessor.Read(r, uncompressedInfo.GetUuid(), FileContentType_Unknown), OrthancException);
-  */
-}
--- a/UnitTestsSources/FrameworkTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1342 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "../Core/EnumerationDictionary.h"
-
-#include "gtest/gtest.h"
-
-#include <ctype.h>
-
-#include "../Core/DicomFormat/DicomTag.h"
-#include "../Core/FileBuffer.h"
-#include "../Core/HttpServer/HttpToolbox.h"
-#include "../Core/Logging.h"
-#include "../Core/MetricsRegistry.h"
-#include "../Core/OrthancException.h"
-#include "../Core/SystemToolbox.h"
-#include "../Core/TemporaryFile.h"
-#include "../Core/Toolbox.h"
-
-
-using namespace Orthanc;
-
-
-TEST(Uuid, Generation)
-{
-  for (int i = 0; i < 10; i++)
-  {
-    std::string s = Toolbox::GenerateUuid();
-    ASSERT_TRUE(Toolbox::IsUuid(s));
-  }
-}
-
-TEST(Uuid, Test)
-{
-  ASSERT_FALSE(Toolbox::IsUuid(""));
-  ASSERT_FALSE(Toolbox::IsUuid("012345678901234567890123456789012345"));
-  ASSERT_TRUE(Toolbox::IsUuid("550e8400-e29b-41d4-a716-446655440000"));
-  ASSERT_FALSE(Toolbox::IsUuid("550e8400-e29b-41d4-a716-44665544000_"));
-  ASSERT_FALSE(Toolbox::IsUuid("01234567890123456789012345678901234_"));
-  ASSERT_FALSE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-44665544000"));
-  ASSERT_TRUE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000"));
-  ASSERT_TRUE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000 ok"));
-  ASSERT_FALSE(Toolbox::StartsWithUuid("550e8400-e29b-41d4-a716-446655440000ok"));
-}
-
-TEST(Toolbox, IsSHA1)
-{
-  ASSERT_FALSE(Toolbox::IsSHA1(""));
-  ASSERT_FALSE(Toolbox::IsSHA1("01234567890123456789012345678901234567890123"));
-  ASSERT_FALSE(Toolbox::IsSHA1("012345678901234567890123456789012345678901234"));
-  ASSERT_TRUE(Toolbox::IsSHA1("b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b9"));
-
-  std::string sha = "         b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b9          ";
-  ASSERT_TRUE(Toolbox::IsSHA1(sha));
-  sha[3] = '\0';
-  sha[53] = '\0';
-  ASSERT_TRUE(Toolbox::IsSHA1(sha));
-  sha[40] = '\0';
-  ASSERT_FALSE(Toolbox::IsSHA1(sha));
-  ASSERT_FALSE(Toolbox::IsSHA1("       "));
-
-  ASSERT_TRUE(Toolbox::IsSHA1("16738bc3-e47ed42a-43ce044c-a3414a45-cb069bd0"));
-
-  std::string s;
-  Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog");
-  ASSERT_TRUE(Toolbox::IsSHA1(s));
-  ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s);
-
-  ASSERT_FALSE(Toolbox::IsSHA1("b5ed549f-956400ce-69a8c063-bf5b78be-2732a4b_"));
-}
-
-
-TEST(ParseGetArguments, Basic)
-{
-  IHttpHandler::GetArguments b;
-  HttpToolbox::ParseGetArguments(b, "aaa=baaa&bb=a&aa=c");
-
-  IHttpHandler::Arguments a;
-  HttpToolbox::CompileGetArguments(a, b);
-
-  ASSERT_EQ(3u, a.size());
-  ASSERT_EQ(a["aaa"], "baaa");
-  ASSERT_EQ(a["bb"], "a");
-  ASSERT_EQ(a["aa"], "c");
-}
-
-TEST(ParseGetArguments, BasicEmpty)
-{
-  IHttpHandler::GetArguments b;
-  HttpToolbox::ParseGetArguments(b, "aaa&bb=aa&aa");
-
-  IHttpHandler::Arguments a;
-  HttpToolbox::CompileGetArguments(a, b);
-
-  ASSERT_EQ(3u, a.size());
-  ASSERT_EQ(a["aaa"], "");
-  ASSERT_EQ(a["bb"], "aa");
-  ASSERT_EQ(a["aa"], "");
-}
-
-TEST(ParseGetArguments, Single)
-{
-  IHttpHandler::GetArguments b;
-  HttpToolbox::ParseGetArguments(b, "aaa=baaa");
-
-  IHttpHandler::Arguments a;
-  HttpToolbox::CompileGetArguments(a, b);
-
-  ASSERT_EQ(1u, a.size());
-  ASSERT_EQ(a["aaa"], "baaa");
-}
-
-TEST(ParseGetArguments, SingleEmpty)
-{
-  IHttpHandler::GetArguments b;
-  HttpToolbox::ParseGetArguments(b, "aaa");
-
-  IHttpHandler::Arguments a;
-  HttpToolbox::CompileGetArguments(a, b);
-
-  ASSERT_EQ(1u, a.size());
-  ASSERT_EQ(a["aaa"], "");
-}
-
-TEST(ParseGetQuery, Test1)
-{
-  UriComponents uri;
-  IHttpHandler::GetArguments b;
-  HttpToolbox::ParseGetQuery(uri, b, "/instances/test/world?aaa=baaa&bb=a&aa=c");
-
-  IHttpHandler::Arguments a;
-  HttpToolbox::CompileGetArguments(a, b);
-
-  ASSERT_EQ(3u, uri.size());
-  ASSERT_EQ("instances", uri[0]);
-  ASSERT_EQ("test", uri[1]);
-  ASSERT_EQ("world", uri[2]);
-  ASSERT_EQ(3u, a.size());
-  ASSERT_EQ(a["aaa"], "baaa");
-  ASSERT_EQ(a["bb"], "a");
-  ASSERT_EQ(a["aa"], "c");
-}
-
-TEST(ParseGetQuery, Test2)
-{
-  UriComponents uri;
-  IHttpHandler::GetArguments b;
-  HttpToolbox::ParseGetQuery(uri, b, "/instances/test/world");
-
-  IHttpHandler::Arguments a;
-  HttpToolbox::CompileGetArguments(a, b);
-
-  ASSERT_EQ(3u, uri.size());
-  ASSERT_EQ("instances", uri[0]);
-  ASSERT_EQ("test", uri[1]);
-  ASSERT_EQ("world", uri[2]);
-  ASSERT_EQ(0u, a.size());
-}
-
-TEST(Uri, SplitUriComponents)
-{
-  UriComponents c, d;
-  Toolbox::SplitUriComponents(c, "/cou/hello/world");
-  ASSERT_EQ(3u, c.size());
-  ASSERT_EQ("cou", c[0]);
-  ASSERT_EQ("hello", c[1]);
-  ASSERT_EQ("world", c[2]);
-
-  Toolbox::SplitUriComponents(c, "/cou/hello/world/");
-  ASSERT_EQ(3u, c.size());
-  ASSERT_EQ("cou", c[0]);
-  ASSERT_EQ("hello", c[1]);
-  ASSERT_EQ("world", c[2]);
-
-  Toolbox::SplitUriComponents(c, "/cou/hello/world/a");
-  ASSERT_EQ(4u, c.size());
-  ASSERT_EQ("cou", c[0]);
-  ASSERT_EQ("hello", c[1]);
-  ASSERT_EQ("world", c[2]);
-  ASSERT_EQ("a", c[3]);
-
-  Toolbox::SplitUriComponents(c, "/");
-  ASSERT_EQ(0u, c.size());
-
-  Toolbox::SplitUriComponents(c, "/hello");
-  ASSERT_EQ(1u, c.size());
-  ASSERT_EQ("hello", c[0]);
-
-  Toolbox::SplitUriComponents(c, "/hello/");
-  ASSERT_EQ(1u, c.size());
-  ASSERT_EQ("hello", c[0]);
-
-  ASSERT_THROW(Toolbox::SplitUriComponents(c, ""), OrthancException);
-  ASSERT_THROW(Toolbox::SplitUriComponents(c, "a"), OrthancException);
-  ASSERT_THROW(Toolbox::SplitUriComponents(c, "/coucou//coucou"), OrthancException);
-
-  c.clear();
-  c.push_back("test");
-  ASSERT_EQ("/", Toolbox::FlattenUri(c, 10));
-}
-
-
-TEST(Uri, Truncate)
-{
-  UriComponents c, d;
-  Toolbox::SplitUriComponents(c, "/cou/hello/world");
-
-  Toolbox::TruncateUri(d, c, 0);
-  ASSERT_EQ(3u, d.size());
-  ASSERT_EQ("cou", d[0]);
-  ASSERT_EQ("hello", d[1]);
-  ASSERT_EQ("world", d[2]);
-
-  Toolbox::TruncateUri(d, c, 1);
-  ASSERT_EQ(2u, d.size());
-  ASSERT_EQ("hello", d[0]);
-  ASSERT_EQ("world", d[1]);
-
-  Toolbox::TruncateUri(d, c, 2);
-  ASSERT_EQ(1u, d.size());
-  ASSERT_EQ("world", d[0]);
-
-  Toolbox::TruncateUri(d, c, 3);
-  ASSERT_EQ(0u, d.size());
-
-  Toolbox::TruncateUri(d, c, 4);
-  ASSERT_EQ(0u, d.size());
-
-  Toolbox::TruncateUri(d, c, 5);
-  ASSERT_EQ(0u, d.size());
-}
-
-
-TEST(Uri, Child)
-{
-  UriComponents c1;  Toolbox::SplitUriComponents(c1, "/hello/world");  
-  UriComponents c2;  Toolbox::SplitUriComponents(c2, "/hello/hello");  
-  UriComponents c3;  Toolbox::SplitUriComponents(c3, "/hello");  
-  UriComponents c4;  Toolbox::SplitUriComponents(c4, "/world");  
-  UriComponents c5;  Toolbox::SplitUriComponents(c5, "/");  
-
-  ASSERT_TRUE(Toolbox::IsChildUri(c1, c1));
-  ASSERT_FALSE(Toolbox::IsChildUri(c1, c2));
-  ASSERT_FALSE(Toolbox::IsChildUri(c1, c3));
-  ASSERT_FALSE(Toolbox::IsChildUri(c1, c4));
-  ASSERT_FALSE(Toolbox::IsChildUri(c1, c5));
-
-  ASSERT_FALSE(Toolbox::IsChildUri(c2, c1));
-  ASSERT_TRUE(Toolbox::IsChildUri(c2, c2));
-  ASSERT_FALSE(Toolbox::IsChildUri(c2, c3));
-  ASSERT_FALSE(Toolbox::IsChildUri(c2, c4));
-  ASSERT_FALSE(Toolbox::IsChildUri(c2, c5));
-
-  ASSERT_TRUE(Toolbox::IsChildUri(c3, c1));
-  ASSERT_TRUE(Toolbox::IsChildUri(c3, c2));
-  ASSERT_TRUE(Toolbox::IsChildUri(c3, c3));
-  ASSERT_FALSE(Toolbox::IsChildUri(c3, c4));
-  ASSERT_FALSE(Toolbox::IsChildUri(c3, c5));
-
-  ASSERT_FALSE(Toolbox::IsChildUri(c4, c1));
-  ASSERT_FALSE(Toolbox::IsChildUri(c4, c2));
-  ASSERT_FALSE(Toolbox::IsChildUri(c4, c3));
-  ASSERT_TRUE(Toolbox::IsChildUri(c4, c4));
-  ASSERT_FALSE(Toolbox::IsChildUri(c4, c5));
-
-  ASSERT_TRUE(Toolbox::IsChildUri(c5, c1));
-  ASSERT_TRUE(Toolbox::IsChildUri(c5, c2));
-  ASSERT_TRUE(Toolbox::IsChildUri(c5, c3));
-  ASSERT_TRUE(Toolbox::IsChildUri(c5, c4));
-  ASSERT_TRUE(Toolbox::IsChildUri(c5, c5));
-}
-
-TEST(Uri, AutodetectMimeType)
-{
-  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("../NOTES"));
-  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType(""));
-  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("/"));
-  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("a/a"));
-  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\"));
-  ASSERT_EQ(MimeType_Binary, SystemToolbox::AutodetectMimeType("..\\a\\a"));
-
-  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../NOTES.txt"));
-  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("../coucou.xml/NOTES.txt"));
-  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("..\\coucou.\\NOTES.xml"));
-  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.xml"));
-  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("../.XmL"));
-
-  ASSERT_EQ(MimeType_JavaScript, SystemToolbox::AutodetectMimeType("NOTES.js"));
-  ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.json"));
-  ASSERT_EQ(MimeType_Pdf, SystemToolbox::AutodetectMimeType("NOTES.pdf"));
-  ASSERT_EQ(MimeType_Css, SystemToolbox::AutodetectMimeType("NOTES.css"));
-  ASSERT_EQ(MimeType_Html, SystemToolbox::AutodetectMimeType("NOTES.html"));
-  ASSERT_EQ(MimeType_PlainText, SystemToolbox::AutodetectMimeType("NOTES.txt"));
-  ASSERT_EQ(MimeType_Xml, SystemToolbox::AutodetectMimeType("NOTES.xml"));
-  ASSERT_EQ(MimeType_Gif, SystemToolbox::AutodetectMimeType("NOTES.gif"));
-  ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpg"));
-  ASSERT_EQ(MimeType_Jpeg, SystemToolbox::AutodetectMimeType("NOTES.jpeg"));
-  ASSERT_EQ(MimeType_Png, SystemToolbox::AutodetectMimeType("NOTES.png"));
-  ASSERT_EQ(MimeType_NaCl, SystemToolbox::AutodetectMimeType("NOTES.nexe"));
-  ASSERT_EQ(MimeType_Json, SystemToolbox::AutodetectMimeType("NOTES.nmf"));
-  ASSERT_EQ(MimeType_PNaCl, SystemToolbox::AutodetectMimeType("NOTES.pexe"));
-  ASSERT_EQ(MimeType_Svg, SystemToolbox::AutodetectMimeType("NOTES.svg"));
-  ASSERT_EQ(MimeType_Woff, SystemToolbox::AutodetectMimeType("NOTES.woff"));
-  ASSERT_EQ(MimeType_Woff2, SystemToolbox::AutodetectMimeType("NOTES.woff2"));
-
-  // Test primitives from the "RegisterDefaultExtensions()" that was
-  // present in the sample "Serve Folders plugin" of Orthanc 1.4.2
-  ASSERT_STREQ("application/javascript", EnumerationToString(SystemToolbox::AutodetectMimeType(".js")));
-  ASSERT_STREQ("application/json", EnumerationToString(SystemToolbox::AutodetectMimeType(".json")));
-  ASSERT_STREQ("application/json", EnumerationToString(SystemToolbox::AutodetectMimeType(".nmf")));
-  ASSERT_STREQ("application/octet-stream", EnumerationToString(SystemToolbox::AutodetectMimeType("")));
-  ASSERT_STREQ("application/wasm", EnumerationToString(SystemToolbox::AutodetectMimeType(".wasm")));
-  ASSERT_STREQ("application/x-font-woff", EnumerationToString(SystemToolbox::AutodetectMimeType(".woff")));
-  ASSERT_STREQ("application/x-nacl", EnumerationToString(SystemToolbox::AutodetectMimeType(".nexe")));
-  ASSERT_STREQ("application/x-pnacl", EnumerationToString(SystemToolbox::AutodetectMimeType(".pexe")));
-  ASSERT_STREQ("application/xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".xml")));
-  ASSERT_STREQ("font/woff2", EnumerationToString(SystemToolbox::AutodetectMimeType(".woff2")));
-  ASSERT_STREQ("image/gif", EnumerationToString(SystemToolbox::AutodetectMimeType(".gif")));
-  ASSERT_STREQ("image/jpeg", EnumerationToString(SystemToolbox::AutodetectMimeType(".jpeg")));
-  ASSERT_STREQ("image/jpeg", EnumerationToString(SystemToolbox::AutodetectMimeType(".jpg")));
-  ASSERT_STREQ("image/png", EnumerationToString(SystemToolbox::AutodetectMimeType(".png")));
-  ASSERT_STREQ("image/svg+xml", EnumerationToString(SystemToolbox::AutodetectMimeType(".svg")));
-  ASSERT_STREQ("text/css", EnumerationToString(SystemToolbox::AutodetectMimeType(".css")));
-  ASSERT_STREQ("text/html", EnumerationToString(SystemToolbox::AutodetectMimeType(".html")));
-}
-
-TEST(Toolbox, ComputeMD5)
-{
-  std::string s;
-
-  // # echo -n "Hello" | md5sum
-
-  Toolbox::ComputeMD5(s, "Hello");
-  ASSERT_EQ("8b1a9953c4611296a827abf8c47804d7", s);
-  Toolbox::ComputeMD5(s, "");
-  ASSERT_EQ("d41d8cd98f00b204e9800998ecf8427e", s);
-}
-
-TEST(Toolbox, ComputeSHA1)
-{
-  std::string s;
-  
-  Toolbox::ComputeSHA1(s, "The quick brown fox jumps over the lazy dog");
-  ASSERT_EQ("2fd4e1c6-7a2d28fc-ed849ee1-bb76e739-1b93eb12", s);
-  Toolbox::ComputeSHA1(s, "");
-  ASSERT_EQ("da39a3ee-5e6b4b0d-3255bfef-95601890-afd80709", s);
-}
-
-TEST(Toolbox, PathToExecutable)
-{
-  printf("[%s]\n", SystemToolbox::GetPathToExecutable().c_str());
-  printf("[%s]\n", SystemToolbox::GetDirectoryOfExecutable().c_str());
-}
-
-TEST(Toolbox, StripSpaces)
-{
-  ASSERT_EQ("", Toolbox::StripSpaces("       \t  \r   \n  "));
-  ASSERT_EQ("coucou", Toolbox::StripSpaces("    coucou   \t  \r   \n  "));
-  ASSERT_EQ("cou   cou", Toolbox::StripSpaces("    cou   cou    \n  "));
-  ASSERT_EQ("c", Toolbox::StripSpaces("    \n\t c\r    \n  "));
-}
-
-TEST(Toolbox, Case)
-{
-  std::string s = "CoU";
-  std::string ss;
-
-  Toolbox::ToUpperCase(ss, s);
-  ASSERT_EQ("COU", ss);
-  Toolbox::ToLowerCase(ss, s);
-  ASSERT_EQ("cou", ss); 
-
-  s = "CoU";
-  Toolbox::ToUpperCase(s);
-  ASSERT_EQ("COU", s);
-
-  s = "CoU";
-  Toolbox::ToLowerCase(s);
-  ASSERT_EQ("cou", s);
-}
-
-
-TEST(Logger, Basic)
-{
-  LOG(INFO) << "I say hello";
-}
-
-TEST(Toolbox, ConvertFromLatin1)
-{
-  // This is a Latin-1 test string
-  const unsigned char data[10] = { 0xe0, 0xe9, 0xea, 0xe7, 0x26, 0xc6, 0x61, 0x62, 0x63, 0x00 };
-  
-  std::string s((char*) &data[0], 10);
-  ASSERT_EQ("&abc", Toolbox::ConvertToAscii(s));
-
-  // Open in Emacs, then save with UTF-8 encoding, then "hexdump -C"
-  std::string utf8 = Toolbox::ConvertToUtf8(s, Encoding_Latin1, false);
-  ASSERT_EQ(15u, utf8.size());
-  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[0]));
-  ASSERT_EQ(0xa0, static_cast<unsigned char>(utf8[1]));
-  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[2]));
-  ASSERT_EQ(0xa9, static_cast<unsigned char>(utf8[3]));
-  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[4]));
-  ASSERT_EQ(0xaa, static_cast<unsigned char>(utf8[5]));
-  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[6]));
-  ASSERT_EQ(0xa7, static_cast<unsigned char>(utf8[7]));
-  ASSERT_EQ(0x26, static_cast<unsigned char>(utf8[8]));
-  ASSERT_EQ(0xc3, static_cast<unsigned char>(utf8[9]));
-  ASSERT_EQ(0x86, static_cast<unsigned char>(utf8[10]));
-  ASSERT_EQ(0x61, static_cast<unsigned char>(utf8[11]));
-  ASSERT_EQ(0x62, static_cast<unsigned char>(utf8[12]));
-  ASSERT_EQ(0x63, static_cast<unsigned char>(utf8[13]));
-  ASSERT_EQ(0x00, static_cast<unsigned char>(utf8[14]));  // Null-terminated string
-}
-
-
-TEST(Toolbox, FixUtf8)
-{
-  // This is a Latin-1 test string: "crane" with a circumflex accent
-  const unsigned char latin1[] = { 0x63, 0x72, 0xe2, 0x6e, 0x65 };
-
-  std::string s((char*) &latin1[0], sizeof(latin1) / sizeof(char));
-
-  ASSERT_EQ(s, Toolbox::ConvertFromUtf8(Toolbox::ConvertToUtf8(s, Encoding_Latin1, false), Encoding_Latin1));
-  ASSERT_EQ("cre", Toolbox::ConvertToUtf8(s, Encoding_Utf8, false));
-}
-
-
-static int32_t GetUnicode(const uint8_t* data,
-                          size_t size,
-                          size_t expectedLength)
-{
-  std::string s((char*) &data[0], size);
-  uint32_t unicode;
-  size_t length;
-  Toolbox::Utf8ToUnicodeCharacter(unicode, length, s, 0);
-  if (length != expectedLength)
-  {
-    return -1;  // Error case
-  }
-  else
-  {
-    return unicode;
-  }
-}
-
-
-TEST(Toolbox, Utf8ToUnicode)
-{
-  // https://en.wikipedia.org/wiki/UTF-8
-  
-  ASSERT_EQ(1u, sizeof(char));
-  ASSERT_EQ(1u, sizeof(uint8_t));
-  
-  {
-    const uint8_t data[] = { 0x24 };
-    ASSERT_EQ(0x24, GetUnicode(data, 1, 1));
-    ASSERT_THROW(GetUnicode(data, 0, 1), OrthancException);
-  }
-  
-  {
-    const uint8_t data[] = { 0xc2, 0xa2 };
-    ASSERT_EQ(0xa2, GetUnicode(data, 2, 2));
-    ASSERT_THROW(GetUnicode(data, 1, 2), OrthancException);
-  }
-  
-  {
-    const uint8_t data[] = { 0xe0, 0xa4, 0xb9 };
-    ASSERT_EQ(0x0939, GetUnicode(data, 3, 3));
-    ASSERT_THROW(GetUnicode(data, 2, 3), OrthancException);
-  }
-  
-  {
-    const uint8_t data[] = { 0xe2, 0x82, 0xac };
-    ASSERT_EQ(0x20ac, GetUnicode(data, 3, 3));
-    ASSERT_THROW(GetUnicode(data, 2, 3), OrthancException);
-  }
-  
-  {
-    const uint8_t data[] = { 0xf0, 0x90, 0x8d, 0x88 };
-    ASSERT_EQ(0x010348, GetUnicode(data, 4, 4));
-    ASSERT_THROW(GetUnicode(data, 3, 4), OrthancException);
-  }
-  
-  {
-    const uint8_t data[] = { 0xe0 };
-    ASSERT_THROW(GetUnicode(data, 1, 1), OrthancException);
-  }
-}
-
-
-TEST(Toolbox, UrlDecode)
-{
-  std::string s;
-
-  s = "Hello%20World";
-  Toolbox::UrlDecode(s);
-  ASSERT_EQ("Hello World", s);
-
-  s = "%21%23%24%26%27%28%29%2A%2B%2c%2f%3A%3b%3d%3f%40%5B%5D%90%ff";
-  Toolbox::UrlDecode(s);
-  std::string ss = "!#$&'()*+,/:;=?@[]"; 
-  ss.push_back((char) 144); 
-  ss.push_back((char) 255);
-  ASSERT_EQ(ss, s);
-
-  s = "(2000%2C00A4)+Other";
-  Toolbox::UrlDecode(s);
-  ASSERT_EQ("(2000,00A4) Other", s);
-}
-
-
-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));
-
-  ASSERT_TRUE(Toolbox::IsAsciiString("Hello\nworld"));
-  ASSERT_FALSE(Toolbox::IsAsciiString("Hello\rworld"));
-
-  ASSERT_EQ("Hello\nworld", Toolbox::ConvertToAscii("Hello\nworld"));
-  ASSERT_EQ("Helloworld", Toolbox::ConvertToAscii("Hello\r\tworld"));
-}
-
-
-#if defined(__linux__)
-TEST(Toolbox, AbsoluteDirectory)
-{
-  ASSERT_EQ("/tmp/hello", SystemToolbox::InterpretRelativePath("/tmp", "hello"));
-  ASSERT_EQ("/tmp", SystemToolbox::InterpretRelativePath("/tmp", "/tmp"));
-}
-#endif
-
-
-TEST(Toolbox, WriteFile)
-{
-  std::string path;
-
-  {
-    TemporaryFile tmp;
-    path = tmp.GetPath();
-
-    std::string s;
-    s.append("Hello");
-    s.push_back('\0');
-    s.append("World");
-    ASSERT_EQ(11u, s.size());
-
-    SystemToolbox::WriteFile(s, path.c_str());
-
-    std::string t;
-    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(SystemToolbox::ReadFile(u, path.c_str()), OrthancException);
-}
-
-
-TEST(Toolbox, FileBuffer)
-{
-  FileBuffer f;
-  f.Append("a", 1);
-  f.Append("", 0);
-  f.Append("bc", 2);
-
-  std::string s;
-  f.Read(s);
-  ASSERT_EQ("abc", s);
-
-  ASSERT_THROW(f.Append("d", 1), OrthancException);  // File is closed
-}
-
-
-TEST(Toolbox, Wildcard)
-{
-  ASSERT_EQ("abcd", Toolbox::WildcardToRegularExpression("abcd"));
-  ASSERT_EQ("ab.*cd", Toolbox::WildcardToRegularExpression("ab*cd"));
-  ASSERT_EQ("ab..cd", Toolbox::WildcardToRegularExpression("ab??cd"));
-  ASSERT_EQ("a.*b.c.*d", Toolbox::WildcardToRegularExpression("a*b?c*d"));
-  ASSERT_EQ("a\\{b\\]", Toolbox::WildcardToRegularExpression("a{b]"));
-}
-
-
-TEST(Toolbox, Tokenize)
-{
-  std::vector<std::string> t;
-  
-  Toolbox::TokenizeString(t, "", ','); 
-  ASSERT_EQ(1u, t.size());
-  ASSERT_EQ("", t[0]);
-  
-  Toolbox::TokenizeString(t, "abc", ','); 
-  ASSERT_EQ(1u, t.size());
-  ASSERT_EQ("abc", t[0]);
-  
-  Toolbox::TokenizeString(t, "ab,cd,ef,", ','); 
-  ASSERT_EQ(4u, t.size());
-  ASSERT_EQ("ab", t[0]);
-  ASSERT_EQ("cd", t[1]);
-  ASSERT_EQ("ef", t[2]);
-  ASSERT_EQ("", t[3]);
-}
-
-TEST(Toolbox, Enumerations)
-{
-  ASSERT_EQ(Encoding_Utf8, StringToEncoding(EnumerationToString(Encoding_Utf8)));
-  ASSERT_EQ(Encoding_Ascii, StringToEncoding(EnumerationToString(Encoding_Ascii)));
-  ASSERT_EQ(Encoding_Latin1, StringToEncoding(EnumerationToString(Encoding_Latin1)));
-  ASSERT_EQ(Encoding_Latin2, StringToEncoding(EnumerationToString(Encoding_Latin2)));
-  ASSERT_EQ(Encoding_Latin3, StringToEncoding(EnumerationToString(Encoding_Latin3)));
-  ASSERT_EQ(Encoding_Latin4, StringToEncoding(EnumerationToString(Encoding_Latin4)));
-  ASSERT_EQ(Encoding_Latin5, StringToEncoding(EnumerationToString(Encoding_Latin5)));
-  ASSERT_EQ(Encoding_Cyrillic, StringToEncoding(EnumerationToString(Encoding_Cyrillic)));
-  ASSERT_EQ(Encoding_Arabic, StringToEncoding(EnumerationToString(Encoding_Arabic)));
-  ASSERT_EQ(Encoding_Greek, StringToEncoding(EnumerationToString(Encoding_Greek)));
-  ASSERT_EQ(Encoding_Hebrew, StringToEncoding(EnumerationToString(Encoding_Hebrew)));
-  ASSERT_EQ(Encoding_Japanese, StringToEncoding(EnumerationToString(Encoding_Japanese)));
-  ASSERT_EQ(Encoding_Chinese, StringToEncoding(EnumerationToString(Encoding_Chinese)));
-  ASSERT_EQ(Encoding_Thai, StringToEncoding(EnumerationToString(Encoding_Thai)));
-  ASSERT_EQ(Encoding_Korean, StringToEncoding(EnumerationToString(Encoding_Korean)));
-  ASSERT_EQ(Encoding_JapaneseKanji, StringToEncoding(EnumerationToString(Encoding_JapaneseKanji)));
-  ASSERT_EQ(Encoding_SimplifiedChinese, StringToEncoding(EnumerationToString(Encoding_SimplifiedChinese)));
-
-  ASSERT_EQ(ResourceType_Patient, StringToResourceType(EnumerationToString(ResourceType_Patient)));
-  ASSERT_EQ(ResourceType_Study, StringToResourceType(EnumerationToString(ResourceType_Study)));
-  ASSERT_EQ(ResourceType_Series, StringToResourceType(EnumerationToString(ResourceType_Series)));
-  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);
-
-  ASSERT_EQ(MimeType_Binary, StringToMimeType(EnumerationToString(MimeType_Binary)));
-  ASSERT_EQ(MimeType_Css, StringToMimeType(EnumerationToString(MimeType_Css)));
-  ASSERT_EQ(MimeType_Dicom, StringToMimeType(EnumerationToString(MimeType_Dicom)));
-  ASSERT_EQ(MimeType_Gif, StringToMimeType(EnumerationToString(MimeType_Gif)));
-  ASSERT_EQ(MimeType_Gzip, StringToMimeType(EnumerationToString(MimeType_Gzip)));
-  ASSERT_EQ(MimeType_Html, StringToMimeType(EnumerationToString(MimeType_Html)));
-  ASSERT_EQ(MimeType_JavaScript, StringToMimeType(EnumerationToString(MimeType_JavaScript)));
-  ASSERT_EQ(MimeType_Jpeg, StringToMimeType(EnumerationToString(MimeType_Jpeg)));
-  ASSERT_EQ(MimeType_Jpeg2000, StringToMimeType(EnumerationToString(MimeType_Jpeg2000)));
-  ASSERT_EQ(MimeType_Json, StringToMimeType(EnumerationToString(MimeType_Json)));
-  ASSERT_EQ(MimeType_NaCl, StringToMimeType(EnumerationToString(MimeType_NaCl)));
-  ASSERT_EQ(MimeType_PNaCl, StringToMimeType(EnumerationToString(MimeType_PNaCl)));
-  ASSERT_EQ(MimeType_Pam, StringToMimeType(EnumerationToString(MimeType_Pam)));
-  ASSERT_EQ(MimeType_Pdf, StringToMimeType(EnumerationToString(MimeType_Pdf)));
-  ASSERT_EQ(MimeType_PlainText, StringToMimeType(EnumerationToString(MimeType_PlainText)));
-  ASSERT_EQ(MimeType_Png, StringToMimeType(EnumerationToString(MimeType_Png)));
-  ASSERT_EQ(MimeType_Svg, StringToMimeType(EnumerationToString(MimeType_Svg)));
-  ASSERT_EQ(MimeType_WebAssembly, StringToMimeType(EnumerationToString(MimeType_WebAssembly)));
-  ASSERT_EQ(MimeType_Xml, StringToMimeType("application/xml"));
-  ASSERT_EQ(MimeType_Xml, StringToMimeType("text/xml"));
-  ASSERT_EQ(MimeType_Xml, StringToMimeType(EnumerationToString(MimeType_Xml)));
-  ASSERT_EQ(MimeType_DicomWebJson, StringToMimeType(EnumerationToString(MimeType_DicomWebJson)));
-  ASSERT_EQ(MimeType_DicomWebXml, StringToMimeType(EnumerationToString(MimeType_DicomWebXml)));
-  ASSERT_THROW(StringToMimeType("nope"), OrthancException);
-
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Patient));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Study));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Series));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Patient, ResourceType_Instance));
-
-  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Patient));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Study));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Series));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Study, ResourceType_Instance));
-
-  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Patient));
-  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Study));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Series));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Series, ResourceType_Instance));
-
-  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Patient));
-  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Study));
-  ASSERT_FALSE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Series));
-  ASSERT_TRUE(IsResourceLevelAboveOrEqual(ResourceType_Instance, ResourceType_Instance));
-}
-
-
-#if defined(__linux__) || defined(__OpenBSD__)
-#include <endian.h>
-#elif defined(__FreeBSD__)
-#include <machine/endian.h>
-#endif
-
-
-TEST(Toolbox, Endianness)
-{
-  // Parts of this test come from Adam Conrad
-  // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=728822#5
-
-
-  /**
-   * Windows and OS X are assumed to always little-endian.
-   **/
-  
-#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__)
-
-#if !defined(__BYTE_ORDER)
-#  error Support your platform here
-#endif
-
-#  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
-}
-
-
-#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;
-  a["hello"] = "world";
-  a["42"] = 43;
-  a["b"] = Json::arrayValue;
-  a["b"].append("test");
-  a["b"].append("test2");
-
-  std::string s;
-  Toolbox::JsonToXml(s, a);
-
-  std::cout << s;
-}
-#endif
-
-
-#if !defined(_WIN32)
-TEST(Toolbox, ExecuteSystemCommand)
-{
-  std::vector<std::string> args(2);
-  args[0] = "Hello";
-  args[1] = "World";
-
-  SystemToolbox::ExecuteSystemCommand("echo", args);
-}
-#endif
-
-
-TEST(Toolbox, IsInteger)
-{
-  ASSERT_TRUE(Toolbox::IsInteger("00236"));
-  ASSERT_TRUE(Toolbox::IsInteger("-0042"));
-  ASSERT_TRUE(Toolbox::IsInteger("0"));
-  ASSERT_TRUE(Toolbox::IsInteger("-0"));
-
-  ASSERT_FALSE(Toolbox::IsInteger(""));
-  ASSERT_FALSE(Toolbox::IsInteger("42a"));
-  ASSERT_FALSE(Toolbox::IsInteger("42-"));
-}
-
-
-TEST(Toolbox, StartsWith)
-{
-  ASSERT_TRUE(Toolbox::StartsWith("hello world", ""));
-  ASSERT_TRUE(Toolbox::StartsWith("hello world", "hello"));
-  ASSERT_TRUE(Toolbox::StartsWith("hello world", "h"));
-  ASSERT_FALSE(Toolbox::StartsWith("hello world", "H"));
-  ASSERT_FALSE(Toolbox::StartsWith("h", "hello"));
-  ASSERT_TRUE(Toolbox::StartsWith("h", "h"));
-  ASSERT_FALSE(Toolbox::StartsWith("", "h"));
-}
-
-
-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));
-  }
-}
-
-
-TEST(Toolbox, SubstituteVariables)
-{
-  std::map<std::string, std::string> env;
-  env["NOPE"] = "nope";
-  env["WORLD"] = "world";
-
-  ASSERT_EQ("Hello world\r\nWorld \r\nDone world\r\n",
-            Toolbox::SubstituteVariables(
-              "Hello ${WORLD}\r\nWorld ${HELLO}\r\nDone ${WORLD}\r\n",
-              env));
-
-  ASSERT_EQ("world A a B world C 'c' D {\"a\":\"b\"} E ",
-            Toolbox::SubstituteVariables(
-              "${WORLD} A ${WORLD2:-a} B ${WORLD:-b} C ${WORLD2:-\"'c'\"} D ${WORLD2:-'{\"a\":\"b\"}'} E ${WORLD2:-}",
-              env));
-  
-  SystemToolbox::GetEnvironmentVariables(env);
-  ASSERT_TRUE(env.find("NOPE") == env.end());
-
-  // The "PATH" environment variable should always be available on
-  // machines running the unit tests
-  ASSERT_TRUE(env.find("PATH") != env.end() /* Case used by UNIX */ ||
-              env.find("Path") != env.end() /* Case used by Windows */);
-
-  env["PATH"] = "hello";
-  ASSERT_EQ("AhelloB",
-            Toolbox::SubstituteVariables("A${PATH}B", env));
-}
-
-
-TEST(MetricsRegistry, Basic)
-{
-  {
-    MetricsRegistry m;
-    m.SetEnabled(false);
-    m.SetValue("hello.world", 42.5f);
-    
-    std::string s;
-    m.ExportPrometheusText(s);
-    ASSERT_TRUE(s.empty());
-  }
-
-  {
-    MetricsRegistry m;
-    m.Register("hello.world", MetricsType_Default);
-    
-    std::string s;
-    m.ExportPrometheusText(s);
-    ASSERT_TRUE(s.empty());
-  }
-
-  {
-    MetricsRegistry m;
-    m.SetValue("hello.world", 42.5f);
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.world"));
-    ASSERT_THROW(m.GetMetricsType("nope"), OrthancException);
-    
-    std::string s;
-    m.ExportPrometheusText(s);
-
-    std::vector<std::string> t;
-    Toolbox::TokenizeString(t, s, '\n');
-    ASSERT_EQ(2u, t.size());
-    ASSERT_EQ("hello.world 42.5 ", t[0].substr(0, 17));
-    ASSERT_TRUE(t[1].empty());
-  }
-
-  {
-    MetricsRegistry m;
-    m.Register("hello.max", MetricsType_MaxOver10Seconds);
-    m.SetValue("hello.max", 10);
-    m.SetValue("hello.max", 20);
-    m.SetValue("hello.max", -10);
-    m.SetValue("hello.max", 5);
-
-    m.Register("hello.min", MetricsType_MinOver10Seconds);
-    m.SetValue("hello.min", 10);
-    m.SetValue("hello.min", 20);
-    m.SetValue("hello.min", -10);
-    m.SetValue("hello.min", 5);
-    
-    m.Register("hello.default", MetricsType_Default);
-    m.SetValue("hello.default", 10);
-    m.SetValue("hello.default", 20);
-    m.SetValue("hello.default", -10);
-    m.SetValue("hello.default", 5);
-    
-    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("hello.max"));
-    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("hello.min"));
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.default"));
-
-    std::string s;
-    m.ExportPrometheusText(s);
-
-    std::vector<std::string> t;
-    Toolbox::TokenizeString(t, s, '\n');
-    ASSERT_EQ(4u, t.size());
-    ASSERT_TRUE(t[3].empty());
-
-    std::map<std::string, std::string> u;
-    for (size_t i = 0; i < t.size() - 1; i++)
-    {
-      std::vector<std::string> v;
-      Toolbox::TokenizeString(v, t[i], ' ');
-      u[v[0]] = v[1];
-    }
-
-    ASSERT_EQ("20", u["hello.max"]);
-    ASSERT_EQ("-10", u["hello.min"]);
-    ASSERT_EQ("5", u["hello.default"]);
-  }
-
-  {
-    MetricsRegistry m;
-
-    m.SetValue("a", 10);
-    m.SetValue("b", 10, MetricsType_MinOver10Seconds);
-
-    m.Register("c", MetricsType_MaxOver10Seconds);
-    m.SetValue("c", 10, MetricsType_MinOver10Seconds);
-
-    m.Register("d", MetricsType_MaxOver10Seconds);
-    m.Register("d", MetricsType_Default);
-
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("a"));
-    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
-    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("c"));
-    ASSERT_EQ(MetricsType_Default, m.GetMetricsType("d"));
-  }
-
-  {
-    MetricsRegistry m;
-
-    {
-      MetricsRegistry::Timer t1(m, "a");
-      MetricsRegistry::Timer t2(m, "b", MetricsType_MinOver10Seconds);
-    }
-
-    ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("a"));
-    ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b"));
-  }
-}
--- a/UnitTestsSources/FromDcmtkTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2034 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Compatibility.h"
-#include "../Core/DicomNetworking/DicomFindAnswers.h"
-#include "../Core/DicomParsing/DicomModification.h"
-#include "../Core/DicomParsing/DicomWebJsonVisitor.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
-#include "../Core/DicomParsing/ToDcmtkBridge.h"
-#include "../Core/Endianness.h"
-#include "../Core/Images/Image.h"
-#include "../Core/Images/ImageBuffer.h"
-#include "../Core/Images/ImageProcessing.h"
-#include "../Core/Images/PngReader.h"
-#include "../Core/Images/PngWriter.h"
-#include "../Core/OrthancException.h"
-#include "../Core/SystemToolbox.h"
-#include "../OrthancServer/ServerToolbox.h"
-#include "../Plugins/Engine/PluginsEnumerations.h"
-#include "../Resources/EncodingTests.h"
-
-#include <dcmtk/dcmdata/dcelem.h>
-#include <dcmtk/dcmdata/dcdeftag.h>
-#include <boost/algorithm/string/predicate.hpp>
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-#  include <pugixml.hpp>
-#endif
-
-using namespace Orthanc;
-
-TEST(DicomFormat, Tag)
-{
-  ASSERT_EQ("PatientName", FromDcmtkBridge::GetTagName(DicomTag(0x0010, 0x0010), ""));
-
-  DicomTag t = FromDcmtkBridge::ParseTag("SeriesDescription");
-  ASSERT_EQ(0x0008, t.GetGroup());
-  ASSERT_EQ(0x103E, t.GetElement());
-
-  t = FromDcmtkBridge::ParseTag("0020-e040");
-  ASSERT_EQ(0x0020, t.GetGroup());
-  ASSERT_EQ(0xe040, t.GetElement());
-
-  // Test ==() and !=() operators
-  ASSERT_TRUE(DICOM_TAG_PATIENT_ID == DicomTag(0x0010, 0x0020));
-  ASSERT_FALSE(DICOM_TAG_PATIENT_ID != DicomTag(0x0010, 0x0020));
-}
-
-
-TEST(DicomModification, Basic)
-{
-  DicomModification m;
-  m.SetupAnonymization(DicomVersion_2008);
-  //m.SetLevel(DicomRootLevel_Study);
-  //m.ReplacePlainString(DICOM_TAG_PATIENT_ID, "coucou");
-  //m.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
-
-  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::unique_ptr<ParsedDicomFile> f(o.Clone(false));
-    if (i > 4)
-      o.ReplacePlainString(DICOM_TAG_SERIES_INSTANCE_UID, "coucou");
-    m.Apply(*f);
-    f->SaveToFile(b);
-  }
-}
-
-
-TEST(DicomModification, Anonymization)
-{
-  ASSERT_EQ(DICOM_TAG_PATIENT_NAME, FromDcmtkBridge::ParseTag("PatientName"));
-
-  const DicomTag privateTag(0x0045, 0x1010);
-  const DicomTag privateTag2(FromDcmtkBridge::ParseTag("0031-1020"));
-  ASSERT_TRUE(privateTag.IsPrivate());
-  ASSERT_TRUE(privateTag2.IsPrivate());
-  ASSERT_EQ(0x0031, privateTag2.GetGroup());
-  ASSERT_EQ(0x1020, privateTag2.GetElement());
-
-  std::string s;
-  ParsedDicomFile o(true);
-  o.ReplacePlainString(DICOM_TAG_PATIENT_NAME, "coucou");
-  ASSERT_FALSE(o.GetTagValue(s, privateTag));
-  o.Insert(privateTag, "private tag", false, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag));
-  ASSERT_STREQ("private tag", s.c_str());
-
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  ASSERT_THROW(o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_ThrowIfAbsent, "OrthancCreator"), OrthancException);
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_IgnoreIfAbsent, "OrthancCreator");
-  ASSERT_FALSE(o.GetTagValue(s, privateTag2));
-  o.Replace(privateTag2, std::string("hello"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
-  ASSERT_STREQ("hello", s.c_str());
-  o.Replace(privateTag2, std::string("hello world"), false, DicomReplaceMode_InsertIfAbsent, "OrthancCreator");
-  ASSERT_TRUE(o.GetTagValue(s, privateTag2));
-  ASSERT_STREQ("hello world", s.c_str());
-
-  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-  ASSERT_FALSE(Toolbox::IsUuid(s));
-
-  DicomModification m;
-  m.SetupAnonymization(DicomVersion_2008);
-  m.Keep(privateTag);
-
-  m.Apply(o);
-
-  ASSERT_TRUE(o.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-  ASSERT_TRUE(Toolbox::IsUuid(s));
-  ASSERT_TRUE(o.GetTagValue(s, privateTag));
-  ASSERT_STREQ("private tag", s.c_str());
-  
-  m.SetupAnonymization(DicomVersion_2008);
-  m.Apply(o);
-  ASSERT_FALSE(o.GetTagValue(s, privateTag));
-}
-
-
-#include <dcmtk/dcmdata/dcuid.h>
-
-TEST(DicomModification, Png)
-{
-  // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image)
-  std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
-
-  std::string m, cc;
-  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(m, cc, s));
-
-  ASSERT_EQ("image/png", m);
-
-  PngReader reader;
-  reader.ReadFromMemory(cc);
-
-  ASSERT_EQ(5u, reader.GetHeight());
-  ASSERT_EQ(5u, reader.GetWidth());
-  ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat());
-
-  ParsedDicomFile o(true);
-  o.EmbedContent(s);
-  o.SaveToFile("UnitTestsResults/png1.dcm");
-
-  // Red dot, without alpha channel
-  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII=";
-  o.EmbedContent(s);
-  o.SaveToFile("UnitTestsResults/png2.dcm");
-
-  // Check box in Graylevel8
-  s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII=";
-  o.EmbedContent(s);
-  //o.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing);
-  o.SaveToFile("UnitTestsResults/png3.dcm");
-
-
-  {
-    // Gradient in Graylevel16
-
-    ImageBuffer img;
-    img.SetWidth(256);
-    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*>(accessor.GetRow(y));
-      for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++)
-      {
-        *p = v;
-      }
-    }
-
-    o.EmbedImage(accessor);
-    o.SaveToFile("UnitTestsResults/png4.dcm");
-  }
-}
-
-
-TEST(FromDcmtkBridge, Encodings1)
-{
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    std::string source(testEncodingsEncoded[i]);
-    std::string expected(testEncodingsExpected[i]);
-    std::string s = Toolbox::ConvertToUtf8(source, testEncodings[i], false);
-    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
-    EXPECT_EQ(expected, s);
-  }
-}
-
-
-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_Ascii, e);
-
-  // 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);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO_IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
-  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 166"));  ASSERT_EQ(Encoding_Thai, 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);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 110"));  ASSERT_EQ(Encoding_Latin4, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 144"));  ASSERT_EQ(Encoding_Cyrillic, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 127"));  ASSERT_EQ(Encoding_Arabic, e);
-  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 166"));  ASSERT_EQ(Encoding_Thai, e);
-
-  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#table_C.12-4
-  ASSERT_TRUE(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_TRUE(GetDicomEncoding(e, "ISO 2022 IR 149"));   ASSERT_EQ(Encoding_Korean, e);
-  ASSERT_TRUE(GetDicomEncoding(e, "ISO 2022 IR 58"));    ASSERT_EQ(Encoding_SimplifiedChinese, e);
-
-  // 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, "GBK"));         ASSERT_EQ(Encoding_Chinese, e);
-}
-
-
-TEST(FromDcmtkBridge, Encodings3)
-{
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
-    std::string dicom;
-
-    {
-      ParsedDicomFile f(true);
-      f.SetEncoding(testEncodings[i]);
-
-      std::string s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
-      f.Insert(DICOM_TAG_PATIENT_NAME, s, false, "");
-      f.SaveToMemoryBuffer(dicom);
-    }
-
-    if (testEncodings[i] != Encoding_Windows1251)
-    {
-      ParsedDicomFile g(dicom);
-
-      if (testEncodings[i] != Encoding_Ascii)
-      {
-        bool hasCodeExtensions;
-        ASSERT_EQ(testEncodings[i], g.DetectEncoding(hasCodeExtensions));
-        ASSERT_FALSE(hasCodeExtensions);
-      }
-
-      std::string tag;
-      ASSERT_TRUE(g.GetTagValue(tag, DICOM_TAG_PATIENT_NAME));
-      ASSERT_EQ(std::string(testEncodingsExpected[i]), tag);
-    }
-  }
-}
-
-
-TEST(FromDcmtkBridge, ValueRepresentation)
-{
-  ASSERT_EQ(ValueRepresentation_PersonName, 
-            FromDcmtkBridge::LookupValueRepresentation(DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(ValueRepresentation_Date, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0020) /* StudyDate */));
-  ASSERT_EQ(ValueRepresentation_Time, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x0030) /* StudyTime */));
-  ASSERT_EQ(ValueRepresentation_DateTime, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0008, 0x002a) /* AcquisitionDateTime */));
-  ASSERT_EQ(ValueRepresentation_NotSupported, 
-            FromDcmtkBridge::LookupValueRepresentation(DicomTag(0x0001, 0x0001) /* some private tag */));
-}
-
-
-TEST(FromDcmtkBridge, ValueRepresentationConversions)
-{
-#if ORTHANC_ENABLE_PLUGINS == 1
-  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));
-  }
-#endif
-}
-
-
-
-static const DicomTag REFERENCED_STUDY_SEQUENCE(0x0008, 0x1110);
-static const DicomTag REFERENCED_PATIENT_SEQUENCE(0x0008, 0x1120);
-
-static void CreateSampleJson(Json::Value& a)
-{
-  {
-    Json::Value b = Json::objectValue;
-    b["PatientName"] = "Hello";
-    b["PatientID"] = "World";
-    b["StudyDescription"] = "Toto";
-    a.append(b);
-  }
-
-  {
-    Json::Value b = Json::objectValue;
-    b["PatientName"] = "data:application/octet-stream;base64,SGVsbG8y";  // echo -n "Hello2" | base64
-    b["PatientID"] = "World2";
-    a.append(b);
-  }
-}
-
-
-namespace Orthanc
-{
-  // 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)
-  {
-    std::unique_ptr<DcmElement> element;
-
-    {
-      Json::Value a;
-      a = "Hello";
-      element.reset(FromDcmtkBridge::FromJson(DICOM_TAG_PATIENT_NAME, a, false, Encoding_Utf8, ""));
-
-      Json::Value b;
-      std::set<DicomTag> ignoreTagLength;
-      ignoreTagLength.insert(DICOM_TAG_PATIENT_ID);
-
-      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, 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, false, ignoreTagLength);
-      ASSERT_TRUE(b["0010,0010"].isNull()); // "Hello" has more than 3 characters
-
-      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Full,
-                                     DicomToJsonFlags_Default, 3, Encoding_Ascii, false, 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, false, 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;
-      std::set<DicomTag> ignoreTagLength;
-      FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                     DicomToJsonFlags_Default, 0, Encoding_Ascii, false, 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 b;
-        std::set<DicomTag> ignoreTagLength;
-        FromDcmtkBridge::ElementToJson(b, *element, DicomToJsonFormat_Short,
-                                       DicomToJsonFlags_Default, 0, Encoding_Ascii, false, 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;
-
-        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, false, 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(true);
-
-  f.Insert(DICOM_TAG_PATIENT_NAME, "World", false, "");
-  ASSERT_THROW(f.Insert(DICOM_TAG_PATIENT_ID, "Hello", false, ""), OrthancException);  // Already existing tag
-  f.ReplacePlainString(DICOM_TAG_SOP_INSTANCE_UID, "Toto");  // (*)
-  f.ReplacePlainString(DICOM_TAG_SOP_CLASS_UID, "Tata");  // (**)
-
-  std::string s;
-  ASSERT_TRUE(f.LookupTransferSyntax(s));
-  // The default transfer syntax depends on the OS endianness
-  ASSERT_TRUE(s == GetTransferSyntaxUid(DicomTransferSyntax_LittleEndianExplicit) ||
-              s == GetTransferSyntaxUid(DicomTransferSyntax_BigEndianExplicit));
-
-  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, 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, 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, std::string("Accession3"), false, DicomReplaceMode_ThrowIfAbsent, "");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_ACCESSION_NUMBER));
-  ASSERT_EQ(s, "Accession3");
-
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(s, "World");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_INSTANCE_UID));
-  ASSERT_EQ(s, "Toto");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
-  ASSERT_EQ(s, "Toto");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
-  ASSERT_EQ(s, "Tata");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
-  ASSERT_EQ(s, "Tata");
-}
-
-
-
-
-TEST(ParsedDicomFile, InsertReplaceJson)
-{
-  ParsedDicomFile f(true);
-
-  Json::Value a;
-  CreateSampleJson(a);
-
-  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  f.Remove(REFERENCED_STUDY_SEQUENCE);  // No effect
-  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
-  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  ASSERT_THROW(f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, ""), OrthancException);
-  f.Remove(REFERENCED_STUDY_SEQUENCE);
-  ASSERT_FALSE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-  f.Insert(REFERENCED_STUDY_SEQUENCE, a, true, "");
-  ASSERT_TRUE(f.HasTag(REFERENCED_STUDY_SEQUENCE));
-
-  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  ASSERT_THROW(f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_ThrowIfAbsent, ""), OrthancException);
-  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_IgnoreIfAbsent, "");
-  ASSERT_FALSE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-  f.Replace(REFERENCED_PATIENT_SEQUENCE, a, false, DicomReplaceMode_InsertIfAbsent, "");
-  ASSERT_TRUE(f.HasTag(REFERENCED_PATIENT_SEQUENCE));
-
-  {
-    Json::Value b;
-    f.DatasetToJson(b, DicomToJsonFormat_Full, DicomToJsonFlags_Default, 0);
-
-    Json::Value c;
-    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, 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));
-  ASSERT_EQ(s, a.asString());
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_INSTANCE_UID));  // Implicitly modified by (*)
-  ASSERT_EQ(s, a.asString());
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_SOP_CLASS_UID));
-  ASSERT_EQ(s, "Tata");
-  ASSERT_TRUE(f.GetTagValue(s, DICOM_TAG_MEDIA_STORAGE_SOP_CLASS_UID));  // Implicitly modified by (**)
-  ASSERT_EQ(s, "Tata");
-}
-
-
-TEST(ParsedDicomFile, JsonEncoding)
-{
-  ParsedDicomFile f(true);
-
-  for (unsigned int i = 0; i < testEncodingsCount; i++)
-  {
-    if (testEncodings[i] != Encoding_Windows1251)
-    {
-      //std::cout << EnumerationToString(testEncodings[i]) << std::endl;
-      f.SetEncoding(testEncodings[i]);
-
-      if (testEncodings[i] != Encoding_Ascii)
-      {
-        bool hasCodeExtensions;
-        ASSERT_EQ(testEncodings[i], f.DetectEncoding(hasCodeExtensions));
-        ASSERT_FALSE(hasCodeExtensions);
-      }
-
-      Json::Value s = Toolbox::ConvertToUtf8(testEncodingsEncoded[i], testEncodings[i], false);
-      f.Replace(DICOM_TAG_PATIENT_NAME, s, false, DicomReplaceMode_InsertIfAbsent, "");
-
-      Json::Value v;
-      f.DatasetToJson(v, DicomToJsonFormat_Human, DicomToJsonFlags_Default, 0);
-      ASSERT_EQ(v["PatientName"].asString(), std::string(testEncodingsExpected[i]));
-    }
-  }
-}
-
-
-TEST(ParsedDicomFile, ToJsonFlags1)
-{
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7053, 0x1000), ValueRepresentation_OtherByte, "MyPrivateTag", 1, 1, "OrthancCreator");
-  FromDcmtkBridge::RegisterDictionaryTag(DicomTag(0x7050, 0x1000), ValueRepresentation_PersonName, "Declared public tag", 1, 1, "");
-
-  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, "OrthancCreator");  // Odd group => private tag
-
-  Json::Value v;
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  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.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 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());
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7052,1000"));
-  ASSERT_TRUE(v.isMember("7050,1000"));
-  ASSERT_FALSE(v.isMember("7053,1000"));
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePrivateTags | DicomToJsonFlags_IncludeBinary), 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());
-  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.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary | DicomToJsonFlags_ConvertBinaryToNull), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  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());
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludeUnknownTags | DicomToJsonFlags_IncludeBinary), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  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_IncludeBinary | 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());
-  ASSERT_EQ(Json::nullValue, v["7053,1000"].type());
-}
-
-
-TEST(ParsedDicomFile, ToJsonFlags2)
-{
-  ParsedDicomFile f(true);
-
-  {
-    // "ParsedDicomFile" uses Little Endian => 'B' (least significant
-    // byte) will be stored first in the memory buffer and in the
-    // file, then 'A'. Hence the expected "BA" value below.
-    Uint16 v[] = { 'A' * 256 + 'B', 0 };
-    ASSERT_TRUE(f.GetDcmtkObject().getDataset()->putAndInsertUint16Array(DCM_PixelData, v, 2).good());
-  }
-
-  Json::Value v;
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_None, 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(5u, v.getMemberNames().size());
-  ASSERT_FALSE(v.isMember("7fe0,0010"));  
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToNull), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7fe0,0010"));  
-  ASSERT_EQ(Json::nullValue, v["7fe0,0010"].type());  
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, static_cast<DicomToJsonFlags>(DicomToJsonFlags_IncludePixelData | DicomToJsonFlags_ConvertBinaryToAscii), 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7fe0,0010"));  
-  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());  
-  ASSERT_EQ("BA", v["7fe0,0010"].asString().substr(0, 2));
-
-  f.DatasetToJson(v, DicomToJsonFormat_Short, DicomToJsonFlags_IncludePixelData, 0);
-  ASSERT_EQ(Json::objectValue, v.type());
-  ASSERT_EQ(6u, v.getMemberNames().size());
-  ASSERT_TRUE(v.isMember("7fe0,0010"));  
-  ASSERT_EQ(Json::stringValue, v["7fe0,0010"].type());
-  std::string mime, content;
-  ASSERT_TRUE(Toolbox::DecodeDataUriScheme(mime, content, v["7fe0,0010"].asString()));
-  ASSERT_EQ("application/octet-stream", mime);
-  ASSERT_EQ("BA", content.substr(0, 2));
-}
-
-
-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::unique_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::unique_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::unique_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::unique_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::unique_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::unique_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::unique_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, GetDefaultDicomEncoding(), false);
-    ASSERT_EQ(1u, dicom.GetDcmtkObject().getDataset()->card());
-    CheckEncoding(dicom, Encoding_Ascii);
-  }
-
-  {
-    DicomMap m;
-    ParsedDicomFile dicom(m, Encoding_Latin4, false);
-    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, GetDefaultDicomEncoding(), false);
-    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, false);
-    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], false);
-        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], false);
-          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], false);
-    
-        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], false);
-
-        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, false);
-      bool hasCodeExtensions;
-      ASSERT_EQ(Encoding_Utf8, dicom.DetectEncoding(hasCodeExtensions));
-      ASSERT_FALSE(hasCodeExtensions);
-      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.DetectEncoding(hasCodeExtensions));
-      ASSERT_FALSE(hasCodeExtensions);
-      
-      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 */, false);
-
-    bool hasCodeExtensions;
-    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
-    ASSERT_FALSE(hasCodeExtensions);
-  }
-  
-  {
-    // 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 */, false);
-
-    bool hasCodeExtensions;
-    ASSERT_EQ(Encoding_Japanese, d.DetectEncoding(hasCodeExtensions));
-    ASSERT_FALSE(hasCodeExtensions);
-  }
-  
-  {
-    // 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, false),
-                 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, false),
-                 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 */, false);
-
-    bool hasCodeExtensions;
-    ASSERT_EQ(Encoding_Latin3, d.DetectEncoding(hasCodeExtensions));
-    ASSERT_FALSE(hasCodeExtensions);
-  }
-}
-
-
-
-TEST(Toolbox, RemoveIso2022EscapeSequences)
-{
-  // +----------------------------------+
-  // | one-byte control messages        |
-  // +----------------------------------+
-
-  static const uint8_t iso2022_cstr_oneByteControl[] = {
-    0x0f, 0x41, 
-    0x0e, 0x42, 
-    0x8e, 0x1b, 0x4e, 0x43, 
-    0x8f, 0x1b, 0x4f, 0x44,
-    0x8e, 0x1b, 0x4a, 0x45, 
-    0x8f, 0x1b, 0x4a, 0x46,
-    0x50, 0x51, 0x52, 0x00
-  };
-  
-  static const uint8_t iso2022_cstr_oneByteControl_ref[] = {
-    0x41,
-    0x42,
-    0x43,
-    0x44,
-    0x8e, 0x1b, 0x4a, 0x45, 
-    0x8f, 0x1b, 0x4a, 0x46,
-    0x50, 0x51, 0x52, 0x00
-  };
-
-  // +----------------------------------+
-  // | two-byte control messages        |
-  // +----------------------------------+
-
-  static const uint8_t iso2022_cstr_twoByteControl[] = {
-    0x1b, 0x6e, 0x41,
-    0x1b, 0x6f, 0x42,
-    0x1b, 0x4e, 0x43,
-    0x1b, 0x4f, 0x44,
-    0x1b, 0x7e, 0x45,
-    0x1b, 0x7d, 0x46,
-    0x1b, 0x7c, 0x47, 0x00
-  };
-  
-  static const uint8_t iso2022_cstr_twoByteControl_ref[] = {
-    0x41,
-    0x42,
-    0x43,
-    0x44,
-    0x45,
-    0x46,
-    0x47, 0x00
-  };
-
-  // +----------------------------------+
-  // | various-length escape sequences  |
-  // +----------------------------------+
-
-  static const uint8_t iso2022_cstr_escapeSequence[] = {
-    0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq)
-    0x1b, 0x50, 0x42, // ditto 
-    0x1b, 0x7f, 0x43, // ditto
-    0x1b, 0x21, 0x4a, 0x44, // this will match
-    0x1b, 0x20, 0x21, 0x2f, 0x40, 0x45, // this will match
-    0x1b, 0x20, 0x21, 0x2f, 0x2f, 0x40, 0x46, // this will match too
-    0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match!
-  };
-  
-  static const uint8_t iso2022_cstr_escapeSequence_ref[] = {
-    0x1b, 0x40, 0x41, // 1b and 40 should not be removed (invalid esc seq)
-    0x1b, 0x50, 0x42, // ditto 
-    0x1b, 0x7f, 0x43, // ditto
-    0x44, // this will match
-    0x45, // this will match
-    0x46, // this will match too
-    0x1b, 0x20, 0x21, 0x2f, 0x1f, 0x47, 0x48, 0x00 // this will NOT match!
-  };
-
-  
-  // +----------------------------------+
-  // | a real-world japanese sample     |
-  // +----------------------------------+
-
-  static const uint8_t iso2022_cstr_real_ir13[] = {
-    0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3,
-    0x3d, 0x1b, 0x24, 0x42, 0x3b, 0x33, 0x45, 0x44,
-    0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x42,
-    0x40, 0x4f, 0x3a, 0x1b, 0x28, 0x4a, 0x3d, 0x1b,
-    0x24, 0x42, 0x24, 0x64, 0x24, 0x5e, 0x24, 0x40,
-    0x1b, 0x28, 0x4a, 0x5e, 0x1b, 0x24, 0x42, 0x24,
-    0x3f, 0x24, 0x6d, 0x24, 0x26, 0x1b, 0x28, 0x4a, 0x00
-  };
-
-  static const uint8_t iso2022_cstr_real_ir13_ref[] = {
-    0xd4, 0xcf, 0xc0, 0xde, 0x5e, 0xc0, 0xdb, 0xb3,
-    0x3d,
-    0x3b, 0x33, 0x45, 0x44,
-    0x5e,
-    0x42,
-    0x40, 0x4f, 0x3a,
-    0x3d,
-    0x24, 0x64, 0x24, 0x5e, 0x24, 0x40,
-    0x5e,
-    0x24,
-    0x3f, 0x24, 0x6d, 0x24, 0x26, 0x00
-  };
-
-
-
-  // +----------------------------------+
-  // | the actual test                  |
-  // +----------------------------------+
-
-  std::string iso2022_str_oneByteControl(
-    reinterpret_cast<const char*>(iso2022_cstr_oneByteControl));
-  std::string iso2022_str_oneByteControl_ref(
-    reinterpret_cast<const char*>(iso2022_cstr_oneByteControl_ref));
-  std::string iso2022_str_twoByteControl(
-    reinterpret_cast<const char*>(iso2022_cstr_twoByteControl));
-  std::string iso2022_str_twoByteControl_ref(
-    reinterpret_cast<const char*>(iso2022_cstr_twoByteControl_ref));
-  std::string iso2022_str_escapeSequence(
-    reinterpret_cast<const char*>(iso2022_cstr_escapeSequence));
-  std::string iso2022_str_escapeSequence_ref(
-    reinterpret_cast<const char*>(iso2022_cstr_escapeSequence_ref));
-  std::string iso2022_str_real_ir13(
-    reinterpret_cast<const char*>(iso2022_cstr_real_ir13));
-  std::string iso2022_str_real_ir13_ref(
-    reinterpret_cast<const char*>(iso2022_cstr_real_ir13_ref));
-
-  std::string dest;
-
-  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_oneByteControl);
-  ASSERT_EQ(dest, iso2022_str_oneByteControl_ref);
-
-  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_twoByteControl);
-  ASSERT_EQ(dest, iso2022_str_twoByteControl_ref);
-
-  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_escapeSequence);
-  ASSERT_EQ(dest, iso2022_str_escapeSequence_ref);
-
-  Toolbox::RemoveIso2022EscapeSequences(dest, iso2022_str_real_ir13);
-  ASSERT_EQ(dest, iso2022_str_real_ir13_ref);
-}
-
-
-
-static std::string DecodeFromSpecification(const std::string& s)
-{
-  std::vector<std::string> tokens;
-  Toolbox::TokenizeString(tokens, s, ' ');
-
-  std::string result;
-  result.resize(tokens.size());
-  
-  for (size_t i = 0; i < tokens.size(); i++)
-  {
-    std::vector<std::string> components;
-    Toolbox::TokenizeString(components, tokens[i], '/');
-
-    if (components.size() != 2)
-    {
-      throw;
-    }
-
-    int a = boost::lexical_cast<int>(components[0]);
-    int b = boost::lexical_cast<int>(components[1]);
-    if (a < 0 || a > 15 ||
-        b < 0 || b > 15 ||
-        (a == 0 && b == 0))
-    {
-      throw;
-    }
-
-    result[i] = static_cast<uint8_t>(a * 16 + b);
-  }
-
-  return result;
-}
-
-
-
-// Compatibility wrapper
-static pugi::xpath_node SelectNode(const pugi::xml_document& doc,
-                                   const char* xpath)
-{
-#if PUGIXML_VERSION <= 140
-  return doc.select_single_node(xpath);  // Deprecated in pugixml 1.5
-#else
-  return doc.select_node(xpath);
-#endif
-}
-
-
-TEST(Toolbox, EncodingsKorean)
-{
-  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_I.2.html
-
-  std::string korean = DecodeFromSpecification(
-    "04/08 06/15 06/14 06/07 05/14 04/07 06/09 06/12 06/04 06/15 06/14 06/07 03/13 "
-    "01/11 02/04 02/09 04/03 15/11 15/03 05/14 01/11 02/04 02/09 04/03 13/01 12/14 "
-    "13/04 13/07 03/13 01/11 02/04 02/09 04/03 12/08 10/11 05/14 01/11 02/04 02/09 "
-    "04/03 11/01 14/06 11/05 11/15");
-
-  // This array can be re-generated using command-line:
-  // echo -n "Hong^Gildong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
-  static const uint8_t utf8raw[] = {
-    0x48, 0x6f, 0x6e, 0x67, 0x5e, 0x47, 0x69, 0x6c, 0x64, 0x6f, 0x6e, 0x67, 0x3d, 0xe6,
-    0xb4, 0xaa, 0x5e, 0xe5, 0x90, 0x89, 0xe6, 0xb4, 0x9e, 0x3d, 0xed, 0x99, 0x8d, 0x5e,
-    0xea, 0xb8, 0xb8, 0xeb, 0x8f, 0x99
-  };
-
-  std::string utf8(reinterpret_cast<const char*>(utf8raw), sizeof(utf8raw));
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 149");
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
-              (DCM_PatientName, korean.c_str(), OFBool(true)).good());
-
-  bool hasCodeExtensions;
-  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-  ASSERT_EQ(Encoding_Korean, encoding);
-  ASSERT_TRUE(hasCodeExtensions);
-  
-  std::string value;
-  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(utf8, value);
-  
-  DicomWebJsonVisitor visitor;
-  dicom.Apply(visitor);
-  ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString());
-  ASSERT_EQ(utf8.substr(13, 10), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString());
-  ASSERT_EQ(utf8.substr(24), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString());
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
-  std::string xml;
-  visitor.FormatXml(xml);
-
-  pugi::xml_document doc;
-  doc.load_buffer(xml.c_str(), xml.size());
-
-  pugi::xpath_node node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
-  ASSERT_STREQ("ISO 2022 IR 149", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
-  ASSERT_STREQ("CS", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
-  ASSERT_STREQ("PN", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
-  ASSERT_STREQ("Hong", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
-  ASSERT_STREQ("Gildong", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
-  ASSERT_EQ(utf8.substr(13, 3), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
-  ASSERT_EQ(utf8.substr(17, 6), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
-  ASSERT_EQ(utf8.substr(24, 3), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
-  ASSERT_EQ(utf8.substr(28), node.node().text().as_string());
-#endif
-  
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(2u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
-    ASSERT_EQ("ISO 2022 IR 149", s);
-
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-    std::vector<std::string> v;
-    Toolbox::TokenizeString(v, s, '=');
-    ASSERT_EQ(3u, v.size());
-    ASSERT_EQ("Hong^Gildong", v[0]);
-    ASSERT_EQ(utf8, s);
-  }
-}
-
-
-TEST(Toolbox, EncodingsJapaneseKanji)
-{
-  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_H.3.html
-
-  std::string japanese = DecodeFromSpecification(
-    "05/09 06/01 06/13 06/01 06/04 06/01 05/14 05/04 06/01 07/02 06/15 07/05 03/13 "
-    "01/11 02/04 04/02 03/11 03/03 04/05 04/04 01/11 02/08 04/02 05/14 01/11 02/04 "
-    "04/02 04/02 04/00 04/15 03/10 01/11 02/08 04/02 03/13 01/11 02/04 04/02 02/04 "
-    "06/04 02/04 05/14 02/04 04/00 01/11 02/08 04/02 05/14 01/11 02/04 04/02 02/04 "
-    "03/15 02/04 06/13 02/04 02/06 01/11 02/08 04/02");
-
-  // This array can be re-generated using command-line:
-  // echo -n "Yamada^Tarou=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
-  static const uint8_t utf8raw[] = {
-    0x59, 0x61, 0x6d, 0x61, 0x64, 0x61, 0x5e, 0x54, 0x61, 0x72, 0x6f, 0x75, 0x3d, 0xe5,
-    0xb1, 0xb1, 0xe7, 0x94, 0xb0, 0x5e, 0xe5, 0xa4, 0xaa, 0xe9, 0x83, 0x8e, 0x3d, 0xe3,
-    0x82, 0x84, 0xe3, 0x81, 0xbe, 0xe3, 0x81, 0xa0, 0x5e, 0xe3, 0x81, 0x9f, 0xe3, 0x82,
-    0x8d, 0xe3, 0x81, 0x86
-  };
-
-  std::string utf8(reinterpret_cast<const char*>(utf8raw), sizeof(utf8raw));
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 87");
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
-              (DCM_PatientName, japanese.c_str(), OFBool(true)).good());
-
-  bool hasCodeExtensions;
-  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-  ASSERT_EQ(Encoding_JapaneseKanji, encoding);
-  ASSERT_TRUE(hasCodeExtensions);
-
-  std::string value;
-  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(utf8, value);
-  
-  DicomWebJsonVisitor visitor;
-  dicom.Apply(visitor);
-  ASSERT_EQ(utf8.substr(0, 12), visitor.GetResult()["00100010"]["Value"][0]["Alphabetic"].asString());
-  ASSERT_EQ(utf8.substr(13, 13), visitor.GetResult()["00100010"]["Value"][0]["Ideographic"].asString());
-  ASSERT_EQ(utf8.substr(27), visitor.GetResult()["00100010"]["Value"][0]["Phonetic"].asString());
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-  // http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_F.3.html#table_F.3.1-1
-  std::string xml;
-  visitor.FormatXml(xml);
-
-  pugi::xml_document doc;
-  doc.load_buffer(xml.c_str(), xml.size());
-
-  pugi::xpath_node node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]/Value");
-  ASSERT_STREQ("ISO 2022 IR 87", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00080005\"]");
-  ASSERT_STREQ("CS", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]");
-  ASSERT_STREQ("PN", node.node().attribute("vr").value());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/FamilyName");
-  ASSERT_STREQ("Yamada", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Alphabetic/GivenName");
-  ASSERT_STREQ("Tarou", node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/FamilyName");
-  ASSERT_EQ(utf8.substr(13, 6), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Ideographic/GivenName");
-  ASSERT_EQ(utf8.substr(20, 6), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/FamilyName");
-  ASSERT_EQ(utf8.substr(27, 9), node.node().text().as_string());
-
-  node = SelectNode(doc, "//NativeDicomModel/DicomAttribute[@tag=\"00100010\"]/PersonName/Phonetic/GivenName");
-  ASSERT_EQ(utf8.substr(37), node.node().text().as_string());
-#endif  
-  
-  {
-    DicomMap m;
-    m.FromDicomWeb(visitor.GetResult());
-    ASSERT_EQ(2u, m.GetSize());
-
-    std::string s;
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_SPECIFIC_CHARACTER_SET, false));
-    ASSERT_EQ("ISO 2022 IR 87", s);
-
-    ASSERT_TRUE(m.LookupStringValue(s, DICOM_TAG_PATIENT_NAME, false));
-    std::vector<std::string> v;
-    Toolbox::TokenizeString(v, s, '=');
-    ASSERT_EQ(3u, v.size());
-    ASSERT_EQ("Yamada^Tarou", v[0]);
-    ASSERT_EQ(utf8, s);
-  }
-}
-
-
-
-TEST(Toolbox, EncodingsChinese3)
-{
-  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.3.html
-
-  static const uint8_t chinese[] = {
-    0x57, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f,
-    0x6e, 0x67, 0x3d, 0xcd, 0xf5, 0x5e, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x00
-  };
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030");
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
-              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
-
-  bool hasCodeExtensions;
-  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-  ASSERT_EQ(Encoding_Chinese, encoding);
-  ASSERT_FALSE(hasCodeExtensions);
-
-  std::string value;
-  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
-
-  std::vector<std::string> tokens;
-  Orthanc::Toolbox::TokenizeString(tokens, value, '=');
-  ASSERT_EQ(3u, tokens.size());
-  ASSERT_EQ("Wang^XiaoDong", tokens[0]);
-  ASSERT_TRUE(tokens[2].empty());
-
-  std::vector<std::string> middle;
-  Orthanc::Toolbox::TokenizeString(middle, tokens[1], '^');
-  ASSERT_EQ(2u, middle.size());
-  ASSERT_EQ(3u, middle[0].size());
-  ASSERT_EQ(6u, middle[1].size());
-
-  // CDF5 in GB18030
-  ASSERT_EQ(static_cast<char>(0xe7), middle[0][0]);
-  ASSERT_EQ(static_cast<char>(0x8e), middle[0][1]);
-  ASSERT_EQ(static_cast<char>(0x8b), middle[0][2]);
-
-  // D0A1 in GB18030
-  ASSERT_EQ(static_cast<char>(0xe5), middle[1][0]);
-  ASSERT_EQ(static_cast<char>(0xb0), middle[1][1]);
-  ASSERT_EQ(static_cast<char>(0x8f), middle[1][2]);
-
-  // B6AB in GB18030
-  ASSERT_EQ(static_cast<char>(0xe4), middle[1][3]);
-  ASSERT_EQ(static_cast<char>(0xb8), middle[1][4]);
-  ASSERT_EQ(static_cast<char>(0x9c), middle[1][5]);
-}
-
-
-TEST(Toolbox, EncodingsChinese4)
-{
-  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_J.4.html
-
-  static const uint8_t chinese[] = {
-    0x54, 0x68, 0x65, 0x20, 0x66, 0x69, 0x72, 0x73, 0x74, 0x20, 0x6c, 0x69, 0x6e,
-    0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x73, 0xd6, 0xd0, 0xce,
-    0xc4, 0x2e, 0x0d, 0x0a, 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e,
-    0x64, 0x20, 0x6c, 0x69, 0x6e, 0x65, 0x20, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64,
-    0x65, 0x73, 0xd6, 0xd0, 0xce, 0xc4, 0x2c, 0x20, 0x74, 0x6f, 0x6f, 0x2e, 0x0d,
-    0x0a, 0x54, 0x68, 0x65, 0x20, 0x74, 0x68, 0x69, 0x72, 0x64, 0x20, 0x6c, 0x69,
-    0x6e, 0x65, 0x2e, 0x0d, 0x0a, 0x00
-  };
-
-  static const uint8_t patternRaw[] = {
-    0xe4, 0xb8, 0xad, 0xe6, 0x96, 0x87
-  };
-
-  const std::string pattern(reinterpret_cast<const char*>(patternRaw), sizeof(patternRaw));
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "GB18030");
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
-              (DCM_PatientComments, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
-
-  bool hasCodeExtensions;
-  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-  ASSERT_EQ(Encoding_Chinese, encoding);
-  ASSERT_FALSE(hasCodeExtensions);
-
-  std::string value;
-  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_COMMENTS));
-
-  std::vector<std::string> lines;
-  Orthanc::Toolbox::TokenizeString(lines, value, '\n');
-  ASSERT_EQ(4u, lines.size());
-  ASSERT_TRUE(boost::starts_with(lines[0], "The first line includes"));
-  ASSERT_TRUE(boost::ends_with(lines[0], ".\r"));
-  ASSERT_TRUE(lines[0].find(pattern) != std::string::npos);
-  ASSERT_TRUE(boost::starts_with(lines[1], "The second line includes"));
-  ASSERT_TRUE(boost::ends_with(lines[1], ", too.\r"));
-  ASSERT_TRUE(lines[1].find(pattern) != std::string::npos);
-  ASSERT_EQ("The third line.\r", lines[2]);
-  ASSERT_FALSE(lines[1].find(pattern) == std::string::npos);
-  ASSERT_TRUE(lines[3].empty());
-}
-
-
-TEST(Toolbox, EncodingsSimplifiedChinese2)
-{
-  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html
-
-  static const uint8_t chinese[] = {
-    0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f,
-    0x6e, 0x67, 0x3d, 0x1b, 0x24, 0x29, 0x41, 0xd5, 0xc5, 0x5e, 0x1b, 0x24,
-    0x29, 0x41, 0xd0, 0xa1, 0xb6, 0xab, 0x3d, 0x20, 0x00
-  };
-
-  // echo -n "Zhang^XiaoDong=..." | hexdump -v -e '14/1 "0x%02x, "' -e '"\n"'
-  static const uint8_t utf8[] = {
-    0x5a, 0x68, 0x61, 0x6e, 0x67, 0x5e, 0x58, 0x69, 0x61, 0x6f, 0x44, 0x6f, 0x6e, 0x67,
-    0x3d, 0xe5, 0xbc, 0xa0, 0x5e, 0xe5, 0xb0, 0x8f, 0xe4, 0xb8, 0x9c, 0x3d
-  };
-  
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58");
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
-              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
-
-  bool hasCodeExtensions;
-  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-  ASSERT_EQ(Encoding_SimplifiedChinese, encoding);
-  ASSERT_TRUE(hasCodeExtensions);
-
-  std::string value;
-  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
-  ASSERT_EQ(value, std::string(reinterpret_cast<const char*>(utf8), sizeof(utf8)));
-}
-
-
-TEST(Toolbox, EncodingsSimplifiedChinese3)
-{
-  // http://dicom.nema.org/MEDICAL/dicom/current/output/chtml/part05/sect_K.2.html
-
-  static const uint8_t chinese[] = {
-    0x31, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xd2, 0xbb, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a,
-    0x32, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xb6, 0xfe, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a,
-    0x33, 0x2e, 0x1b, 0x24, 0x29, 0x41, 0xb5, 0xda, 0xc8, 0xfd, 0xd0, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xa1, 0xa3, 0x0d, 0x0a, 0x00
-  };
-
-  static const uint8_t line1[] = {
-    0x31, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x80, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
-    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
-  };
-
-  static const uint8_t line2[] = {
-    0x32, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xba, 0x8c, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
-    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
-  };
-
-  static const uint8_t line3[] = {
-    0x33, 0x2e, 0xe7, 0xac, 0xac, 0xe4, 0xb8, 0x89, 0xe8, 0xa1, 0x8c, 0xe6, 0x96, 0x87,
-    0xe5, 0xad, 0x97, 0xe3, 0x80, 0x82, '\r'
-  };
-
-  ParsedDicomFile dicom(false);
-  dicom.ReplacePlainString(DICOM_TAG_SPECIFIC_CHARACTER_SET, "\\ISO 2022 IR 58");
-  ASSERT_TRUE(dicom.GetDcmtkObject().getDataset()->putAndInsertString
-              (DCM_PatientName, reinterpret_cast<const char*>(chinese), OFBool(true)).good());
-
-  bool hasCodeExtensions;
-  Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-  ASSERT_EQ(Encoding_SimplifiedChinese, encoding);
-  ASSERT_TRUE(hasCodeExtensions);
-
-  std::string value;
-  ASSERT_TRUE(dicom.GetTagValue(value, DICOM_TAG_PATIENT_NAME));
-
-  std::vector<std::string> lines;
-  Toolbox::TokenizeString(lines, value, '\n');
-  ASSERT_EQ(4u, lines.size());
-  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line1), sizeof(line1)), lines[0]);
-  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line2), sizeof(line2)), lines[1]);
-  ASSERT_EQ(std::string(reinterpret_cast<const char*>(line3), sizeof(line3)), lines[2]);
-  ASSERT_TRUE(lines[3].empty());
-}
-
-
-
-
-#if ORTHANC_ENABLE_DCMTK_TRANSCODING == 1
-
-#include "../Core/DicomNetworking/DicomStoreUserConnection.h"
-#include "../Core/DicomParsing/DcmtkTranscoder.h"
-
-TEST(Toto, DISABLED_Transcode3)
-{
-  DicomAssociationParameters p;
-  p.SetRemotePort(2000);
-
-  DicomStoreUserConnection scu(p);
-  scu.SetCommonClassesProposed(false);
-  scu.SetRetiredBigEndianProposed(true);
-
-  DcmtkTranscoder transcoder;
-
-  for (int j = 0; j < 2; j++)
-    for (int i = 0; i <= DicomTransferSyntax_XML; i++)
-    {
-      DicomTransferSyntax a = (DicomTransferSyntax) i;
-
-      std::string path = ("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/" +
-                          std::string(GetTransferSyntaxUid(a)) + ".dcm");
-      if (Orthanc::SystemToolbox::IsRegularFile(path))
-      {
-        printf("\n======= %s\n", GetTransferSyntaxUid(a));
-
-        std::string source;
-        Orthanc::SystemToolbox::ReadFile(source, path);
-
-        std::string c, i;
-        try
-        {
-          scu.Transcode(c, i, transcoder, source.c_str(), source.size(), false, "", 0);
-        }
-        catch (OrthancException& e)
-        {
-          if (e.GetErrorCode() == ErrorCode_NotImplemented)
-          {
-            LOG(ERROR) << "cannot transcode " << GetTransferSyntaxUid(a);
-          }
-          else
-          {
-            throw e;
-          }
-        }
-      }
-    }
-}
-
-
-TEST(Toto, DISABLED_Transcode4)
-{
-  std::string source;
-  Orthanc::SystemToolbox::ReadFile(source, "/home/jodogne/Subversion/orthanc-tests/Database/KarstenHilbertRF.dcm");
-
-  std::unique_ptr<DcmFileFormat> toto(FromDcmtkBridge::LoadFromMemoryBuffer(source.c_str(), source.size()));
-  const std::string sourceUid = IDicomTranscoder::GetSopInstanceUid(*toto);
-  
-  DicomTransferSyntax sourceSyntax;
-  ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(sourceSyntax, *toto));
-
-  DcmtkTranscoder transcoder;
-
-  for (int i = 0; i <= DicomTransferSyntax_XML; i++)
-  {
-    DicomTransferSyntax a = (DicomTransferSyntax) i;
-    
-    std::set<DicomTransferSyntax> s;
-    s.insert(a);
-
-    std::string t;
-
-    IDicomTranscoder::DicomImage source, target;
-    source.AcquireParsed(dynamic_cast<DcmFileFormat*>(toto->clone()));
-
-    if (!transcoder.Transcode(target, source, s, true))
-    {
-      printf("**************** CANNOT: [%s] => [%s]\n",
-             GetTransferSyntaxUid(sourceSyntax), GetTransferSyntaxUid(a));
-    }
-    else
-    {
-      DicomTransferSyntax targetSyntax;
-      ASSERT_TRUE(FromDcmtkBridge::LookupOrthancTransferSyntax(targetSyntax, target.GetParsed()));
-      
-      ASSERT_EQ(targetSyntax, a);
-      bool lossy = (a == DicomTransferSyntax_JPEGProcess1 ||
-                    a == DicomTransferSyntax_JPEGProcess2_4 ||
-                    a == DicomTransferSyntax_JPEGLSLossy);
-      
-      printf("SIZE: %lu\n", t.size());
-      if (sourceUid == IDicomTranscoder::GetSopInstanceUid(target.GetParsed()))
-      {
-        ASSERT_FALSE(lossy);
-      }
-      else
-      {
-        ASSERT_TRUE(lossy);
-      }
-    }
-  }
-}
-
-#endif
--- a/UnitTestsSources/ImageProcessingTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1018 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Compatibility.h"
-#include "../Core/DicomFormat/DicomImageInformation.h"
-#include "../Core/Images/Image.h"
-#include "../Core/Images/ImageProcessing.h"
-#include "../Core/Images/ImageTraits.h"
-#include "../Core/OrthancException.h"
-
-#include <memory>
-
-using namespace Orthanc;
-
-
-TEST(DicomImageInformation, ExtractPixelFormat1)
-{
-  // Cardiac/MR*
-  DicomMap m;
-  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, false));
-  ASSERT_EQ(PixelFormat_Grayscale16, format);
-}
-
-
-TEST(DicomImageInformation, ExtractPixelFormat2)
-{
-  // Delphine CT
-  DicomMap m;
-  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, false));
-  ASSERT_EQ(PixelFormat_SignedGrayscale16, format);
-}
-
-
-
-namespace
-{
-  template <typename T>
-  class TestImageTraits : public ::testing::Test
-  {
-  private:
-    std::unique_ptr<Image>  image_;
-
-  protected:
-    virtual void SetUp() ORTHANC_OVERRIDE
-    {
-      image_.reset(new Image(ImageTraits::PixelTraits::GetPixelFormat(), 7, 9, false));
-    }
-
-    virtual void TearDown() ORTHANC_OVERRIDE
-    {
-      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());
-
-  float c = 0.0f;
-  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.0f;
-  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));
-    }
-  }
-}
-
-TYPED_TEST(TestIntegerImageTraits, FillPolygon)
-{
-  ImageAccessor& image = this->GetImage();
-
-  ImageProcessing::Set(image, 128);
-
-  // draw a triangle
-  std::vector<ImageProcessing::ImagePoint> points;
-  points.push_back(ImageProcessing::ImagePoint(1,1));
-  points.push_back(ImageProcessing::ImagePoint(1,5));
-  points.push_back(ImageProcessing::ImagePoint(5,5));
-
-  ImageProcessing::FillPolygon(image, points, 255);
-
-  // outside polygon
-  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 0, 0));
-  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 0, 6));
-  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 6, 6));
-  ASSERT_FLOAT_EQ(128, TestFixture::ImageTraits::GetFloatPixel(image, 6, 0));
-
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 1));
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 2));
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 1, 5));
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 2, 4));
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 5, 5));
-}
-
-TYPED_TEST(TestIntegerImageTraits, FillPolygonLargerThanImage)
-{
-  ImageAccessor& image = this->GetImage();
-
-  ImageProcessing::Set(image, 0);
-
-  std::vector<ImageProcessing::ImagePoint> points;
-  points.push_back(ImageProcessing::ImagePoint(0, 0));
-  points.push_back(ImageProcessing::ImagePoint(image.GetWidth(),0));
-  points.push_back(ImageProcessing::ImagePoint(image.GetWidth(),image.GetHeight()));
-  points.push_back(ImageProcessing::ImagePoint(0,image.GetHeight()));
-
-  ASSERT_THROW(ImageProcessing::FillPolygon(image, points, 255), OrthancException);
-}
-
-TYPED_TEST(TestIntegerImageTraits, FillPolygonFullImage)
-{
-  ImageAccessor& image = this->GetImage();
-
-  ImageProcessing::Set(image, 0);
-
-  std::vector<ImageProcessing::ImagePoint> points;
-  points.push_back(ImageProcessing::ImagePoint(0, 0));
-  points.push_back(ImageProcessing::ImagePoint(image.GetWidth() - 1,0));
-  points.push_back(ImageProcessing::ImagePoint(image.GetWidth() - 1,image.GetHeight() - 1));
-  points.push_back(ImageProcessing::ImagePoint(0,image.GetHeight() - 1));
-
-  ImageProcessing::FillPolygon(image, points, 255);
-
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, 0, 0));
-  ASSERT_FLOAT_EQ(255, TestFixture::ImageTraits::GetFloatPixel(image, image.GetWidth() - 1, image.GetHeight() - 1));
-}
-
-
-
-
-static void SetGrayscale8Pixel(ImageAccessor& image,
-                               unsigned int x,
-                               unsigned int y,
-                               uint8_t value)
-{
-  ImageTraits<PixelFormat_Grayscale8>::SetPixel(image, value, x, y);
-}
-
-static bool TestGrayscale8Pixel(const ImageAccessor& image,
-                                unsigned int x,
-                                unsigned int y,
-                                uint8_t value)
-{
-  PixelTraits<PixelFormat_Grayscale8>::PixelType p;
-  ImageTraits<PixelFormat_Grayscale8>::GetPixel(p, image, x, y);
-  if (p != value) printf("%d %d\n", p, value);
-  return p == value;
-}
-
-static void SetGrayscale16Pixel(ImageAccessor& image,
-                                unsigned int x,
-                                unsigned int y,
-                                uint16_t value)
-{
-  ImageTraits<PixelFormat_Grayscale16>::SetPixel(image, value, x, y);
-}
-
-static bool TestGrayscale16Pixel(const ImageAccessor& image,
-                                 unsigned int x,
-                                 unsigned int y,
-                                 uint16_t value)
-{
-  PixelTraits<PixelFormat_Grayscale16>::PixelType p;
-  ImageTraits<PixelFormat_Grayscale16>::GetPixel(p, image, x, y);
-  if (p != value) printf("%d %d\n", p, value);
-  return p == value;
-}
-
-static void SetSignedGrayscale16Pixel(ImageAccessor& image,
-                                unsigned int x,
-                                unsigned int y,
-                                int16_t value)
-{
-  ImageTraits<PixelFormat_SignedGrayscale16>::SetPixel(image, value, x, y);
-}
-
-static bool TestSignedGrayscale16Pixel(const ImageAccessor& image,
-                                       unsigned int x,
-                                       unsigned int y,
-                                       int16_t value)
-{
-  PixelTraits<PixelFormat_SignedGrayscale16>::PixelType p;
-  ImageTraits<PixelFormat_SignedGrayscale16>::GetPixel(p, image, x, y);
-  if (p != value) printf("%d %d\n", p, value);
-  return p == value;
-}
-
-static void SetRGB24Pixel(ImageAccessor& image,
-                          unsigned int x,
-                          unsigned int y,
-                          uint8_t red,
-                          uint8_t green,
-                          uint8_t blue)
-{
-  PixelTraits<PixelFormat_RGB24>::PixelType p;
-  p.red_ = red;
-  p.green_ = green;
-  p.blue_ = blue;
-  ImageTraits<PixelFormat_RGB24>::SetPixel(image, p, x, y);
-}
-
-static bool TestRGB24Pixel(const ImageAccessor& image,
-                           unsigned int x,
-                           unsigned int y,
-                           uint8_t red,
-                           uint8_t green,
-                           uint8_t blue)
-{
-  PixelTraits<PixelFormat_RGB24>::PixelType p;
-  ImageTraits<PixelFormat_RGB24>::GetPixel(p, image, x, y);
-  bool ok = (p.red_ == red &&
-             p.green_ == green &&
-             p.blue_ == blue);
-  if (!ok) printf("%d,%d,%d  %d,%d,%d\n", p.red_, p.green_, p.blue_, red, green, blue);
-  return ok;
-}
-
-
-TEST(ImageProcessing, FlipGrayscale8)
-{
-  {
-    Image image(PixelFormat_Grayscale8, 0, 0, false);
-    ImageProcessing::FlipX(image);
-    ImageProcessing::FlipY(image);
-  }
-
-  {
-    Image image(PixelFormat_Grayscale8, 1, 1, false);
-    SetGrayscale8Pixel(image, 0, 0, 128);
-    ImageProcessing::FlipX(image);
-    ImageProcessing::FlipY(image);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 128));
-  }
-
-  {
-    Image image(PixelFormat_Grayscale8, 3, 2, false);
-    SetGrayscale8Pixel(image, 0, 0, 10);
-    SetGrayscale8Pixel(image, 1, 0, 20);
-    SetGrayscale8Pixel(image, 2, 0, 30);
-    SetGrayscale8Pixel(image, 0, 1, 40);
-    SetGrayscale8Pixel(image, 1, 1, 50);
-    SetGrayscale8Pixel(image, 2, 1, 60);
-
-    ImageProcessing::FlipX(image);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 30));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 20));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 10));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 60));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 50));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 40));
-
-    ImageProcessing::FlipY(image);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 60));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 50));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 40));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 30));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 20));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 10));
-  }
-}
-
-
-
-TEST(ImageProcessing, FlipRGB24)
-{
-  Image image(PixelFormat_RGB24, 2, 2, false);
-  SetRGB24Pixel(image, 0, 0, 10, 100, 110);
-  SetRGB24Pixel(image, 1, 0, 20, 100, 110);
-  SetRGB24Pixel(image, 0, 1, 30, 100, 110);
-  SetRGB24Pixel(image, 1, 1, 40, 100, 110);
-
-  ImageProcessing::FlipX(image);
-  ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 20, 100, 110));
-  ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 10, 100, 110));
-  ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 40, 100, 110));
-  ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 30, 100, 110));
-
-  ImageProcessing::FlipY(image);
-  ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 40, 100, 110));
-  ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 30, 100, 110));
-  ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 20, 100, 110));
-  ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 10, 100, 110));
-}
-
-
-TEST(ImageProcessing, ResizeBasicGrayscale8)
-{
-  Image source(PixelFormat_Grayscale8, 2, 2, false);
-  SetGrayscale8Pixel(source, 0, 0, 10);
-  SetGrayscale8Pixel(source, 1, 0, 20);
-  SetGrayscale8Pixel(source, 0, 1, 30);
-  SetGrayscale8Pixel(source, 1, 1, 40);
-
-  {
-    Image target(PixelFormat_Grayscale8, 2, 4, false);
-    ImageProcessing::Resize(target, source);
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 10));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 20));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 10));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 20));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 2, 30));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 2, 40));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 3, 30));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 3, 40));
-  }
-
-  {
-    Image target(PixelFormat_Grayscale8, 4, 2, false);
-    ImageProcessing::Resize(target, source);
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 10));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 10));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 20));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 20));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 30));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 30));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 1, 40));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 1, 40));
-  }
-}
-
-
-TEST(ImageProcessing, ResizeBasicRGB24)
-{
-  Image source(PixelFormat_RGB24, 2, 2, false);
-  SetRGB24Pixel(source, 0, 0, 10, 100, 110);
-  SetRGB24Pixel(source, 1, 0, 20, 100, 110);
-  SetRGB24Pixel(source, 0, 1, 30, 100, 110);
-  SetRGB24Pixel(source, 1, 1, 40, 100, 110);
-
-  {
-    Image target(PixelFormat_RGB24, 2, 4, false);
-    ImageProcessing::Resize(target, source);
-    ASSERT_TRUE(TestRGB24Pixel(target, 0, 0, 10, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 1, 0, 20, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 0, 1, 10, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 1, 1, 20, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 0, 2, 30, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 1, 2, 40, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 0, 3, 30, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 1, 3, 40, 100, 110));
-  }
-
-  {
-    Image target(PixelFormat_RGB24, 4, 2, false);
-    ImageProcessing::Resize(target, source);
-    ASSERT_TRUE(TestRGB24Pixel(target, 0, 0, 10, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 1, 0, 10, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 2, 0, 20, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 3, 0, 20, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 0, 1, 30, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 1, 1, 30, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 2, 1, 40, 100, 110));
-    ASSERT_TRUE(TestRGB24Pixel(target, 3, 1, 40, 100, 110));
-  }
-}
-
-
-TEST(ImageProcessing, ResizeEmptyGrayscale8)
-{
-  {
-    Image source(PixelFormat_Grayscale8, 0, 0, false);
-    Image target(PixelFormat_Grayscale8, 2, 2, false);
-    ImageProcessing::Resize(target, source);
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 1, 0));
-    ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 1, 0));
-  }
-
-  {
-    Image source(PixelFormat_Grayscale8, 2, 2, false);
-    Image target(PixelFormat_Grayscale8, 0, 0, false);
-    ImageProcessing::Resize(target, source);
-  }
-}
-
-
-TEST(ImageProcessing, Convolution)
-{
-  std::vector<float> k1(5, 1);
-  std::vector<float> k2(1, 1);
-
-  {
-    Image image(PixelFormat_Grayscale8, 1, 1, false);
-    SetGrayscale8Pixel(image, 0, 0, 100);    
-    ImageProcessing::SeparableConvolution(image, k1, 2, k2, 0);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
-    ImageProcessing::SeparableConvolution(image, k1, 2, k1, 2);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
-    ImageProcessing::SeparableConvolution(image, k2, 0, k1, 2);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
-    ImageProcessing::SeparableConvolution(image, k2, 0, k2, 0);
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 100));
-  }
-  
-  {
-    Image image(PixelFormat_RGB24, 1, 1, false);
-    SetRGB24Pixel(image, 0, 0, 10, 20, 30);    
-    ImageProcessing::SeparableConvolution(image, k1, 2, k2, 0);
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
-    ImageProcessing::SeparableConvolution(image, k1, 2, k1, 2);
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
-    ImageProcessing::SeparableConvolution(image, k2, 0, k1, 2);
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
-    ImageProcessing::SeparableConvolution(image, k2, 0, k2, 0);
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 10, 20, 30));
-  }
-
-  {  
-    Image dirac(PixelFormat_Grayscale8, 9, 1, false);
-    ImageProcessing::Set(dirac, 0);
-    SetGrayscale8Pixel(dirac, 4, 0, 100);
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 100));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 1, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 2, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 3, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 4, 0, 100));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 5, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 6, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 7, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 8, 0, 0));    
-    }
-  }
-
-  {  
-    Image dirac(PixelFormat_Grayscale8, 1, 9, false);
-    ImageProcessing::Set(dirac, 0);
-    SetGrayscale8Pixel(dirac, 0, 4, 100);
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 20));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 100));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 1, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 2, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 3, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 4, 100));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 5, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 6, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 7, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(*image, 0, 8, 0));    
-    }
-  }
-
-  {
-    Image dirac(PixelFormat_RGB24, 9, 1, false);
-    ImageProcessing::Set(dirac, 0);
-    SetRGB24Pixel(dirac, 4, 0, 100, 120, 140);
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 100, 120, 140));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 1, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 2, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 3, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 4, 0, 100, 120, 140));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 5, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 6, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 7, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 8, 0, 0, 0, 0));    
-    }
-  }
-
-  {
-    Image dirac(PixelFormat_RGB24, 1, 9, false);
-    ImageProcessing::Set(dirac, 0);
-    SetRGB24Pixel(dirac, 0, 4, 100, 120, 140);
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k1, 2);
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 20, 24, 28));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k1, 2, k2, 0);
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 100, 120, 140));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0));    
-    }
-
-    {
-      std::unique_ptr<ImageAccessor> image(Image::Clone(dirac));
-      ImageProcessing::SeparableConvolution(*image, k2, 0, k2, 0);
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 0, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 1, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 2, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 3, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 4, 100, 120, 140));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 5, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 6, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 7, 0, 0, 0));
-      ASSERT_TRUE(TestRGB24Pixel(*image, 0, 8, 0, 0, 0));    
-    }
-  }
-}
-
-
-TEST(ImageProcessing, SmoothGaussian5x5)
-{
-  /**
-     Test the point spread function, as can be seen in Octave:
-     g1 = [ 1 4 6 4 1 ];
-     g1 /= sum(g1);
-     g2 = conv2(g1, g1');
-     floor(conv2(diag([ 0 0 100 0 0 ]), g2, 'same'))  % red/green channels
-     floor(conv2(diag([ 0 0 200 0 0 ]), g2, 'same'))  % blue channel
-  **/
-
-  {
-    Image image(PixelFormat_Grayscale8, 5, 5, false);
-    ImageProcessing::Set(image, 0);
-    SetGrayscale8Pixel(image, 2, 2, 100);
-    ImageProcessing::SmoothGaussian5x5(image);
-
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 2));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 0));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 1, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 1, 6));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 1, 9));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 1, 6));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 1, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 2, 2));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 2, 9));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 2, 14));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 2, 9));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 2, 2));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 3, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 3, 6));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 3, 9));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 3, 6));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 3, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 4, 0));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 4, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 4, 2));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 4, 1));
-    ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 4, 0));
-  }
-
-  {
-    Image image(PixelFormat_RGB24, 5, 5, false);
-    ImageProcessing::Set(image, 0);
-    SetRGB24Pixel(image, 2, 2, 100, 100, 200);
-    ImageProcessing::SmoothGaussian5x5(image);
-
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 0, 0, 0, 0));
-    ASSERT_TRUE(TestRGB24Pixel(image, 1, 0, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 2, 0, 2, 2, 4));
-    ASSERT_TRUE(TestRGB24Pixel(image, 3, 0, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 4, 0, 0, 0, 0));
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 1, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 1, 1, 6, 6, 12));
-    ASSERT_TRUE(TestRGB24Pixel(image, 2, 1, 9, 9, 18));
-    ASSERT_TRUE(TestRGB24Pixel(image, 3, 1, 6, 6, 12));
-    ASSERT_TRUE(TestRGB24Pixel(image, 4, 1, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 2, 2, 2, 4));
-    ASSERT_TRUE(TestRGB24Pixel(image, 1, 2, 9, 9, 18));
-    ASSERT_TRUE(TestRGB24Pixel(image, 2, 2, 14, 14, 28));
-    ASSERT_TRUE(TestRGB24Pixel(image, 3, 2, 9, 9, 18));
-    ASSERT_TRUE(TestRGB24Pixel(image, 4, 2, 2, 2, 4));
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 3, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 1, 3, 6, 6, 12));
-    ASSERT_TRUE(TestRGB24Pixel(image, 2, 3, 9, 9, 18));
-    ASSERT_TRUE(TestRGB24Pixel(image, 3, 3, 6, 6, 12));
-    ASSERT_TRUE(TestRGB24Pixel(image, 4, 3, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 0, 4, 0, 0, 0));
-    ASSERT_TRUE(TestRGB24Pixel(image, 1, 4, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 2, 4, 2, 2, 4));
-    ASSERT_TRUE(TestRGB24Pixel(image, 3, 4, 1, 1, 3));
-    ASSERT_TRUE(TestRGB24Pixel(image, 4, 4, 0, 0, 0));
-  }
-}
-
-TEST(ImageProcessing, ApplyWindowingFloatToGrayScale8)
-{
-  {
-    Image image(PixelFormat_Float32, 6, 1, false);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, -5.0f, 0, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 0.0f, 1, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 5.0f, 2, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 10.0f, 3, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 1000.0f, 4, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 2.0f, 5, 0);
-
-    {
-      Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
-
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 128));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255*2/10));
-    }
-
-    {
-      Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, true);
-
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 127));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255 - 255*2/10));
-    }
-
-    {
-      Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, false);
-
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 128));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255*2/10));
-    }
-
-    {
-      Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5000.0f, 10000.01f, 1000.0f, 0.0f, true);
-
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 255));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 127));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 0));
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 255 - 256*2/10));
-    }
-
-    {
-      Image target(PixelFormat_Grayscale8, 6, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 50.0f, 100.1f, 10.0f, 30.0f, false);
-
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 0, 0, 0));  // (-5 * 10) + 30 => pixel value = -20 => 0
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 1, 0, 256*30/100));  // ((0 * 10) + 30 => pixel value = 30 => 30%
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 2, 0, 256*80/100)); // ((5 * 10) + 30 => pixel value = 80 => 80%
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 3, 0, 255)); // ((10 * 10) + 30 => pixel value = 130 => 100%
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 4, 0, 255)); // ((1000 * 10) + 30 => pixel value = 10030 => 100%
-      ASSERT_TRUE(TestGrayscale8Pixel(target, 5, 0, 128)); // ((2 * 10) + 30 => pixel value = 50 => 50%
-    }
-
-  }
-}
-
-TEST(ImageProcessing, ApplyWindowingFloatToGrayScale16)
-{
-  {
-    Image image(PixelFormat_Float32, 6, 1, false);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, -5.0f, 0, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 0.0f, 1, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 5.0f, 2, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 10.0f, 3, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 1000.0f, 4, 0);
-    ImageTraits<PixelFormat_Float32>::SetFloatPixel(image, 2.0f, 5, 0);
-
-    {
-      Image target(PixelFormat_Grayscale16, 6, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
-
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 0));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 32768));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 5, 0, 65536*2/10));
-    }
-  }
-}
-
-TEST(ImageProcessing, ApplyWindowingGrayScale8ToGrayScale16)
-{
-  {
-    Image image(PixelFormat_Grayscale8, 5, 1, false);
-    SetGrayscale8Pixel(image, 0, 0, 0);
-    SetGrayscale8Pixel(image, 1, 0, 2);
-    SetGrayscale8Pixel(image, 2, 0, 5);
-    SetGrayscale8Pixel(image, 3, 0, 10);
-    SetGrayscale8Pixel(image, 4, 0, 255);
-
-    {
-      Image target(PixelFormat_Grayscale16, 5, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
-
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 65536*5/10));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535));
-    }
-  }
-}
-
-TEST(ImageProcessing, ApplyWindowingGrayScale16ToGrayScale16)
-{
-  {
-    Image image(PixelFormat_Grayscale16, 5, 1, false);
-    SetGrayscale16Pixel(image, 0, 0, 0);
-    SetGrayscale16Pixel(image, 1, 0, 2);
-    SetGrayscale16Pixel(image, 2, 0, 5);
-    SetGrayscale16Pixel(image, 3, 0, 10);
-    SetGrayscale16Pixel(image, 4, 0, 255);
-
-    {
-      Image target(PixelFormat_Grayscale16, 5, 1, false);
-      ImageProcessing::ApplyWindowing_Deprecated(target, image, 5.0f, 10.0f, 1.0f, 0.0f, false);
-
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 0, 0, 0));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 1, 0, 65536*2/10));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 2, 0, 65536*5/10));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 3, 0, 65535));
-      ASSERT_TRUE(TestGrayscale16Pixel(target, 4, 0, 65535));
-    }
-  }
-}
-
-
-TEST(ImageProcessing, ShiftScaleGrayscale8)
-{
-  Image image(PixelFormat_Grayscale8, 5, 1, false);
-  SetGrayscale8Pixel(image, 0, 0, 0);
-  SetGrayscale8Pixel(image, 1, 0, 2);
-  SetGrayscale8Pixel(image, 2, 0, 5);
-  SetGrayscale8Pixel(image, 3, 0, 10);
-  SetGrayscale8Pixel(image, 4, 0, 255);
-
-  ImageProcessing::ShiftScale(image, -1.1, 1.5, true);
-  ASSERT_TRUE(TestGrayscale8Pixel(image, 0, 0, 0));
-  ASSERT_TRUE(TestGrayscale8Pixel(image, 1, 0, 1));
-  ASSERT_TRUE(TestGrayscale8Pixel(image, 2, 0, 6));
-  ASSERT_TRUE(TestGrayscale8Pixel(image, 3, 0, 13));
-  ASSERT_TRUE(TestGrayscale8Pixel(image, 4, 0, 255));
-}
-
-
-TEST(ImageProcessing, ShiftScaleGrayscale16)
-{
-  Image image(PixelFormat_Grayscale16, 5, 1, false);
-  SetGrayscale16Pixel(image, 0, 0, 0);
-  SetGrayscale16Pixel(image, 1, 0, 2);
-  SetGrayscale16Pixel(image, 2, 0, 5);
-  SetGrayscale16Pixel(image, 3, 0, 10);
-  SetGrayscale16Pixel(image, 4, 0, 255);
-
-  ImageProcessing::ShiftScale(image, -1.1, 1.5, true);
-  ASSERT_TRUE(TestGrayscale16Pixel(image, 0, 0, 0));
-  ASSERT_TRUE(TestGrayscale16Pixel(image, 1, 0, 1));
-  ASSERT_TRUE(TestGrayscale16Pixel(image, 2, 0, 6));
-  ASSERT_TRUE(TestGrayscale16Pixel(image, 3, 0, 13));
-  ASSERT_TRUE(TestGrayscale16Pixel(image, 4, 0, 381));
-}
-
-
-TEST(ImageProcessing, ShiftScaleSignedGrayscale16)
-{
-  Image image(PixelFormat_SignedGrayscale16, 5, 1, false);
-  SetSignedGrayscale16Pixel(image, 0, 0, 0);
-  SetSignedGrayscale16Pixel(image, 1, 0, 2);
-  SetSignedGrayscale16Pixel(image, 2, 0, 5);
-  SetSignedGrayscale16Pixel(image, 3, 0, 10);
-  SetSignedGrayscale16Pixel(image, 4, 0, 255);
-
-  ImageProcessing::ShiftScale(image, -17.1, 11.5, true);
-  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 0, 0, -197));
-  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 1, 0, -174));
-  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 2, 0, -139));
-  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 3, 0, -82));
-  ASSERT_TRUE(TestSignedGrayscale16Pixel(image, 4, 0, 2736));
-}
--- a/UnitTestsSources/ImageTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,483 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Images/Font.h"
-#include "../Core/Images/Image.h"
-#include "../Core/Images/ImageProcessing.h"
-#include "../Core/Images/JpegReader.h"
-#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/SystemToolbox.h"
-#include "../Core/Toolbox.h"
-#include "../Core/TemporaryFile.h"
-#include "../OrthancServer/OrthancConfiguration.h"  // For the FontRegistry
-
-#include <stdint.h>
-
-
-TEST(PngWriter, ColorPattern)
-{
-  Orthanc::PngWriter 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.png", accessor);
-
-  std::string f, md5;
-  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/ColorPattern.png");
-  Orthanc::Toolbox::ComputeMD5(md5, f);
-  ASSERT_EQ("604e785f53c99cae6ea4584870b2c41d", md5);
-}
-
-TEST(PngWriter, Gray8Pattern)
-{
-  Orthanc::PngWriter 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.png", accessor);
-
-  std::string f, md5;
-  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray8Pattern.png");
-  Orthanc::Toolbox::ComputeMD5(md5, f);
-  ASSERT_EQ("5a9b98bea3d0a6d983980cc38bfbcdb3", md5);
-}
-
-TEST(PngWriter, Gray16Pattern)
-{
-  Orthanc::PngWriter 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.png", accessor);
-
-  std::string f, md5;
-  Orthanc::SystemToolbox::ReadFile(f, "UnitTestsResults/Gray16Pattern.png");
-  Orthanc::Toolbox::ComputeMD5(md5, f);
-  ASSERT_EQ("0785866a08bf0a02d2eeff87f658571c", md5);
-}
-
-TEST(PngWriter, EndToEnd)
-{
-  Orthanc::PngWriter 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::PngReader 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(*p, v);
-      }
-    }
-  }
-
-  {
-    Orthanc::TemporaryFile tmp;
-    tmp.Write(s);
-
-    Orthanc::PngReader 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);
-      }
-    }
-  }
-}
-
-
-
-
-TEST(JpegWriter, Basic)
-{
-  std::string s;
-
-  {
-    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));
-      for (unsigned int x = 0; x < img.GetWidth(); x++, p++)
-      {
-        *p = value++;
-      }
-    }
-
-    Orthanc::JpegWriter w;
-    w.WriteToFile("UnitTestsResults/hello.jpg", img);
-
-    w.WriteToMemory(s, img);
-    Orthanc::SystemToolbox::WriteFile(s, "UnitTestsResults/hello2.jpg");
-
-    std::string t;
-    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()));
-  }
-
-  {
-    Orthanc::JpegReader r1, r2;
-    r1.ReadFromFile("UnitTestsResults/hello.jpg");
-    ASSERT_EQ(16u, r1.GetWidth());
-    ASSERT_EQ(16u, r1.GetHeight());
-
-    r2.ReadFromMemory(s);
-    ASSERT_EQ(16u, r2.GetWidth());
-    ASSERT_EQ(16u, r2.GetHeight());
-
-    for (unsigned int y = 0; y < r1.GetHeight(); y++)
-    {
-      const uint8_t* p1 = reinterpret_cast<const uint8_t*>(r1.GetConstRow(y));
-      const uint8_t* p2 = reinterpret_cast<const uint8_t*>(r2.GetConstRow(y));
-      for (unsigned int x = 0; x < r1.GetWidth(); x++)
-      {
-        ASSERT_EQ(*p1, *p2);
-      }
-    }
-  }
-}
-
-
-TEST(Font, Basic)
-{
-  Orthanc::Image s(Orthanc::PixelFormat_RGB24, 640, 480, false);
-  memset(s.GetBuffer(), 0, s.GetPitch() * s.GetHeight());
-
-  {
-    Orthanc::OrthancConfiguration::ReaderLock lock;
-    ASSERT_GE(1u, lock.GetConfiguration().GetFontRegistry().GetSize());
-    lock.GetConfiguration().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);
-      }
-    }
-  }
-
-  {
-    // true means "enforce alignment by using a temporary buffer"
-    Orthanc::PamReader r(true);
-    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;
-    tmp.Write(s);
-
-    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);
-      }
-    }
-  }
-
-  {
-    Orthanc::TemporaryFile tmp;
-    tmp.Write(s);
-
-    // true means "enforce alignment by using a temporary buffer"
-    Orthanc::PamReader r2(true);
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/DicomParsing/Internals/DicomImageDecoder.h"
-
-#if ORTHANC_ENABLE_JPEG_LOSSLESS == 1
-
-#include <dcmtk/dcmdata/dcfilefo.h>
-
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Images/ImageBuffer.h"
-#include "../Core/Images/PngWriter.h"
-
-using namespace Orthanc;
-
-
-
-// TODO Write a test
-
-
-#endif
--- a/UnitTestsSources/LoggingTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,209 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-#include <boost/regex.hpp>
-#include <sstream>
-
-#include "../Core/Logging.h"
-
-using namespace Orthanc::Logging;
-
-static std::stringstream testErrorStream;
-void TestError(const char* message)
-{
-  testErrorStream << message;
-}
-
-static std::stringstream testWarningStream;
-void TestWarning(const char* message)
-{
-  testWarningStream << message;
-}
-
-static std::stringstream testInfoStream;
-void TestInfo(const char* message)
-{
-  testInfoStream << message;
-}
-
-/**
-   Extracts the log line payload
-
-   "E0423 16:55:43.001194 LoggingTests.cpp:102] Foo bar?\n"
-   -->
-   "Foo bar"
-
-   If the log line cannot be matched, the function returns false.
-*/
-
-#define EOLSTRING "\n"
-
-static bool GetLogLinePayload(std::string& payload,
-                              const std::string& logLine)
-{
-  const char* regexStr = "[A-Z][0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} "
-    "[a-zA-Z\\.\\-_]+:[0-9]+\\] (.*)" EOLSTRING "$";
-
-  boost::regex regexObj(regexStr);
-
-  //std::stringstream regexSStr;
-  //regexSStr << "E[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6} "
-  //  "[a-zA-Z\\.\\-_]+:[0-9]+\\](.*)\r\n$";
-  //std::string regexStr = regexSStr.str();
-  boost::regex pattern(regexStr);
-  boost::cmatch what;
-  if (regex_match(logLine.c_str(), what, regexObj))
-  {
-    payload = what[1];
-    return true;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-namespace
-{
-  class LoggingMementoScope
-  {
-  public:
-    LoggingMementoScope()
-    {
-    }
-    
-    ~LoggingMementoScope()
-    {
-      Orthanc::Logging::Reset();
-    }
-  };
-
-
-  /**
-   * std::streambuf subclass used in FunctionCallingStream
-   **/
-  template<typename T>
-  class FuncStreamBuf : public std::stringbuf
-  {
-  public:
-    FuncStreamBuf(T func) : func_(func) {}
-
-    virtual int sync()
-    {
-      std::string text = this->str();
-      const char* buf = text.c_str();
-      func_(buf);
-      this->str("");
-      return 0;
-    }
-  private:
-    T func_;
-  };
-}
-
-
-TEST(FuncStreamBuf, BasicTest)
-{
-  LoggingMementoScope loggingConfiguration;
-
-  EnableTraceLevel(true);
-
-  typedef void(*LoggingFunctionFunc)(const char*);
-
-  FuncStreamBuf<LoggingFunctionFunc> errorStreamBuf(TestError);
-  std::ostream errorStream(&errorStreamBuf);
-
-  FuncStreamBuf<LoggingFunctionFunc> warningStreamBuf(TestWarning);
-  std::ostream warningStream(&warningStreamBuf);
-
-  FuncStreamBuf<LoggingFunctionFunc> infoStreamBuf(TestInfo);
-  std::ostream infoStream(&infoStreamBuf);
-
-  SetErrorWarnInfoLoggingStreams(errorStream, warningStream, infoStream);
-
-  {
-    const char* text = "E is the set of all sets that do not contain themselves. Does E contain itself?";
-    LOG(ERROR) << text;
-    std::string logLine = testErrorStream.str();
-    testErrorStream.str("");
-    testErrorStream.clear();
-    std::string payload;
-    bool ok = GetLogLinePayload(payload, logLine);
-    ASSERT_TRUE(ok);
-    ASSERT_STREQ(payload.c_str(), text);
-  }
-
-  // make sure loglines do not accumulate
-  {
-    const char* text = "some more nonsensical babblingiciously stupid gibberish";
-    LOG(ERROR) << text;
-    std::string logLine = testErrorStream.str();
-    testErrorStream.str("");
-    testErrorStream.clear();
-    std::string payload;
-    bool ok = GetLogLinePayload(payload, logLine);
-    ASSERT_TRUE(ok);
-    ASSERT_STREQ(payload.c_str(), text);
-  }
-
-  {
-    const char* text = "Trougoudou 53535345345353";
-    LOG(WARNING) << text;
-    std::string logLine = testWarningStream.str();
-    testWarningStream.str("");
-    testWarningStream.clear();
-    std::string payload;
-    bool ok = GetLogLinePayload(payload, logLine);
-    ASSERT_TRUE(ok);
-    ASSERT_STREQ(payload.c_str(), text);
-  }
-
-  {
-    const char* text = "Prout 111929";
-    LOG(INFO) << text;
-    std::string logLine = testInfoStream.str();
-    testInfoStream.str("");
-    testInfoStream.clear();
-    std::string payload;
-    bool ok = GetLogLinePayload(payload, logLine);
-    ASSERT_TRUE(ok);
-    ASSERT_STREQ(payload.c_str(), text);
-  }
-}
--- a/UnitTestsSources/LuaTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,372 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/OrthancException.h"
-#include "../Core/Toolbox.h"
-#include "../Core/Lua/LuaFunctionCall.h"
-
-#include <OrthancServerResources.h>
-
-#include <boost/lexical_cast.hpp>
-
-#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
-#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS to 0 or 1"
-#endif
-
-
-TEST(Lua, Json)
-{
-  Orthanc::LuaContext lua;
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
-  {
-    std::string command;
-    Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
-    lua.Execute(command);
-  }
-#endif
-
-  lua.Execute("a={}");
-  lua.Execute("a['x'] = 10");
-  lua.Execute("a['y'] = {}");
-  lua.Execute("a['y'][1] = 20");
-  lua.Execute("a['y'][2] = 20");
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
-  lua.Execute("PrintRecursive(a)");
-#endif
-
-  lua.Execute("function f(a) print(a.bool) return a.bool,20,30,40,50,60 end");
-
-  Json::Value v, vv, o;
-  //v["a"] = "b";
-  v.append("hello");
-  v.append("world");
-  v.append("42");
-  vv.append("sub");
-  vv.append("set");
-  v.append(vv);
-  o = Json::objectValue;
-  o["x"] = 10;
-  o["y"] = 20;
-  o["z"] = 20.5f;
-  v.append(o);
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushJson(v);
-    f.Execute();
-  }
-#endif
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "f");
-    f.PushJson(o);
-    ASSERT_THROW(f.ExecutePredicate(), Orthanc::OrthancException);
-  }
-
-  o["bool"] = false;
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "f");
-    f.PushJson(o);
-    ASSERT_FALSE(f.ExecutePredicate());
-  }
-
-  o["bool"] = true;
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "f");
-    f.PushJson(o);
-    ASSERT_TRUE(f.ExecutePredicate());
-  }
-}
-
-
-TEST(Lua, Existing)
-{
-  Orthanc::LuaContext lua;
-  lua.Execute("a={}");
-  lua.Execute("function f() end");
-
-  ASSERT_TRUE(lua.IsExistingFunction("f"));
-  ASSERT_FALSE(lua.IsExistingFunction("a"));
-  ASSERT_FALSE(lua.IsExistingFunction("Dummy"));
-}
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1
-TEST(Lua, Simple)
-{
-  Orthanc::LuaContext lua;
-
-  {
-    std::string command;
-    Orthanc::ServerResources::GetFileResource(command, Orthanc::ServerResources::LUA_TOOLBOX);
-    lua.Execute(command);
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushString("hello");
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushBoolean(true);
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushInteger(42);
-    f.Execute();
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "PrintRecursive");
-    f.PushDouble(3.1415);
-    f.Execute();
-  }
-}
-#endif
-
-
-TEST(Lua, ReturnJson)
-{
-  Json::Value b = Json::objectValue;
-  b["a"] = 42;
-  b["b"] = 44.37;
-  b["c"] = -43;
-
-  Json::Value c = Json::arrayValue;
-  c.append("test3");
-  c.append("test1");
-  c.append("test2");
-
-  Json::Value a = Json::objectValue;
-  a["Hello"] = "World";
-  a["List"] = Json::arrayValue;
-  a["List"].append(b);
-  a["List"].append(c);
-
-  Orthanc::LuaContext lua;
-
-  // This is the identity function (it simply returns its input)
-  lua.Execute("function identity(a) return a end");
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson("hello");
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_EQ("hello", v.asString());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson(42.25);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_FLOAT_EQ(42.25f, v.asFloat());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson(-42);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_EQ(-42, v.asInt());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    Json::Value vv = Json::arrayValue;
-    f.PushJson(vv);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_EQ(Json::arrayValue, v.type());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    Json::Value vv = Json::objectValue;
-    f.PushJson(vv);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    // Lua does not make the distinction between empty lists and empty objects
-    ASSERT_EQ(Json::arrayValue, v.type());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson(b);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_EQ(Json::objectValue, v.type());
-    ASSERT_FLOAT_EQ(42.0f, v["a"].asFloat());
-    ASSERT_FLOAT_EQ(44.37f, v["b"].asFloat());
-    ASSERT_FLOAT_EQ(-43.0f, v["c"].asFloat());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson(c);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_EQ(Json::arrayValue, v.type());
-    ASSERT_EQ("test3", v[0].asString());
-    ASSERT_EQ("test1", v[1].asString());
-    ASSERT_EQ("test2", v[2].asString());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson(a);
-    Json::Value v;
-    f.ExecuteToJson(v, false);
-    ASSERT_EQ("World", v["Hello"].asString());
-    ASSERT_EQ(Json::intValue, v["List"][0]["a"].type());
-    ASSERT_EQ(Json::realValue, v["List"][0]["b"].type());
-    ASSERT_EQ(Json::intValue, v["List"][0]["c"].type());
-    ASSERT_EQ(42, v["List"][0]["a"].asInt());
-    ASSERT_FLOAT_EQ(44.37f, v["List"][0]["b"].asFloat());
-    ASSERT_EQ(44, v["List"][0]["b"].asInt());
-    ASSERT_EQ(-43, v["List"][0]["c"].asInt());
-    ASSERT_EQ("test3", v["List"][1][0].asString());
-    ASSERT_EQ("test1", v["List"][1][1].asString());
-    ASSERT_EQ("test2", v["List"][1][2].asString());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "identity");
-    f.PushJson(a);
-    Json::Value v;
-    f.ExecuteToJson(v, true);
-    ASSERT_EQ("World", v["Hello"].asString());
-    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_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());
-  }
-
-  {
-    Orthanc::LuaFunctionCall f(lua, "DumpJson");
-    f.PushJson(a);
-    std::string s;
-    f.ExecuteToString(s);
-
-    Json::FastWriter writer;
-    std::string t = writer.write(a);
-
-    ASSERT_EQ(s, t);
-  }
-}
-
-
-
-TEST(Lua, Http)
-{
-  Orthanc::LuaContext lua;
-
-#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/";
-
-#if LUA_VERSION_NUM >= 502
-  // Since Lua >= 5.2.0, the function "loadstring" has been replaced by "load"
-  lua.Execute("JSON = load(HttpGet('" + BASE + "JSON.lua')) ()");
-#else
-  lua.Execute("JSON = loadstring(HttpGet('" + BASE + "JSON.lua')) ()");
-#endif
-
-  const std::string url(BASE + "Product.json");
-#endif
-
-  std::string s;
-  lua.Execute(s, "print(HttpGet({}))");
-  ASSERT_EQ("nil", Orthanc::Toolbox::StripSpaces(s));
-
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1  
-  lua.Execute(s, "print(string.len(HttpGet(\"" + url + "\")))");
-  ASSERT_LE(100, boost::lexical_cast<int>(Orthanc::Toolbox::StripSpaces(s)));
-
-  // Parse a JSON file
-  lua.Execute(s, "print(JSON:decode(HttpGet(\"" + url + "\")) ['Product'])");
-  ASSERT_EQ("OrthancClient", Orthanc::Toolbox::StripSpaces(s));
-
-#if 0
-  // This part of the test can only be executed if one instance of
-  // Orthanc is running on the localhost
-
-  lua.Execute("modality = {}");
-  lua.Execute("table.insert(modality, 'ORTHANC')");
-  lua.Execute("table.insert(modality, 'localhost')");
-  lua.Execute("table.insert(modality, 4242)");
-  
-  lua.Execute(s, "print(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('hello world')\"))");
-  ASSERT_EQ("hello world", Orthanc::Toolbox::StripSpaces(s));
-
-  lua.Execute(s, "print(JSON:decode(HttpPost(\"http://localhost:8042/tools/execute-script\", \"print('[10,42,1000]')\")) [2])");
-  ASSERT_EQ("42", Orthanc::Toolbox::StripSpaces(s));
-
-  // Add/remove a modality with Lua
-  Json::Value v;
-  lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))");
-  ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size());
-  lua.Execute(s, "print(HttpPut('http://localhost:8042/modalities/lua', JSON:encode(modality)))");
-  lua.Execute(v, "print(HttpGet('http://localhost:8042/modalities/lua'))");
-  ASSERT_TRUE(v.type() == Json::arrayValue);
-  lua.Execute(s, "print(HttpDelete('http://localhost:8042/modalities/lua'))");
-  lua.Execute(s, "print(HttpGet('http://localhost:8042/modalities/lua'))");
-  ASSERT_EQ(0, Orthanc::Toolbox::StripSpaces(s).size());
-#endif
-
-#endif
-}
--- a/UnitTestsSources/MemoryCacheTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,460 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <memory>
-#include <algorithm>
-#include <boost/thread.hpp>
-#include <boost/lexical_cast.hpp>
-
-#include "../Core/Cache/MemoryCache.h"
-#include "../Core/Cache/MemoryStringCache.h"
-#include "../Core/Cache/SharedArchive.h"
-#include "../Core/IDynamicObject.h"
-#include "../Core/Logging.h"
-#include "../OrthancServer/StorageCommitmentReports.h"
-
-
-TEST(LRU, Basic)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string> r;
-  
-  r.Add("d");
-  r.Add("a");
-  r.Add("c");
-  r.Add("b");
-
-  r.MakeMostRecent("a");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("b");
-  r.MakeMostRecent("c");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("c");
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ("a", r.RemoveOldest());
-  ASSERT_EQ("b", r.GetOldest());
-  ASSERT_EQ("b", r.RemoveOldest());
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ("d", r.RemoveOldest());
-  ASSERT_EQ("c", r.GetOldest());
-  ASSERT_EQ("c", r.RemoveOldest());
-
-  ASSERT_TRUE(r.IsEmpty());
-
-  ASSERT_THROW(r.GetOldest(), Orthanc::OrthancException);
-  ASSERT_THROW(r.RemoveOldest(), Orthanc::OrthancException);
-}
-
-
-TEST(LRU, Payload)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  
-  r.Add("a", 420);
-  r.Add("b", 421);
-  r.Add("c", 422);
-  r.Add("d", 423);
-
-  r.MakeMostRecent("a");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("b");
-  r.MakeMostRecent("c");
-  r.MakeMostRecent("d");
-  r.MakeMostRecent("c");
-
-  ASSERT_TRUE(r.Contains("b"));
-  ASSERT_EQ(421, r.Invalidate("b"));
-  ASSERT_FALSE(r.Contains("b"));
-
-  int p;
-  ASSERT_TRUE(r.Contains("a", p)); ASSERT_EQ(420, p);
-  ASSERT_TRUE(r.Contains("c", p)); ASSERT_EQ(422, p);
-  ASSERT_TRUE(r.Contains("d", p)); ASSERT_EQ(423, p);
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ(420, r.GetOldestPayload());
-  ASSERT_EQ("a", r.RemoveOldest(p)); ASSERT_EQ(420, p);
-
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ(423, r.GetOldestPayload());
-  ASSERT_EQ("d", r.RemoveOldest(p)); ASSERT_EQ(423, p);
-
-  ASSERT_EQ("c", r.GetOldest());
-  ASSERT_EQ(422, r.GetOldestPayload());
-  ASSERT_EQ("c", r.RemoveOldest(p)); ASSERT_EQ(422, p);
-
-  ASSERT_TRUE(r.IsEmpty());
-}
-
-
-TEST(LRU, PayloadUpdate)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  
-  r.Add("a", 420);
-  r.Add("b", 421);
-  r.Add("d", 423);
-
-  r.MakeMostRecent("a", 424);
-  r.MakeMostRecent("d", 421);
-
-  ASSERT_EQ("b", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ(424, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_TRUE(r.IsEmpty());
-}
-
-
-
-TEST(LRU, PayloadUpdateBis)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  
-  r.AddOrMakeMostRecent("a", 420);
-  r.AddOrMakeMostRecent("b", 421);
-  r.AddOrMakeMostRecent("d", 423);
-  r.AddOrMakeMostRecent("a", 424);
-  r.AddOrMakeMostRecent("d", 421);
-
-  ASSERT_EQ("b", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("a", r.GetOldest());
-  ASSERT_EQ(424, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_EQ("d", r.GetOldest());
-  ASSERT_EQ(421, r.GetOldestPayload());
-  r.RemoveOldest();
-
-  ASSERT_TRUE(r.IsEmpty());
-}
-
-TEST(LRU, GetAllKeys)
-{
-  Orthanc::LeastRecentlyUsedIndex<std::string, int> r;
-  std::vector<std::string> keys;
-
-  r.AddOrMakeMostRecent("a", 420);
-  r.GetAllKeys(keys);
-
-  ASSERT_EQ(1u, keys.size());
-  ASSERT_EQ("a", keys[0]);
-
-  r.AddOrMakeMostRecent("b", 421);
-  r.GetAllKeys(keys);
-
-  ASSERT_EQ(2u, keys.size());
-  ASSERT_TRUE(std::find(keys.begin(), keys.end(),"a") != keys.end());
-  ASSERT_TRUE(std::find(keys.begin(), keys.end(),"b") != keys.end());
-}
-
-
-
-namespace
-{
-  class Integer : public Orthanc::IDynamicObject
-  {
-  private:
-    std::string& log_;
-    int value_;
-
-  public:
-    Integer(std::string& log, int v) : log_(log), value_(v)
-    {
-    }
-
-    virtual ~Integer() ORTHANC_OVERRIDE
-    {
-      LOG(INFO) << "Removing cache entry for " << value_;
-      log_ += boost::lexical_cast<std::string>(value_) + " ";
-    }
-  };
-
-  class IntegerProvider : public Orthanc::Deprecated::ICachePageProvider
-  {
-  public:
-    std::string log_;
-
-    virtual Orthanc::IDynamicObject* Provide(const std::string& s) ORTHANC_OVERRIDE
-    {
-      LOG(INFO) << "Providing " << s;
-      return new Integer(log_, boost::lexical_cast<int>(s));
-    }
-  };
-}
-
-
-TEST(MemoryCache, Basic)
-{
-  IntegerProvider provider;
-
-  {
-    Orthanc::Deprecated::MemoryCache cache(provider, 3);
-    cache.Access("42");  // 42 -> exit
-    cache.Access("43");  // 43, 42 -> exit
-    cache.Access("45");  // 45, 43, 42 -> exit
-    cache.Access("42");  // 42, 45, 43 -> exit
-    cache.Access("43");  // 43, 42, 45 -> exit
-    cache.Access("47");  // 45 is removed; 47, 43, 42 -> exit 
-    cache.Access("44");  // 42 is removed; 44, 47, 43 -> exit
-    cache.Access("42");  // 43 is removed; 42, 44, 47 -> exit
-    // Closing the cache: 47, 44, 42 are successively removed
-  }
-
-  ASSERT_EQ("45 42 43 47 44 42 ", provider.log_);
-}
-
-
-
-
-
-namespace
-{
-  class S : public Orthanc::IDynamicObject
-  {
-  private:
-    std::string value_;
-
-  public:
-    S(const std::string& value) : value_(value)
-    {
-    }
-
-    const std::string& GetValue() const
-    {
-      return value_;
-    }
-  };
-}
-
-
-TEST(LRU, SharedArchive)
-{
-  std::string first, second;
-  Orthanc::SharedArchive a(3);
-  first = a.Add(new S("First item"));
-  second = a.Add(new S("Second item"));
-
-  for (int i = 1; i < 100; i++)
-  {
-    a.Add(new S("Item " + boost::lexical_cast<std::string>(i)));
-    
-    // Continuously protect the two first items
-    {
-      Orthanc::SharedArchive::Accessor accessor(a, first);
-      ASSERT_TRUE(accessor.IsValid());
-      ASSERT_EQ("First item", dynamic_cast<S&>(accessor.GetItem()).GetValue());
-    }
-
-    {
-      Orthanc::SharedArchive::Accessor accessor(a, second);
-      ASSERT_TRUE(accessor.IsValid());
-      ASSERT_EQ("Second item", dynamic_cast<S&>(accessor.GetItem()).GetValue());
-    }
-
-    {
-      Orthanc::SharedArchive::Accessor accessor(a, "nope");
-      ASSERT_FALSE(accessor.IsValid());
-      ASSERT_THROW(accessor.GetItem(), Orthanc::OrthancException);
-    }
-  }
-
-  std::list<std::string> i;
-  a.List(i);
-
-  size_t count = 0;
-  for (std::list<std::string>::const_iterator
-         it = i.begin(); it != i.end(); it++)
-  {
-    if (*it == first ||
-        *it == second)
-    {
-      count++;
-    }
-  }
-
-  ASSERT_EQ(2u, count);
-}
-
-
-TEST(MemoryStringCache, Basic)
-{
-  Orthanc::MemoryStringCache c;
-  ASSERT_THROW(c.SetMaximumSize(0), Orthanc::OrthancException);
-  
-  c.SetMaximumSize(2);
-
-  std::string v;
-  ASSERT_FALSE(c.Fetch(v, "hello"));
-
-  c.Add("hello", "a");
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_FALSE(c.Fetch(v, "hello2"));
-  ASSERT_FALSE(c.Fetch(v, "hello3"));
-
-  c.Add("hello2", "b");
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-  ASSERT_FALSE(c.Fetch(v, "hello3"));
-
-  c.Add("hello3", "too large value");
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-  ASSERT_FALSE(c.Fetch(v, "hello3"));
-  
-  c.Add("hello3", "c");
-  ASSERT_FALSE(c.Fetch(v, "hello"));  // Recycled
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-  ASSERT_TRUE(c.Fetch(v, "hello3"));  ASSERT_EQ("c", v);
-}
-
-
-TEST(MemoryStringCache, Invalidate)
-{
-  Orthanc::MemoryStringCache c;
-  c.Add("hello", "a");
-  c.Add("hello2", "b");
-
-  std::string v;
-  ASSERT_TRUE(c.Fetch(v, "hello"));   ASSERT_EQ("a", v);
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-
-  c.Invalidate("hello");
-  ASSERT_FALSE(c.Fetch(v, "hello"));
-  ASSERT_TRUE(c.Fetch(v, "hello2"));  ASSERT_EQ("b", v);
-}
-
-
-TEST(StorageCommitmentReports, Basic)
-{
-  Orthanc::StorageCommitmentReports reports(2);
-  ASSERT_EQ(2u, reports.GetMaxSize());
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "nope");
-    ASSERT_EQ("nope", accessor.GetTransactionUid());
-    ASSERT_FALSE(accessor.IsValid());
-    ASSERT_THROW(accessor.GetReport(), Orthanc::OrthancException);
-  }
-
-  reports.Store("a", new Orthanc::StorageCommitmentReports::Report("aet_a"));
-  reports.Store("b", new Orthanc::StorageCommitmentReports::Report("aet_b"));
-  reports.Store("c", new Orthanc::StorageCommitmentReports::Report("aet_c"));
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
-    ASSERT_FALSE(accessor.IsValid());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
-    ASSERT_TRUE(accessor.IsValid());
-    ASSERT_EQ("aet_b", accessor.GetReport().GetRemoteAet());
-    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Pending,
-              accessor.GetReport().GetStatus());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
-    ASSERT_EQ("aet_c", accessor.GetReport().GetRemoteAet());
-    ASSERT_TRUE(accessor.IsValid());
-  }
-
-  {
-    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
-      (new Orthanc::StorageCommitmentReports::Report("aet"));
-    report->AddSuccess("class1", "instance1");
-    report->AddFailure("class2", "instance2",
-                       Orthanc::StorageCommitmentFailureReason_ReferencedSOPClassNotSupported);
-    report->MarkAsComplete();
-    reports.Store("a", report.release());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
-    ASSERT_TRUE(accessor.IsValid());
-    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
-    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Failure,
-              accessor.GetReport().GetStatus());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
-    ASSERT_FALSE(accessor.IsValid());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
-    ASSERT_TRUE(accessor.IsValid());
-  }
-
-  {
-    std::unique_ptr<Orthanc::StorageCommitmentReports::Report> report
-      (new Orthanc::StorageCommitmentReports::Report("aet"));
-    report->AddSuccess("class1", "instance1");
-    report->MarkAsComplete();
-    reports.Store("a", report.release());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "a");
-    ASSERT_TRUE(accessor.IsValid());
-    ASSERT_EQ("aet", accessor.GetReport().GetRemoteAet());
-    ASSERT_EQ(Orthanc::StorageCommitmentReports::Report::Status_Success,
-              accessor.GetReport().GetStatus());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "b");
-    ASSERT_FALSE(accessor.IsValid());
-  }
-
-  {
-    Orthanc::StorageCommitmentReports::Accessor accessor(reports, "c");
-    ASSERT_TRUE(accessor.IsValid());
-  }
-}
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2228 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Compatibility.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 "../OrthancServer/Database/SQLiteDatabaseWrapper.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/DicomMoveScuJob.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() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void Reset() ORTHANC_OVERRIDE
-    {
-    }
-    
-    virtual JobStepResult Step(const std::string& jobId) ORTHANC_OVERRIDE
-    {
-      if (fails_)
-      {
-        return JobStepResult::Failure(ErrorCode_ParameterOutOfRange, NULL);
-      }
-      else if (count_ == steps_ - 1)
-      {
-        return JobStepResult::Success();
-      }
-      else
-      {
-        count_++;
-        return JobStepResult::Continue();
-      }
-    }
-
-    virtual void Stop(JobStopReason reason) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual float GetProgress() ORTHANC_OVERRIDE
-    {
-      return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
-    }
-
-    virtual void GetJobType(std::string& type) ORTHANC_OVERRIDE
-    {
-      type = "DummyJob";
-    }
-
-    virtual bool Serialize(Json::Value& value) ORTHANC_OVERRIDE
-    {
-      value = Json::objectValue;
-      value["Type"] = "DummyJob";
-      return true;
-    }
-
-    virtual void GetPublicContent(Json::Value& value) ORTHANC_OVERRIDE
-    {
-      value["hello"] = "world";
-    }
-
-    virtual bool GetOutput(std::string& output,
-                           MimeType& mime,
-                           const std::string& key) ORTHANC_OVERRIDE
-    {
-      return false;
-    }
-  };
-
-
-  class DummyInstancesJob : public SetOfInstancesJob
-  {
-  private:
-    bool   trailingStepDone_;
-    
-  protected:
-    virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
-    {
-      return (instance != "nope");
-    }
-
-    virtual bool HandleTrailingStep() ORTHANC_OVERRIDE
-    {
-      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) ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void GetJobType(std::string& s) ORTHANC_OVERRIDE
-    {
-      s = "DummyInstancesJob";
-    }
-  };
-
-
-  class DummyUnserializer : public GenericJobUnserializer
-  {
-  public:
-    virtual IJob* UnserializeJob(const Json::Value& value) ORTHANC_OVERRIDE
-    {
-      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:
-    int value_;
-    std::set<int>& target_;
-
-  public:
-    DynamicInteger(int value, std::set<int>& target) : 
-      value_(value), target_(target)
-    {
-    }
-
-    int GetValue() const
-    {
-      return value_;
-    }
-  };
-}
-
-
-TEST(MultiThreading, SharedMessageQueueBasic)
-{
-  std::set<int> s;
-
-  SharedMessageQueue q;
-  ASSERT_TRUE(q.WaitEmpty(0));
-  q.Enqueue(new DynamicInteger(10, s));
-  ASSERT_FALSE(q.WaitEmpty(1));
-  q.Enqueue(new DynamicInteger(20, s));
-  q.Enqueue(new DynamicInteger(30, s));
-  q.Enqueue(new DynamicInteger(40, s));
-
-  std::unique_ptr<DynamicInteger> i;
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(10, i->GetValue());
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(20, i->GetValue());
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(30, i->GetValue());
-  ASSERT_FALSE(q.WaitEmpty(1));
-  i.reset(dynamic_cast<DynamicInteger*>(q.Dequeue(1))); ASSERT_EQ(40, i->GetValue());
-  ASSERT_TRUE(q.WaitEmpty(0));
-  ASSERT_EQ(NULL, q.Dequeue(1));
-}
-
-
-TEST(MultiThreading, SharedMessageQueueClean)
-{
-  std::set<int> s;
-
-  try
-  {
-    SharedMessageQueue q;
-    q.Enqueue(new DynamicInteger(10, s));
-    q.Enqueue(new DynamicInteger(20, s));  
-    throw OrthancException(ErrorCode_InternalError);
-  }
-  catch (OrthancException&)
-  {
-  }
-}
-
-
-
-
-static bool CheckState(JobsRegistry& registry,
-                       const std::string& id,
-                       JobState state)
-{
-  JobState s;
-  if (registry.GetState(s, id))
-  {
-    return state == s;
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-static bool CheckErrorCode(JobsRegistry& registry,
-                           const std::string& id,
-                           ErrorCode code)
-{
-  JobInfo s;
-  if (registry.GetJobInfo(s, id))
-  {
-    return code == s.GetStatus().GetErrorCode();
-  }
-  else
-  {
-    return false;
-  }
-}
-
-
-TEST(JobsRegistry, Priority)
-{
-  JobsRegistry registry(10);
-
-  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(10);
-
-  std::string i1, i2;
-  registry.Submit(i1, new DummyJob(), 20);
-  registry.Submit(i2, new DummyJob(), 10);
-
-  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)
-{
-  JobsRegistry registry(10);
-
-  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(10);
-
-  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));
-
-  {
-    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(10);
-
-  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(10);
-
-  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(10);
-
-  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.Pause(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, Cancel)
-{
-  JobsRegistry registry(10);
-
-  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));
-  }
-
-  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));
-}
-
-
-
-TEST(JobsEngine, SubmitAndWait)
-{
-  JobsEngine engine(10);
-  engine.SetThreadSleep(10);
-  engine.SetWorkersCount(3);
-  engine.Start();
-
-  Json::Value content = Json::nullValue;
-  engine.GetRegistry().SubmitAndWait(content, new DummyJob(), rand() % 10);
-  ASSERT_EQ(Json::objectValue, content.type());
-  ASSERT_EQ("world", content["hello"].asString());
-
-  content = Json::nullValue;
-  ASSERT_THROW(engine.GetRegistry().SubmitAndWait(content, new DummyJob(true), rand() % 10), OrthancException);
-  ASSERT_EQ(Json::nullValue, content.type());
-
-  engine.Stop();
-}
-
-
-TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
-{
-  JobsEngine engine(10);
-  engine.SetThreadSleep(10);
-  engine.SetWorkersCount(3);
-  engine.Start();
-
-  std::string id;
-  SequenceOfOperationsJob* job = NULL;
-
-  {
-    std::unique_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(10);
-  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)
-{
-  Json::Value a = 42;
-  
-  if (!job.Serialize(a))
-  {
-    return false;
-  }
-  else
-  {
-    std::unique_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::unique_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::unique_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
-  
-  Json::Value b = 43;
-  unserialized->Serialize(b);
-
-  return CheckSameJson(a, b);
-}
-
-
-static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
-                                         JobOperationValue& value)
-{
-  Json::Value a = 42;
-  value.Serialize(a);
-  
-  std::unique_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::unique_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);
-  }
-
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
-
-  std::unique_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;
-
-  {
-    LogJobOperation operation;
-
-    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-    operation.Serialize(s);
-  }
-
-  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
-  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
-
-  {
-    std::unique_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("jobId"), OrthancException);  // Not started yet
-    ASSERT_FALSE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    job.Start();
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-
-    {
-      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::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    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
-
-  {
-    SequenceOfOperationsJob job;
-    job.SetDescription("hello");
-
-    {
-      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.SetTrailingOperationTimeout(300);
-    }
-
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").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::unique_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;
-
-  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::unique_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::unique_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));
-  }
-}
-
-
-TEST(JobsSerialization, DicomInstanceOrigin)
-{   
-  Json::Value s;
-  std::string t;
-
-  {
-    DicomInstanceOrigin origin;
-
-    s = 42;
-    origin.Serialize(s);
-  }
-
-  {
-    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));
-  }
-
-  {
-    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));
-  }
-}
-
-
-namespace
-{
-  class OrthancJobsSerialization : public testing::Test
-  {
-  private:
-    MemoryStorageArea              storage_;
-    SQLiteDatabaseWrapper          db_;   // The SQLite DB is in memory
-    std::unique_ptr<ServerContext>   context_;
-
-  public:
-    OrthancJobsSerialization()
-    {
-      db_.Open();
-      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */, 10));
-      context_->SetupJobsEngine(true, false);
-    }
-
-    virtual ~OrthancJobsSerialization() ORTHANC_OVERRIDE
-    {
-      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, StoreInstanceMode_Default) == StoreStatus_Success);
-    }
-  };
-}
-
-
-TEST_F(OrthancJobsSerialization, Values)
-{
-  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::unique_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::unique_ptr<IJobOperation> operation;
-
-  {
-    operation.reset(unserializer.UnserializeOperation(s));
-
-    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
-
-  {
-    TimeoutDicomConnectionManager luaManager;
-    
-    {
-      RemoteModalityParameters modality;
-      modality.SetApplicationEntityTitle("REMOTE");
-      modality.SetHost("192.168.1.1");
-      modality.SetPortNumber(1000);
-      modality.SetManufacturer(ModalityManufacturer_StoreScp);
-
-      StoreScuOperation operation(GetContext(), luaManager, "TEST", modality);
-
-      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
-      operation.Serialize(s);
-    }
-
-    {
-      operation.reset(unserializer.UnserializeOperation(s));
-
-      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));
-
-    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
-
-  {
-    std::unique_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));
-  }
-}
-
-
-TEST_F(OrthancJobsSerialization, Jobs)
-{
-  Json::Value s;
-
-  // ArchiveJob
-
-  {
-    ArchiveJob job(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::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
-    ASSERT_EQ("LOCAL", tmp.GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("REMOTE", tmp.GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("192.168.1.1", tmp.GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(1000, tmp.GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetParameters().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);
-
-    OrthancPeerStoreJob job(GetContext());
-    job.SetPeer(peer);
-    
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_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());
-    ASSERT_FALSE(tmp.IsTranscode());
-    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
-  }
-
-  {
-    OrthancPeerStoreJob job(GetContext());
-    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
-    job.SetTranscode("1.2.840.10008.1.2.4.50");
-    
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
-    ASSERT_EQ("http://127.0.0.1:8042/", tmp.GetPeer().GetUrl());
-    ASSERT_EQ("", tmp.GetPeer().GetUsername());
-    ASSERT_EQ("", tmp.GetPeer().GetPassword());
-    ASSERT_FALSE(tmp.GetPeer().IsPkcs11Enabled());
-    ASSERT_TRUE(tmp.IsTranscode());
-    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
-  }
-
-  // ResourceModificationJob
-
-  {
-    std::unique_ptr<DicomModification> modification(new DicomModification);
-    modification->SetupAnonymization(DicomVersion_2008);    
-
-    ResourceModificationJob job(GetContext());
-    job.SetModification(modification.release(), ResourceType_Patient, true);
-    job.SetOrigin(DicomInstanceOrigin::FromLua());
-
-    job.AddTrailingStep();  // Necessary since 1.7.0
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
-    ASSERT_TRUE(tmp.IsAnonymization());
-    ASSERT_FALSE(tmp.IsTranscode());
-    ASSERT_THROW(tmp.GetTransferSyntax(), OrthancException);
-    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
-    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
-  }
-
-  {
-    ResourceModificationJob job(GetContext());
-    ASSERT_THROW(job.SetTranscode("nope"), OrthancException);
-    job.SetTranscode(DicomTransferSyntax_JPEGProcess1);
-
-    job.AddTrailingStep();  // Necessary since 1.7.0
-    ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    ASSERT_TRUE(job.Serialize(s));
-  }
-
-  {
-    std::unique_ptr<IJob> job;
-    job.reset(unserializer.UnserializeJob(s));
-
-    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
-    ASSERT_FALSE(tmp.IsAnonymization());
-    ASSERT_TRUE(tmp.IsTranscode());
-    ASSERT_EQ(DicomTransferSyntax_JPEGProcess1, tmp.GetTransferSyntax());
-    ASSERT_EQ(RequestOrigin_Unknown, tmp.GetOrigin().GetRequestOrigin());
-  }
-
-  // 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());
-  }
-
-  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("jobId").GetCode());
-      ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-
-      study2 = job.GetTargetStudy();
-      ASSERT_FALSE(study2.empty());
-
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-      ASSERT_TRUE(job.Serialize(s));
-    }
-
-    {
-      std::unique_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));
-    }
-  }
-
-  {
-    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
-
-  {
-    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("jobId").GetCode());
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").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::unique_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(10);
-    registry.Submit(i1, new DummyJob(), 10);
-    registry.Submit(i2, new SequenceOfOperationsJob(), 30);
-    registry.Serialize(s);
-  }
-
-  {
-    DummyUnserializer unserializer;
-    JobsRegistry registry(unserializer, s, 10);
-
-    Json::Value t;
-    registry.Serialize(t);
-    ASSERT_TRUE(CheckSameJson(s, t));
-  }
-}
-
-
-TEST(JobsSerialization, TrailingStep)
-{
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    ASSERT_EQ(0u, job.GetCommandsCount());
-    ASSERT_EQ(0u, job.GetInstancesCount());
-
-    job.Start();
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_FALSE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    job.AddInstance("hello");
-    job.AddInstance("world");
-    ASSERT_EQ(2u, job.GetCommandsCount());
-    ASSERT_EQ(2u, job.GetInstancesCount());
-
-    job.Start();
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_FALSE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(2u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    ASSERT_EQ(0u, job.GetInstancesCount());
-    ASSERT_EQ(0u, job.GetCommandsCount());
-    job.AddTrailingStep();
-    ASSERT_EQ(0u, job.GetInstancesCount());
-    ASSERT_EQ(1u, job.GetCommandsCount());
-
-    job.Start(); // This adds the trailing step
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_TRUE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_TRUE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), OrthancException);
-  }
-
-  {
-    Json::Value s;
-    
-    DummyInstancesJob job;
-    job.AddInstance("hello");
-    ASSERT_EQ(1u, job.GetInstancesCount());
-    ASSERT_EQ(1u, job.GetCommandsCount());
-    job.AddTrailingStep();
-    ASSERT_EQ(1u, job.GetInstancesCount());
-    ASSERT_EQ(2u, job.GetCommandsCount());
-    
-    job.Start();
-    ASSERT_EQ(2u, job.GetCommandsCount());
-    ASSERT_EQ(0u, job.GetPosition());
-    ASSERT_TRUE(job.HasTrailingStep());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-    
-    ASSERT_EQ(JobStepCode_Continue, job.Step("jobId").GetCode());
-    ASSERT_EQ(1u, job.GetPosition());
-    ASSERT_FALSE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_EQ(JobStepCode_Success, job.Step("jobId").GetCode());
-    ASSERT_EQ(2u, job.GetPosition());
-    ASSERT_TRUE(job.IsTrailingStepDone());
-    
-    {
-      DummyUnserializer unserializer;
-      ASSERT_TRUE(CheckIdempotentSetOfInstances(unserializer, job));
-    }
-
-    ASSERT_THROW(job.Step("jobId"), 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));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-
-  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_GenericNoWildcardInDates);
-    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_GenericNoWildcardInDates, 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));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-
-  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);
-  operations.insert(DicomRequestType_NAction);
-  operations.insert(DicomRequestType_NEventReport);
-
-  ASSERT_EQ(7u, 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));
-        }
-      }
-    }
-  }
-
-  {
-    Json::Value s;
-    s["AllowStorageCommitment"] = false;
-    s["AET"] = "AET";
-    s["Host"] = "host";
-    s["Port"] = "104";
-    
-    RemoteModalityParameters modality(s);
-    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
-    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("host", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-
-  {
-    Json::Value s;
-    s["AllowNAction"] = false;
-    s["AllowNEventReport"] = true;
-    s["AET"] = "AET";
-    s["Host"] = "host";
-    s["Port"] = "104";
-    s["AllowTranscoding"] = false;
-    
-    RemoteModalityParameters modality(s);
-    ASSERT_TRUE(modality.IsAdvancedFormatNeeded());
-    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("host", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_FALSE(modality.IsTranscodingAllowed());
-  }
-
-  {
-    Json::Value s;
-    s["AllowNAction"] = true;
-    s["AllowNEventReport"] = true;
-    s["AET"] = "AET";
-    s["Host"] = "host";
-    s["Port"] = "104";
-    
-    RemoteModalityParameters modality(s);
-    ASSERT_FALSE(modality.IsAdvancedFormatNeeded());
-    ASSERT_EQ("AET", modality.GetApplicationEntityTitle());
-    ASSERT_EQ("host", modality.GetHost());
-    ASSERT_EQ(104u, modality.GetPortNumber());
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction));
-    ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport));
-    ASSERT_TRUE(modality.IsTranscodingAllowed());
-  }
-}
-
-
-TEST_F(OrthancJobsSerialization, DicomAssociationParameters)
-{
-  Json::Value v;
-
-  {
-    v = Json::objectValue;
-    DicomAssociationParameters p;
-    p.SerializeJob(v);
-  }
-
-  {
-    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
-    ASSERT_EQ("ORTHANC", p.GetLocalApplicationEntityTitle());
-    ASSERT_EQ("ANY-SCP", p.GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ(104u, p.GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
-    ASSERT_EQ("127.0.0.1", p.GetRemoteModality().GetHost());
-    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), p.GetTimeout());
-  }
-
-  {
-    v = Json::objectValue;
-    DicomAssociationParameters p;
-    p.SetLocalApplicationEntityTitle("HELLO");
-    p.SetRemoteApplicationEntityTitle("WORLD");
-    p.SetRemotePort(42);
-    p.SetRemoteHost("MY_HOST");
-    p.SetTimeout(43);
-    p.SerializeJob(v);
-  }
-
-  {
-    DicomAssociationParameters p = DicomAssociationParameters::UnserializeJob(v);
-    ASSERT_EQ("HELLO", p.GetLocalApplicationEntityTitle());
-    ASSERT_EQ("WORLD", p.GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ(42u, p.GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, p.GetRemoteModality().GetManufacturer());
-    ASSERT_EQ("MY_HOST", p.GetRemoteModality().GetHost());
-    ASSERT_EQ(43u, p.GetTimeout());
-  }
-  
-  {
-    DicomModalityStoreJob job(GetContext());
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomModalityStoreJob> job(
-      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
-    ASSERT_FALSE(job->HasMoveOriginator());
-    ASSERT_THROW(job->GetMoveOriginatorAet(), OrthancException);
-    ASSERT_THROW(job->GetMoveOriginatorId(), OrthancException);
-    ASSERT_FALSE(job->HasStorageCommitment());
-  }
-  
-  {
-    RemoteModalityParameters r;
-    r.SetApplicationEntityTitle("HELLO");
-    r.SetPortNumber(42);
-    r.SetHost("MY_HOST");
-
-    DicomModalityStoreJob job(GetContext());
-    job.SetLocalAet("WORLD");
-    job.SetRemoteModality(r);
-    job.SetTimeout(43);
-    job.SetMoveOriginator("ORIGINATOR", 100);
-    job.EnableStorageCommitment(true);
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomModalityStoreJob> job(
-      dynamic_cast<DicomModalityStoreJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
-    ASSERT_TRUE(job->HasMoveOriginator());
-    ASSERT_EQ("ORIGINATOR", job->GetMoveOriginatorAet());
-    ASSERT_EQ(100, job->GetMoveOriginatorId());
-    ASSERT_TRUE(job->HasStorageCommitment());
-  }
-    
-  {
-    DicomMoveScuJob job(GetContext());
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomMoveScuJob> job(
-      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("ORTHANC", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("ANY-SCP", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("127.0.0.1", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(104u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), job->GetParameters().GetTimeout());
-  }
-  
-  {
-    RemoteModalityParameters r;
-    r.SetApplicationEntityTitle("HELLO");
-    r.SetPortNumber(42);
-    r.SetHost("MY_HOST");
-
-    DicomMoveScuJob job(GetContext());
-    job.SetLocalAet("WORLD");
-    job.SetRemoteModality(r);
-    job.SetTimeout(43);
-    job.Serialize(v);
-  }
-  
-  {
-    OrthancJobUnserializer unserializer(GetContext());
-    std::unique_ptr<DicomMoveScuJob> job(
-      dynamic_cast<DicomMoveScuJob*>(unserializer.UnserializeJob(v)));
-    ASSERT_EQ("WORLD", job->GetParameters().GetLocalApplicationEntityTitle());
-    ASSERT_EQ("HELLO", job->GetParameters().GetRemoteModality().GetApplicationEntityTitle());
-    ASSERT_EQ("MY_HOST", job->GetParameters().GetRemoteModality().GetHost());
-    ASSERT_EQ(42u, job->GetParameters().GetRemoteModality().GetPortNumber());
-    ASSERT_EQ(ModalityManufacturer_Generic, job->GetParameters().GetRemoteModality().GetManufacturer());
-    ASSERT_EQ(43u, job->GetParameters().GetTimeout());
-  }
-}
--- a/UnitTestsSources/PluginsTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/OrthancException.h"
-#include "../Plugins/Engine/PluginsManager.h"
-
-using namespace Orthanc;
-
-
-#if ORTHANC_ENABLE_PLUGINS == 1
-
-TEST(SharedLibrary, Enumerations)
-{
-  // The plugin engine cannot work if the size of an enumeration does
-  // not correspond to the size of "int32_t"
-  ASSERT_EQ(sizeof(int32_t), sizeof(OrthancPluginErrorCode));
-}
-
-
-TEST(SharedLibrary, Basic)
-{
-#if defined(_WIN32)
-  SharedLibrary l("kernel32.dll");
-  ASSERT_THROW(l.GetFunction("world"), OrthancException);
-  ASSERT_TRUE(l.GetFunction("GetVersionExW") != NULL);
-  ASSERT_TRUE(l.HasFunction("GetVersionExW"));
-  ASSERT_FALSE(l.HasFunction("world"));
-
-#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_FALSE(l.HasFunction("world"));
-
-  /**
-   * On the Docker image "debian:buster-slim", the "libSegFault.so"
-   * library does exist, but does not contain any public symbol:
-   * 
-   *  $ sudo docker run -i -t --rm --entrypoint=bash debian:buster-slim
-   *  # apt-get update && apt-get install -y binutils
-   *  # nm -C /lib/x86_64-linux-gnu/libSegFault.so
-   *  nm: /lib/x86_64-linux-gnu/libSegFault.so: no symbols
-   *
-   * As a consequence, this part of the test is disabled since Orthanc
-   * 1.5.1, until we locate another shared library that is widely
-   * spread. Reference:
-   * https://groups.google.com/d/msg/orthanc-users/v-QFzpOzgJY/4Hm5NgxKBwAJ
-   **/
-  
-  //ASSERT_TRUE(l.GetFunction("_init") != NULL);
-  //ASSERT_TRUE(l.HasFunction("_init"));
-  
-#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__) || 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");
-  ASSERT_THROW(l.GetFunction("world"), OrthancException);
-  ASSERT_TRUE(l.GetFunction("pthread_create") != NULL);
-  ASSERT_TRUE(l.HasFunction("pthread_cancel"));
-  ASSERT_FALSE(l.HasFunction("world"));
-
-#elif defined(__APPLE__) && defined(__MACH__)
-  SharedLibrary l("libdl.dylib");
-  ASSERT_THROW(l.GetFunction("world"), OrthancException);
-  ASSERT_TRUE(l.GetFunction("dlopen") != NULL);
-  ASSERT_TRUE(l.HasFunction("dlclose"));
-  ASSERT_FALSE(l.HasFunction("world"));
-
-#else
-#error Support your platform here
-#endif
-}
-
-#endif
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
--- a/UnitTestsSources/PrecompiledHeadersUnitTests.h	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "../OrthancServer/PrecompiledHeadersServer.h"
-
-#if ORTHANC_USE_PRECOMPILED_HEADERS == 1
-#include "../Core/EnumerationDictionary.h"
-#include <gtest/gtest.h>
-#endif
--- a/UnitTestsSources/RestApiTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,871 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#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/OrthancException.h"
-#include "../Core/Compression/ZlibCompressor.h"
-#include "../Core/RestApi/RestApiHierarchy.h"
-#include "../Core/HttpServer/HttpContentNegociation.h"
-#include "../Core/HttpServer/MultipartStreamReader.h"
-
-
-using namespace Orthanc;
-
-#if !defined(UNIT_TESTS_WITH_HTTP_CONNEXIONS)
-#error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS"
-#endif
-
-
-
-TEST(HttpClient, Basic)
-{
-  HttpClient c;
-  ASSERT_FALSE(c.IsVerbose());
-  c.SetVerbose(true);
-  ASSERT_TRUE(c.IsVerbose());
-  c.SetVerbose(false);
-  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(BASE + "Product.json");
-
-  c.Apply(v);
-  ASSERT_TRUE(v.type() == Json::objectValue);
-  ASSERT_TRUE(v.isMember("Description"));
-#endif
-}
-
-
-#if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 && ORTHANC_ENABLE_SSL == 1
-
-/**
-   The HTTPS CA certificates for BitBucket were extracted as follows:
-   
-   (1) We retrieve the certification chain of BitBucket:
-
-   # echo | openssl s_client -showcerts -connect www.bitbucket.org:443
-
-   (2) We see that the certification authority (CA) is
-   "www.digicert.com", and the root certificate is "DigiCert High
-   Assurance EV Root CA". As a consequence, we navigate to DigiCert to
-   find the URL to this CA certificate:
-
-   firefox https://www.digicert.com/digicert-root-certificates.htm
-
-   (3) Once we get the URL to the CA certificate, we convert it to a C
-   macro that can be used by libcurl:
-
-   # cd UnitTestsSources
-   # ../Resources/RetrieveCACertificates.py BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt > BitbucketCACertificates.h
-**/
-
-#include "BitbucketCACertificates.h"
-
-TEST(HttpClient, Ssl)
-{
-  SystemToolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert");
-
-  /*{
-    std::string s;
-    SystemToolbox::ReadFile(s, "/usr/share/ca-certificates/mozilla/WoSign.crt");
-    SystemToolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert");
-    }*/
-
-  HttpClient c;
-  c.SetHttpsVerifyPeers(true);
-  c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert");
-
-  // Test file modified on 2020-04-20, in order to use a git
-  // repository on BitBucket instead of a Mercurial repository
-  // (because Mercurial support disappears on 2020-05-31)
-  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
-
-  Json::Value v;
-  c.Apply(v);
-  ASSERT_TRUE(v.isMember("ServeFolders"));
-}
-
-TEST(HttpClient, SslNoVerification)
-{
-  HttpClient c;
-  c.SetHttpsVerifyPeers(false);
-  c.SetUrl("https://bitbucket.org/osimis/orthanc-setup-samples/raw/master/docker/serve-folders/orthanc/serve-folders.json");
-
-  Json::Value v;
-  c.Apply(v);
-  ASSERT_TRUE(v.isMember("ServeFolders"));
-}
-
-#endif
-
-
-TEST(RestApi, ChunkedBuffer)
-{
-  ChunkedBuffer b;
-  ASSERT_EQ(0u, b.GetNumBytes());
-
-  b.AddChunk("hello", 5);
-  ASSERT_EQ(5u, b.GetNumBytes());
-
-  b.AddChunk("world", 5);
-  ASSERT_EQ(10u, b.GetNumBytes());
-
-  std::string s;
-  b.Flatten(s);
-  ASSERT_EQ("helloworld", s);
-}
-
-TEST(RestApi, ParseCookies)
-{
-  IHttpHandler::Arguments headers;
-  IHttpHandler::Arguments cookies;
-
-  headers["cookie"] = "a=b;c=d;;;e=f;;g=h;";
-  HttpToolbox::ParseCookies(cookies, headers);
-  ASSERT_EQ(4u, cookies.size());
-  ASSERT_EQ("b", cookies["a"]);
-  ASSERT_EQ("d", cookies["c"]);
-  ASSERT_EQ("f", cookies["e"]);
-  ASSERT_EQ("h", cookies["g"]);
-
-  headers["cookie"] = "  name =  value  ; name2=value2";
-  HttpToolbox::ParseCookies(cookies, headers);
-  ASSERT_EQ(2u, cookies.size());
-  ASSERT_EQ("value", cookies["name"]);
-  ASSERT_EQ("value2", cookies["name2"]);
-
-  headers["cookie"] = "  ;;;    ";
-  HttpToolbox::ParseCookies(cookies, headers);
-  ASSERT_EQ(0u, cookies.size());
-
-  headers["cookie"] = "  ;   n=v  ;;    ";
-  HttpToolbox::ParseCookies(cookies, headers);
-  ASSERT_EQ(1u, cookies.size());
-  ASSERT_EQ("v", cookies["n"]);
-}
-
-TEST(RestApi, RestApiPath)
-{
-  IHttpHandler::Arguments args;
-  UriComponents trail;
-
-  {
-    RestApiPath uri("/coucou/{abc}/d/*");
-    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
-    ASSERT_EQ(1u, args.size());
-    ASSERT_EQ(3u, trail.size());
-    ASSERT_EQ("moi", args["abc"]);
-    ASSERT_EQ("e", trail[0]);
-    ASSERT_EQ("f", trail[1]);
-    ASSERT_EQ("g", trail[2]);
-
-    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f"));
-    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/"));
-    ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d"));
-    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi"));
-
-    ASSERT_EQ(3u, uri.GetLevelCount());
-    ASSERT_TRUE(uri.IsUniversalTrailing());
-
-    ASSERT_EQ("coucou", uri.GetLevelName(0));
-    ASSERT_THROW(uri.GetWildcardName(0), OrthancException);
-
-    ASSERT_EQ("abc", uri.GetWildcardName(1));
-    ASSERT_THROW(uri.GetLevelName(1), OrthancException);
-
-    ASSERT_EQ("d", uri.GetLevelName(2));
-    ASSERT_THROW(uri.GetWildcardName(2), OrthancException);
-  }
-
-  {
-    RestApiPath uri("/coucou/{abc}/d");
-    ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g"));
-    ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d"));
-    ASSERT_EQ(1u, args.size());
-    ASSERT_EQ(0u, trail.size());
-    ASSERT_EQ("moi", args["abc"]);
-
-    ASSERT_EQ(3u, uri.GetLevelCount());
-    ASSERT_FALSE(uri.IsUniversalTrailing());
-
-    ASSERT_EQ("coucou", uri.GetLevelName(0));
-    ASSERT_THROW(uri.GetWildcardName(0), OrthancException);
-
-    ASSERT_EQ("abc", uri.GetWildcardName(1));
-    ASSERT_THROW(uri.GetLevelName(1), OrthancException);
-
-    ASSERT_EQ("d", uri.GetLevelName(2));
-    ASSERT_THROW(uri.GetWildcardName(2), OrthancException);
-  }
-
-  {
-    RestApiPath uri("/*");
-    ASSERT_TRUE(uri.Match(args, trail, "/a/b/c"));
-    ASSERT_EQ(0u, args.size());
-    ASSERT_EQ(3u, trail.size());
-    ASSERT_EQ("a", trail[0]);
-    ASSERT_EQ("b", trail[1]);
-    ASSERT_EQ("c", trail[2]);
-
-    ASSERT_EQ(0u, uri.GetLevelCount());
-    ASSERT_TRUE(uri.IsUniversalTrailing());
-  }
-}
-
-
-
-
-
-
-static int testValue;
-
-template <int value>
-static void SetValue(RestApiGetCall& get)
-{
-  testValue = value;
-}
-
-
-static bool GetDirectory(Json::Value& target,
-                         RestApiHierarchy& hierarchy, 
-                         const std::string& uri)
-{
-  UriComponents p;
-  Toolbox::SplitUriComponents(p, uri);
-  return hierarchy.GetDirectory(target, p);
-}
-
-
-
-namespace
-{
-  class MyVisitor : public RestApiHierarchy::IVisitor
-  {
-  public:
-    virtual bool Visit(const RestApiHierarchy::Resource& resource,
-                       const UriComponents& uri,
-                       const IHttpHandler::Arguments& components,
-                       const UriComponents& trailing) ORTHANC_OVERRIDE
-    {
-      return resource.Handle(*(RestApiGetCall*) NULL);
-    }
-  };
-}
-
-
-static bool HandleGet(RestApiHierarchy& hierarchy, 
-                      const std::string& uri)
-{
-  UriComponents p;
-  Toolbox::SplitUriComponents(p, uri);
-  MyVisitor visitor;
-  return hierarchy.LookupResource(p, visitor);
-}
-
-
-TEST(RestApi, RestApiHierarchy)
-{
-  RestApiHierarchy root;
-  root.Register("/hello/world/test", SetValue<1>);
-  root.Register("/hello/world/test2", SetValue<2>);
-  root.Register("/hello/{world}/test3/test4", SetValue<3>);
-  root.Register("/hello2/*", SetValue<4>);
-
-  Json::Value m;
-  root.CreateSiteMap(m);
-  std::cout << m;
-
-  Json::Value d;
-  ASSERT_FALSE(GetDirectory(d, root, "/hello"));
-
-  ASSERT_TRUE(GetDirectory(d, root, "/hello/a")); 
-  ASSERT_EQ(1u, d.size());
-  ASSERT_EQ("test3", d[0].asString());
-
-  ASSERT_TRUE(GetDirectory(d, root, "/hello/world"));
-  ASSERT_EQ(2u, d.size());
-
-  ASSERT_TRUE(GetDirectory(d, root, "/hello/a/test3"));
-  ASSERT_EQ(1u, d.size());
-  ASSERT_EQ("test4", d[0].asString());
-
-  ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test"));
-  ASSERT_TRUE(GetDirectory(d, root, "/hello/world/test2"));
-  ASSERT_FALSE(GetDirectory(d, root, "/hello2"));
-
-  testValue = 0;
-  ASSERT_TRUE(HandleGet(root, "/hello/world/test"));
-  ASSERT_EQ(testValue, 1);
-  ASSERT_TRUE(HandleGet(root, "/hello/world/test2"));
-  ASSERT_EQ(testValue, 2);
-  ASSERT_TRUE(HandleGet(root, "/hello/b/test3/test4"));
-  ASSERT_EQ(testValue, 3);
-  ASSERT_FALSE(HandleGet(root, "/hello/b/test3/test"));
-  ASSERT_EQ(testValue, 3);
-  ASSERT_TRUE(HandleGet(root, "/hello2/a/b"));
-  ASSERT_EQ(testValue, 4);
-}
-
-
-
-
-
-namespace
-{
-  class AcceptHandler : public 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) ORTHANC_OVERRIDE
-    {
-      type_ = type;
-      subtype_ = subtype;
-    }
-  };
-}
-
-
-TEST(RestApi, HttpContentNegociation)
-{
-  // Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
-
-  AcceptHandler h;
-
-  {
-    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";
-  
-  {
-    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());
-  }
-  
-  {
-    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());
-  }
-  
-  {
-    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");
-  }
-  
-  {
-    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());
-  }
-  
-  {
-    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"));
-  }
-}
-
-
-TEST(StringMatcher, Basic)
-{
-  StringMatcher matcher("---");
-
-  ASSERT_THROW(matcher.GetMatchBegin(), OrthancException);
-
-  {
-    const std::string s = "";
-    ASSERT_FALSE(matcher.Apply(s));
-  }
-
-  {
-    const std::string s = "abc----def";
-    ASSERT_TRUE(matcher.Apply(s));
-    ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin()));
-    ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd()));
-  }
-
-  {
-    const std::string s = "abc---";
-    ASSERT_TRUE(matcher.Apply(s));
-    ASSERT_EQ(3, std::distance(s.begin(), matcher.GetMatchBegin()));
-    ASSERT_EQ(s.end(), matcher.GetMatchEnd());
-    ASSERT_EQ("---", std::string(matcher.GetMatchBegin(), matcher.GetMatchEnd()));
-    ASSERT_EQ("", std::string(matcher.GetMatchEnd(), s.end()));
-  }
-
-  {
-    const std::string s = "abc--def";
-    ASSERT_FALSE(matcher.Apply(s));
-    ASSERT_THROW(matcher.GetMatchBegin(), OrthancException);
-    ASSERT_THROW(matcher.GetMatchEnd(), OrthancException);
-  }
-
-  {
-    std::string s(10u, '\0');  // String with null values
-    ASSERT_EQ(10u, s.size());
-    ASSERT_EQ(10u, s.size());
-    ASSERT_FALSE(matcher.Apply(s));
-
-    s[9] = '-';
-    ASSERT_FALSE(matcher.Apply(s));
-
-    s[8] = '-';
-    ASSERT_FALSE(matcher.Apply(s));
-
-    s[7] = '-';
-    ASSERT_TRUE(matcher.Apply(s));
-    ASSERT_EQ(s.c_str() + 7, matcher.GetPointerBegin());
-    ASSERT_EQ(s.c_str() + 10, matcher.GetPointerEnd());
-    ASSERT_EQ(s.end() - 3, matcher.GetMatchBegin());
-    ASSERT_EQ(s.end(), matcher.GetMatchEnd());
-  }
-}
-
-
-
-class MultipartTester : public MultipartStreamReader::IHandler
-{
-private:
-  struct Part
-  {
-    MultipartStreamReader::HttpHeaders   headers_;
-    std::string  data_;
-
-    Part(const MultipartStreamReader::HttpHeaders& headers,
-         const void* part,
-         size_t size) :
-      headers_(headers),
-      data_(reinterpret_cast<const char*>(part), size)
-    {
-    }
-  };
-
-  std::vector<Part> parts_;
-
-public:
-  virtual void HandlePart(const MultipartStreamReader::HttpHeaders& headers,
-                          const void* part,
-                          size_t size)
-  {
-    parts_.push_back(Part(headers, part, size));
-  }
-
-  unsigned int GetCount() const
-  {
-    return parts_.size();
-  }
-
-  MultipartStreamReader::HttpHeaders& GetHeaders(size_t i)
-  {
-    return parts_[i].headers_;
-  }
-
-  const std::string& GetData(size_t i) const
-  {
-    return parts_[i].data_;
-  }
-};
-
-
-TEST(MultipartStreamReader, ParseHeaders)
-{
-  std::string ct, b, st, header;
-
-  {
-    MultipartStreamReader::HttpHeaders h;
-    h["hello"] = "world";
-    h["Content-Type"] = "world";  // Should be in lower-case
-    h["CONTENT-type"] = "world";  // Should be in lower-case
-    ASSERT_FALSE(MultipartStreamReader::GetMainContentType(header, h));
-  }
-
-  {
-    MultipartStreamReader::HttpHeaders h;
-    h["content-type"] = "world";
-    ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); 
-    ASSERT_EQ(header, "world");
-    ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header));
-  }
-
-  {
-    MultipartStreamReader::HttpHeaders h;
-    h["content-type"] = "multipart/related; dummy=value; boundary=1234; hello=world";
-    ASSERT_TRUE(MultipartStreamReader::GetMainContentType(header, h)); 
-    ASSERT_EQ(header, h["content-type"]);
-    ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType(ct, st, b, header));
-    ASSERT_EQ(ct, "multipart/related");
-    ASSERT_EQ(b, "1234");
-    ASSERT_TRUE(st.empty());
-  }
-
-  {
-    ASSERT_FALSE(MultipartStreamReader::ParseMultipartContentType
-                 (ct, st, b, "multipart/related; boundary="));  // Empty boundary
-  }
-
-  {
-    ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType
-                (ct, st, b, "Multipart/Related; TYPE=Application/Dicom; Boundary=heLLO"));
-    ASSERT_EQ(ct, "multipart/related");
-    ASSERT_EQ(b, "heLLO");
-    ASSERT_EQ(st, "application/dicom");
-  }
-
-  {
-    ASSERT_TRUE(MultipartStreamReader::ParseMultipartContentType
-                (ct, st, b, "Multipart/Related; type=\"application/DICOM\"; Boundary=a"));
-    ASSERT_EQ(ct, "multipart/related");
-    ASSERT_EQ(b, "a");
-    ASSERT_EQ(st, "application/dicom");
-  }
-}
-
-
-TEST(MultipartStreamReader, BytePerByte)
-{
-  std::string stream = "GARBAGE";
-
-  std::string boundary = "123456789123456789";
-
-  {
-    for (size_t i = 0; i < 10; i++)
-    {
-      std::string f = "hello " + boost::lexical_cast<std::string>(i);
-    
-      stream += "\r\n--" + boundary + "\r\n";
-      if (i % 2 == 0)
-        stream += "Content-Length: " + boost::lexical_cast<std::string>(f.size()) + "\r\n";
-      stream += "Content-Type: toto " + boost::lexical_cast<std::string>(i) + "\r\n\r\n";
-      stream += f;
-    }
-  
-    stream += "\r\n--" + boundary + "--";
-    stream += "GARBAGE";
-  }
-
-  for (unsigned int k = 0; k < 2; k++)
-  {
-    MultipartTester decoded;
-
-    MultipartStreamReader reader(boundary);
-    reader.SetBlockSize(1);
-    reader.SetHandler(decoded);
-
-    if (k == 0)
-    {
-      for (size_t i = 0; i < stream.size(); i++)
-      {
-        reader.AddChunk(&stream[i], 1);
-      }
-    }
-    else
-    {
-      reader.AddChunk(stream);
-    }
-
-    reader.CloseStream();
-
-    ASSERT_EQ(10u, decoded.GetCount());
-
-    for (size_t i = 0; i < 10; i++)
-    {
-      ASSERT_EQ("hello " + boost::lexical_cast<std::string>(i), decoded.GetData(i));
-      ASSERT_EQ("toto " + boost::lexical_cast<std::string>(i), decoded.GetHeaders(i)["content-type"]);
-
-      if (i % 2 == 0)
-      {
-        ASSERT_EQ(2u, decoded.GetHeaders(i).size());
-        ASSERT_TRUE(decoded.GetHeaders(i).find("content-length") != decoded.GetHeaders(i).end());
-      }
-    }
-  }
-}
--- a/UnitTestsSources/SQLiteChromiumTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,388 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Toolbox.h"
-#include "../Core/SQLite/Connection.h"
-#include "../Core/SQLite/Statement.h"
-#include "../Core/SQLite/Transaction.h"
-
-#include <sqlite3.h>
-
-
-using namespace Orthanc;
-using namespace Orthanc::SQLite;
-
-
-/********************************************************************
- ** Tests from
- ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc
- ********************************************************************/
-
-namespace
-{
-  class SQLConnectionTest : public testing::Test 
-  {
-  public:
-    SQLConnectionTest()
-    {
-    }
-
-    virtual ~SQLConnectionTest() ORTHANC_OVERRIDE
-    {
-    }
-
-    virtual void SetUp() ORTHANC_OVERRIDE
-    {
-      db_.OpenInMemory();
-    }
-
-    virtual void TearDown() ORTHANC_OVERRIDE
-    {
-      db_.Close();
-    }
-
-    Connection& db() 
-    { 
-      return db_; 
-    }
-
-  private:
-    Connection db_;
-  };
-}
-
-
-
-TEST_F(SQLConnectionTest, Execute) 
-{
-  // Valid statement should return true.
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  EXPECT_EQ(SQLITE_OK, db().GetErrorCode());
-
-  // Invalid statement should fail.
-  ASSERT_EQ(SQLITE_ERROR,
-            db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b"));
-  EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode());
-}
-
-TEST_F(SQLConnectionTest, ExecuteWithErrorCode) {
-  ASSERT_EQ(SQLITE_OK,
-            db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)"));
-  ASSERT_EQ(SQLITE_ERROR,
-            db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE"));
-  ASSERT_EQ(SQLITE_ERROR,
-            db().ExecuteAndReturnErrorCode(
-              "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)"));
-}
-
-TEST_F(SQLConnectionTest, CachedStatement) {
-  StatementId id1("foo", 12);
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)"));
-
-  // Create a new cached statement.
-  {
-    Statement s(db(), id1, "SELECT a FROM foo");
-    ASSERT_TRUE(s.Step());
-    EXPECT_EQ(12, s.ColumnInt(0));
-  }
-
-  // The statement should be cached still.
-  EXPECT_TRUE(db().HasCachedStatement(id1));
-
-  {
-    // Get the same statement using different SQL. This should ignore our
-    // SQL and use the cached one (so it will be valid).
-    Statement s(db(), id1, "something invalid(");
-    ASSERT_TRUE(s.Step());
-    EXPECT_EQ(12, s.ColumnInt(0));
-  }
-
-  // Make sure other statements aren't marked as cached.
-  EXPECT_FALSE(db().HasCachedStatement(SQLITE_FROM_HERE));
-}
-
-TEST_F(SQLConnectionTest, IsSQLValidTest) {
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo"));
-  ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo"));
-}
-
-
-
-TEST_F(SQLConnectionTest, DoesStuffExist) {
-  // Test DoesTableExist.
-  EXPECT_FALSE(db().DoesTableExist("foo"));
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  EXPECT_TRUE(db().DoesTableExist("foo"));
-
-  // Should be case sensitive.
-  EXPECT_FALSE(db().DoesTableExist("FOO"));
-
-  // Test DoesColumnExist.
-  EXPECT_FALSE(db().DoesColumnExist("foo", "bar"));
-  EXPECT_TRUE(db().DoesColumnExist("foo", "a"));
-
-  // Testing for a column on a nonexistent table.
-  EXPECT_FALSE(db().DoesColumnExist("bar", "b"));
-}
-
-TEST_F(SQLConnectionTest, GetLastInsertRowId) {
-  ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"));
-
-  ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)"));
-
-  // Last insert row ID should be valid.
-  int64_t row = db().GetLastInsertRowId();
-  EXPECT_LT(0, row);
-
-  // It should be the primary key of the row we just inserted.
-  Statement s(db(), "SELECT value FROM foo WHERE id=?");
-  s.BindInt64(0, row);
-  ASSERT_TRUE(s.Step());
-  EXPECT_EQ(12, s.ColumnInt(0));
-}
-
-TEST_F(SQLConnectionTest, Rollback) {
-  ASSERT_TRUE(db().BeginTransaction());
-  ASSERT_TRUE(db().BeginTransaction());
-  EXPECT_EQ(2, db().GetTransactionNesting());
-  db().RollbackTransaction();
-  EXPECT_FALSE(db().CommitTransaction());
-  EXPECT_TRUE(db().BeginTransaction());
-}
-
-
-
-
-/********************************************************************
- ** Tests from
- ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/statement_unittest.cc
- ********************************************************************/
-
-namespace Orthanc
-{
-  namespace SQLite
-  {
-    class SQLStatementTest : public SQLConnectionTest
-    {
-    };
-
-    TEST_F(SQLStatementTest, Run) {
-      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
-
-      Statement s(db(), "SELECT b FROM foo WHERE a=?");
-      // Stepping it won't work since we haven't bound the value.
-      EXPECT_FALSE(s.Step());
-
-      // Run should fail since this produces output, and we should use Step(). This
-      // gets a bit wonky since sqlite says this is OK so succeeded is set.
-      s.Reset(true);
-      s.BindInt(0, 3);
-      EXPECT_FALSE(s.Run());
-      EXPECT_EQ(SQLITE_ROW, db().GetErrorCode());
-
-      // Resetting it should put it back to the previous state (not runnable).
-      s.Reset(true);
-
-      // Binding and stepping should produce one row.
-      s.BindInt(0, 3);
-      EXPECT_TRUE(s.Step());
-      EXPECT_EQ(12, s.ColumnInt(0));
-      EXPECT_FALSE(s.Step());
-    }
-
-    TEST_F(SQLStatementTest, BasicErrorCallback) {
-      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)"));
-      // Insert in the foo table the primary key. It is an error to insert
-      // something other than an number. This error causes the error callback
-      // handler to be called with SQLITE_MISMATCH as error code.
-      Statement s(db(), "INSERT INTO foo (a) VALUES (?)");
-      s.BindCString(0, "bad bad");
-      EXPECT_THROW(s.Run(), OrthancException);
-    }
-
-    TEST_F(SQLStatementTest, Reset) {
-      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)"));
-      ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)"));
-
-      Statement s(db(), "SELECT b FROM foo WHERE a = ? ");
-      s.BindInt(0, 3);
-      ASSERT_TRUE(s.Step());
-      EXPECT_EQ(12, s.ColumnInt(0));
-      ASSERT_FALSE(s.Step());
-
-      s.Reset(false);
-      // Verify that we can get all rows again.
-      ASSERT_TRUE(s.Step());
-      EXPECT_EQ(12, s.ColumnInt(0));
-      EXPECT_FALSE(s.Step());
-
-      s.Reset(true);
-      ASSERT_FALSE(s.Step());
-    }
-  }
-}
-
-
-
-
-
-
-/********************************************************************
- ** Tests from
- ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc
- ********************************************************************/
-
-namespace
-{
-  class SQLTransactionTest : public SQLConnectionTest
-  {
-  public:
-    virtual void SetUp() ORTHANC_OVERRIDE
-    {
-      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);
-    }
-  };
-}
-
-
-TEST_F(SQLTransactionTest, Commit) {
-  {
-    Transaction t(db());
-    EXPECT_FALSE(t.IsOpen());
-    t.Begin();
-    EXPECT_TRUE(t.IsOpen());
-
-    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-
-    t.Commit();
-    EXPECT_FALSE(t.IsOpen());
-  }
-
-  EXPECT_EQ(1, CountFoo());
-}
-
-TEST_F(SQLTransactionTest, Rollback) {
-  // Test some basic initialization, and that rollback runs when you exit the
-  // scope.
-  {
-    Transaction t(db());
-    EXPECT_FALSE(t.IsOpen());
-    t.Begin();
-    EXPECT_TRUE(t.IsOpen());
-
-    EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-  }
-
-  // Nothing should have been committed since it was implicitly rolled back.
-  EXPECT_EQ(0, CountFoo());
-
-  // Test explicit rollback.
-  Transaction t2(db());
-  EXPECT_FALSE(t2.IsOpen());
-  t2.Begin();
-
-  EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-  t2.Rollback();
-  EXPECT_FALSE(t2.IsOpen());
-
-  // Nothing should have been committed since it was explicitly rolled back.
-  EXPECT_EQ(0, CountFoo());
-}
-
-// Rolling back any part of a transaction should roll back all of them.
-TEST_F(SQLTransactionTest, NestedRollback) {
-  EXPECT_EQ(0, db().GetTransactionNesting());
-
-  // Outermost transaction.
-  {
-    Transaction outer(db());
-    outer.Begin();
-    EXPECT_EQ(1, db().GetTransactionNesting());
-
-    // The first inner one gets committed.
-    {
-      Transaction inner1(db());
-      inner1.Begin();
-      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-      EXPECT_EQ(2, db().GetTransactionNesting());
-
-      inner1.Commit();
-      EXPECT_EQ(1, db().GetTransactionNesting());
-    }
-
-    // One row should have gotten inserted.
-    EXPECT_EQ(1, CountFoo());
-
-    // The second inner one gets rolled back.
-    {
-      Transaction inner2(db());
-      inner2.Begin();
-      EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)"));
-      EXPECT_EQ(2, db().GetTransactionNesting());
-
-      inner2.Rollback();
-      EXPECT_EQ(1, db().GetTransactionNesting());
-    }
-
-    // A third inner one will fail in Begin since one has already been rolled
-    // back.
-    EXPECT_EQ(1, db().GetTransactionNesting());
-    {
-      Transaction inner3(db());
-      EXPECT_THROW(inner3.Begin(), OrthancException);
-      EXPECT_EQ(1, db().GetTransactionNesting());
-    }
-  }
-  EXPECT_EQ(0, db().GetTransactionNesting());
-  EXPECT_EQ(0, CountFoo());
-}
--- a/UnitTestsSources/SQLiteTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,346 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/SystemToolbox.h"
-#include "../Core/SQLite/Connection.h"
-#include "../Core/SQLite/Statement.h"
-#include "../Core/SQLite/Transaction.h"
-
-#include <sqlite3.h>
-
-using namespace Orthanc;
-
-
-TEST(SQLite, Configuration)
-{
-  /**
-   * The system-wide version of SQLite under OS X uses
-   * SQLITE_THREADSAFE==2 (SQLITE_CONFIG_SERIALIZED), whereas the
-   * static builds of Orthanc use SQLITE_THREADSAFE==1
-   * (SQLITE_CONFIG_MULTITHREAD). In any case, we wish to ensure that
-   * SQLITE_THREADSAFE!=0 (SQLITE_CONFIG_SINGLETHREAD).
-   **/
-  ASSERT_NE(0, sqlite3_threadsafe());
-}
-
-
-TEST(SQLite, Connection)
-{
-  SystemToolbox::RemoveFile("UnitTestsResults/coucou");
-  SQLite::Connection c;
-  c.Open("UnitTestsResults/coucou");
-  c.Execute("CREATE TABLE c(k INTEGER PRIMARY KEY AUTOINCREMENT, v INTEGER)");
-  c.Execute("INSERT INTO c VALUES(NULL, 42);");
-}
-
-
-TEST(SQLite, StatementReferenceBasic)
-{
-  sqlite3* db;
-  sqlite3_open(":memory:", &db);
-
-  {
-    SQLite::StatementReference r(db, "SELECT * FROM sqlite_master");
-    ASSERT_EQ(0u, r.GetReferenceCount());
-
-    {
-      SQLite::StatementReference r1(r);
-      ASSERT_EQ(1u, r.GetReferenceCount());
-      ASSERT_EQ(0u, r1.GetReferenceCount());
-
-      {
-        SQLite::StatementReference r2(r);
-        ASSERT_EQ(2u, r.GetReferenceCount());
-        ASSERT_EQ(0u, r1.GetReferenceCount());
-        ASSERT_EQ(0u, r2.GetReferenceCount());
-
-        SQLite::StatementReference r3(r2);
-        ASSERT_EQ(3u, r.GetReferenceCount());
-        ASSERT_EQ(0u, r1.GetReferenceCount());
-        ASSERT_EQ(0u, r2.GetReferenceCount());
-        ASSERT_EQ(0u, r3.GetReferenceCount());
-      }
-
-      ASSERT_EQ(1u, r.GetReferenceCount());
-      ASSERT_EQ(0u, r1.GetReferenceCount());
-
-      {
-        SQLite::StatementReference r2(r);
-        ASSERT_EQ(2u, r.GetReferenceCount());
-        ASSERT_EQ(0u, r1.GetReferenceCount());
-        ASSERT_EQ(0u, r2.GetReferenceCount());
-      }
-
-      ASSERT_EQ(1u, r.GetReferenceCount());
-      ASSERT_EQ(0u, r1.GetReferenceCount());
-    }
-
-    ASSERT_EQ(0u, r.GetReferenceCount());
-  }
-
-  sqlite3_close(db);
-}
-
-TEST(SQLite, StatementBasic)
-{
-  SQLite::Connection c;
-  c.OpenInMemory();
-  
-  SQLite::Statement s(c, "SELECT * from sqlite_master");
-  s.Run();
-
-  for (unsigned int i = 0; i < 5; i++)
-  {
-    SQLite::Statement cs(c, SQLITE_FROM_HERE, "SELECT * from sqlite_master");
-    cs.Step();
-  }
-}
-
-
-namespace
-{
-  static bool destroyed;
-
-  class MyFunc : public SQLite::IScalarFunction
-  {
-  public:
-    MyFunc()
-    {
-      destroyed = false;
-    }
-
-    virtual ~MyFunc() ORTHANC_OVERRIDE
-    {
-      destroyed = true;
-    }
-
-    virtual const char* GetName() const ORTHANC_OVERRIDE
-    {
-      return "MYFUNC";
-    }
-
-    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
-    {
-      return 2;
-    }
-
-    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
-    {
-      context.SetIntResult(1000 + context.GetIntValue(0) * context.GetIntValue(1));
-    }
-  };
-
-  class MyDelete : public SQLite::IScalarFunction
-  {
-  public:
-    std::set<int> deleted_;
-
-    virtual const char* GetName() const ORTHANC_OVERRIDE
-    {
-      return "MYDELETE";
-    }
-
-    virtual unsigned int GetCardinality() const ORTHANC_OVERRIDE
-    {
-      return 1;
-    }
-
-    virtual void Compute(SQLite::FunctionContext& context) ORTHANC_OVERRIDE
-    {
-      deleted_.insert(context.GetIntValue(0));
-      context.SetNullResult();
-    }
-  };
-}
-
-TEST(SQLite, ScalarFunction)
-{
-  {
-    SQLite::Connection c;
-    c.OpenInMemory();
-    c.Register(new MyFunc());
-    c.Execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v1 INTEGER, v2 INTEGER);");
-    c.Execute("INSERT INTO t VALUES(NULL, 2, 3);");
-    c.Execute("INSERT INTO t VALUES(NULL, 4, 4);");
-    c.Execute("INSERT INTO t VALUES(NULL, 6, 5);");
-    SQLite::Statement t(c, "SELECT MYFUNC(v1, v2), v1, v2 FROM t");
-    int i = 0;
-    while (t.Step())
-    {
-      ASSERT_EQ(t.ColumnInt(0), 1000 + t.ColumnInt(1) * t.ColumnInt(2));
-      i++;
-    }
-    ASSERT_EQ(3, i);
-    ASSERT_FALSE(destroyed);
-  }
-  ASSERT_TRUE(destroyed);
-}
-
-TEST(SQLite, CascadedDeleteCallback)
-{
-  SQLite::Connection c;
-  c.OpenInMemory();
-  MyDelete *func = new MyDelete();
-  c.Register(func);
-  c.Execute("CREATE TABLE parent(id INTEGER PRIMARY KEY, dummy INTEGER);");
-  c.Execute("CREATE TABLE child("
-            "  id INTEGER PRIMARY KEY, "
-            "  parent INTEGER REFERENCES parent(id) ON DELETE CASCADE, "
-            "  value INTEGER);");
-  c.Execute("CREATE TRIGGER childRemoved "
-            "AFTER DELETE ON child "
-            "FOR EACH ROW BEGIN "
-            "  SELECT MYDELETE(old.value); "
-            "END;");
-
-  c.Execute("INSERT INTO parent VALUES(42, 100);");
-  c.Execute("INSERT INTO parent VALUES(43, 101);");
-
-  c.Execute("INSERT INTO child VALUES(NULL, 42, 4200);");
-  c.Execute("INSERT INTO child VALUES(NULL, 42, 4201);");
-
-  c.Execute("INSERT INTO child VALUES(NULL, 43, 4300);");
-  c.Execute("INSERT INTO child VALUES(NULL, 43, 4301);");
-
-  // The following command deletes "parent(43, 101)", then in turns
-  // "child(NULL, 43, 4300/4301)", then calls the MyDelete on 4300 and
-  // 4301
-  c.Execute("DELETE FROM parent WHERE dummy=101");
-
-  ASSERT_EQ(2u, func->deleted_.size());
-  ASSERT_TRUE(func->deleted_.find(4300) != func->deleted_.end());
-  ASSERT_TRUE(func->deleted_.find(4301) != func->deleted_.end());
-}
-
-
-TEST(SQLite, EmptyTransactions)
-{
-  try
-  {
-    SQLite::Connection c;
-    c.OpenInMemory();
-
-    c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY);");
-    c.Execute("INSERT INTO a VALUES(NULL)");
-      
-    {
-      SQLite::Transaction t(c);
-      t.Begin();
-      {
-        SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
-        s.Step();
-      }
-      //t.Commit();
-    }
-
-    {
-      SQLite::Statement s(c, SQLITE_FROM_HERE, "SELECT * FROM a");
-      s.Step();
-    }
-  }
-  catch (OrthancException& e)
-  {
-    fprintf(stderr, "Exception: [%s]\n", e.What());
-    throw e;
-  }
-}
-
-
-TEST(SQLite, Types)
-{
-  SQLite::Connection c;
-  c.OpenInMemory();
-  c.Execute("CREATE TABLE a(id INTEGER PRIMARY KEY, value)");
-
-  {
-    SQLite::Statement s(c, std::string("SELECT * FROM a"));
-    ASSERT_EQ(2, s.ColumnCount());
-    ASSERT_FALSE(s.Step());
-  }
-
-  {
-    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
-    ASSERT_FALSE(s.Step());
-    ASSERT_EQ("SELECT * FROM a", s.GetOriginalSQLStatement());
-  }
-
-  {
-    SQLite::Statement s(c, SQLITE_FROM_HERE, "INSERT INTO a VALUES(NULL, ?);");
-    s.BindNull(0);             ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindBool(0, true);       ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindInt(0, 42);          ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindInt64(0, 42ll);      ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindDouble(0, 42.5);     ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindCString(0, "Hello"); ASSERT_TRUE(s.Run()); s.Reset();
-    s.BindBlob(0, "Hello", 5); ASSERT_TRUE(s.Run()); s.Reset();
-  }
-
-  {
-    SQLite::Statement s(c, SQLITE_FROM_HERE, std::string("SELECT * FROM a"));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_NULL, s.GetColumnType(1));
-    ASSERT_TRUE(s.ColumnIsNull(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
-    ASSERT_TRUE(s.ColumnBool(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
-    ASSERT_EQ(42, s.ColumnInt(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_INTEGER, s.GetColumnType(1));
-    ASSERT_EQ(42ll, s.ColumnInt64(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_FLOAT, s.GetColumnType(1));
-    ASSERT_DOUBLE_EQ(42.5, s.ColumnDouble(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_TEXT, s.GetColumnType(1));
-    ASSERT_EQ("Hello", s.ColumnString(1));
-    ASSERT_TRUE(s.Step());
-    ASSERT_EQ(SQLite::COLUMN_TYPE_BLOB, s.GetColumnType(1));
-    ASSERT_EQ(5, s.ColumnByteLength(1));
-    ASSERT_TRUE(!memcmp("Hello", s.ColumnBlob(1), 5));
-
-    std::string t;
-    ASSERT_TRUE(s.ColumnBlobAsString(1, &t));
-    ASSERT_EQ("Hello", t);
-
-    ASSERT_FALSE(s.Step());
-  }
-}
--- a/UnitTestsSources/ServerIndexTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,915 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/Compatibility.h"
-#include "../Core/FileStorage/FilesystemStorage.h"
-#include "../Core/FileStorage/MemoryStorageArea.h"
-#include "../Core/Logging.h"
-#include "../OrthancServer/Database/SQLiteDatabaseWrapper.h"
-#include "../OrthancServer/Search/DatabaseLookup.h"
-#include "../OrthancServer/ServerContext.h"
-#include "../OrthancServer/ServerToolbox.h"
-
-#include <ctype.h>
-#include <algorithm>
-
-using namespace Orthanc;
-
-namespace
-{
-  class TestDatabaseListener : public IDatabaseListener
-  {
-  public:
-    std::vector<std::string> deletedFiles_;
-    std::vector<std::string> deletedResources_;
-    std::string ancestorId_;
-    ResourceType ancestorType_;
-
-    void Reset()
-    {
-      ancestorId_ = "";
-      deletedFiles_.clear();
-    }
-
-    virtual void SignalRemainingAncestor(ResourceType type,
-                                         const std::string& publicId)
-      ORTHANC_OVERRIDE
-    {
-      ancestorId_ = publicId;
-      ancestorType_ = type;
-    }
-
-    virtual void SignalFileDeleted(const FileInfo& info) ORTHANC_OVERRIDE
-    {
-      const std::string fileUuid = info.GetUuid();
-      deletedFiles_.push_back(fileUuid);
-      LOG(INFO) << "A file must be removed: " << fileUuid;
-    }       
-
-    virtual void SignalChange(const ServerIndexChange& change) ORTHANC_OVERRIDE
-    {
-      if (change.GetChangeType() == ChangeType_Deleted)
-      {
-        deletedResources_.push_back(change.GetPublicId());        
-      }
-
-      LOG(INFO) << "Change related to resource " << change.GetPublicId() << " of type " 
-                << EnumerationToString(change.GetResourceType()) << ": " 
-                << EnumerationToString(change.GetChangeType());
-    }
-  };
-
-
-  class DatabaseWrapperTest : public ::testing::Test
-  {
-  protected:
-    std::unique_ptr<TestDatabaseListener>  listener_;
-    std::unique_ptr<SQLiteDatabaseWrapper> index_;
-
-  public:
-    DatabaseWrapperTest()
-    {
-    }
-
-    virtual void SetUp() ORTHANC_OVERRIDE
-    {
-      listener_.reset(new TestDatabaseListener);
-      index_.reset(new SQLiteDatabaseWrapper);
-      index_->SetListener(*listener_);
-      index_->Open();
-    }
-
-    virtual void TearDown() ORTHANC_OVERRIDE
-    {
-      index_->Close();
-      index_.reset(NULL);
-      listener_.reset(NULL);
-    }
-
-    void CheckTableRecordCount(uint32_t expected, const char* table)
-    {
-      ASSERT_EQ(expected, index_->GetTableRecordCount(table));
-    }
-
-    void CheckNoParent(int64_t id)
-    {
-      std::string s;
-      ASSERT_FALSE(index_->GetParentPublicId(s, id));
-    }
-
-    void CheckParentPublicId(const char* expected, int64_t id)
-    {
-      std::string s;
-      ASSERT_TRUE(index_->GetParentPublicId(s, id));
-      ASSERT_EQ(expected, s);
-    }
-
-    void CheckNoChild(int64_t id)
-    {
-      std::list<std::string> j;
-      index_->GetChildren(j, id);
-      ASSERT_EQ(0u, j.size());
-    }
-
-    void CheckOneChild(const char* expected, int64_t id)
-    {
-      std::list<std::string> j;
-      index_->GetChildren(j, id);
-      ASSERT_EQ(1u, j.size());
-      ASSERT_EQ(expected, j.front());
-    }
-
-    void CheckTwoChildren(const char* expected1,
-                          const char* expected2,
-                          int64_t id)
-    {
-      std::list<std::string> j;
-      index_->GetChildren(j, id);
-      ASSERT_EQ(2u, j.size());
-      ASSERT_TRUE((expected1 == j.front() && expected2 == j.back()) ||
-                  (expected1 == j.back() && expected2 == j.front()));                    
-    }
-
-    void DoLookupIdentifier(std::list<std::string>& result,
-                            ResourceType level,
-                            const DicomTag& tag,
-                            ConstraintType type,
-                            const std::string& value)
-    {
-      assert(ServerToolbox::IsIdentifier(tag, level));
-      
-      DicomTagConstraint c(tag, type, value, true, true);
-      
-      std::vector<DatabaseConstraint> lookup;
-      lookup.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      
-      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
-    }    
-
-    void DoLookupIdentifier2(std::list<std::string>& result,
-                             ResourceType level,
-                             const DicomTag& tag,
-                             ConstraintType type1,
-                             const std::string& value1,
-                             ConstraintType type2,
-                             const std::string& value2)
-    {
-      assert(ServerToolbox::IsIdentifier(tag, level));
-      
-      DicomTagConstraint c1(tag, type1, value1, true, true);
-      DicomTagConstraint c2(tag, type2, value2, true, true);
-      
-      std::vector<DatabaseConstraint> lookup;
-      lookup.push_back(c1.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      lookup.push_back(c2.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-      
-      index_->ApplyLookupResources(result, NULL, lookup, level, 0 /* no limit */);
-    }
-  };
-}
-
-
-TEST_F(DatabaseWrapperTest, Simple)
-{
-  int64_t a[] = {
-    index_->CreateResource("a", ResourceType_Patient),   // 0
-    index_->CreateResource("b", ResourceType_Study),     // 1
-    index_->CreateResource("c", ResourceType_Series),    // 2
-    index_->CreateResource("d", ResourceType_Instance),  // 3
-    index_->CreateResource("e", ResourceType_Instance),  // 4
-    index_->CreateResource("f", ResourceType_Instance),  // 5
-    index_->CreateResource("g", ResourceType_Study)      // 6
-  };
-
-  ASSERT_EQ("a", index_->GetPublicId(a[0]));
-  ASSERT_EQ("b", index_->GetPublicId(a[1]));
-  ASSERT_EQ("c", index_->GetPublicId(a[2]));
-  ASSERT_EQ("d", index_->GetPublicId(a[3]));
-  ASSERT_EQ("e", index_->GetPublicId(a[4]));
-  ASSERT_EQ("f", index_->GetPublicId(a[5]));
-  ASSERT_EQ("g", index_->GetPublicId(a[6]));
-
-  ASSERT_EQ(ResourceType_Patient, index_->GetResourceType(a[0]));
-  ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[1]));
-  ASSERT_EQ(ResourceType_Series, index_->GetResourceType(a[2]));
-  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[3]));
-  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[4]));
-  ASSERT_EQ(ResourceType_Instance, index_->GetResourceType(a[5]));
-  ASSERT_EQ(ResourceType_Study, index_->GetResourceType(a[6]));
-
-  {
-    std::list<std::string> t;
-    index_->GetAllPublicIds(t, ResourceType_Patient);
-
-    ASSERT_EQ(1u, t.size());
-    ASSERT_EQ("a", t.front());
-
-    index_->GetAllPublicIds(t, ResourceType_Series);
-    ASSERT_EQ(1u, t.size());
-    ASSERT_EQ("c", t.front());
-
-    index_->GetAllPublicIds(t, ResourceType_Study);
-    ASSERT_EQ(2u, t.size());
-
-    index_->GetAllPublicIds(t, ResourceType_Instance);
-    ASSERT_EQ(3u, t.size());
-  }
-
-  index_->SetGlobalProperty(GlobalProperty_FlushSleep, "World");
-
-  index_->AttachChild(a[0], a[1]);
-  index_->AttachChild(a[1], a[2]);
-  index_->AttachChild(a[2], a[3]);
-  index_->AttachChild(a[2], a[4]);
-  index_->AttachChild(a[6], a[5]);
-
-  int64_t parent;
-  ASSERT_FALSE(index_->LookupParent(parent, a[0]));
-  ASSERT_TRUE(index_->LookupParent(parent, a[1])); ASSERT_EQ(a[0], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[2])); ASSERT_EQ(a[1], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[3])); ASSERT_EQ(a[2], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[4])); ASSERT_EQ(a[2], parent);
-  ASSERT_TRUE(index_->LookupParent(parent, a[5])); ASSERT_EQ(a[6], parent);
-  ASSERT_FALSE(index_->LookupParent(parent, a[6]));
-
-  std::string s;
-
-  CheckNoParent(a[0]);
-  CheckNoParent(a[6]);
-  CheckParentPublicId("a", a[1]);
-  CheckParentPublicId("b", a[2]);
-  CheckParentPublicId("c", a[3]);
-  CheckParentPublicId("c", a[4]);
-  CheckParentPublicId("g", a[5]);
-
-  std::list<std::string> l;
-  index_->GetChildrenPublicId(l, a[0]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("b", l.front());
-  index_->GetChildrenPublicId(l, a[1]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("c", l.front());
-  index_->GetChildrenPublicId(l, a[3]); ASSERT_EQ(0u, l.size()); 
-  index_->GetChildrenPublicId(l, a[4]); ASSERT_EQ(0u, l.size()); 
-  index_->GetChildrenPublicId(l, a[5]); ASSERT_EQ(0u, l.size()); 
-  index_->GetChildrenPublicId(l, a[6]); ASSERT_EQ(1u, l.size()); ASSERT_EQ("f", l.front());
-
-  index_->GetChildrenPublicId(l, a[2]); ASSERT_EQ(2u, l.size()); 
-  if (l.front() == "d")
-  {
-    ASSERT_EQ("e", l.back());
-  }
-  else
-  {
-    ASSERT_EQ("d", l.back());
-    ASSERT_EQ("e", l.front());
-  }
-
-  std::map<MetadataType, std::string> md;
-  index_->GetAllMetadata(md, a[4]);
-  ASSERT_EQ(0u, md.size());
-
-  index_->AddAttachment(a[4], FileInfo("my json file", FileContentType_DicomAsJson, 42, "md5", 
-                                       CompressionType_ZlibWithSize, 21, "compressedMD5"));
-  index_->AddAttachment(a[4], FileInfo("my dicom file", FileContentType_Dicom, 42, "md5"));
-  index_->AddAttachment(a[6], FileInfo("world", FileContentType_Dicom, 44, "md5"));
-  index_->SetMetadata(a[4], MetadataType_Instance_RemoteAet, "PINNACLE");
-  
-  index_->GetAllMetadata(md, a[4]);
-  ASSERT_EQ(1u, md.size());
-  ASSERT_EQ("PINNACLE", md[MetadataType_Instance_RemoteAet]);
-  index_->SetMetadata(a[4], MetadataType_ModifiedFrom, "TUTU");
-  index_->GetAllMetadata(md, a[4]);
-  ASSERT_EQ(2u, md.size());
-
-  std::map<MetadataType, std::string> md2;
-  index_->GetAllMetadata(md2, a[4]);
-  ASSERT_EQ(2u, md2.size());
-  ASSERT_EQ("TUTU", md2[MetadataType_ModifiedFrom]);
-  ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
-
-  index_->DeleteMetadata(a[4], MetadataType_ModifiedFrom);
-  index_->GetAllMetadata(md, a[4]);
-  ASSERT_EQ(1u, md.size());
-  ASSERT_EQ("PINNACLE", md[MetadataType_Instance_RemoteAet]);
-
-  index_->GetAllMetadata(md2, a[4]);
-  ASSERT_EQ(1u, md2.size());
-  ASSERT_EQ("PINNACLE", md2[MetadataType_Instance_RemoteAet]);
-
-
-  ASSERT_EQ(21u + 42u + 44u, index_->GetTotalCompressedSize());
-  ASSERT_EQ(42u + 42u + 44u, index_->GetTotalUncompressedSize());
-
-  index_->SetMainDicomTag(a[3], DicomTag(0x0010, 0x0010), "PatientName");
-
-  int64_t b;
-  ResourceType t;
-  ASSERT_TRUE(index_->LookupResource(b, t, "g"));
-  ASSERT_EQ(7, b);
-  ASSERT_EQ(ResourceType_Study, t);
-
-  ASSERT_TRUE(index_->LookupMetadata(s, a[4], MetadataType_Instance_RemoteAet));
-  ASSERT_FALSE(index_->LookupMetadata(s, a[4], MetadataType_Instance_IndexInSeries));
-  ASSERT_EQ("PINNACLE", s);
-
-  std::string u;
-  ASSERT_TRUE(index_->LookupMetadata(u, a[4], MetadataType_Instance_RemoteAet));
-  ASSERT_EQ("PINNACLE", u);
-  ASSERT_FALSE(index_->LookupMetadata(u, a[4], MetadataType_Instance_IndexInSeries));
-
-  ASSERT_TRUE(index_->LookupGlobalProperty(s, GlobalProperty_FlushSleep));
-  ASSERT_FALSE(index_->LookupGlobalProperty(s, static_cast<GlobalProperty>(42)));
-  ASSERT_EQ("World", s);
-
-  FileInfo att;
-  ASSERT_TRUE(index_->LookupAttachment(att, a[4], FileContentType_DicomAsJson));
-  ASSERT_EQ("my json file", att.GetUuid());
-  ASSERT_EQ(21u, att.GetCompressedSize());
-  ASSERT_EQ("md5", att.GetUncompressedMD5());
-  ASSERT_EQ("compressedMD5", att.GetCompressedMD5());
-  ASSERT_EQ(42u, att.GetUncompressedSize());
-  ASSERT_EQ(CompressionType_ZlibWithSize, att.GetCompressionType());
-
-  ASSERT_TRUE(index_->LookupAttachment(att, a[6], FileContentType_Dicom));
-  ASSERT_EQ("world", att.GetUuid());
-  ASSERT_EQ(44u, att.GetCompressedSize());
-  ASSERT_EQ("md5", att.GetUncompressedMD5());
-  ASSERT_EQ("md5", att.GetCompressedMD5());
-  ASSERT_EQ(44u, att.GetUncompressedSize());
-  ASSERT_EQ(CompressionType_None, att.GetCompressionType());
-
-  ASSERT_EQ(0u, listener_->deletedFiles_.size());
-  ASSERT_EQ(0u, listener_->deletedResources_.size());
-
-  CheckTableRecordCount(7, "Resources");
-  CheckTableRecordCount(3, "AttachedFiles");
-  CheckTableRecordCount(1, "Metadata");
-  CheckTableRecordCount(1, "MainDicomTags");
-
-  index_->DeleteResource(a[0]);
-  ASSERT_EQ(5u, listener_->deletedResources_.size());
-  ASSERT_EQ(2u, listener_->deletedFiles_.size());
-  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
-                         listener_->deletedFiles_.end(),
-                         "my json file") == listener_->deletedFiles_.end());
-  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
-                         listener_->deletedFiles_.end(),
-                         "my dicom file") == listener_->deletedFiles_.end());
-
-  CheckTableRecordCount(2, "Resources");
-  CheckTableRecordCount(0, "Metadata");
-  CheckTableRecordCount(1, "AttachedFiles");
-  CheckTableRecordCount(0, "MainDicomTags");
-
-  index_->DeleteResource(a[5]);
-  ASSERT_EQ(7u, listener_->deletedResources_.size());
-
-  CheckTableRecordCount(0, "Resources");
-  CheckTableRecordCount(0, "AttachedFiles");
-  CheckTableRecordCount(3, "GlobalProperties");
-
-  std::string tmp;
-  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_DatabaseSchemaVersion));
-  ASSERT_EQ("6", tmp);
-  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_FlushSleep));
-  ASSERT_EQ("World", tmp);
-  ASSERT_TRUE(index_->LookupGlobalProperty(tmp, GlobalProperty_GetTotalSizeIsFast));
-  ASSERT_EQ("1", tmp);
-
-  ASSERT_EQ(3u, listener_->deletedFiles_.size());
-  ASSERT_FALSE(std::find(listener_->deletedFiles_.begin(), 
-                         listener_->deletedFiles_.end(),
-                         "world") == listener_->deletedFiles_.end());
-}
-
-
-TEST_F(DatabaseWrapperTest, Upward)
-{
-  int64_t a[] = {
-    index_->CreateResource("a", ResourceType_Patient),   // 0
-    index_->CreateResource("b", ResourceType_Study),     // 1
-    index_->CreateResource("c", ResourceType_Series),    // 2
-    index_->CreateResource("d", ResourceType_Instance),  // 3
-    index_->CreateResource("e", ResourceType_Instance),  // 4
-    index_->CreateResource("f", ResourceType_Study),     // 5
-    index_->CreateResource("g", ResourceType_Series),    // 6
-    index_->CreateResource("h", ResourceType_Series)     // 7
-  };
-
-  index_->AttachChild(a[0], a[1]);
-  index_->AttachChild(a[1], a[2]);
-  index_->AttachChild(a[2], a[3]);
-  index_->AttachChild(a[2], a[4]);
-  index_->AttachChild(a[1], a[6]);
-  index_->AttachChild(a[0], a[5]);
-  index_->AttachChild(a[5], a[7]);
-
-  CheckTwoChildren("b", "f", a[0]);
-  CheckTwoChildren("c", "g", a[1]);
-  CheckTwoChildren("d", "e", a[2]);
-  CheckNoChild(a[3]);
-  CheckNoChild(a[4]);
-  CheckOneChild("h", a[5]);
-  CheckNoChild(a[6]);
-  CheckNoChild(a[7]);
-
-  listener_->Reset();
-  index_->DeleteResource(a[3]);
-  ASSERT_EQ("c", listener_->ancestorId_);
-  ASSERT_EQ(ResourceType_Series, listener_->ancestorType_);
-
-  listener_->Reset();
-  index_->DeleteResource(a[4]);
-  ASSERT_EQ("b", listener_->ancestorId_);
-  ASSERT_EQ(ResourceType_Study, listener_->ancestorType_);
-
-  listener_->Reset();
-  index_->DeleteResource(a[7]);
-  ASSERT_EQ("a", listener_->ancestorId_);
-  ASSERT_EQ(ResourceType_Patient, listener_->ancestorType_);
-
-  listener_->Reset();
-  index_->DeleteResource(a[6]);
-  ASSERT_EQ("", listener_->ancestorId_);  // No more ancestor
-}
-
-
-TEST_F(DatabaseWrapperTest, PatientRecycling)
-{
-  std::vector<int64_t> patients;
-  for (int i = 0; i < 10; i++)
-  {
-    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
-    patients.push_back(index_->CreateResource(p, ResourceType_Patient));
-    index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10, 
-                                                "md5-" + boost::lexical_cast<std::string>(i)));
-    ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
-  }
-
-  CheckTableRecordCount(10u, "Resources");
-  CheckTableRecordCount(10u, "PatientRecyclingOrder");
-
-  listener_->Reset();
-  ASSERT_EQ(0u, listener_->deletedResources_.size());
-
-  index_->DeleteResource(patients[5]);
-  index_->DeleteResource(patients[0]);
-  ASSERT_EQ(2u, listener_->deletedResources_.size());
-
-  CheckTableRecordCount(8u, "Resources");
-  CheckTableRecordCount(8u, "PatientRecyclingOrder");
-
-  ASSERT_EQ(2u, listener_->deletedFiles_.size());
-  ASSERT_EQ("Patient 5", listener_->deletedFiles_[0]);
-  ASSERT_EQ("Patient 0", listener_->deletedFiles_[1]);
-
-  int64_t p;
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(3u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(4u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(5u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(6u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[6]);
-  index_->DeleteResource(p);
-  index_->DeleteResource(patients[8]);
-  ASSERT_EQ(8u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[7]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(9u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[9]);
-  index_->DeleteResource(p);
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p));
-  ASSERT_EQ(10u, listener_->deletedResources_.size());
-
-  ASSERT_EQ(10u, listener_->deletedFiles_.size());
-
-  CheckTableRecordCount(0, "Resources");
-  CheckTableRecordCount(0, "PatientRecyclingOrder");
-}
-
-
-TEST_F(DatabaseWrapperTest, PatientProtection)
-{
-  std::vector<int64_t> patients;
-  for (int i = 0; i < 5; i++)
-  {
-    std::string p = "Patient " + boost::lexical_cast<std::string>(i);
-    patients.push_back(index_->CreateResource(p, ResourceType_Patient));
-    index_->AddAttachment(patients[i], FileInfo(p, FileContentType_Dicom, i + 10,
-                                                "md5-" + boost::lexical_cast<std::string>(i)));
-    ASSERT_FALSE(index_->IsProtectedPatient(patients[i]));
-  }
-
-  CheckTableRecordCount(5, "Resources");
-  CheckTableRecordCount(5, "PatientRecyclingOrder");
-
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  index_->SetProtectedPatient(patients[2], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
-  CheckTableRecordCount(5, "Resources");
-  CheckTableRecordCount(4, "PatientRecyclingOrder");
-
-  index_->SetProtectedPatient(patients[2], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
-  CheckTableRecordCount(4, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[2], false);
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  CheckTableRecordCount(5, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[2], false);
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  CheckTableRecordCount(5, "PatientRecyclingOrder");
-  CheckTableRecordCount(5, "Resources");
-  index_->SetProtectedPatient(patients[2], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[2]));
-  CheckTableRecordCount(4, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[2], false);
-  ASSERT_FALSE(index_->IsProtectedPatient(patients[2]));
-  CheckTableRecordCount(5, "PatientRecyclingOrder");
-  index_->SetProtectedPatient(patients[3], true);
-  ASSERT_TRUE(index_->IsProtectedPatient(patients[3]));
-  CheckTableRecordCount(4, "PatientRecyclingOrder");
-
-  CheckTableRecordCount(5, "Resources");
-  ASSERT_EQ(0u, listener_->deletedFiles_.size());
-
-  // Unprotecting a patient puts it at the last position in the recycling queue
-  int64_t p;
-  ASSERT_EQ(0u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[0]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(1u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[1])); ASSERT_EQ(p, patients[4]);
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[1]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(2u, listener_->deletedResources_.size());
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[4]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(3u, listener_->deletedResources_.size());
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[2]));
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[2]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(4u, listener_->deletedResources_.size());
-  // "patients[3]" is still protected
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p));
-
-  ASSERT_EQ(4u, listener_->deletedFiles_.size());
-  CheckTableRecordCount(1, "Resources");
-  CheckTableRecordCount(0, "PatientRecyclingOrder");
-
-  index_->SetProtectedPatient(patients[3], false);
-  CheckTableRecordCount(1, "PatientRecyclingOrder");
-  ASSERT_FALSE(index_->SelectPatientToRecycle(p, patients[3]));
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p, patients[2]));
-  ASSERT_TRUE(index_->SelectPatientToRecycle(p)); ASSERT_EQ(p, patients[3]);
-  index_->DeleteResource(p);
-  ASSERT_EQ(5u, listener_->deletedResources_.size());
-
-  ASSERT_EQ(5u, listener_->deletedFiles_.size());
-  CheckTableRecordCount(0, "Resources");
-  CheckTableRecordCount(0, "PatientRecyclingOrder");
-}
-
-
-TEST(ServerIndex, Sequence)
-{
-  const std::string path = "UnitTestsStorage";
-
-  SystemToolbox::RemoveFile(path + "/index");
-  FilesystemStorage storage(path);
-  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
-  db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10);
-  context.SetupJobsEngine(true, false);
-
-  ServerIndex& index = context.GetIndex();
-
-  ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(2u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(3u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-  ASSERT_EQ(4u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
-
-  context.Stop();
-  db.Close();
-}
-
-
-TEST_F(DatabaseWrapperTest, LookupIdentifier)
-{
-  int64_t a[] = {
-    index_->CreateResource("a", ResourceType_Study),   // 0
-    index_->CreateResource("b", ResourceType_Study),   // 1
-    index_->CreateResource("c", ResourceType_Study),   // 2
-    index_->CreateResource("d", ResourceType_Series)   // 3
-  };
-
-  index_->SetIdentifierTag(a[0], DICOM_TAG_STUDY_INSTANCE_UID, "0");
-  index_->SetIdentifierTag(a[1], DICOM_TAG_STUDY_INSTANCE_UID, "1");
-  index_->SetIdentifierTag(a[2], DICOM_TAG_STUDY_INSTANCE_UID, "0");
-  index_->SetIdentifierTag(a[3], DICOM_TAG_SERIES_INSTANCE_UID, "0");
-
-  std::list<std::string> s;
-
-  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "0");
-  ASSERT_EQ(2u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), "a") != s.end());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), "c") != s.end());
-
-  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "0");
-  ASSERT_EQ(1u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), "d") != s.end());
-
-  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
-  ASSERT_EQ(1u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
-
-  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_Equal, "1");
-  ASSERT_EQ(1u, s.size());
-  ASSERT_TRUE(std::find(s.begin(), s.end(), "b") != s.end());
-
-  DoLookupIdentifier(s, ResourceType_Series, DICOM_TAG_SERIES_INSTANCE_UID, ConstraintType_Equal, "1");
-  ASSERT_EQ(0u, s.size());
-
-  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "0");
-  ASSERT_EQ(3u, s.size());
-
-  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "1");
-  ASSERT_EQ(1u, s.size());
-
-  DoLookupIdentifier(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID, ConstraintType_GreaterOrEqual, "2");
-  ASSERT_EQ(0u, s.size());
-
-  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
-                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "0");
-  ASSERT_EQ(2u, s.size());
-
-  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
-                      ConstraintType_GreaterOrEqual, "1", ConstraintType_SmallerOrEqual, "1");
-  ASSERT_EQ(1u, s.size());
-
-  DoLookupIdentifier2(s, ResourceType_Study, DICOM_TAG_STUDY_INSTANCE_UID,
-                      ConstraintType_GreaterOrEqual, "0", ConstraintType_SmallerOrEqual, "1");
-  ASSERT_EQ(3u, s.size());
-}
-
-
-TEST(ServerIndex, AttachmentRecycling)
-{
-  const std::string path = "UnitTestsStorage";
-
-  SystemToolbox::RemoveFile(path + "/index");
-  FilesystemStorage storage(path);
-  SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
-  db.Open();
-  ServerContext context(db, storage, true /* running unit tests */, 10);
-  context.SetupJobsEngine(true, false);
-  ServerIndex& index = context.GetIndex();
-
-  index.SetMaximumStorageSize(10);
-
-  uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
-  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                            countStudies, countSeries, countInstances);
-
-  ASSERT_EQ(0u, countPatients);
-  ASSERT_EQ(0u, diskSize);
-
-  ServerIndex::Attachments attachments;
-
-  std::vector<std::string> ids;
-  for (int i = 0; i < 10; i++)
-  {
-    std::string id = boost::lexical_cast<std::string>(i);
-    DicomMap instance;
-    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;
-    DicomInstanceToStore toStore;
-    toStore.SetSummary(instance);
-    ASSERT_EQ(StoreStatus_Success, index.Store(instanceMetadata, toStore, attachments,
-                                               false /* don't overwrite */));
-    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());
-
-    // The default transfer syntax depends on the OS endianness
-    std::string s = instanceMetadata[MetadataType_Instance_TransferSyntax];
-    ASSERT_TRUE(s == "1.2.840.10008.1.2.1" ||
-                s == "1.2.840.10008.1.2.2");
-
-    ASSERT_EQ("1.2.840.10008.5.1.4.1.1.1", instanceMetadata[MetadataType_Instance_SopClassUid]);
-
-    DicomInstanceHasher hasher(instance);
-    ids.push_back(hasher.HashPatient());
-    ids.push_back(hasher.HashStudy());
-    ids.push_back(hasher.HashSeries());
-    ids.push_back(hasher.HashInstance());
-
-    ASSERT_EQ(hasher.HashPatient(), toStore.GetHasher().HashPatient());
-    ASSERT_EQ(hasher.HashStudy(), toStore.GetHasher().HashStudy());
-    ASSERT_EQ(hasher.HashSeries(), toStore.GetHasher().HashSeries());
-    ASSERT_EQ(hasher.HashInstance(), toStore.GetHasher().HashInstance());
-  }
-
-  index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                            countStudies, countSeries, countInstances);
-  ASSERT_EQ(10u, countPatients);
-  ASSERT_EQ(0u, diskSize);
-
-  for (size_t i = 0; i < ids.size(); i++)
-  {
-    FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5");
-    index.AddAttachment(info, ids[i]);
-
-    index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                              countStudies, countSeries, countInstances);
-    ASSERT_GE(10u, diskSize);
-  }
-
-  // Because the DB is in memory, the SQLite index must not have been created
-  ASSERT_FALSE(SystemToolbox::IsRegularFile(path + "/index"));
-
-  context.Stop();
-  db.Close();
-}
-
-
-TEST(ServerIndex, 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;
-    SQLiteDatabaseWrapper db;   // The SQLite DB is in memory
-    db.Open();
-    ServerContext context(db, storage, true /* running unit tests */, 10);
-    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.SetOverwriteInstances(overwrite);
-
-    uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances;
-    context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                                           countStudies, countSeries, countInstances);
-
-    ASSERT_EQ(0u, countInstances);
-    ASSERT_EQ(0u, diskSize);
-
-    {
-      DicomInstanceToStore toStore;
-      toStore.SetSummary(instance);
-      toStore.SetOrigin(DicomInstanceOrigin::FromPlugins());
-      ASSERT_EQ(id, toStore.GetHasher().HashInstance());
-
-      std::string id2;
-      ASSERT_EQ(StoreStatus_Success, context.Store(id2, toStore, StoreInstanceMode_Default));
-      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().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                                           countStudies, countSeries, countInstances);
-    ASSERT_EQ(1u, countInstances);
-    ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), diskSize);
-    ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), uncompressedSize);
-
-    Json::Value tmp;
-    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, StoreInstanceMode_Default));
-      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().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, 
-                                           countStudies, countSeries, countInstances);
-    ASSERT_EQ(1u, countInstances);
-    ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), diskSize);
-    ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), uncompressedSize);
-
-    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	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,335 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/SystemToolbox.h"
-#include "../Core/Toolbox.h"
-#include "../Core/OrthancException.h"
-#include "../Core/HttpServer/BufferHttpSender.h"
-#include "../Core/HttpServer/FilesystemHttpSender.h"
-#include "../Core/HttpServer/HttpStreamTranscoder.h"
-#include "../Core/Compression/ZlibCompressor.h"
-#include "../Core/Compression/GzipCompressor.h"
-
-
-using namespace Orthanc;
-
-
-TEST(Gzip, Basic)
-{
-  std::string s = "Hello world";
- 
-  std::string compressed;
-  GzipCompressor c;
-  ASSERT_FALSE(c.HasPrefixWithUncompressedSize());
-  IBufferCompressor::Compress(compressed, c, s);
-
-  std::string uncompressed;
-  IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_EQ(s.size(), uncompressed.size());
-  ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size()));
-}
-
-
-TEST(Gzip, Empty)
-{
-  std::string s;
- 
-  std::string compressed;
-  GzipCompressor c;
-  ASSERT_FALSE(c.HasPrefixWithUncompressedSize());
-  c.SetPrefixWithUncompressedSize(false);
-  IBufferCompressor::Compress(compressed, c, s);
-
-  std::string uncompressed;
-  IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_TRUE(uncompressed.empty());
-}
-
-
-TEST(Gzip, BasicWithPrefix)
-{
-  std::string s = "Hello world";
- 
-  std::string compressed;
-  GzipCompressor c;
-  c.SetPrefixWithUncompressedSize(true);
-  ASSERT_TRUE(c.HasPrefixWithUncompressedSize());
-  IBufferCompressor::Compress(compressed, c, s);
-
-  std::string uncompressed;
-  IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_EQ(s.size(), uncompressed.size());
-  ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size()));
-}
-
-
-TEST(Gzip, EmptyWithPrefix)
-{
-  std::string s;
- 
-  std::string compressed;
-  GzipCompressor c;
-  c.SetPrefixWithUncompressedSize(true);
-  ASSERT_TRUE(c.HasPrefixWithUncompressedSize());
-  IBufferCompressor::Compress(compressed, c, s);
-
-  std::string uncompressed;
-  IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_TRUE(uncompressed.empty());
-}
-
-
-TEST(Zlib, Basic)
-{
-  std::string s = Toolbox::GenerateUuid();
-  s = s + s + s + s;
- 
-  std::string compressed, compressed2;
-  ZlibCompressor c;
-  ASSERT_TRUE(c.HasPrefixWithUncompressedSize());
-  IBufferCompressor::Compress(compressed, c, s);
-
-  std::string uncompressed;
-  IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_EQ(s.size(), uncompressed.size());
-  ASSERT_EQ(0, memcmp(&s[0], &uncompressed[0], s.size()));
-}
-
-
-TEST(Zlib, Level)
-{
-  std::string s = Toolbox::GenerateUuid();
-  s = s + s + s + s;
- 
-  std::string compressed, compressed2;
-  ZlibCompressor c;
-  c.SetCompressionLevel(9);
-  IBufferCompressor::Compress(compressed, c, s);
-
-  c.SetCompressionLevel(0);
-  IBufferCompressor::Compress(compressed2, c, s);
-
-  ASSERT_TRUE(compressed.size() < compressed2.size());
-}
-
-
-TEST(Zlib, DISABLED_Corrupted)  // Disabled because it may result in a crash
-{
-  std::string s = Toolbox::GenerateUuid();
-  s = s + s + s + s;
- 
-  std::string compressed;
-  ZlibCompressor c;
-  IBufferCompressor::Compress(compressed, c, s);
-
-  ASSERT_FALSE(compressed.empty());
-  compressed[compressed.size() - 1] = 'a';
-  std::string u;
-
-  ASSERT_THROW(IBufferCompressor::Uncompress(u, c, compressed), OrthancException);
-}
-
-
-TEST(Zlib, Empty)
-{
-  std::string s = "";
- 
-  std::string compressed, compressed2;
-  ZlibCompressor c;
-  IBufferCompressor::Compress(compressed, c, s);
-  ASSERT_EQ(compressed, compressed2);
-
-  std::string uncompressed;
-  IBufferCompressor::Uncompress(uncompressed, c, compressed);
-  ASSERT_TRUE(uncompressed.empty());
-}
-
-
-static bool ReadAllStream(std::string& result,
-                          IHttpStreamAnswer& stream,
-                          bool allowGzip = false,
-                          bool allowDeflate = false)
-{
-  stream.SetupHttpCompression(allowGzip, allowDeflate);
-
-  result.resize(static_cast<size_t>(stream.GetContentLength()));
-
-  size_t pos = 0;
-  while (stream.ReadNextChunk())
-  {
-    size_t s = stream.GetChunkSize();
-    if (pos + s > result.size())
-    {
-      return false;
-    }
-
-    memcpy(&result[pos], stream.GetChunkContent(), s);
-    pos += s;
-  }
-
-  return pos == result.size();
-}
-
-
-TEST(BufferHttpSender, Basic)
-{
-  const std::string s = "Hello world";
-  std::string t;
-
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(1);
-    ASSERT_TRUE(ReadAllStream(t, sender));
-    ASSERT_EQ(0u, t.size());
-  }
-
-  for (int cs = 0; cs < 5; cs++)
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(cs);
-    sender.GetBuffer() = s;
-    ASSERT_TRUE(ReadAllStream(t, sender));
-    ASSERT_EQ(s, t);
-  }
-}
-
-
-TEST(FilesystemHttpSender, Basic)
-{
-  const std::string& path = "UnitTestsResults/stream";
-  const std::string s = "Hello world";
-  std::string t;
-
-  {
-    SystemToolbox::WriteFile(s, path);
-    FilesystemHttpSender sender(path);
-    ASSERT_TRUE(ReadAllStream(t, sender));
-    ASSERT_EQ(s, t);
-  }
-
-  {
-    SystemToolbox::WriteFile("", path);
-    FilesystemHttpSender sender(path);
-    ASSERT_TRUE(ReadAllStream(t, sender));
-    ASSERT_EQ(0u, t.size());
-  }
-}
-
-
-TEST(HttpStreamTranscoder, Basic)
-{
-  ZlibCompressor compressor;
-
-  const std::string s = "Hello world " + Toolbox::GenerateUuid();
-
-  std::string t;
-  IBufferCompressor::Compress(t, compressor, s);
-
-  for (int cs = 0; cs < 5; cs++)
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(cs);
-    sender.GetBuffer() = t;
-    std::string u;
-    ASSERT_TRUE(ReadAllStream(u, sender));
-
-    std::string v;
-    IBufferCompressor::Uncompress(v, compressor, u);
-    ASSERT_EQ(s, v);
-  }
-
-  // Pass-through test, no decompression occurs
-  for (int cs = 0; cs < 5; cs++)
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(cs);
-    sender.GetBuffer() = t;
-
-    HttpStreamTranscoder transcode(sender, CompressionType_None);
-    
-    std::string u;
-    ASSERT_TRUE(ReadAllStream(u, transcode));
-    
-    ASSERT_EQ(t, u);
-  }
-
-  // Pass-through test, decompression occurs
-  for (int cs = 0; cs < 5; cs++)
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(cs);
-    sender.GetBuffer() = t;
-
-    HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize);
-    
-    std::string u;
-    ASSERT_TRUE(ReadAllStream(u, transcode, false, false));
-    
-    ASSERT_EQ(s, u);
-  }
-
-  // Pass-through test with zlib, no decompression occurs but deflate is sent
-  for (int cs = 0; cs < 16; cs++)
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(cs);
-    sender.GetBuffer() = t;
-
-    HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize);
-    
-    std::string u;
-    ASSERT_TRUE(ReadAllStream(u, transcode, false, true));
-    
-    ASSERT_EQ(t.size() - sizeof(uint64_t), u.size());
-    ASSERT_EQ(t.substr(sizeof(uint64_t)), u);
-  }
-
-  for (int cs = 0; cs < 3; cs++)
-  {
-    BufferHttpSender sender;
-    sender.SetChunkSize(cs);
-
-    HttpStreamTranscoder transcode(sender, CompressionType_ZlibWithSize);
-    std::string u;
-    ASSERT_TRUE(ReadAllStream(u, transcode, false, true));
-    
-    ASSERT_EQ(0u, u.size());
-  }
-}
--- a/UnitTestsSources/ToolboxTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-#include "../Core/Compatibility.h"
-#include "../Core/IDynamicObject.h"
-#include "../Core/OrthancException.h"
-#include "../Core/Toolbox.h"
-
-using namespace Orthanc;
-
-TEST(Toolbox, Base64_allByteValues)
-{
-  std::string toEncode;
-  std::string base64Result;
-  std::string decodedResult;
-
-  size_t size = 2*256;
-  toEncode.reserve(size);
-  for (size_t i = 0; i < size; i++)
-    toEncode.push_back(i % 256);
-
-  Toolbox::EncodeBase64(base64Result, toEncode);
-  Toolbox::DecodeBase64(decodedResult, base64Result);
-
-  ASSERT_EQ(toEncode, decodedResult);
-}
-
-TEST(Toolbox, Base64_multipleSizes)
-{
-  std::string toEncode;
-  std::string base64Result;
-  std::string decodedResult;
-
-  for (size_t size = 0; size <= 5; size++)
-  {
-    printf("base64, testing size %zu\n", size);
-    toEncode.clear();
-    toEncode.reserve(size);
-    for (size_t i = 0; i < size; i++)
-      toEncode.push_back(i % 256);
-
-    Toolbox::EncodeBase64(base64Result, toEncode);
-    Toolbox::DecodeBase64(decodedResult, base64Result);
-
-    ASSERT_EQ(toEncode, decodedResult);
-  }
-}
-
-static std::string EncodeBase64Bis(const std::string& s)
-{
-  std::string result;
-  Toolbox::EncodeBase64(result, s);
-  return result;
-}
-
-
-TEST(Toolbox, Base64)
-{
-  ASSERT_EQ("", EncodeBase64Bis(""));
-  ASSERT_EQ("YQ==", EncodeBase64Bis("a"));
-
-  const std::string hello = "SGVsbG8gd29ybGQ=";
-  ASSERT_EQ(hello, EncodeBase64Bis("Hello world"));
-
-  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+/=");
-}
-
-
-#if 0 // enable only when compiling in Release with a C++ 11 compiler
-#include <chrono> // I had troubles to link with boost::chrono ...
-
-TEST(Toolbox, Base64_largeString)
-{
-  std::string toEncode;
-  std::string base64Result;
-  std::string decodedResult;
-
-  size_t size = 10 * 1024 * 1024;
-  toEncode.reserve(size);
-  for (size_t i = 0; i < size; i++)
-    toEncode.push_back(i % 256);
-
-  std::chrono::high_resolution_clock::time_point start;
-  std::chrono::high_resolution_clock::time_point afterEncoding;
-  std::chrono::high_resolution_clock::time_point afterDecoding;
-
-  start = std::chrono::high_resolution_clock::now();
-  Orthanc::Toolbox::EncodeBase64(base64Result, toEncode);
-  afterEncoding = std::chrono::high_resolution_clock::now();
-  Orthanc::Toolbox::DecodeBase64(decodedResult, base64Result);
-  afterDecoding = std::chrono::high_resolution_clock::now();
-
-  ASSERT_EQ(toEncode, decodedResult);
-
-  printf("encoding took %zu ms\n", (std::chrono::duration_cast<std::chrono::milliseconds>(afterEncoding - start)));
-  printf("decoding took %zu ms\n", (std::chrono::duration_cast<std::chrono::milliseconds>(afterDecoding - afterEncoding)));
-}
-#endif
-
-
-TEST(Toolbox, LargeHexadecimalToDecimal)
-{
-  // https://stackoverflow.com/a/16967286/881731
-  ASSERT_EQ(
-    "166089946137986168535368849184301740204613753693156360462575217560130904921953976324839782808018277000296027060873747803291797869684516494894741699267674246881622658654267131250470956587908385447044319923040838072975636163137212887824248575510341104029461758594855159174329892125993844566497176102668262139513",
-    Toolbox::LargeHexadecimalToDecimal("EC851A69B8ACD843164E10CFF70CF9E86DC2FEE3CF6F374B43C854E3342A2F1AC3E30C741CC41E679DF6D07CE6FA3A66083EC9B8C8BF3AF05D8BDBB0AA6Cb3ef8c5baa2a5e531ba9e28592f99e0fe4f95169a6c63f635d0197e325c5ec76219b907e4ebdcd401fb1986e4e3ca661ff73e7e2b8fd9988e753b7042b2bbca76679"));
-
-  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal(""));
-  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0"));
-  ASSERT_EQ("0", Toolbox::LargeHexadecimalToDecimal("0000"));
-  ASSERT_EQ("255", Toolbox::LargeHexadecimalToDecimal("00000ff"));
-
-  ASSERT_THROW(Toolbox::LargeHexadecimalToDecimal("g"), Orthanc::OrthancException);
-}
-
-
-TEST(Toolbox, GenerateDicomPrivateUniqueIdentifier)
-{
-  std::string s = Toolbox::GenerateDicomPrivateUniqueIdentifier();
-  ASSERT_EQ("2.25.", s.substr(0, 5));
-}
-
-
-TEST(Toolbox, UniquePtr)
-{
-  std::unique_ptr<int> i(new int(42));
-  ASSERT_EQ(42, *i);
-
-  std::unique_ptr<SingleValueObject<int> > j(new SingleValueObject<int>(42));
-  ASSERT_EQ(42, j->GetValue());
-}
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,136 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "../Core/EnumerationDictionary.h"
-
-#include "gtest/gtest.h"
-
-#include "../Core/Logging.h"
-#include "../Core/Toolbox.h"
-#include "../Core/OrthancException.h"
-
-#include "../OrthancServer/OrthancInitialization.h"
-#include "../OrthancServer/ServerEnumerations.h"
-
-
-using namespace Orthanc;
-
-
-TEST(EnumerationDictionary, Simple)
-{
-  EnumerationDictionary<MetadataType>  d;
-
-  ASSERT_THROW(d.Translate("ReceptionDate"), OrthancException);
-  ASSERT_EQ(MetadataType_ModifiedFrom, d.Translate("5"));
-  ASSERT_EQ(256, d.Translate("256"));
-
-  d.Add(MetadataType_Instance_ReceptionDate, "ReceptionDate");
-
-  ASSERT_EQ(MetadataType_Instance_ReceptionDate, d.Translate("ReceptionDate"));
-  ASSERT_EQ(MetadataType_Instance_ReceptionDate, d.Translate("2"));
-  ASSERT_EQ("ReceptionDate", d.Translate(MetadataType_Instance_ReceptionDate));
-
-  ASSERT_THROW(d.Add(MetadataType_Instance_ReceptionDate, "Hello"), OrthancException);
-  ASSERT_THROW(d.Add(MetadataType_ModifiedFrom, "ReceptionDate"), OrthancException); // already used
-  ASSERT_THROW(d.Add(MetadataType_ModifiedFrom, "1024"), OrthancException); // cannot register numbers
-  d.Add(MetadataType_ModifiedFrom, "ModifiedFrom");  // ok
-}
-
-
-TEST(EnumerationDictionary, ServerEnumerations)
-{
-  ASSERT_STREQ("Patient", EnumerationToString(ResourceType_Patient));
-  ASSERT_STREQ("Study", EnumerationToString(ResourceType_Study));
-  ASSERT_STREQ("Series", EnumerationToString(ResourceType_Series));
-  ASSERT_STREQ("Instance", EnumerationToString(ResourceType_Instance));
-
-  ASSERT_STREQ("ModifiedSeries", EnumerationToString(ChangeType_ModifiedSeries));
-
-  ASSERT_STREQ("Failure", EnumerationToString(StoreStatus_Failure));
-  ASSERT_STREQ("Success", EnumerationToString(StoreStatus_Success));
-
-  ASSERT_STREQ("CompletedSeries", EnumerationToString(ChangeType_CompletedSeries));
-
-  ASSERT_EQ("IndexInSeries", EnumerationToString(MetadataType_Instance_IndexInSeries));
-  ASSERT_EQ("LastUpdate", EnumerationToString(MetadataType_LastUpdate));
-
-  ASSERT_EQ(ResourceType_Patient, StringToResourceType("PATienT"));
-  ASSERT_EQ(ResourceType_Study, StringToResourceType("STudy"));
-  ASSERT_EQ(ResourceType_Series, StringToResourceType("SeRiEs"));
-  ASSERT_EQ(ResourceType_Instance, StringToResourceType("INStance"));
-  ASSERT_EQ(ResourceType_Instance, StringToResourceType("IMagE"));
-  ASSERT_THROW(StringToResourceType("heLLo"), OrthancException);
-
-  ASSERT_EQ(2047, StringToMetadata("2047"));
-  ASSERT_THROW(StringToMetadata("Ceci est un test"), OrthancException);
-  ASSERT_THROW(RegisterUserMetadata(128, ""), OrthancException); // too low (< 1024)
-  ASSERT_THROW(RegisterUserMetadata(128000, ""), OrthancException); // too high (> 65535)
-  RegisterUserMetadata(2047, "Ceci est un test");
-  ASSERT_EQ(2047, StringToMetadata("2047"));
-  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("Vitrea", EnumerationToString(StringToModalityManufacturer("Vitrea")));
-  ASSERT_STREQ("GE", EnumerationToString(StringToModalityManufacturer("GE")));
-  // 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("Generic", EnumerationToString(StringToModalityManufacturer("ClearCanvas")));
-  ASSERT_STREQ("Generic", EnumerationToString(StringToModalityManufacturer("Dcm4Chee")));
-  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("SyngoVia")));
-  ASSERT_STREQ("GenericNoWildcardInDates", EnumerationToString(StringToModalityManufacturer("AgfaImpax")));
-}
-
-
-
-int main(int argc, char **argv)
-{
-  Logging::Initialize();
-  Toolbox::InitializeGlobalLocale(NULL);
-  Logging::EnableInfoLevel(true);
-  Toolbox::DetectEndianness();
-  SystemToolbox::MakeDirectory("UnitTestsResults");
-  OrthancInitialize();
-
-  ::testing::InitGoogleTest(&argc, argv);
-  int result = RUN_ALL_TESTS();
-
-  OrthancFinalize();
-  Logging::Finalize();
-
-  return result;
-}
--- a/UnitTestsSources/VersionsTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,227 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * 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 "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include <stdint.h>
-#include <math.h>
-#include <png.h>
-#include <ctype.h>
-#include <zlib.h>
-#include <curl/curl.h>
-#include <boost/version.hpp>
-#include <sqlite3.h>
-#include <lua.h>
-#include <jpeglib.h>
-
-#if BUILDING_LIBICONV == 1
-#  include <iconv.h>
-#endif
-
-#if ORTHANC_ENABLE_SSL == 1
-#  include <openssl/opensslv.h>
-#endif
-
-#if (ORTHANC_ENABLE_CIVETWEB == 1 &&            \
-     ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1)
-#  include <civetweb.h>
-#endif
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-#  include <pugixml.hpp>
-#endif
-
-
-TEST(Versions, Zlib)
-{
-  ASSERT_STREQ(zlibVersion(), ZLIB_VERSION);
-}
-
-TEST(Versions, Curl)
-{
-  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ(LIBCURL_VERSION, v->version);
-}
-
-TEST(Versions, Png)
-{
-  ASSERT_EQ(PNG_LIBPNG_VER_MAJOR * 10000 + PNG_LIBPNG_VER_MINOR * 100 + PNG_LIBPNG_VER_RELEASE,
-            static_cast<int>(png_access_version_number()));
-}
-
-TEST(Versions, SQLite)
-{
-#if defined(__APPLE__)
-  // Under OS X, there might exist minor differences between the
-  // version of the headers and the version of the library, for the
-  // system-wide SQLite. Ignore these differences.
-#else
-  // http://www.sqlite.org/capi3ref.html#sqlite3_libversion
-  EXPECT_EQ(sqlite3_libversion_number(), SQLITE_VERSION_NUMBER);
-  EXPECT_STREQ(sqlite3_libversion(), SQLITE_VERSION);
-  
-  /**
-   * On Orthanc > 1.5.8, we comment out the following test, that is
-   * too strict for some GNU/Linux distributions to apply their own
-   * security fixes. Checking the main version macros is sufficient.
-   * https://bugzilla.suse.com/show_bug.cgi?id=1154550#c2
-   **/
-  // EXPECT_STREQ(sqlite3_sourceid(), SQLITE_SOURCE_ID);
-#endif
-
-  // Ensure that the SQLite version is above 3.7.0.
-  // "sqlite3_create_function_v2" is not defined in previous versions.
-  ASSERT_GE(SQLITE_VERSION_NUMBER, 3007000);
-}
-
-
-TEST(Versions, Lua)
-{
-  // Ensure that the Lua version is above 5.1.0. This version has
-  // introduced some API changes.
-  ASSERT_GE(LUA_VERSION_NUM, 501);
-}
-
-
-#if (ORTHANC_ENABLE_CIVETWEB == 1 &&            \
-     ORTHANC_UNIT_TESTS_LINK_FRAMEWORK != 1)
-TEST(Version, CivetwebCompression)
-{
-  ASSERT_TRUE(mg_check_feature(MG_FEATURES_COMPRESSION));
-}
-#endif
-
-
-#if ORTHANC_STATIC == 1
-
-TEST(Versions, ZlibStatic)
-{
-  ASSERT_STREQ("1.2.11", zlibVersion());
-}
-
-TEST(Versions, BoostStatic)
-{
-  ASSERT_STREQ("1_69", BOOST_LIB_VERSION);
-}
-
-TEST(Versions, CurlStatic)
-{
-  curl_version_info_data* v = curl_version_info(CURLVERSION_NOW);
-  ASSERT_STREQ("7.64.0", v->version);
-}
-
-TEST(Versions, PngStatic)
-{
-  ASSERT_EQ(10636u, png_access_version_number());
-  ASSERT_STREQ("1.6.36", PNG_LIBPNG_VER_STRING);
-}
-
-TEST(Versions, JpegStatic)
-{
-  ASSERT_EQ(9, JPEG_LIB_VERSION_MAJOR);
-  ASSERT_EQ(3, JPEG_LIB_VERSION_MINOR);
-}
-
-TEST(Versions, CurlSslStatic)
-{
-  curl_version_info_data * vinfo = curl_version_info(CURLVERSION_NOW);
-
-  // Check that SSL support is enabled when required
-  bool curlSupportsSsl = (vinfo->features & CURL_VERSION_SSL) != 0;
-
-#if ORTHANC_ENABLE_SSL == 0
-  ASSERT_FALSE(curlSupportsSsl);
-#else
-  ASSERT_TRUE(curlSupportsSsl);
-#endif
-}
-
-TEST(Version, LuaStatic)
-{
-  ASSERT_STREQ("Lua 5.3.5", LUA_RELEASE);
-}
-
-
-#if BUILDING_LIBICONV == 1
-TEST(Version, LibIconvStatic)
-{
-  static const int major = 1;
-  static const int minor = 15;  
-  ASSERT_EQ((major << 8) + minor, _LIBICONV_VERSION);
-}
-#endif
-
-
-#if ORTHANC_ENABLE_SSL == 1
-TEST(Version, OpenSslStatic)
-{
-  ASSERT_TRUE(OPENSSL_VERSION_NUMBER == 0x1000210fL /* openssl-1.0.2p */ ||
-              OPENSSL_VERSION_NUMBER == 0x1010107fL /* openssl-1.1.1g */);
-}
-#endif
-
-
-#include <json/version.h>
-
-TEST(Version, JsonCpp)
-{
-#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
-}
-
-
-#if ORTHANC_ENABLE_CIVETWEB == 1
-TEST(Version, Civetweb)
-{
-  ASSERT_EQ(1, CIVETWEB_VERSION_MAJOR);
-  ASSERT_EQ(12, CIVETWEB_VERSION_MINOR);
-  ASSERT_EQ(0, CIVETWEB_VERSION_PATCH);
-}
-#endif
-
-
-#if ORTHANC_ENABLE_PUGIXML == 1
-TEST(Version, Pugixml)
-{
-  ASSERT_EQ(190, PUGIXML_VERSION);
-}
-#endif
-
-
-#endif
--- a/UnitTestsSources/ZipTests.cpp	Wed Jun 10 18:49:21 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,193 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
- * Department, University Hospital of Liege, Belgium
- * Copyright (C) 2017-2020 Osimis S.A., Belgium
- *
- * This program is free software: you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * In addition, as a special exception, the copyright holders of this
- * program give permission to link the code of its release with the
- * OpenSSL project's "OpenSSL" library (or with modified versions of it
- * that use the same license as the "OpenSSL" library), and distribute
- * the linked executables. You must obey the GNU General Public License
- * in all respects for all of the code used other than "OpenSSL". If you
- * modify file(s) with this exception, you may extend this exception to
- * your version of the file(s), but you are not obligated to do so. If
- * you do not wish to do so, delete this exception statement from your
- * version. If you delete this exception statement from all source files
- * in the program, then also delete it here.
- * 
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- **/
-
-
-#if ORTHANC_UNIT_TESTS_LINK_FRAMEWORK == 1
-#  include <OrthancFramework.h>
-#endif
-
-#include "PrecompiledHeadersUnitTests.h"
-#include "gtest/gtest.h"
-
-#include "../Core/OrthancException.h"
-#include "../Core/Compression/ZipWriter.h"
-#include "../Core/Compression/HierarchicalZipWriter.h"
-#include "../Core/Toolbox.h"
-
-
-using namespace Orthanc;
-
-TEST(ZipWriter, Basic)
-{
-  Orthanc::ZipWriter w;
-  w.SetOutputPath("UnitTestsResults/hello.zip");
-  w.Open();
-  w.OpenFile("world/hello");
-  w.Write("Hello world");
-}
-
-
-TEST(ZipWriter, Basic64)
-{
-  Orthanc::ZipWriter w;
-  w.SetOutputPath("UnitTestsResults/hello64.zip");
-  w.SetZip64(true);
-  w.Open();
-  w.OpenFile("world/hello");
-  w.Write("Hello world");
-}
-
-
-TEST(ZipWriter, Exceptions)
-{
-  Orthanc::ZipWriter w;
-  ASSERT_THROW(w.Open(), Orthanc::OrthancException);
-  w.SetOutputPath("UnitTestsResults/hello3.zip");
-  w.Open();
-  ASSERT_THROW(w.Write("hello world"), Orthanc::OrthancException);
-}
-
-
-TEST(ZipWriter, Append)
-{
-  {
-    Orthanc::ZipWriter w;
-    w.SetAppendToExisting(false);
-    w.SetOutputPath("UnitTestsResults/append.zip");
-    w.Open();
-    w.OpenFile("world/hello");
-    w.Write("Hello world 1");
-  }
-
-  {
-    Orthanc::ZipWriter w;
-    w.SetAppendToExisting(true);
-    w.SetOutputPath("UnitTestsResults/append.zip");
-    w.Open();
-    w.OpenFile("world/appended");
-    w.Write("Hello world 2");
-  }
-}
-
-
-
-
-
-namespace Orthanc
-{
-  // The namespace is necessary
-  // http://code.google.com/p/googletest/wiki/AdvancedGuide#Private_Class_Members
-
-  TEST(HierarchicalZipWriter, Index)
-  {
-    HierarchicalZipWriter::Index i;
-    ASSERT_EQ("hello", i.OpenFile("hello"));
-    ASSERT_EQ("hello-2", i.OpenFile("hello"));
-    ASSERT_EQ("coucou", i.OpenFile("coucou"));
-    ASSERT_EQ("hello-3", i.OpenFile("hello"));
-
-    i.OpenDirectory("coucou");
-
-    ASSERT_EQ("coucou-2/world", i.OpenFile("world"));
-    ASSERT_EQ("coucou-2/world-2", i.OpenFile("world"));
-
-    i.OpenDirectory("world");
-  
-    ASSERT_EQ("coucou-2/world-3/hello", i.OpenFile("hello"));
-    ASSERT_EQ("coucou-2/world-3/hello-2", i.OpenFile("hello"));
-
-    i.CloseDirectory();
-
-    ASSERT_EQ("coucou-2/world-4", i.OpenFile("world"));
-
-    i.CloseDirectory();
-
-    ASSERT_EQ("coucou-3", i.OpenFile("coucou"));
-
-    ASSERT_THROW(i.CloseDirectory(), OrthancException);
-  }
-
-
-  TEST(HierarchicalZipWriter, Filenames)
-  {
-    ASSERT_EQ("trE hell", HierarchicalZipWriter::Index::KeepAlphanumeric("    ÊtrE hellô  "));
-
-    // The "^" character is considered as a space in DICOM
-    ASSERT_EQ("Hel lo world", HierarchicalZipWriter::Index::KeepAlphanumeric("    Hel^^  ^\r\n\t^^lo  \t  <world>  "));
-  }
-}
-
-
-TEST(HierarchicalZipWriter, Basic)
-{
-  static const std::string SPACES = "                             ";
-
-  HierarchicalZipWriter w("UnitTestsResults/hello2.zip");
-
-  w.SetCompressionLevel(0);
-
-  // Inside "/"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello\n");
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello-2\n");
-  w.OpenDirectory("hello");
-
-  // Inside "/hello-3"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello\n");
-  w.OpenDirectory("hello");
-
-  w.SetCompressionLevel(9);
-
-  // Inside "/hello-3/hello-2"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello\n");
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello-2\n");
-  w.CloseDirectory();
-
-  // Inside "/hello-3"
-  w.OpenFile("hello");
-  w.Write(SPACES + "hello-3\n");
-
-  /**
-
-     TO CHECK THE CONTENT OF THE "hello2.zip" FILE:
-
-     # unzip -v hello2.zip 
-
-     => There must be 6 files. The first 3 files must have a negative
-     compression ratio.
-
-  **/
-}